@amcharts/amcharts5-mcp 1.1.6 → 1.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/content/flow.md +1 -1
- package/content/map.md +662 -66
- package/package.json +1 -1
package/content/flow.md
CHANGED
|
@@ -42,7 +42,7 @@ Node IDs ("A", "B", etc.) are automatically extracted — you don't define nodes
|
|
|
42
42
|
| `am5flow.ChordNonRibbon` | Chord with line links | Arc nodes + line links |
|
|
43
43
|
| `am5flow.ArcDiagram` | Arc connections between linear nodes | Linear nodes + arc links |
|
|
44
44
|
|
|
45
|
-
**Geographic Sankey (MapSankeySeries):** For Sankey-style flow bands drawn on a geographic map, use `am5map.MapSankeySeries` from the **map** package (not flow). See `references/map.md`
|
|
45
|
+
**Geographic Sankey (MapSankeySeries):** For Sankey-style flow bands drawn on a geographic map, use `am5map.MapSankeySeries` from the **map** package (not flow). See `references/map.md` > MapSankeySeries section.
|
|
46
46
|
|
|
47
47
|
## Sankey diagram
|
|
48
48
|
|
package/content/map.md
CHANGED
|
@@ -36,7 +36,7 @@ CDN: `https://cdn.amcharts.com/lib/5/geodata/{mapName}.js`
|
|
|
36
36
|
Pattern: camelCase country name + detail level. E.g. `germanyLow`, `canadaHigh`, `chinaLow`.
|
|
37
37
|
|
|
38
38
|
**IMPORTANT — regional/continent geodata:**
|
|
39
|
-
- Individual country maps: `geodata/{countryName}Low.js`
|
|
39
|
+
- Individual country maps: `geodata/{countryName}Low.js` > global `am5geodata_{countryName}Low`
|
|
40
40
|
- **There are NO continent-level maps** like `europeLow.js` or `asiaLow.js` at the top-level CDN path
|
|
41
41
|
- Regional maps are in subdirectories: `geodata/region/world/{regionName}Low.js`
|
|
42
42
|
- CDN: `https://cdn.amcharts.com/lib/5/geodata/region/world/europeLow.js`
|
|
@@ -239,18 +239,26 @@ var sankeySeries = chart.series.push(
|
|
|
239
239
|
|
|
240
240
|
#### Data format
|
|
241
241
|
|
|
242
|
-
|
|
242
|
+
Two data patterns — polygon IDs or explicit coordinates.
|
|
243
|
+
|
|
244
|
+
**Pattern 1: `sourceId` / `targetId` (country codes)**
|
|
245
|
+
|
|
246
|
+
IDs use ISO 3166-1 alpha-2 codes matching the geodata. **CRITICAL:** Data MUST be set inside `polygonSeries.events.once("datavalidated", ...)` — the polygon series needs to finish parsing geoJSON before MapSankeySeries can resolve country IDs to centroids. Without this wrapper, `getDataItemById()` returns null and flows silently don't appear (no errors, no bands).
|
|
243
247
|
|
|
244
248
|
```js
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
249
|
+
polygonSeries.events.once("datavalidated", function() {
|
|
250
|
+
sankeySeries.data.setAll([
|
|
251
|
+
{ sourceId: "BR", targetId: "DE", value: 350 },
|
|
252
|
+
{ sourceId: "BR", targetId: "US", value: 450 },
|
|
253
|
+
{ sourceId: "DE", targetId: "FR", value: 150 },
|
|
254
|
+
{ sourceId: "DE", targetId: "PL", value: 100 }
|
|
255
|
+
]);
|
|
256
|
+
});
|
|
251
257
|
```
|
|
252
258
|
|
|
253
|
-
**Explicit coordinates
|
|
259
|
+
**Pattern 2: Explicit coordinates (no timing needed)**
|
|
260
|
+
|
|
261
|
+
When using `sourceLongitude`/`sourceLatitude`/`targetLongitude`/`targetLatitude`, data can be set immediately — no `datavalidated` wrapper required:
|
|
254
262
|
```js
|
|
255
263
|
sankeySeries.data.setAll([
|
|
256
264
|
{
|
|
@@ -263,15 +271,28 @@ sankeySeries.data.setAll([
|
|
|
263
271
|
|
|
264
272
|
**Waypoints** — route flows through intermediate points:
|
|
265
273
|
```js
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
274
|
+
polygonSeries.events.once("datavalidated", function() {
|
|
275
|
+
sankeySeries.data.setAll([
|
|
276
|
+
{
|
|
277
|
+
sourceId: "GB", targetId: "JP", value: 40,
|
|
278
|
+
waypoints: [
|
|
279
|
+
{ longitude: 30, latitude: 65 },
|
|
280
|
+
{ longitude: 90, latitude: 55 }
|
|
281
|
+
]
|
|
282
|
+
}
|
|
283
|
+
]);
|
|
284
|
+
});
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
**WARNING:** Never call `polygonSeries.data.setAll()` with custom objects — this replaces all geoJSON-derived data items, breaking MapSankeySeries centroid resolution. To set per-country properties (like colors), iterate existing data items after `datavalidated` instead:
|
|
288
|
+
```js
|
|
289
|
+
polygonSeries.events.on("datavalidated", function() {
|
|
290
|
+
am5.array.each(polygonSeries.dataItems, function(di) {
|
|
291
|
+
if (di.get("id") === "BR") {
|
|
292
|
+
di.get("mapPolygon").setAll({ fill: am5.color(0x8fae7e) });
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
});
|
|
275
296
|
```
|
|
276
297
|
|
|
277
298
|
#### Key settings
|
|
@@ -307,7 +328,7 @@ sankeySeries.mapPolygons.template.setAll({
|
|
|
307
328
|
fill: am5.color(0xff6b35),
|
|
308
329
|
fillOpacity: 0.6,
|
|
309
330
|
strokeOpacity: 0,
|
|
310
|
-
tooltipText: "{sourceId}
|
|
331
|
+
tooltipText: "{sourceId} > {targetId}: {value}"
|
|
311
332
|
});
|
|
312
333
|
```
|
|
313
334
|
|
|
@@ -384,14 +405,16 @@ Bullets automatically hide on the back side of the globe (orthographic projectio
|
|
|
384
405
|
The same country can be both target and source — incoming and outgoing bands share unified stacking at intermediate nodes:
|
|
385
406
|
|
|
386
407
|
```js
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
408
|
+
polygonSeries.events.once("datavalidated", function() {
|
|
409
|
+
sankeySeries.data.setAll([
|
|
410
|
+
// Level 1: Producers > Hubs
|
|
411
|
+
{ sourceId: "BR", targetId: "DE", value: 350 },
|
|
412
|
+
{ sourceId: "VN", targetId: "DE", value: 200 },
|
|
413
|
+
// Level 2: Hubs > Consumers
|
|
414
|
+
{ sourceId: "DE", targetId: "FR", value: 150 },
|
|
415
|
+
{ sourceId: "DE", targetId: "PL", value: 100 }
|
|
416
|
+
]);
|
|
417
|
+
});
|
|
395
418
|
```
|
|
396
419
|
|
|
397
420
|
### Animating bullets along lines (positionOnLine)
|
|
@@ -430,7 +453,7 @@ plane.on("positionOnLine", function(pos) {
|
|
|
430
453
|
|
|
431
454
|
**IMPORTANT — Multi-segment lines and `positionOnLine`:**
|
|
432
455
|
- `positionOnLine` treats the entire line as one unit (0 = start, 1 = end). If a line has multiple segments (i.e. `pointsToConnect` has 3+ points), position 0.5 is the midpoint of the *entire* path.
|
|
433
|
-
- For advanced per-segment animations (e.g., scaling the bullet differently at the midpoint of each segment, or pausing between segments), use **single-segment lines** (2 points each) instead of one multi-segment line. This gives you full control: animate `positionOnLine` from 0
|
|
456
|
+
- For advanced per-segment animations (e.g., scaling the bullet differently at the midpoint of each segment, or pausing between segments), use **single-segment lines** (2 points each) instead of one multi-segment line. This gives you full control: animate `positionOnLine` from 0 to 1 on each segment line sequentially, with independent timing, easing, and callbacks per segment.
|
|
434
457
|
- Example: instead of one line with points [A, B, C, D], create three lines: [A,B], [B,C], [C,D] and animate the bullet across them one at a time.
|
|
435
458
|
|
|
436
459
|
## Zoom controls
|
|
@@ -491,7 +514,7 @@ backgroundSeries.data.push({
|
|
|
491
514
|
chart.series.push(am5map.GraticuleSeries.new(root, {}));
|
|
492
515
|
```
|
|
493
516
|
|
|
494
|
-
## Drill-down map (country
|
|
517
|
+
## Drill-down map (country > states)
|
|
495
518
|
|
|
496
519
|
```js
|
|
497
520
|
// World-level polygons
|
|
@@ -1051,86 +1074,659 @@ Demonstrates: orthographic projection, single-segment lines for per-segment anim
|
|
|
1051
1074
|
</html>
|
|
1052
1075
|
```
|
|
1053
1076
|
|
|
1054
|
-
## Example 5: Map Sankey —
|
|
1077
|
+
## Example 5: Map Sankey — Global Coffee Supply Chain (sourceId/targetId)
|
|
1055
1078
|
|
|
1056
|
-
Demonstrates: `MapSankeySeries` with
|
|
1079
|
+
Demonstrates: `MapSankeySeries` with `sourceId`/`targetId` data (country codes), multi-level flows (producers > hubs > consumers), custom theme, country highlighting, animated coffee bean bullets, Globe/Map projection toggle, and auto-rotation. Based on the `map-sankey` shared example.
|
|
1057
1080
|
|
|
1058
1081
|
```html
|
|
1059
1082
|
<!DOCTYPE html>
|
|
1060
1083
|
<html>
|
|
1061
1084
|
<head>
|
|
1062
1085
|
<meta charset="UTF-8">
|
|
1063
|
-
<title>Map Sankey —
|
|
1086
|
+
<title>Map Sankey — Global Coffee Supply Chain</title>
|
|
1064
1087
|
<script src="https://cdn.amcharts.com/lib/5/index.js"></script>
|
|
1065
1088
|
<script src="https://cdn.amcharts.com/lib/5/map.js"></script>
|
|
1066
1089
|
<script src="https://cdn.amcharts.com/lib/5/geodata/worldLow.js"></script>
|
|
1067
1090
|
<script src="https://cdn.amcharts.com/lib/5/themes/Animated.js"></script>
|
|
1068
|
-
<style
|
|
1091
|
+
<style>body { margin: 0; background: #f0e6d6; } #chartdiv { width: 100%; height: 600px; }</style>
|
|
1069
1092
|
</head>
|
|
1070
1093
|
<body>
|
|
1071
1094
|
<div id="chartdiv"></div>
|
|
1072
1095
|
<script>
|
|
1073
1096
|
var root = am5.Root.new("chartdiv");
|
|
1074
|
-
|
|
1097
|
+
|
|
1098
|
+
// Coffee theme — warm earth tones
|
|
1099
|
+
var coffeeTheme = am5.Theme.new(root);
|
|
1100
|
+
coffeeTheme.rule("InterfaceColors").setAll({
|
|
1101
|
+
primaryButton: am5.color(0x8b5e3c),
|
|
1102
|
+
primaryButtonHover: am5.color(0x5c3a1e),
|
|
1103
|
+
primaryButtonDown: am5.color(0x3c1e0e),
|
|
1104
|
+
primaryButtonActive: am5.color(0xc4956a),
|
|
1105
|
+
primaryButtonText: am5.color(0xf5ece0),
|
|
1106
|
+
secondaryButton: am5.color(0xe8d5b7),
|
|
1107
|
+
secondaryButtonHover: am5.color(0xd4c4a8),
|
|
1108
|
+
secondaryButtonDown: am5.color(0xc4956a),
|
|
1109
|
+
secondaryButtonText: am5.color(0x3c1e0e),
|
|
1110
|
+
background: am5.color(0xe8d5b7),
|
|
1111
|
+
text: am5.color(0x3c1e0e)
|
|
1112
|
+
});
|
|
1113
|
+
root.setThemes([am5themes_Animated.new(root), coffeeTheme]);
|
|
1114
|
+
|
|
1115
|
+
// Grainy paper background
|
|
1116
|
+
root.container.set("background", am5.Rectangle.new(root, {
|
|
1117
|
+
fill: am5.color(0xf0e6d6),
|
|
1118
|
+
fillPattern: am5.GrainPattern.new(root, {
|
|
1119
|
+
density: 0.4,
|
|
1120
|
+
maxOpacity: 0.07,
|
|
1121
|
+
colors: [am5.color(0x000000)]
|
|
1122
|
+
})
|
|
1123
|
+
}));
|
|
1124
|
+
|
|
1125
|
+
// Coffee palette
|
|
1126
|
+
var espresso = am5.color(0x3c1e0e);
|
|
1127
|
+
var darkRoast = am5.color(0x5c3a1e);
|
|
1128
|
+
var mediumRoast = am5.color(0x8b5e3c);
|
|
1129
|
+
var lightRoast = am5.color(0xc4956a);
|
|
1130
|
+
var crema = am5.color(0xe8d5b7);
|
|
1131
|
+
var cream = am5.color(0xf5ece0);
|
|
1075
1132
|
|
|
1076
1133
|
var chart = root.container.children.push(am5map.MapChart.new(root, {
|
|
1077
1134
|
panX: "rotateX",
|
|
1078
|
-
panY: "
|
|
1079
|
-
projection: am5map.
|
|
1135
|
+
panY: "rotateY",
|
|
1136
|
+
projection: am5map.geoOrthographic(),
|
|
1137
|
+
rotationX: -15,
|
|
1138
|
+
rotationY: -20,
|
|
1139
|
+
minZoomLevel: 0.5,
|
|
1140
|
+
zoomLevel: 0.9
|
|
1080
1141
|
}));
|
|
1081
1142
|
|
|
1082
|
-
//
|
|
1143
|
+
// Ocean / globe background
|
|
1144
|
+
var bgSeries = chart.series.push(am5map.MapPolygonSeries.new(root, {}));
|
|
1145
|
+
bgSeries.mapPolygons.template.setAll({
|
|
1146
|
+
fill: am5.color(0xede4d4), fillOpacity: 1, strokeOpacity: 0
|
|
1147
|
+
});
|
|
1148
|
+
bgSeries.data.push({ geometry: am5map.getGeoRectangle(90, 180, -90, -180) });
|
|
1149
|
+
|
|
1150
|
+
// Graticule
|
|
1151
|
+
var graticuleSeries = chart.series.push(am5map.GraticuleSeries.new(root, {}));
|
|
1152
|
+
graticuleSeries.mapLines.template.setAll({
|
|
1153
|
+
stroke: mediumRoast, strokeOpacity: 0.15, strokeWidth: 0.5
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1156
|
+
// Countries
|
|
1083
1157
|
var polygonSeries = chart.series.push(am5map.MapPolygonSeries.new(root, {
|
|
1084
1158
|
geoJSON: am5geodata_worldLow
|
|
1085
1159
|
}));
|
|
1086
1160
|
polygonSeries.mapPolygons.template.setAll({
|
|
1087
|
-
fill:
|
|
1088
|
-
stroke: am5.color(0xffffff),
|
|
1089
|
-
strokeWidth: 0.5
|
|
1161
|
+
fill: cream, stroke: lightRoast, strokeWidth: 0.5, strokeOpacity: 0.5
|
|
1090
1162
|
});
|
|
1091
1163
|
|
|
1092
|
-
//
|
|
1164
|
+
// Highlight producer, hub, and consumer countries
|
|
1165
|
+
var producerIds = ["BR", "VN", "CO", "ET", "ID", "HN"];
|
|
1166
|
+
var hubIds = ["DE", "BE", "IT", "US"];
|
|
1167
|
+
var consumerIds = ["FR", "PL", "SE", "RU", "GB", "NL", "GR", "AT", "CA", "JP"];
|
|
1168
|
+
|
|
1169
|
+
polygonSeries.events.on("datavalidated", function() {
|
|
1170
|
+
am5.array.each(polygonSeries.dataItems, function(di) {
|
|
1171
|
+
var id = di.get("id");
|
|
1172
|
+
if (id && producerIds.indexOf(id) !== -1) {
|
|
1173
|
+
di.get("mapPolygon").setAll({ fill: am5.color(0x8fae7e) });
|
|
1174
|
+
} else if (id && hubIds.indexOf(id) !== -1) {
|
|
1175
|
+
di.get("mapPolygon").setAll({ fill: am5.color(0xc4a878) });
|
|
1176
|
+
} else if (id && consumerIds.indexOf(id) !== -1) {
|
|
1177
|
+
di.get("mapPolygon").setAll({ fill: am5.color(0xddc8a0) });
|
|
1178
|
+
}
|
|
1179
|
+
});
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1182
|
+
// MapSankeySeries — coffee supply chain
|
|
1093
1183
|
var sankeySeries = chart.series.push(am5map.MapSankeySeries.new(root, {
|
|
1094
1184
|
polygonSeries: polygonSeries,
|
|
1095
|
-
maxWidth:
|
|
1185
|
+
maxWidth: 2,
|
|
1096
1186
|
controlPointDistance: 0.4,
|
|
1097
|
-
|
|
1187
|
+
resolution: 60,
|
|
1188
|
+
nodePadding: 0.3
|
|
1098
1189
|
}));
|
|
1099
1190
|
|
|
1100
|
-
// Band appearance
|
|
1101
1191
|
sankeySeries.mapPolygons.template.setAll({
|
|
1102
|
-
fill:
|
|
1103
|
-
fillOpacity: 0.
|
|
1192
|
+
fill: mediumRoast,
|
|
1193
|
+
fillOpacity: 0.65,
|
|
1104
1194
|
strokeOpacity: 0,
|
|
1105
|
-
tooltipText: "{
|
|
1195
|
+
tooltipText: "{sourceNode.name} > {targetNode.name}\n{value}k tonnes"
|
|
1106
1196
|
});
|
|
1107
1197
|
|
|
1108
|
-
// Node appearance
|
|
1109
1198
|
sankeySeries.nodes.mapPolygons.template.setAll({
|
|
1110
|
-
fill:
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1199
|
+
fill: espresso,
|
|
1200
|
+
stroke: crema,
|
|
1201
|
+
strokeWidth: 1.5,
|
|
1202
|
+
fillOpacity: 0.95,
|
|
1203
|
+
strokeOpacity: 1,
|
|
1204
|
+
tooltipText: "{name}\n{sum}k tonnes"
|
|
1205
|
+
});
|
|
1206
|
+
|
|
1207
|
+
// Animated coffee bean bullets
|
|
1208
|
+
sankeySeries.bullets.push(function() {
|
|
1209
|
+
return am5.Bullet.new(root, {
|
|
1210
|
+
locationX: 0,
|
|
1211
|
+
autoRotate: true,
|
|
1212
|
+
sprite: am5.Graphics.new(root, {
|
|
1213
|
+
svgPath: "M-4,-2.5 C-4,-5 -1.5,-6.5 1,-6.5 C3.5,-6.5 5,-4.5 5,-2 C5,1 3,3.5 0.5,5 C-0.5,5.7 -1.5,5.7 -2.5,5 C-5,3.5 -6,1 -4,-2.5 Z M-1,-5 C-1,-1 -1,2 -0.5,4.5",
|
|
1214
|
+
fill: espresso,
|
|
1215
|
+
stroke: darkRoast,
|
|
1216
|
+
strokeWidth: 0.5,
|
|
1217
|
+
centerX: am5.p50,
|
|
1218
|
+
centerY: am5.p50,
|
|
1219
|
+
scale: 0.35,
|
|
1220
|
+
visible: false
|
|
1221
|
+
})
|
|
1222
|
+
});
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1225
|
+
// Set data after polygonSeries loads (sourceId/targetId need centroid lookup)
|
|
1226
|
+
// CRITICAL: data MUST be set inside datavalidated handler
|
|
1227
|
+
polygonSeries.events.once("datavalidated", function() {
|
|
1228
|
+
sankeySeries.data.setAll([
|
|
1229
|
+
// Producers > Hubs
|
|
1230
|
+
{ sourceId: "BR", targetId: "DE", value: 350 },
|
|
1231
|
+
{ sourceId: "BR", targetId: "US", value: 450 },
|
|
1232
|
+
{ sourceId: "BR", targetId: "IT", value: 200 },
|
|
1233
|
+
{ sourceId: "VN", targetId: "DE", value: 200 },
|
|
1234
|
+
{ sourceId: "VN", targetId: "BE", value: 150 },
|
|
1235
|
+
{ sourceId: "CO", targetId: "US", value: 250 },
|
|
1236
|
+
{ sourceId: "CO", targetId: "DE", value: 80 },
|
|
1237
|
+
{ sourceId: "ET", targetId: "DE", value: 60 },
|
|
1238
|
+
{ sourceId: "ET", targetId: "BE", value: 40 },
|
|
1239
|
+
{ sourceId: "ID", targetId: "US", value: 80 },
|
|
1240
|
+
{ sourceId: "HN", targetId: "DE", value: 60 },
|
|
1241
|
+
{ sourceId: "HN", targetId: "BE", value: 40 },
|
|
1242
|
+
// Hubs > Consumer Markets
|
|
1243
|
+
{ sourceId: "DE", targetId: "FR", value: 150 },
|
|
1244
|
+
{ sourceId: "DE", targetId: "PL", value: 100 },
|
|
1245
|
+
{ sourceId: "DE", targetId: "SE", value: 80 },
|
|
1246
|
+
{ sourceId: "DE", targetId: "RU", value: 120 },
|
|
1247
|
+
{ sourceId: "BE", targetId: "GB", value: 100 },
|
|
1248
|
+
{ sourceId: "BE", targetId: "NL", value: 80 },
|
|
1249
|
+
{ sourceId: "IT", targetId: "GR", value: 50 },
|
|
1250
|
+
{ sourceId: "IT", targetId: "AT", value: 40 },
|
|
1251
|
+
{ sourceId: "US", targetId: "CA", value: 120 },
|
|
1252
|
+
{ sourceId: "US", targetId: "JP", value: 80 }
|
|
1253
|
+
]);
|
|
1254
|
+
});
|
|
1255
|
+
|
|
1256
|
+
// Set country names on auto-created nodes and animate bullets
|
|
1257
|
+
var countryNames = {
|
|
1258
|
+
BR: "Brazil", VN: "Vietnam", CO: "Colombia", ET: "Ethiopia",
|
|
1259
|
+
ID: "Indonesia", HN: "Honduras", DE: "Germany", BE: "Belgium",
|
|
1260
|
+
IT: "Italy", US: "United States", FR: "France", PL: "Poland",
|
|
1261
|
+
SE: "Sweden", RU: "Russia", GB: "United Kingdom", NL: "Netherlands",
|
|
1262
|
+
GR: "Greece", AT: "Austria", CA: "Canada", JP: "Japan"
|
|
1263
|
+
};
|
|
1264
|
+
|
|
1265
|
+
sankeySeries.events.on("datavalidated", function() {
|
|
1266
|
+
am5.array.each(sankeySeries.nodes.dataItems, function(di) {
|
|
1267
|
+
var id = di.get("id");
|
|
1268
|
+
if (id && countryNames[id]) {
|
|
1269
|
+
di.set("name", countryNames[id]);
|
|
1270
|
+
}
|
|
1271
|
+
});
|
|
1272
|
+
|
|
1273
|
+
// Defer bullet animation — bullets are not yet instantiated
|
|
1274
|
+
// when datavalidated fires inside polygonSeries handler
|
|
1275
|
+
setTimeout(function() {
|
|
1276
|
+
am5.array.each(sankeySeries.dataItems, function(dataItem) {
|
|
1277
|
+
var bullets = dataItem.bullets;
|
|
1278
|
+
if (bullets) {
|
|
1279
|
+
am5.array.each(bullets, function(bullet) {
|
|
1280
|
+
var randomDur = 3000 + Math.random() * 3000;
|
|
1281
|
+
var delay = Math.random() * randomDur;
|
|
1282
|
+
setTimeout(function() {
|
|
1283
|
+
var sprite = bullet.get("sprite");
|
|
1284
|
+
if (sprite) sprite.set("visible", true);
|
|
1285
|
+
bullet.animate({
|
|
1286
|
+
key: "locationX",
|
|
1287
|
+
from: 0,
|
|
1288
|
+
to: 1,
|
|
1289
|
+
duration: randomDur,
|
|
1290
|
+
easing: am5.ease.linear,
|
|
1291
|
+
loops: Infinity
|
|
1292
|
+
});
|
|
1293
|
+
}, delay);
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
});
|
|
1297
|
+
}, 100);
|
|
1298
|
+
});
|
|
1299
|
+
|
|
1300
|
+
// Title
|
|
1301
|
+
var titleCont = chart.children.push(am5.Container.new(root, {
|
|
1302
|
+
layout: root.verticalLayout,
|
|
1303
|
+
x: am5.p50, centerX: am5.p50,
|
|
1304
|
+
y: am5.p100, centerY: am5.p100,
|
|
1305
|
+
position: "absolute", paddingBottom: 16
|
|
1306
|
+
}));
|
|
1307
|
+
titleCont.children.push(am5.Label.new(root, {
|
|
1308
|
+
text: "Global Coffee Supply Chain",
|
|
1309
|
+
fontSize: 18, fontWeight: "600", fill: espresso,
|
|
1310
|
+
x: am5.p50, centerX: am5.p50
|
|
1311
|
+
}));
|
|
1312
|
+
titleCont.children.push(am5.Label.new(root, {
|
|
1313
|
+
text: "(Thousands of tonnes)",
|
|
1314
|
+
fontSize: 11, fill: mediumRoast,
|
|
1315
|
+
x: am5.p50, centerX: am5.p50
|
|
1316
|
+
}));
|
|
1317
|
+
|
|
1318
|
+
// Globe / Map toggle
|
|
1319
|
+
var switchCont = chart.children.push(am5.Container.new(root, {
|
|
1320
|
+
layout: root.horizontalLayout, x: 20, y: 40
|
|
1321
|
+
}));
|
|
1322
|
+
switchCont.children.push(am5.Label.new(root, {
|
|
1323
|
+
centerY: am5.p50, text: "Globe", fill: espresso, fontSize: 13
|
|
1324
|
+
}));
|
|
1325
|
+
var switchButton = switchCont.children.push(am5.Button.new(root, {
|
|
1326
|
+
themeTags: ["switch"], centerY: am5.p50,
|
|
1327
|
+
icon: am5.Circle.new(root, { themeTags: ["icon"] })
|
|
1328
|
+
}));
|
|
1329
|
+
|
|
1330
|
+
var easing = am5.ease.inOut(am5.ease.cubic);
|
|
1331
|
+
var duration = 1500;
|
|
1332
|
+
var fadeDuration = 300;
|
|
1333
|
+
|
|
1334
|
+
function zoomToGlobe() {
|
|
1335
|
+
chart.set("projection", am5map.geoOrthographic());
|
|
1336
|
+
chart.set("panX", "rotateX");
|
|
1337
|
+
chart.set("panY", "rotateY");
|
|
1338
|
+
chart.animate({ key: "rotationX", to: -15, duration: duration, easing: easing });
|
|
1339
|
+
chart.animate({ key: "rotationY", to: -20, duration: duration, easing: easing });
|
|
1340
|
+
bgSeries.mapPolygons.template.set("fillOpacity", 1);
|
|
1341
|
+
chart.set("minZoomLevel", 0.9);
|
|
1342
|
+
chart.animate({ key: "zoomLevel", to: 0.9, duration: duration, easing: easing });
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
function zoomToMap() {
|
|
1346
|
+
chart.set("projection", am5map.geoMercator());
|
|
1347
|
+
chart.set("panX", "translateX");
|
|
1348
|
+
chart.set("panY", "translateY");
|
|
1349
|
+
chart.animate({ key: "rotationX", to: 0, duration: duration, easing: easing });
|
|
1350
|
+
chart.animate({ key: "rotationY", to: 0, duration: duration, easing: easing });
|
|
1351
|
+
bgSeries.mapPolygons.template.set("fillOpacity", 0);
|
|
1352
|
+
chart.set("minZoomLevel", 1);
|
|
1353
|
+
chart.animate({ key: "zoomLevel", to: 1.7, duration: duration, easing: easing });
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
switchButton.on("active", function() {
|
|
1357
|
+
chart.goHome(duration);
|
|
1358
|
+
setTimeout(function() {
|
|
1359
|
+
chart.seriesContainer.animate({ key: "opacity", to: 0, duration: fadeDuration });
|
|
1360
|
+
}, duration - fadeDuration);
|
|
1361
|
+
setTimeout(function() {
|
|
1362
|
+
if (switchButton.get("active")) { zoomToMap(); } else { zoomToGlobe(); }
|
|
1363
|
+
chart.seriesContainer.animate({ key: "opacity", to: 1, duration: fadeDuration });
|
|
1364
|
+
}, duration);
|
|
1365
|
+
});
|
|
1366
|
+
|
|
1367
|
+
switchCont.children.push(am5.Label.new(root, {
|
|
1368
|
+
centerY: am5.p50, text: "Map", fill: espresso, fontSize: 13
|
|
1369
|
+
}));
|
|
1370
|
+
|
|
1371
|
+
// Zoom controls
|
|
1372
|
+
var zoomControl = chart.set("zoomControl", am5map.ZoomControl.new(root, {}));
|
|
1373
|
+
zoomControl.homeButton.set("visible", true);
|
|
1374
|
+
|
|
1375
|
+
// Auto-rotate globe until user interaction
|
|
1376
|
+
var rotationAnimation = chart.animate({
|
|
1377
|
+
key: "rotationX", from: -15, to: -15 + 360,
|
|
1378
|
+
duration: 120000, loops: Infinity, easing: am5.ease.linear
|
|
1379
|
+
});
|
|
1380
|
+
chart.chartContainer.events.on("pointerdown", function() {
|
|
1381
|
+
if (rotationAnimation) { rotationAnimation.stop(); rotationAnimation = null; }
|
|
1382
|
+
});
|
|
1383
|
+
|
|
1384
|
+
chart.appear(1000, 100);
|
|
1385
|
+
</script>
|
|
1386
|
+
</body>
|
|
1387
|
+
</html>
|
|
1388
|
+
```
|
|
1389
|
+
|
|
1390
|
+
## Example 6: Map Sankey — Oil Export Flows with Waypoints (explicit coordinates)
|
|
1391
|
+
|
|
1392
|
+
Demonstrates: `MapSankeySeries` with explicit `sourceLongitude`/`sourceLatitude` coordinates, maritime waypoints for realistic shipping routes, oil drop SVG bullets with proportional animation speed, dark theme, country highlighting, and Globe/Map projection toggle. Based on the `map-sankey-waypoints` shared example.
|
|
1393
|
+
|
|
1394
|
+
**IMPORTANT — waypoints must use `longitude`/`latitude` keys** (not shorthand like `lon`/`lat`). Source/target location objects can use any keys because the `flow()` helper maps them to `sourceLongitude`/`sourceLatitude`, but waypoints are read directly by the series via `wp.longitude` and `wp.latitude`.
|
|
1395
|
+
|
|
1396
|
+
```html
|
|
1397
|
+
<!DOCTYPE html>
|
|
1398
|
+
<html>
|
|
1399
|
+
<head>
|
|
1400
|
+
<meta charset="UTF-8">
|
|
1401
|
+
<title>Map Sankey — Oil Export Flows</title>
|
|
1402
|
+
<script src="https://cdn.amcharts.com/lib/5/index.js"></script>
|
|
1403
|
+
<script src="https://cdn.amcharts.com/lib/5/map.js"></script>
|
|
1404
|
+
<script src="https://cdn.amcharts.com/lib/5/geodata/worldLow.js"></script>
|
|
1405
|
+
<script src="https://cdn.amcharts.com/lib/5/themes/Animated.js"></script>
|
|
1406
|
+
<style>body { margin: 0; background: #0a0e12; } #chartdiv { width: 100%; height: 600px; }</style>
|
|
1407
|
+
</head>
|
|
1408
|
+
<body>
|
|
1409
|
+
<div id="chartdiv"></div>
|
|
1410
|
+
<script>
|
|
1411
|
+
var root = am5.Root.new("chartdiv");
|
|
1412
|
+
|
|
1413
|
+
// Oil/dark theme
|
|
1414
|
+
var oilTheme = am5.Theme.new(root);
|
|
1415
|
+
oilTheme.rule("InterfaceColors").setAll({
|
|
1416
|
+
primaryButton: am5.color(0x1e2824),
|
|
1417
|
+
primaryButtonHover: am5.color(0x3a5040),
|
|
1418
|
+
primaryButtonDown: am5.color(0x0a0e12),
|
|
1419
|
+
primaryButtonActive: am5.color(0xc8890a),
|
|
1420
|
+
primaryButtonText: am5.color(0xf5d090),
|
|
1421
|
+
secondaryButton: am5.color(0x1e2824),
|
|
1422
|
+
secondaryButtonHover: am5.color(0x3a5040),
|
|
1423
|
+
secondaryButtonDown: am5.color(0x141a1a),
|
|
1424
|
+
secondaryButtonText: am5.color(0xf5d090),
|
|
1425
|
+
background: am5.color(0x0a0e12),
|
|
1426
|
+
text: am5.color(0xf5d090)
|
|
1427
|
+
});
|
|
1428
|
+
root.setThemes([am5themes_Animated.new(root), oilTheme]);
|
|
1429
|
+
|
|
1430
|
+
// Palette
|
|
1431
|
+
var deepCrude = am5.color(0x141a1a);
|
|
1432
|
+
var slick = am5.color(0x1e2824);
|
|
1433
|
+
var pipeline = am5.color(0x3a5040);
|
|
1434
|
+
var amber = am5.color(0xc8890a);
|
|
1435
|
+
var sulfur = am5.color(0xa89050);
|
|
1436
|
+
var flare = am5.color(0xf5d090);
|
|
1437
|
+
|
|
1438
|
+
var chart = root.container.children.push(am5map.MapChart.new(root, {
|
|
1439
|
+
panX: "rotateX",
|
|
1440
|
+
panY: "rotateY",
|
|
1441
|
+
projection: am5map.geoOrthographic(),
|
|
1442
|
+
homeGeoPoint: { longitude: 0, latitude: 0 },
|
|
1443
|
+
homeRotationX: 0,
|
|
1444
|
+
homeRotationY: 0,
|
|
1445
|
+
homeZoomLevel: 1,
|
|
1446
|
+
minZoomLevel: 0.8,
|
|
1447
|
+
zoomLevel: 1
|
|
1448
|
+
}));
|
|
1449
|
+
|
|
1450
|
+
// Ocean
|
|
1451
|
+
var bgSeries = chart.series.push(am5map.MapPolygonSeries.new(root, {}));
|
|
1452
|
+
bgSeries.mapPolygons.template.setAll({
|
|
1453
|
+
fill: deepCrude, fillOpacity: 1, strokeOpacity: 0
|
|
1454
|
+
});
|
|
1455
|
+
bgSeries.data.push({ geometry: am5map.getGeoRectangle(90, 180, -90, -180) });
|
|
1456
|
+
|
|
1457
|
+
// Graticule
|
|
1458
|
+
var graticuleSeries = chart.series.push(am5map.GraticuleSeries.new(root, {}));
|
|
1459
|
+
graticuleSeries.mapLines.template.setAll({
|
|
1460
|
+
stroke: pipeline, strokeOpacity: 0.25, strokeWidth: 0.5
|
|
1461
|
+
});
|
|
1462
|
+
|
|
1463
|
+
// Countries
|
|
1464
|
+
var polygonSeries = chart.series.push(am5map.MapPolygonSeries.new(root, {
|
|
1465
|
+
geoJSON: am5geodata_worldLow
|
|
1466
|
+
}));
|
|
1467
|
+
polygonSeries.mapPolygons.template.setAll({
|
|
1468
|
+
fill: slick, stroke: pipeline, strokeWidth: 0.5, strokeOpacity: 0.6
|
|
1469
|
+
});
|
|
1470
|
+
|
|
1471
|
+
// Highlight producer & importer countries
|
|
1472
|
+
var producerIds = ["SA", "IQ", "AE", "KW", "IR", "QA"];
|
|
1473
|
+
var importerIds = ["CN", "JP", "KR", "IN", "SG", "IT", "GR", "NL"];
|
|
1474
|
+
polygonSeries.events.on("datavalidated", function() {
|
|
1475
|
+
am5.array.each(polygonSeries.dataItems, function(di) {
|
|
1476
|
+
var id = di.get("id");
|
|
1477
|
+
if (producerIds.indexOf(id) !== -1) {
|
|
1478
|
+
di.get("mapPolygon").setAll({ fill: am5.color(0x6b5a10), tooltipText: "{name}" });
|
|
1479
|
+
} else if (importerIds.indexOf(id) !== -1) {
|
|
1480
|
+
di.get("mapPolygon").setAll({ fill: am5.color(0x2a4030), tooltipText: "{name}" });
|
|
1481
|
+
}
|
|
1482
|
+
});
|
|
1114
1483
|
});
|
|
1115
1484
|
|
|
1116
|
-
//
|
|
1485
|
+
// MapSankeySeries
|
|
1486
|
+
var sankeySeries = chart.series.push(am5map.MapSankeySeries.new(root, {
|
|
1487
|
+
polygonSeries: polygonSeries,
|
|
1488
|
+
maxWidth: 0.8,
|
|
1489
|
+
controlPointDistance: 0.4,
|
|
1490
|
+
resolution: 60,
|
|
1491
|
+
nodePadding: 0.1
|
|
1492
|
+
}));
|
|
1493
|
+
|
|
1494
|
+
sankeySeries.mapPolygons.template.setAll({
|
|
1495
|
+
fill: amber, fillOpacity: 0.4, strokeOpacity: 0,
|
|
1496
|
+
tooltipText: "{source} > {target}\n{value}k bbl/day"
|
|
1497
|
+
});
|
|
1498
|
+
|
|
1499
|
+
sankeySeries.nodes.mapPolygons.template.setAll({
|
|
1500
|
+
fill: amber, stroke: flare, strokeWidth: 1.5,
|
|
1501
|
+
fillOpacity: 0.95, strokeOpacity: 1, tooltipText: "{name}"
|
|
1502
|
+
});
|
|
1503
|
+
|
|
1504
|
+
// Animated oil drop bullets
|
|
1505
|
+
sankeySeries.bullets.push(function() {
|
|
1506
|
+
return am5.Bullet.new(root, {
|
|
1507
|
+
locationX: 0,
|
|
1508
|
+
autoRotate: true,
|
|
1509
|
+
autoRotateAngle: -90,
|
|
1510
|
+
sprite: am5.Graphics.new(root, {
|
|
1511
|
+
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",
|
|
1512
|
+
fill: amber, stroke: am5.color(0x8b6914), strokeWidth: 0.5,
|
|
1513
|
+
centerX: am5.p50, centerY: am5.p50, scale: 0.45, visible: false
|
|
1514
|
+
})
|
|
1515
|
+
});
|
|
1516
|
+
});
|
|
1517
|
+
|
|
1518
|
+
sankeySeries.events.on("datavalidated", function() {
|
|
1519
|
+
// Scale animation duration proportionally to path length
|
|
1520
|
+
var maxLength = 0;
|
|
1521
|
+
am5.array.each(sankeySeries.dataItems, function(dataItem) {
|
|
1522
|
+
var len = sankeySeries.getPathLength(dataItem);
|
|
1523
|
+
if (len > maxLength) maxLength = len;
|
|
1524
|
+
});
|
|
1525
|
+
|
|
1526
|
+
var baseDuration = 8000;
|
|
1527
|
+
var minDuration = 2000;
|
|
1528
|
+
|
|
1529
|
+
am5.array.each(sankeySeries.dataItems, function(dataItem) {
|
|
1530
|
+
var pathLength = sankeySeries.getPathLength(dataItem) || maxLength;
|
|
1531
|
+
var dur = maxLength > 0
|
|
1532
|
+
? Math.max(minDuration, (pathLength / maxLength) * baseDuration)
|
|
1533
|
+
: baseDuration;
|
|
1534
|
+
var bullets = dataItem.bullets;
|
|
1535
|
+
if (bullets) {
|
|
1536
|
+
am5.array.each(bullets, function(bullet) {
|
|
1537
|
+
var randomDur = dur * (0.8 + Math.random() * 0.4);
|
|
1538
|
+
var delay = Math.random() * randomDur;
|
|
1539
|
+
setTimeout(function() {
|
|
1540
|
+
var sprite = bullet.get("sprite");
|
|
1541
|
+
if (sprite) sprite.set("visible", true);
|
|
1542
|
+
bullet.animate({
|
|
1543
|
+
key: "locationX", from: 0, to: 1,
|
|
1544
|
+
duration: randomDur, easing: am5.ease.linear, loops: Infinity
|
|
1545
|
+
});
|
|
1546
|
+
}, delay);
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1549
|
+
});
|
|
1550
|
+
});
|
|
1551
|
+
|
|
1552
|
+
// Gulf export terminals (source/target objects can use any key names
|
|
1553
|
+
// because the flow() helper maps them to sourceLongitude/sourceLatitude)
|
|
1554
|
+
var rasTanura = { lon: 50.17, lat: 26.64 };
|
|
1555
|
+
var basra = { lon: 48.80, lat: 29.69 };
|
|
1556
|
+
var fujairah = { lon: 56.33, lat: 25.12 };
|
|
1557
|
+
var minaAhmadi = { lon: 48.17, lat: 29.08 };
|
|
1558
|
+
var khargIsland = { lon: 50.32, lat: 29.23 };
|
|
1559
|
+
var rasLaffan = { lon: 51.56, lat: 25.93 };
|
|
1560
|
+
|
|
1561
|
+
// Import terminals
|
|
1562
|
+
var ningbo = { lon: 121.97, lat: 29.87 };
|
|
1563
|
+
var qingdao = { lon: 120.38, lat: 36.07 };
|
|
1564
|
+
var yokohama = { lon: 139.64, lat: 35.44 };
|
|
1565
|
+
var chiba = { lon: 140.10, lat: 35.60 };
|
|
1566
|
+
var ulsan = { lon: 129.38, lat: 35.50 };
|
|
1567
|
+
var jamnagar = { lon: 69.66, lat: 22.42 };
|
|
1568
|
+
var mumbai = { lon: 72.88, lat: 19.08 };
|
|
1569
|
+
var singapore = { lon: 103.84, lat: 1.26 };
|
|
1570
|
+
var trieste = { lon: 13.78, lat: 45.65 };
|
|
1571
|
+
var piraeus = { lon: 23.63, lat: 37.94 };
|
|
1572
|
+
var rotterdam = { lon: 4.12, lat: 51.95 };
|
|
1573
|
+
|
|
1574
|
+
function flow(src, srcName, tgt, tgtName, value, wp) {
|
|
1575
|
+
var result = {
|
|
1576
|
+
sourceLongitude: src.lon, sourceLatitude: src.lat,
|
|
1577
|
+
targetLongitude: tgt.lon, targetLatitude: tgt.lat,
|
|
1578
|
+
source: srcName, target: tgtName, value: value
|
|
1579
|
+
};
|
|
1580
|
+
if (wp) result.waypoints = wp;
|
|
1581
|
+
return result;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
// IMPORTANT: waypoints must use { longitude: ..., latitude: ... }
|
|
1585
|
+
// (NOT shorthand like { lon, lat }) — the series reads wp.longitude / wp.latitude directly
|
|
1586
|
+
var wpHormuz = { longitude: 58, latitude: 24 };
|
|
1587
|
+
var wpArabianSea = { longitude: 64, latitude: 18 };
|
|
1588
|
+
var wpSouthIndia = { longitude: 78, latitude: 6 };
|
|
1589
|
+
var wpMalacca = { longitude: 101, latitude: 3 };
|
|
1590
|
+
var wpAden = { longitude: 47, latitude: 12 };
|
|
1591
|
+
var wpSuez = { longitude: 34, latitude: 29 };
|
|
1592
|
+
|
|
1593
|
+
// Route templates
|
|
1594
|
+
var viaJapan = [{ longitude: 58, latitude: 26 }, { longitude: 80, latitude: 9 }, { longitude: 104, latitude: 7 }];
|
|
1595
|
+
var viaKorea = [{ longitude: 58, latitude: 24 }, { longitude: 78, latitude: 6 }, { longitude: 101, latitude: 3 }];
|
|
1596
|
+
var viaChina = [{ longitude: 58, latitude: 22 }, { longitude: 76, latitude: 3 }, { longitude: 99, latitude: 1 }];
|
|
1597
|
+
var viaChinaNorth = [{ longitude: 58, latitude: 22 }, { longitude: 76, latitude: 3 }, { longitude: 99, latitude: 1 }, { longitude: 124, latitude: 32 }];
|
|
1598
|
+
var viaIndiaNear = [wpHormuz];
|
|
1599
|
+
var viaIndiaFar = [wpHormuz, wpArabianSea];
|
|
1600
|
+
var viaSingapore = [wpHormuz, wpSouthIndia];
|
|
1601
|
+
var viaSuez = [wpHormuz, wpAden, wpSuez];
|
|
1602
|
+
var wpSouthGreece = { longitude: 22, latitude: 35 };
|
|
1603
|
+
var wpOtranto = { longitude: 18.5, latitude: 40 };
|
|
1604
|
+
var viaSuezItaly = [wpHormuz, wpAden, wpSuez, wpSouthGreece, wpOtranto];
|
|
1605
|
+
var wpSicily = { longitude: 13, latitude: 37 };
|
|
1606
|
+
var wpPortugal = { longitude: -10, latitude: 39 };
|
|
1607
|
+
var viaAtlantic = [wpHormuz, wpAden, wpSuez, wpSicily, wpPortugal];
|
|
1608
|
+
|
|
1609
|
+
// No datavalidated wrapper needed — explicit coordinates resolve immediately
|
|
1117
1610
|
sankeySeries.data.setAll([
|
|
1118
|
-
//
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1611
|
+
// Ras Tanura (Saudi Arabia)
|
|
1612
|
+
flow(rasTanura, "Ras Tanura", ningbo, "Ningbo", 1100, viaChina),
|
|
1613
|
+
flow(rasTanura, "Ras Tanura", qingdao, "Qingdao", 600, viaChinaNorth),
|
|
1614
|
+
flow(rasTanura, "Ras Tanura", yokohama, "Yokohama", 700, viaJapan),
|
|
1615
|
+
flow(rasTanura, "Ras Tanura", chiba, "Chiba", 300, viaJapan),
|
|
1616
|
+
flow(rasTanura, "Ras Tanura", ulsan, "Ulsan", 800, viaKorea),
|
|
1617
|
+
flow(rasTanura, "Ras Tanura", jamnagar, "Jamnagar", 500, viaIndiaNear),
|
|
1618
|
+
flow(rasTanura, "Ras Tanura", mumbai, "Mumbai", 300, viaIndiaFar),
|
|
1619
|
+
flow(rasTanura, "Ras Tanura", rotterdam, "Rotterdam", 350, viaAtlantic),
|
|
1620
|
+
// Basra Oil Terminal (Iraq)
|
|
1621
|
+
flow(basra, "Basra", ningbo, "Ningbo", 700, viaChina),
|
|
1622
|
+
flow(basra, "Basra", qingdao, "Qingdao", 400, viaChinaNorth),
|
|
1623
|
+
flow(basra, "Basra", jamnagar, "Jamnagar", 600, viaIndiaNear),
|
|
1624
|
+
flow(basra, "Basra", mumbai, "Mumbai", 400, viaIndiaFar),
|
|
1625
|
+
flow(basra, "Basra", ulsan, "Ulsan", 400, viaKorea),
|
|
1626
|
+
flow(basra, "Basra", trieste, "Trieste", 350, viaSuezItaly),
|
|
1627
|
+
flow(basra, "Basra", piraeus, "Piraeus", 250, viaSuez),
|
|
1628
|
+
// Fujairah (UAE) — already outside Hormuz
|
|
1629
|
+
flow(fujairah, "Fujairah", yokohama, "Yokohama", 400, viaJapan.slice(1)),
|
|
1630
|
+
flow(fujairah, "Fujairah", chiba, "Chiba", 200, viaJapan.slice(1)),
|
|
1631
|
+
flow(fujairah, "Fujairah", jamnagar, "Jamnagar", 300),
|
|
1632
|
+
flow(fujairah, "Fujairah", mumbai, "Mumbai", 200, [wpArabianSea]),
|
|
1633
|
+
flow(fujairah, "Fujairah", ningbo, "Ningbo", 250, viaChina.slice(1)),
|
|
1634
|
+
flow(fujairah, "Fujairah", ulsan, "Ulsan", 350, viaKorea.slice(1)),
|
|
1635
|
+
flow(fujairah, "Fujairah", singapore, "Singapore", 250, [wpSouthIndia]),
|
|
1636
|
+
// Mina Al Ahmadi (Kuwait)
|
|
1637
|
+
flow(minaAhmadi, "Mina Al Ahmadi", ulsan, "Ulsan", 400, viaKorea),
|
|
1638
|
+
flow(minaAhmadi, "Mina Al Ahmadi", yokohama, "Yokohama", 300, viaJapan),
|
|
1639
|
+
flow(minaAhmadi, "Mina Al Ahmadi", ningbo, "Ningbo", 300, viaChina),
|
|
1640
|
+
flow(minaAhmadi, "Mina Al Ahmadi", jamnagar, "Jamnagar", 200, viaIndiaNear),
|
|
1641
|
+
// Kharg Island (Iran)
|
|
1642
|
+
flow(khargIsland, "Kharg Island", ningbo, "Ningbo", 400, viaChina),
|
|
1643
|
+
flow(khargIsland, "Kharg Island", qingdao, "Qingdao", 200, viaChinaNorth),
|
|
1644
|
+
flow(khargIsland, "Kharg Island", jamnagar, "Jamnagar", 200, viaIndiaNear),
|
|
1645
|
+
flow(khargIsland, "Kharg Island", piraeus, "Piraeus", 100, viaSuez),
|
|
1646
|
+
// Ras Laffan (Qatar)
|
|
1647
|
+
flow(rasLaffan, "Ras Laffan", yokohama, "Yokohama", 300, viaJapan),
|
|
1648
|
+
flow(rasLaffan, "Ras Laffan", ulsan, "Ulsan", 200, viaKorea),
|
|
1649
|
+
flow(rasLaffan, "Ras Laffan", jamnagar, "Jamnagar", 150, viaIndiaNear),
|
|
1650
|
+
flow(rasLaffan, "Ras Laffan", singapore,"Singapore", 120, viaSingapore)
|
|
1130
1651
|
]);
|
|
1131
1652
|
|
|
1132
|
-
|
|
1653
|
+
// Title
|
|
1654
|
+
var titleCont = chart.children.push(am5.Container.new(root, {
|
|
1655
|
+
layout: root.verticalLayout,
|
|
1656
|
+
x: am5.p50, centerX: am5.p50, y: 0,
|
|
1657
|
+
position: "absolute", paddingTop: 16
|
|
1658
|
+
}));
|
|
1659
|
+
titleCont.children.push(am5.Label.new(root, {
|
|
1660
|
+
text: "Strait of Hormuz — Oil Export Flows",
|
|
1661
|
+
fontSize: 18, fontWeight: "600", fill: amber,
|
|
1662
|
+
x: am5.p50, centerX: am5.p50
|
|
1663
|
+
}));
|
|
1664
|
+
titleCont.children.push(am5.Label.new(root, {
|
|
1665
|
+
text: "Gulf Producers > Major Importers (thousands of barrels/day)",
|
|
1666
|
+
fontSize: 11, fill: sulfur,
|
|
1667
|
+
x: am5.p50, centerX: am5.p50
|
|
1668
|
+
}));
|
|
1669
|
+
|
|
1670
|
+
// Globe / Map toggle
|
|
1671
|
+
var switchCont = chart.children.push(am5.Container.new(root, {
|
|
1672
|
+
layout: root.horizontalLayout, x: 20, y: 40
|
|
1673
|
+
}));
|
|
1674
|
+
switchCont.children.push(am5.Label.new(root, {
|
|
1675
|
+
centerY: am5.p50, text: "Globe", fill: flare, fontSize: 13
|
|
1676
|
+
}));
|
|
1677
|
+
var switchButton = switchCont.children.push(am5.Button.new(root, {
|
|
1678
|
+
themeTags: ["switch"], centerY: am5.p50,
|
|
1679
|
+
icon: am5.Circle.new(root, { themeTags: ["icon"] })
|
|
1680
|
+
}));
|
|
1681
|
+
|
|
1682
|
+
var easing = am5.ease.inOut(am5.ease.cubic);
|
|
1683
|
+
var duration = 1500;
|
|
1684
|
+
var fadeDuration = 300;
|
|
1685
|
+
|
|
1686
|
+
function zoomToGlobe() {
|
|
1687
|
+
chart.set("projection", am5map.geoOrthographic());
|
|
1688
|
+
chart.set("panX", "rotateX");
|
|
1689
|
+
chart.set("panY", "rotateY");
|
|
1690
|
+
chart.animate({ key: "rotationX", to: -74, duration: duration, easing: easing });
|
|
1691
|
+
chart.animate({ key: "rotationY", to: -30, duration: duration, easing: easing });
|
|
1692
|
+
chart.animate({ key: "zoomLevel", to: 1.6, duration: duration, easing: easing });
|
|
1693
|
+
bgSeries.mapPolygons.template.set("fillOpacity", 1);
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
function zoomToMap() {
|
|
1697
|
+
chart.set("projection", am5map.geoMercator());
|
|
1698
|
+
chart.set("panX", "translateX");
|
|
1699
|
+
chart.set("panY", "translateY");
|
|
1700
|
+
chart.animate({ key: "rotationY", to: 0, duration: duration, easing: easing });
|
|
1701
|
+
setTimeout(function() {
|
|
1702
|
+
chart.zoomToGeoPoint({ longitude: 67, latitude: 32 }, 3.5, true, duration);
|
|
1703
|
+
}, 100);
|
|
1704
|
+
bgSeries.mapPolygons.template.set("fillOpacity", 0);
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
switchButton.on("active", function() {
|
|
1708
|
+
chart.goHome(duration);
|
|
1709
|
+
setTimeout(function() {
|
|
1710
|
+
chart.seriesContainer.animate({ key: "opacity", to: 0, duration: fadeDuration });
|
|
1711
|
+
}, duration - fadeDuration);
|
|
1712
|
+
setTimeout(function() {
|
|
1713
|
+
if (switchButton.get("active")) { zoomToMap(); } else { zoomToGlobe(); }
|
|
1714
|
+
chart.seriesContainer.animate({ key: "opacity", to: 1, duration: fadeDuration });
|
|
1715
|
+
}, duration);
|
|
1716
|
+
});
|
|
1717
|
+
|
|
1718
|
+
switchCont.children.push(am5.Label.new(root, {
|
|
1719
|
+
centerY: am5.p50, text: "Map", fill: flare, fontSize: 13
|
|
1720
|
+
}));
|
|
1721
|
+
|
|
1722
|
+
// Zoom controls
|
|
1723
|
+
var zoomControl = chart.set("zoomControl", am5map.ZoomControl.new(root, {}));
|
|
1724
|
+
zoomControl.homeButton.set("visible", true);
|
|
1725
|
+
|
|
1133
1726
|
chart.appear(1000, 100);
|
|
1727
|
+
|
|
1728
|
+
// Initial animation: rotate to globe position
|
|
1729
|
+
setTimeout(function() { zoomToGlobe(); }, 1000);
|
|
1134
1730
|
</script>
|
|
1135
1731
|
</body>
|
|
1136
1732
|
</html>
|