# Map Charts — Geographic Visualizations ## Required imports ### ES modules ```ts import * as am5 from "@amcharts/amcharts5"; import * as am5map from "@amcharts/amcharts5/map"; import am5themes_Animated from "@amcharts/amcharts5/themes/Animated"; import am5geodata_worldLow from "@amcharts/amcharts5-geodata/worldLow"; ``` ### CDN ```html ``` ## Geodata (map files) NPM: `@amcharts/amcharts5-geodata` CDN: `https://cdn.amcharts.com/lib/5/geodata/{mapName}.js` | Map | ES import | CDN global | |-----|----------|-----------| | World (low detail) | `@amcharts/amcharts5-geodata/worldLow` | `am5geodata_worldLow` | | World (high detail) | `@amcharts/amcharts5-geodata/worldHigh` | `am5geodata_worldHigh` | | USA (low) | `@amcharts/amcharts5-geodata/usaLow` | `am5geodata_usaLow` | | USA (high) | `@amcharts/amcharts5-geodata/usaHigh` | `am5geodata_usaHigh` | | France (low) | `@amcharts/amcharts5-geodata/franceLow` | `am5geodata_franceLow` | | Continents | `@amcharts/amcharts5-geodata/continentsLow` | `am5geodata_continentsLow` | Pattern: camelCase country name + detail level. E.g. `germanyLow`, `canadaHigh`, `chinaLow`. **IMPORTANT — regional/continent geodata:** - Individual country maps: `geodata/{countryName}Low.js` > global `am5geodata_{countryName}Low` - **There are NO continent-level maps** like `europeLow.js` or `asiaLow.js` at the top-level CDN path - Regional maps are in subdirectories: `geodata/region/world/{regionName}Low.js` - CDN: `https://cdn.amcharts.com/lib/5/geodata/region/world/europeLow.js` - Global: `am5geodata_region_world_europeLow` - For a map of European countries, use `worldLow.js` and filter with `include: ["DE", "FR", "IT", ...]` - US congressional districts: `geodata/usaCongressionalLow.js` (may not be available on all CDN versions) ## Core setup ```js const root = am5.Root.new("chartdiv"); root.setThemes([am5themes_Animated.new(root)]); const chart = root.container.children.push( am5map.MapChart.new(root, { panX: "rotateX", // "translateX", "rotateX", "none" panY: "translateY", // "translateY", "rotateY", "none" projection: am5map.geoMercator(), // homeGeoPoint: { latitude: 48.8566, longitude: 2.3522 }, // homeZoomLevel: 5 }) ); // Polygon series (countries/regions) const polygonSeries = chart.series.push( am5map.MapPolygonSeries.new(root, { geoJSON: am5geodata_worldLow // exclude: ["AQ"] // exclude Antarctica (only if needed) // include: ["US", "CA", "MX"] // show only these }) ); chart.appear(1000, 100); ``` ## Available projections ```js am5map.geoMercator() // most common, rectangular am5map.geoNaturalEarth1() // rounded, good for world maps am5map.geoAlbersUsa() // USA with Alaska/Hawaii repositioned am5map.geoOrthographic() // globe am5map.geoEqualEarth() // equal-area am5map.geoEquirectangular() // simple lat/long grid ``` **IMPORTANT — Antarctica (`exclude: ["AQ"]`):** - Do NOT exclude Antarctica by default. Many amCharts demos exclude it because they use Mercator projection where Antarctica appears disproportionately large, but this is a demo-specific choice, not a best practice. - Only exclude Antarctica when the user explicitly asks for it, or when referencing a specific demo that excludes it. - With non-Mercator projections (`geoNaturalEarth1`, `geoEqualEarth`, `geoOrthographic`, `geoEquirectangular`) Antarctica renders at a reasonable size and should be included. ## Series types ### MapPolygonSeries (countries, regions) ```js const polygonSeries = chart.series.push( am5map.MapPolygonSeries.new(root, { geoJSON: am5geodata_worldLow, // exclude: ["AQ"], // only if user requests Antarctica exclusion valueField: "value", // for choropleth coloring calculateAggregates: true // needed for heat rules }) ); // Polygon appearance polygonSeries.mapPolygons.template.setAll({ tooltipText: "{name}", interactive: true, fill: am5.color(0xdadada), strokeWidth: 0.5 }); // Hover state polygonSeries.mapPolygons.template.states.create("hover", { fill: am5.color(0x297373) }); // Supplement geodata with custom values polygonSeries.data.setAll([ { id: "US", name: "United States", value: 331 }, { id: "CN", name: "China", value: 1411 }, { id: "IN", name: "India", value: 1380 } ]); ``` ### Choropleth (heat map) via heat rules ```js polygonSeries.set("heatRules", [{ target: polygonSeries.mapPolygons.template, dataField: "value", min: am5.color(0xf7e9b9), max: am5.color(0x5a1d6e), key: "fill" }]); // Heat legend const heatLegend = chart.children.push(am5.HeatLegend.new(root, { orientation: "horizontal", startColor: am5.color(0xf7e9b9), endColor: am5.color(0x5a1d6e), startText: "Low", endText: "High", stepCount: 5 })); ``` ### MapPointSeries (markers, bubbles, labels) ```js const pointSeries = chart.series.push( am5map.MapPointSeries.new(root, { latitudeField: "latitude", longitudeField: "longitude" }) ); // Define bullet appearance pointSeries.bullets.push(function() { return am5.Bullet.new(root, { sprite: am5.Circle.new(root, { radius: 5, fill: am5.color(0xff0000), tooltipText: "{name}" }) }); }); // Add points via data — requires latitudeField/longitudeField on the series pointSeries.data.setAll([ { latitude: 48.8566, longitude: 2.3522, name: "Paris" }, { latitude: 40.7128, longitude: -74.0060, name: "New York" }, { latitude: 35.6762, longitude: 139.6503, name: "Tokyo" } ]); // OR: place points at polygon centers using polygonIdField const pointSeries2 = chart.series.push( am5map.MapPointSeries.new(root, { polygonIdField: "polygonId" }) ); pointSeries2.data.setAll([ { polygonId: "US", name: "USA" }, { polygonId: "FR", name: "France" } ]); ``` ### MapLineSeries (connections, routes) ```js const lineSeries = chart.series.push( am5map.MapLineSeries.new(root, {}) ); lineSeries.mapLines.template.setAll({ stroke: am5.color(0xff0000), strokeWidth: 2, strokeOpacity: 0.5 }); // Using point data items for connections const pointSeries = chart.series.push(am5map.MapPointSeries.new(root, {})); const nyc = pointSeries.pushDataItem({ latitude: 40.71, longitude: -74.01 }); const london = pointSeries.pushDataItem({ latitude: 51.47, longitude: -0.45 }); lineSeries.pushDataItem({ pointsToConnect: [nyc, london] }); // OR using GeoJSON geometry lineSeries.pushDataItem({ geometry: { type: "LineString", coordinates: [[-74.01, 40.71], [-0.45, 51.47]] // [lng, lat] pairs } }); ``` ### MapSankeySeries (geographic flow bands) Draws variable-width curved bands between geographic points on a map. Band thickness represents a numeric value — like a Sankey diagram projected onto a map. Extends `MapPolygonSeries` and generates actual GeoJSON polygon geometries, so bands follow the map projection during pan, zoom, and rotation. **Requires** a `MapPolygonSeries` to resolve country IDs to geographic centroids. ```js // Create polygon series for countries (required for ID resolution) var polygonSeries = chart.series.push( am5map.MapPolygonSeries.new(root, { geoJSON: am5geodata_worldLow }) ); // Create Sankey flow series var sankeySeries = chart.series.push( am5map.MapSankeySeries.new(root, { polygonSeries: polygonSeries, maxWidth: 5 // max band width in geographic degrees }) ); ``` #### Data format Two data patterns — polygon IDs or explicit coordinates. **Pattern 1: `sourceId` / `targetId` (country codes)** IDs use ISO 3166-1 alpha-2 codes matching the geodata. Set data directly — `MapSankeySeries` auto-resolves `sourceId`/`targetId` once the referenced `polygonSeries` finishes loading, so data can be set at any time: ```js sankeySeries.data.setAll([ { sourceId: "BR", targetId: "DE", value: 350 }, { sourceId: "BR", targetId: "US", value: 450 }, { sourceId: "DE", targetId: "FR", value: 150 }, { sourceId: "DE", targetId: "PL", value: 100 } ]); ``` **Pattern 2: Explicit coordinates** When using `sourceLongitude`/`sourceLatitude`/`targetLongitude`/`targetLatitude`, centroid lookup is skipped entirely: ```js sankeySeries.data.setAll([ { sourceLongitude: 2.35, sourceLatitude: 48.86, // Paris targetLongitude: -74.01, targetLatitude: 40.71, // New York value: 100 } ]); ``` **Waypoints** — route flows through intermediate points: ```js sankeySeries.data.setAll([ { sourceId: "GB", targetId: "JP", value: 40, waypoints: [ { longitude: 30, latitude: 65 }, { longitude: 90, latitude: 55 } ] } ]); ``` **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: ```js polygonSeries.events.on("datavalidated", function() { am5.array.each(polygonSeries.dataItems, function(di) { if (di.get("id") === "BR") { di.get("mapPolygon").setAll({ fill: am5.color(0x8fae7e) }); } }); }); ``` #### Key settings | Setting | Type | Default | Description | |---------|------|---------|-------------| | `polygonSeries` | MapPolygonSeries | — | Reference series for resolving country IDs | | `maxWidth` | number | `5` | Maximum band width in geographic degrees | | `controlPointDistance` | number | `0.5` | Bezier control point distance (0–0.5) | | `controlPointDistanceSource` | number | — | Control point distance at source end | | `controlPointDistanceTarget` | number | — | Control point distance at target end | | `orientation` | string | `"horizontal"` | `"horizontal"` or `"vertical"` — controls departure/arrival direction | | `resolution` | number | `50` | Sample points per bezier segment | | `autoSort` | boolean | `true` | Sort bands by target latitude to prevent overlap | | `nodeType` | string | `"circle"` | `"circle"` or `"bar"` — endpoint node shape | | `nodeWidth` | number | `1` | Bar width in degrees (bar nodeType only) | | `nodePadding` | number | `0.3` | Extra padding on node shapes in degrees | | `antimeridian` | string | `"short"` | `"short"` for shortest path, `"long"` to avoid ±180° crossing | | `sourceIdField` | string | `"sourceId"` | Data field for source polygon ID | | `targetIdField` | string | `"targetId"` | Data field for target polygon ID | | `sourceLongitudeField` | string | `"sourceLongitude"` | Data field for source longitude | | `sourceLatitudeField` | string | `"sourceLatitude"` | Data field for source latitude | | `targetLongitudeField` | string | `"targetLongitude"` | Data field for target longitude | | `targetLatitudeField` | string | `"targetLatitude"` | Data field for target latitude | | `waypointsField` | string | `"waypoints"` | Data field for waypoints array | #### Band appearance Configure flow bands via `mapPolygons.template`: ```js sankeySeries.mapPolygons.template.setAll({ fill: am5.color(0xff6b35), fillOpacity: 0.6, strokeOpacity: 0, tooltipText: "{sourceId} > {targetId}: {value}" }); ``` Per-link control point overrides in data: ```js sankeySeries.data.setAll([ { sourceId: "CN", targetId: "US", value: 300, controlPointDistance: 0.2 }, { sourceId: "CN", targetId: "DE", value: 200, controlPointDistanceSource: 0.4 } ]); ``` #### Node configuration Each unique endpoint gets a node shape. Configure via `nodes.mapPolygons.template`: ```js sankeySeries.nodes.mapPolygons.template.setAll({ fill: am5.color(0x8b5e3c), fillOpacity: 0.9, stroke: am5.color(0xffffff), strokeWidth: 1.5 }); ``` Bar-style nodes (like traditional Sankey): ```js var sankeySeries = chart.series.push( am5map.MapSankeySeries.new(root, { polygonSeries: polygonSeries, nodeType: "bar", nodeWidth: 1.5 // bar width in degrees }) ); ``` #### Animated bullets along bands ```js sankeySeries.bullets.push(function() { return am5.Bullet.new(root, { locationX: 0, autoRotate: true, sprite: am5.Circle.new(root, { radius: 3, fill: am5.color(0xffffff) }) }); }); // Animate bullets along flow paths sankeySeries.events.on("datavalidated", function() { am5.array.each(sankeySeries.dataItems, function(dataItem) { var bullets = dataItem.bullets; if (bullets) { am5.array.each(bullets, function(bullet) { bullet.animate({ key: "locationX", from: 0, to: 1, duration: 3000, easing: am5.ease.linear, loops: Infinity }); }); } }); }); ``` Bullets automatically hide on the back side of the globe (orthographic projection). #### Multi-level flows The same country can be both target and source — incoming and outgoing bands share unified stacking at intermediate nodes: ```js sankeySeries.data.setAll([ // Level 1: Producers > Hubs { sourceId: "BR", targetId: "DE", value: 350 }, { sourceId: "VN", targetId: "DE", value: 200 }, // Level 2: Hubs > Consumers { sourceId: "DE", targetId: "FR", value: 150 }, { sourceId: "DE", targetId: "PL", value: 100 } ]); ``` ### Animating bullets along lines (positionOnLine) `MapPointSeries` data items have a `positionOnLine` setting (0–1) that positions the point along a line. Combined with `lineDataItem` and `autoRotate`, this enables flight-path-style animations. ```js // Create a point that follows a line var planeSeries = chart.series.push(am5map.MapPointSeries.new(root, {})); planeSeries.bullets.push(function() { return am5.Bullet.new(root, { sprite: am5.Graphics.new(root, { svgPath: "m2,106h28l24,30h72l-44,-133h35l80,132h98c21,0 21,34 0,34l-98,0 -80,134h-35l43,-133h-71l-24,30h-28l15,-47", scale: 0.04, centerY: am5.p50, centerX: am5.p50, fill: am5.color(0xff6600) }) }); }); var plane = planeSeries.pushDataItem({ lineDataItem: lineDataItem, // a MapLineSeries data item positionOnLine: 0, autoRotate: true }); // Animate along the line plane.animate({ key: "positionOnLine", to: 1, duration: 3000, easing: am5.ease.inOut(am5.ease.cubic) }); // Track position for globe rotation or scale changes plane.on("positionOnLine", function(pos) { // Center globe on plane's current position chart.set("rotationX", -plane.get("longitude")); chart.set("rotationY", -plane.get("latitude")); }); ``` **IMPORTANT — Multi-segment lines and `positionOnLine`:** - `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. - 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. - 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. ## Zoom controls ```js chart.set("zoomControl", am5map.ZoomControl.new(root, {})); // Programmatic zoom chart.zoomToGeoPoint({ latitude: 48.86, longitude: 2.35 }, 5); // zoom level 5 // Zoom to a polygon polygonSeries.mapPolygons.template.events.on("click", function(ev) { polygonSeries.zoomToDataItem(ev.target.dataItem); }); // Home button chart.chartContainer.children.push(am5.Button.new(root, { label: am5.Label.new(root, { text: "Home" }) })).events.on("click", function() { chart.goHome(); }); ``` ## Globe (orthographic projection) ```js var chart = root.container.children.push(am5map.MapChart.new(root, { projection: am5map.geoOrthographic(), panX: "rotateX", // drag to rotate horizontally panY: "rotateY", // drag to rotate vertically rotationX: -70, // initial longitude center rotationY: -30 // initial latitude center })); // Rotate to a specific point (center it on the globe) // IMPORTANT: use NEGATIVE longitude and NEGATIVE latitude var paris = { latitude: 48.8566, longitude: 2.3522 }; chart.animate({ key: "rotationX", to: -paris.longitude, duration: 1500, easing: am5.ease.out(am5.ease.cubic) }); chart.animate({ key: "rotationY", to: -paris.latitude, duration: 1500, easing: am5.ease.out(am5.ease.cubic) }); ``` **IMPORTANT — Globe rotation direction:** To center the globe on a geographic point, set `rotationX` to **negative longitude** and `rotationY` to **negative latitude**. This is because rotation moves the globe under the camera: rotating X by -2.35 brings longitude 2.35 to the center. A common mistake is using positive coordinates, which rotates the globe in the wrong direction. ## GraticuleSeries (grid lines / ocean background) ```js // Background (ocean) var backgroundSeries = chart.series.unshift(am5map.MapPolygonSeries.new(root, {})); backgroundSeries.mapPolygons.template.setAll({ fill: am5.color(0xddeeff), stroke: am5.color(0xddeeff) }); backgroundSeries.data.push({ geometry: am5map.getGeoRectangle(90, 180, -90, -180) // full globe background }); // Grid lines (graticule) chart.series.push(am5map.GraticuleSeries.new(root, {})); ``` ## Drill-down map (country > states) ```js // World-level polygons var worldSeries = chart.series.push(am5map.MapPolygonSeries.new(root, { geoJSON: am5geodata_worldLow })); // Country-level polygons (initially hidden) var countrySeries = chart.series.push(am5map.MapPolygonSeries.new(root, {})); // Click a country to drill down worldSeries.mapPolygons.template.events.on("click", function(ev) { var country = ev.target.dataItem.dataContext.id; // ISO alpha-2 code, e.g., "US" // Load country geodata dynamically import("@amcharts/amcharts5-geodata/usaLow").then(function(geodata) { countrySeries.set("geoJSON", geodata.default); chart.zoomToDataItem(ev.target.dataItem); }); }); // Back button chart.chartContainer.children.push(am5.Button.new(root, { label: am5.Label.new(root, { text: "Back" }) })).events.on("click", function() { countrySeries.set("geoJSON", undefined); chart.goHome(); }); ``` ## Additional MapChart settings ```js am5map.MapChart.new(root, { maxZoomLevel: 32, // max zoom minZoomLevel: 1, // min zoom maxPanOut: 0.4, // how far user can pan beyond map (0-1) rotationX: 0, // center longitude rotationY: 0, // center latitude rotationZ: 0, // tilt wheelY: "zoom", // "zoom", "none" wheelSensitivity: 1, // zoom speed multiplier }) ``` **Polygon IDs** use ISO 3166-1 alpha-2 codes (e.g., `"US"`, `"CN"`, `"DE"`, `"FR"`). ## Events on map elements ```js // Polygon click polygonSeries.mapPolygons.template.events.on("click", function(ev) { var id = ev.target.dataItem.dataContext.id; // "US", "CN", etc. var name = ev.target.dataItem.dataContext.name; // "United States" console.log("Clicked:", name, id); }); // Point/marker click pointSeries.bullets.push(function(root, series, dataItem) { var circle = am5.Circle.new(root, { radius: 5, fill: am5.color(0xff0000) }); circle.events.on("click", function() { console.log("Marker:", dataItem.dataContext); }); return am5.Bullet.new(root, { sprite: circle }); }); // Map zoom — use settings watch, NOT events (there is no "zoomlevelchanged" event) chart.on("zoomLevel", function(zoomLevel) { console.log("Zoom:", zoomLevel); }); ``` ## Disposal ```js root.dispose(); ``` --- # amCharts 5 Map Charts — Working Examples ## Example 1: World choropleth map ```html World Choropleth Map
``` ## Example 2: Map with point markers (bubble map) ```html Bubble Map
``` ## Example 3: US map with connection lines ```html US Flight Routes
``` ## Example 4: Globe with animated flight path (positionOnLine + globe rotation) Demonstrates: orthographic projection, single-segment lines for per-segment animation control, `positionOnLine` animation, globe auto-rotation to follow the plane, bullet scale animation, and city expand/shrink effects. ```html Globe Flight Path
``` ## Example 5: Map Sankey — Global Coffee Supply Chain (sourceId/targetId) 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. ```html Map Sankey — Global Coffee Supply Chain
``` ## Example 6: Map Sankey — Oil Export Flows with Waypoints (explicit coordinates) 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. **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`. ```html Map Sankey — Oil Export Flows
```