@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 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` → MapSankeySeries section.
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` → global `am5geodata_{countryName}Low`
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
- Each item needs `sourceId`, `targetId`, and `value`. IDs use ISO 3166-1 alpha-2 codes matching the geodata.
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
- sankeySeries.data.setAll([
246
- { sourceId: "BR", targetId: "DE", value: 350 },
247
- { sourceId: "BR", targetId: "US", value: 450 },
248
- { sourceId: "DE", targetId: "FR", value: 150 },
249
- { sourceId: "DE", targetId: "PL", value: 100 }
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** instead of polygon IDs:
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
- sankeySeries.data.setAll([
267
- {
268
- sourceId: "GB", targetId: "JP", value: 40,
269
- waypoints: [
270
- { longitude: 30, latitude: 65 },
271
- { longitude: 90, latitude: 55 }
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} → {targetId}: {value}"
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
- sankeySeries.data.setAll([
388
- // Level 1: Producers → Hubs
389
- { sourceId: "BR", targetId: "DE", value: 350 },
390
- { sourceId: "VN", targetId: "DE", value: 200 },
391
- // Level 2: Hubs → Consumers
392
- { sourceId: "DE", targetId: "FR", value: 150 },
393
- { sourceId: "DE", targetId: "PL", value: 100 }
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→1 on each segment line sequentially, with independent timing, easing, and callbacks per segment.
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 → states)
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 — trade flows between countries
1077
+ ## Example 5: Map Sankey — Global Coffee Supply Chain (sourceId/targetId)
1055
1078
 
1056
- Demonstrates: `MapSankeySeries` with geographic flow bands, node circles, tooltips, and multi-level flows.
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 — Trade Flows</title>
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>#chartdiv { width: 100%; height: 600px; }</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
- root.setThemes([am5themes_Animated.new(root)]);
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: "translateY",
1079
- projection: am5map.geoNaturalEarth1()
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
- // Background countries
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: am5.color(0xdddddd),
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
- // Sankey flow series
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: 4,
1185
+ maxWidth: 2,
1096
1186
  controlPointDistance: 0.4,
1097
- autoSort: true
1187
+ resolution: 60,
1188
+ nodePadding: 0.3
1098
1189
  }));
1099
1190
 
1100
- // Band appearance
1101
1191
  sankeySeries.mapPolygons.template.setAll({
1102
- fill: am5.color(0xff6b35),
1103
- fillOpacity: 0.5,
1192
+ fill: mediumRoast,
1193
+ fillOpacity: 0.65,
1104
1194
  strokeOpacity: 0,
1105
- tooltipText: "{sourceId} → {targetId}: {value}B"
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: am5.color(0x8b5e3c),
1111
- fillOpacity: 0.9,
1112
- stroke: am5.color(0xffffff),
1113
- strokeWidth: 1.5
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
- // Multi-level trade data
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
- // Producers → Trade hubs
1119
- { sourceId: "BR", targetId: "DE", value: 350 },
1120
- { sourceId: "BR", targetId: "US", value: 450 },
1121
- { sourceId: "CN", targetId: "DE", value: 500 },
1122
- { sourceId: "CN", targetId: "US", value: 600 },
1123
- { sourceId: "AU", targetId: "JP", value: 200 },
1124
- { sourceId: "AU", targetId: "CN", value: 300 },
1125
- // Hubs → Consumers
1126
- { sourceId: "DE", targetId: "FR", value: 250 },
1127
- { sourceId: "DE", targetId: "GB", value: 300 },
1128
- { sourceId: "US", targetId: "CA", value: 400 },
1129
- { sourceId: "JP", targetId: "KR", value: 100 }
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
- chart.set("zoomControl", am5map.ZoomControl.new(root, {}));
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>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amcharts/amcharts5-mcp",
3
- "version": "1.1.6",
3
+ "version": "1.1.8",
4
4
  "mcpName": "io.github.amcharts/amcharts5",
5
5
  "description": "On-demand access to amCharts 5 docs, code examples, and API reference",
6
6
  "type": "module",