@amcharts/amcharts5
Version:
amCharts 5
395 lines (346 loc) • 14.9 kB
JavaScript
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";
// ============================================================
// Strait of Hormuz — Oil Export Flows (MapSankeySeries Demo)
// Gulf Producers → Asian & European Importers
// ============================================================
var root = am5.Root.new("chartdiv");
var oilTheme = am5.Theme.new(root);
oilTheme.rule("InterfaceColors").setAll({
primaryButton: am5.color(0x1e2824),
primaryButtonHover: am5.color(0x3a5040),
primaryButtonDown: am5.color(0x0a0e12),
primaryButtonActive: am5.color(0xc8890a),
primaryButtonText: am5.color(0xf5d090),
secondaryButton: am5.color(0x1e2824),
secondaryButtonHover: am5.color(0x3a5040),
secondaryButtonDown: am5.color(0x141a1a),
secondaryButtonText: am5.color(0xf5d090),
background: am5.color(0x0a0e12),
text: am5.color(0xf5d090)
});
root.setThemes([am5themes_Animated.new(root), oilTheme]);
// Crude & Tar palette
var deepCrude = am5.color(0x141a1a); // ocean — very dark teal-black
var slick = am5.color(0x1e2824); // land dark — oil slick green-black
var pipeline = am5.color(0x3a5040); // accents — industrial dark green
var amber = am5.color(0xc8890a); // primary — dark amber / crude oil
var sulfur = am5.color(0xa89050); // secondary — sulfurous yellow-brown
var flare = am5.color(0xf5d090); // text/highlights — gas flare warm
// ---- Chart (start zoomed out at home) ----
var chart = root.container.children.push(am5map.MapChart.new(root, {
panX: "rotateX",
panY: "rotateY",
projection: am5map.geoOrthographic(),
homeGeoPoint: { longitude: 0, latitude: 0 },
homeRotationX: 0,
homeRotationY: 0,
homeZoomLevel: 1,
minZoomLevel: 0.8,
zoomLevel: 1
}));
// ---- Ocean background ----
var bgSeries = chart.series.push(am5map.MapPolygonSeries.new(root, {}));
bgSeries.mapPolygons.template.setAll({
fill: deepCrude,
fillOpacity: 1,
strokeOpacity: 0
});
bgSeries.data.push({ geometry: am5map.getGeoRectangle(90, 180, -90, -180) });
// ---- Graticule ----
var graticuleSeries = chart.series.push(am5map.GraticuleSeries.new(root, {}));
graticuleSeries.mapLines.template.setAll({
stroke: pipeline,
strokeOpacity: 0.25,
strokeWidth: 0.5
});
// ---- Countries ----
var polygonSeries = chart.series.push(am5map.MapPolygonSeries.new(root, {
geoJSON: am5geodata_worldLow
}));
polygonSeries.mapPolygons.template.setAll({
fill: slick,
stroke: pipeline,
strokeWidth: 0.5,
strokeOpacity: 0.6
});
// Highlight producer & importer countries
var producerIds = ["SA", "IQ", "AE", "KW", "IR", "QA"];
var importerIds = ["CN", "JP", "KR", "IN", "SG", "IT", "GR", "NL"];
polygonSeries.events.on("datavalidated", function() {
am5.array.each(polygonSeries.dataItems, function(di) {
var id = di.get("id");
if (producerIds.includes(id)) {
di.get("mapPolygon").setAll({ fill: am5.color(0x6b5a10), tooltipText: "{name}" });
} else if (importerIds.includes(id)) {
di.get("mapPolygon").setAll({ fill: am5.color(0x2a4030), tooltipText: "{name}" });
}
});
});
// ---- MapSankeySeries — Oil flows through Hormuz ----
var sankeySeries = chart.series.push(am5map.MapSankeySeries.new(root, {
polygonSeries: polygonSeries,
maxWidth: 0.8,
controlPointDistance: 0.4,
resolution: 60,
nodePadding: 0.1
}));
sankeySeries.mapPolygons.template.setAll({
fill: amber,
fillOpacity: 0.4,
strokeOpacity: 0,
tooltipText: "{source} > {target}\n{value}k bbl/day"
});
sankeySeries.nodes.mapPolygons.template.setAll({
fill: amber,
stroke: flare,
strokeWidth: 1.5,
fillOpacity: 0.95,
strokeOpacity: 1,
tooltipText: "{name}"
});
// ---- Animated oil drop bullets ----
sankeySeries.bullets.push(function() {
return am5.Bullet.new(root, {
locationX: 0,
autoRotate: true,
autoRotateAngle: -90,
sprite: am5.Graphics.new(root, {
// Oil drop shape
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",
fill: amber,
stroke: am5.color(0x8b6914),
strokeWidth: 0.5,
centerX: am5.p50,
centerY: am5.p50,
scale: 0.45,
visible: false
})
});
});
sankeySeries.events.on("datavalidated", function() {
// Find the longest path to scale durations proportionally
var maxLength = 0;
am5.array.each(sankeySeries.dataItems, function(dataItem) {
var len = sankeySeries.getPathLength(dataItem);
if (len > maxLength) maxLength = len;
});
var baseDuration = 8000; // longest path duration
var minDuration = 2000;
am5.array.each(sankeySeries.dataItems, function(dataItem) {
var pathLength = sankeySeries.getPathLength(dataItem) || maxLength;
var dur = maxLength > 0 ? Math.max(minDuration, (pathLength / maxLength) * baseDuration) : baseDuration;
var bullets = dataItem.bullets;
if (bullets) {
am5.array.each(bullets, function(bullet) {
var randomDur = dur * (0.8 + Math.random() * 0.4);
var delay = Math.random() * randomDur;
setTimeout(function() {
var sprite = bullet.get("sprite");
if (sprite) sprite.set("visible", true);
bullet.animate({
key: "locationX",
from: 0,
to: 1,
duration: randomDur,
easing: am5.ease.linear,
loops: Infinity
});
}, delay);
});
}
});
});
// ---- Oil Export Data — through Strait of Hormuz ----
// Coordinates: actual oil port / terminal locations
// Values in thousands of barrels per day (approximate)
// Gulf export terminals
var rasTanura = { lon: 50.17, lat: 26.64 }; // Saudi Arabia — Ras Tanura
var basra = { lon: 48.80, lat: 29.69 }; // Iraq — Basra Oil Terminal
var fujairah = { lon: 56.33, lat: 25.12 }; // UAE — Port of Fujairah
var minaAhmadi = { lon: 48.17, lat: 29.08 }; // Kuwait — Mina Al Ahmadi
var khargIsland = { lon: 50.32, lat: 29.23 }; // Iran — Kharg Island
var rasLaffan = { lon: 51.56, lat: 25.93 }; // Qatar — Ras Laffan
// Asian import terminals
var ningbo = { lon: 121.97, lat: 29.87 }; // China — Ningbo-Zhoushan
var qingdao = { lon: 120.38, lat: 36.07 }; // China — Qingdao
var yokohama = { lon: 139.64, lat: 35.44 }; // Japan — Yokohama
var chiba = { lon: 140.10, lat: 35.60 }; // Japan — Chiba
var ulsan = { lon: 129.38, lat: 35.50 }; // South Korea — Ulsan
var jamnagar = { lon: 69.66, lat: 22.42 }; // India — Jamnagar / Vadinar
var mumbai = { lon: 72.88, lat: 19.08 }; // India — Mumbai (JNPT)
var singapore = { lon: 103.84, lat: 1.26 }; // Singapore — Jurong Island
// European import terminals
var trieste = { lon: 13.78, lat: 45.65 }; // Italy — Trieste (SIOT)
var piraeus = { lon: 23.63, lat: 37.94 }; // Greece — Piraeus / Agioi Theodoroi
var rotterdam = { lon: 4.12, lat: 51.95 }; // Netherlands — Rotterdam
function flow(src, srcName, tgt, tgtName, value, wp) {
var result = {
sourceLongitude: src.lon, sourceLatitude: src.lat,
targetLongitude: tgt.lon, targetLatitude: tgt.lat,
source: srcName, target: tgtName, value: value
};
if (wp) result.waypoints = wp;
return result;
}
// Maritime waypoints — keep ships on water
var wpHormuz = { longitude: 58, latitude: 24 }; // Strait of Hormuz exit
var wpArabianSea = { longitude: 64, latitude: 18 }; // Mid Arabian Sea
var wpSouthIndia = { longitude: 78, latitude: 6 }; // South of Sri Lanka
var wpMalacca = { longitude: 101, latitude: 3 }; // Malacca Strait
var wpAden = { longitude: 47, latitude: 12 }; // Gulf of Aden
var wpSuez = { longitude: 34, latitude: 29 }; // Suez Canal
// Route templates — fan out East Asia routes so they don't merge
var viaJapan = [{ longitude: 58, latitude: 26 }, { longitude: 80, latitude: 9 }, { longitude: 104, latitude: 7 }];
var viaKorea = [{ longitude: 58, latitude: 24 }, { longitude: 78, latitude: 6 }, { longitude: 101, latitude: 3 }];
var viaChina = [{ longitude: 58, latitude: 22 }, { longitude: 76, latitude: 3 }, { longitude: 99, latitude: 1 }];
var viaChinaNorth = [{ longitude: 58, latitude: 22 }, { longitude: 76, latitude: 3 }, { longitude: 99, latitude: 1 }, { longitude: 124, latitude: 32 }];
var viaIndiaNear = [wpHormuz]; // > Jamnagar (close)
var viaIndiaFar = [wpHormuz, wpArabianSea]; // > Mumbai
var viaSingapore = [wpHormuz, wpSouthIndia]; // > Singapore
var viaSuez = [wpHormuz, wpAden, wpSuez]; // > Mediterranean / Europe
var wpSouthGreece = { longitude: 22, latitude: 35 }; // South of Peloponnese
var wpOtranto = { longitude: 18.5, latitude: 40 }; // Strait of Otranto (Adriatic entry)
var viaSuezItaly = [wpHormuz, wpAden, wpSuez, wpSouthGreece, wpOtranto]; // > Italy via south of Greece
var wpSicily = { longitude: 13, latitude: 37 }; // Near Sicily
var wpPortugal = { longitude: -10, latitude: 39 }; // Off Portugal coast
var viaAtlantic = [wpHormuz, wpAden, wpSuez, wpSicily, wpPortugal]; // > Atlantic Europe
sankeySeries.data.setAll([
// === Ras Tanura (Saudi Arabia) ===
flow(rasTanura, "Ras Tanura", ningbo, "Ningbo", 1100, viaChina),
flow(rasTanura, "Ras Tanura", qingdao, "Qingdao", 600, viaChinaNorth),
flow(rasTanura, "Ras Tanura", yokohama, "Yokohama", 700, viaJapan),
flow(rasTanura, "Ras Tanura", chiba, "Chiba", 300, viaJapan),
flow(rasTanura, "Ras Tanura", ulsan, "Ulsan", 800, viaKorea),
flow(rasTanura, "Ras Tanura", jamnagar, "Jamnagar", 500, viaIndiaNear),
flow(rasTanura, "Ras Tanura", mumbai, "Mumbai", 300, viaIndiaFar),
flow(rasTanura, "Ras Tanura", rotterdam, "Rotterdam", 350, viaAtlantic),
// === Basra Oil Terminal (Iraq) ===
flow(basra, "Basra", ningbo, "Ningbo", 700, viaChina),
flow(basra, "Basra", qingdao, "Qingdao", 400, viaChinaNorth),
flow(basra, "Basra", jamnagar, "Jamnagar", 600, viaIndiaNear),
flow(basra, "Basra", mumbai, "Mumbai", 400, viaIndiaFar),
flow(basra, "Basra", ulsan, "Ulsan", 400, viaKorea),
flow(basra, "Basra", trieste, "Trieste", 350, viaSuezItaly),
flow(basra, "Basra", piraeus, "Piraeus", 250, viaSuez),
// === Fujairah (UAE) — already outside Hormuz ===
flow(fujairah, "Fujairah", yokohama, "Yokohama", 400, viaJapan.slice(1)),
flow(fujairah, "Fujairah", chiba, "Chiba", 200, viaJapan.slice(1)),
flow(fujairah, "Fujairah", jamnagar, "Jamnagar", 300),
flow(fujairah, "Fujairah", mumbai, "Mumbai", 200, [wpArabianSea]),
flow(fujairah, "Fujairah", ningbo, "Ningbo", 250, viaChina.slice(1)),
flow(fujairah, "Fujairah", ulsan, "Ulsan", 350, viaKorea.slice(1)),
flow(fujairah, "Fujairah", singapore, "Singapore", 250, [wpSouthIndia]),
// === Mina Al Ahmadi (Kuwait) ===
flow(minaAhmadi, "Mina Al Ahmadi", ulsan, "Ulsan", 400, viaKorea),
flow(minaAhmadi, "Mina Al Ahmadi", yokohama, "Yokohama", 300, viaJapan),
flow(minaAhmadi, "Mina Al Ahmadi", ningbo, "Ningbo", 300, viaChina),
flow(minaAhmadi, "Mina Al Ahmadi", jamnagar, "Jamnagar", 200, viaIndiaNear),
// === Kharg Island (Iran) ===
flow(khargIsland, "Kharg Island", ningbo, "Ningbo", 400, viaChina),
flow(khargIsland, "Kharg Island", qingdao, "Qingdao", 200, viaChinaNorth),
flow(khargIsland, "Kharg Island", jamnagar, "Jamnagar", 200, viaIndiaNear),
flow(khargIsland, "Kharg Island", piraeus, "Piraeus", 100, viaSuez),
// === Ras Laffan (Qatar) ===
flow(rasLaffan, "Ras Laffan", yokohama, "Yokohama", 300, viaJapan),
flow(rasLaffan, "Ras Laffan", ulsan, "Ulsan", 200, viaKorea),
flow(rasLaffan, "Ras Laffan", jamnagar, "Jamnagar", 150, viaIndiaNear),
flow(rasLaffan, "Ras Laffan", singapore,"Singapore", 120, viaSingapore)
]);
// ---- Title + Subtitle (top center) ----
var titleCont = chart.children.push(am5.Container.new(root, {
layout: root.verticalLayout,
x: am5.p50,
centerX: am5.p50,
y: 0,
position: "absolute",
paddingTop: 16
}));
titleCont.children.push(am5.Label.new(root, {
text: "Strait of Hormuz — Oil Export Flows",
fontSize: 18,
fontWeight: "600",
fill: amber,
x: am5.p50,
centerX: am5.p50
}));
titleCont.children.push(am5.Label.new(root, {
text: "Gulf Producers > Major Importers (thousands of barrels/day)",
fontSize: 11,
fill: sulfur,
x: am5.p50,
centerX: am5.p50
}));
// ---- Globe / Map switch ----
var switchCont = chart.children.push(am5.Container.new(root, {
layout: root.horizontalLayout,
x: 20,
y: 40
}));
switchCont.children.push(am5.Label.new(root, {
centerY: am5.p50,
text: "Globe",
fill: flare,
fontSize: 13
}));
var switchButton = switchCont.children.push(am5.Button.new(root, {
themeTags: ["switch"],
centerY: am5.p50,
icon: am5.Circle.new(root, {
themeTags: ["icon"]
})
}));
var easing = am5.ease.inOut(am5.ease.cubic);
var duration = 1500;
function zoomToGlobe() {
chart.set("projection", am5map.geoOrthographic());
chart.set("panX", "rotateX");
chart.set("panY", "rotateY");
chart.animate({ key: "rotationX", to: -74, duration, easing });
chart.animate({ key: "rotationY", to: -30, duration, easing });
chart.animate({ key: "zoomLevel", to: 1.6, duration, easing });
bgSeries.mapPolygons.template.set("fillOpacity", 1);
}
function zoomToMap() {
chart.set("projection", am5map.geoMercator());
chart.set("panX", "translateX");
chart.set("panY", "translateY");
chart.animate({ key: "rotationY", to: 0, duration, easing });
setTimeout(function() {
chart.zoomToGeoPoint({ longitude: 67, latitude: 32 }, 3.5, true, duration);
}, 100);
bgSeries.mapPolygons.template.set("fillOpacity", 0);
}
var fadeDuration = 300;
switchButton.on("active", function() {
chart.goHome(duration);
// Fade out before projection switch
setTimeout(function() {
chart.seriesContainer.animate({ key: "opacity", to: 0, duration: fadeDuration });
}, duration - fadeDuration);
setTimeout(function() {
if (switchButton.get("active")) {
zoomToMap();
} else {
zoomToGlobe();
}
// Fade in after projection switch
chart.seriesContainer.animate({ key: "opacity", to: 1, duration: fadeDuration });
}, duration);
});
switchCont.children.push(am5.Label.new(root, {
centerY: am5.p50,
text: "Map",
fill: flare,
fontSize: 13
}));
// ---- Zoom controls with home button ----
const zoomControl = chart.set("zoomControl", am5map.ZoomControl.new(root, {}));
zoomControl.homeButton.set("visible", true);
chart.appear(1000, 100);
// Initial animation: rotate to globe position, then zoom in
setTimeout(function() {
zoomToGlobe();
}, 1000);