import * as am5 from "@amcharts/amcharts5"; import * as am5map from "@amcharts/amcharts5/map"; import am5geodata_worldLow from "@amcharts/amcharts5-geodata/worldLow"; import am5themes_Animated from "@amcharts/amcharts5/themes/Animated"; // Create root element // https://www.amcharts.com/docs/v5/getting-started/#Root_element const root = am5.Root.new("chartdiv"); // Set themes // https://www.amcharts.com/docs/v5/concepts/themes/ root.setThemes([ am5themes_Animated.new(root) ]); // Create the map chart: a globe that turns to its home view once it loads // https://www.amcharts.com/docs/v5/charts/map-chart/map-pan-zoom/#Initial_position_and_zoom const chart = root.container.children.push(am5map.MapChart.new(root, { panX: "rotateX", // dragging sideways spins the globe... panY: "rotateY", // ...and dragging up or down tilts it projection: am5map.geoOrthographic(), // a globe homeRotationX: -74, // home faces 74 degrees east... homeRotationY: -22, // ...and 22 degrees north homeZoomLevel: 1.25, // zoomed in a little autoHome: true // turns there on load, animated by the Animated theme })); // Add zoom control // https://www.amcharts.com/docs/v5/charts/map-chart/map-pan-zoom/#Zoom_control const zoomControl = chart.set("zoomControl", am5map.ZoomControl.new(root, {})); zoomControl.homeButton.set("visible", true); // hidden by default; a click goes back home // Create series for background fill // https://www.amcharts.com/docs/v5/charts/map-chart/map-polygon-series/#Background_polygon const backgroundSeries = chart.series.push(am5map.MapPolygonSeries.new(root, {})); backgroundSeries.mapPolygons.template.setAll({ fill: root.interfaceColors.get("alternativeBackground"), // black on a light theme... fillOpacity: 0.1, // ...faded to a light grey disc for the sea strokeOpacity: 0 // no outline }); backgroundSeries.data.push({ geometry: am5map.getGeoRectangle(90, 180, -90, -180) // one polygon over the whole earth }); // Create graticule series // https://www.amcharts.com/docs/v5/charts/map-chart/graticule-series/ // lines of latitude and longitude, every 10 degrees by default const graticuleSeries = chart.series.push(am5map.GraticuleSeries.new(root, {})); graticuleSeries.mapLines.template.setAll({ stroke: root.interfaceColors.get("alternativeBackground"), // black on a light theme... strokeOpacity: 0.08 // ...but very faint }); // Create main polygon series for countries // https://www.amcharts.com/docs/v5/charts/map-chart/map-polygon-series/ const polygonSeries = chart.series.push(am5map.MapPolygonSeries.new(root, { geoJSON: am5geodata_worldLow })); polygonSeries.mapPolygons.template.setAll({ fill: root.interfaceColors.get("alternativeBackground"), // black on a light theme... fillOpacity: 0.15, // ...faded to a grey a little darker than the sea strokeWidth: 0.5, // thin borders... stroke: root.interfaceColors.get("background") // ...in the background color }); // Create Map Sankey series: flows between coordinates // https://www.amcharts.com/docs/v5/charts/map-chart/map-sankey-series/ const oilColor = am5.color(0xc8890a); // an amber for both the flows and the ports const sankeySeries = chart.series.push(am5map.MapSankeySeries.new(root, { maxWidth: 0.8, // the biggest flow is 0.8 degrees wide, the others in proportion nodePadding: 0.1, // each port's circle reaches 0.1 degrees past its flows controlPointDistance: 0.4 // curve handles 0.4 of each leg long, a bit under the default 0.5 })); // the flows sankeySeries.mapPolygons.template.setAll({ fill: oilColor, fillOpacity: 0.5, // see-through, so overlapping flows show strokeOpacity: 0, // no outline tooltipText: "{source} > {target}: {value}k barrels a day" // names from the data row }); // the ports: a circle at each end of the flows sankeySeries.nodes.mapPolygons.template.setAll({ fill: oilColor, stroke: root.interfaceColors.get("background"), // a ring in the background color strokeWidth: 1.5, tooltipText: "{name}: {sum}k barrels a day" // sum: all the port's flows, in and out }); // Add a drop that travels along each flow, at the same speed on every route // https://www.amcharts.com/docs/v5/concepts/animations/ sankeySeries.bullets.push((root, series, dataItem) => { // 5 s for every radian of route (about 6,400 km), so longer routes take longer const duration = sankeySeries.getPathLength(dataItem) * 5000; // each drop waits 0 to 2.5 s before it starts, so they don't all set off together const delay = (series.dataItems.indexOf(dataItem) % 6) * 500; return am5.Bullet.new(root, { locationX: 0, // at the source end of the flow autoRotate: true, // turned to follow the flow's direction... autoRotateAngle: -90, // ...with the drop's tip trailing behind animations: [ // travels from source to target, then starts over, forever { key: "locationX", from: 0, to: 1, duration: duration, delay: delay, loops: 0 } ], sprite: am5.Graphics.new(root, { svgPath: "M0,-7 C2,-4 5,0 5,3 C5,6 3,8 0,8 C-3,8 -5,6 -5,3 C-5,0 -2,-4 0,-7 Z", // a drop scale: 0.5, // half size: about 8 px tall fill: root.interfaceColors.get("text") // the theme's text color }) }); }); // Oil terminals in the Persian Gulf const rasTanura = { name: "Ras Tanura", longitude: 50.17, latitude: 26.64 }; const basra = { name: "Basra", longitude: 48.8, latitude: 29.69 }; const fujairah = { name: "Fujairah", longitude: 56.33, latitude: 25.12 }; const minaAlAhmadi = { name: "Mina Al Ahmadi", longitude: 48.17, latitude: 29.08 }; const khargIsland = { name: "Kharg Island", longitude: 50.32, latitude: 29.23 }; const rasLaffan = { name: "Ras Laffan", longitude: 51.56, latitude: 25.93 }; // Ports the oil goes to const ningbo = { name: "Ningbo", longitude: 121.97, latitude: 29.87 }; const qingdao = { name: "Qingdao", longitude: 120.38, latitude: 36.07 }; const yokohama = { name: "Yokohama", longitude: 139.64, latitude: 35.44 }; const ulsan = { name: "Ulsan", longitude: 129.38, latitude: 35.5 }; const jamnagar = { name: "Jamnagar", longitude: 69.66, latitude: 22.42 }; const mumbai = { name: "Mumbai", longitude: 72.88, latitude: 19.08 }; const singapore = { name: "Singapore", longitude: 103.84, latitude: 1.26 }; const trieste = { name: "Trieste", longitude: 13.78, latitude: 45.65 }; const piraeus = { name: "Piraeus", longitude: 23.63, latitude: 37.94 }; const rotterdam = { name: "Rotterdam", longitude: 4.12, latitude: 51.95 }; // Waypoints take the routes around the land, roughly where the ships go const gulfOfOman = { longitude: 59, latitude: 24.2 }; const arabianSea = { longitude: 64, latitude: 18 }; const omanCoast = { longitude: 60.5, latitude: 19.5 }; const sriLanka = { longitude: 78, latitude: 6 }; const aden = { longitude: 46, latitude: 12.3 }; const suez = { longitude: 33.5, latitude: 28.5 }; // Routes to East Asia are a little apart, so that their flows do not merge const toIndia = [gulfOfOman]; const toMumbai = [gulfOfOman, arabianSea]; const toSingapore = [gulfOfOman, sriLanka]; const toChina = [{ longitude: 59.5, latitude: 23.4 }, { longitude: 76, latitude: 3 }, { longitude: 99, latitude: 1 }]; const toNorthChina = toChina.concat([{ longitude: 124, latitude: 32 }]); const toKorea = [gulfOfOman, sriLanka, { longitude: 101, latitude: 3 }]; const toJapan = [{ longitude: 58.5, latitude: 25 }, { longitude: 80, latitude: 9 }, { longitude: 104, latitude: 7 }]; const toSuez = [gulfOfOman, omanCoast, aden, suez]; const toGreece = toSuez; const toItaly = toSuez.concat([{ longitude: 22, latitude: 35 }, { longitude: 18.5, latitude: 40 }]); const toNetherlands = toSuez.concat([{ longitude: 13, latitude: 37 }, { longitude: -10, latitude: 39 }]); // Makes a data row for a flow from one port to another function flow(source: typeof rasTanura, target: typeof rasTanura, value: number, waypoints: Array) { return { source: source.name, sourceLongitude: source.longitude, sourceLatitude: source.latitude, target: target.name, targetLongitude: target.longitude, targetLatitude: target.latitude, value: value, waypoints: waypoints }; } // Set data: oil exports, in thousands of barrels a day // Fujairah is on the Gulf of Oman already, so its routes skip the first waypoint sankeySeries.data.setAll([ flow(rasTanura, ningbo, 1100, toChina), flow(rasTanura, qingdao, 600, toNorthChina), flow(rasTanura, yokohama, 700, toJapan), flow(rasTanura, ulsan, 800, toKorea), flow(rasTanura, jamnagar, 500, toIndia), flow(rasTanura, mumbai, 300, toMumbai), flow(rasTanura, rotterdam, 350, toNetherlands), flow(basra, ningbo, 700, toChina), flow(basra, ulsan, 400, toKorea), flow(basra, jamnagar, 600, toIndia), flow(basra, mumbai, 400, toMumbai), flow(basra, trieste, 350, toItaly), flow(basra, piraeus, 250, toGreece), flow(fujairah, yokohama, 400, toJapan.slice(1)), flow(fujairah, ulsan, 350, toKorea.slice(1)), flow(fujairah, singapore, 250, toSingapore.slice(1)), flow(fujairah, jamnagar, 300, []), flow(minaAlAhmadi, ulsan, 400, toKorea), flow(minaAlAhmadi, yokohama, 300, toJapan), flow(minaAlAhmadi, ningbo, 300, toChina), flow(khargIsland, ningbo, 400, toChina), flow(khargIsland, qingdao, 200, toNorthChina), flow(khargIsland, piraeus, 100, toGreece), flow(rasLaffan, yokohama, 300, toJapan), flow(rasLaffan, ulsan, 200, toKorea), flow(rasLaffan, singapore, 120, toSingapore) ]); // Make stuff animate on load // https://www.amcharts.com/docs/v5/concepts/animations/#Forcing_appearance_animation chart.appear(1000, 100);