Map Sankey series

Map Sankey series is used to plot Sankey-style flow bands on a map chart. It draws variable-width curved bands between geographic points (countries, cities, etc.), where the thickness of each band represents a numeric value. It supports multi-level flows, animated bullets, endpoint node shapes, and works with all map projections.

It extends MapPolygonSeries and generates actual GeoJSON polygon geometries for each flow, so they properly follow the map projection during panning, zooming, and rotation.

Adding series

Map Sankey series requires a reference MapPolygonSeries to resolve country IDs to geographic centroids. Create both series and pass the polygon series via the polygonSeries setting:

// Create polygon series for countries
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
  })
);
// Create polygon series for countries
let polygonSeries = chart.series.push(
  am5map.MapPolygonSeries.new(root, {
    geoJSON: am5geodata_worldLow
  })
);

// Create Sankey flow series
let sankeySeries = chart.series.push(
  am5map.MapSankeySeries.new(root, {
    polygonSeries: polygonSeries
  })
);

Important: The polygon series provides the geoJSON-derived data that MapSankeySeries depends on for resolving sourceId/targetId to geographic centroids. Do not call polygonSeries.data.setAll() with your own objects — this replaces the geoJSON-derived data items and breaks centroid resolution. To customize polygon appearance (e.g. highlight specific countries), use an adapter on polygonSeries.mapPolygons.template or iterate over existing data items in a datavalidated event.

Data

Each data item represents one flow between two geographic points. At minimum, it needs sourceId, targetId, and value:

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 }
]);
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 }
]);

Country IDs use ISO 3166-1 alpha-2 codes (e.g. "US", "CN", "DE"), matching the IDs in the geodata.

Data timing with sourceId/targetId

When using sourceId/targetId, the polygon series must have finished parsing its geoJSON before the sankey series can resolve country IDs to geographic centroids. If you set sankey data too early, the centroid lookup will silently fail and no flow bands will appear.

Wrap sankeySeries.data.setAll() inside a polygonSeries.events.once("datavalidated", ...) handler to ensure the geoJSON is ready:

polygonSeries.events.once("datavalidated", function() {
  sankeySeries.data.setAll([
    { sourceId: "BR", targetId: "DE", value: 350 },
    { sourceId: "BR", targetId: "US", value: 450 }
  ]);
});
polygonSeries.events.once("datavalidated", () => {
  sankeySeries.data.setAll([
    { sourceId: "BR", targetId: "DE", value: 350 },
    { sourceId: "BR", targetId: "US", value: 450 }
  ]);
});

This is only needed when using sourceId/targetId. When using explicit coordinates (sourceLongitude/sourceLatitude), data can be set at any time since no centroid lookup is required.

Using explicit coordinates

Instead of polygon IDs, you can specify exact latitude/longitude for either endpoint:

sankeySeries.data.setAll([
  {
    sourceLongitude: 2.35, sourceLatitude: 48.86,  // Paris
    targetLongitude: -74.01, targetLatitude: 40.71,    // New York
    value: 100
  }
]);
sankeySeries.data.setAll([
  {
    sourceLongitude: 2.35, sourceLatitude: 48.86,  // Paris
    targetLongitude: -74.01, targetLatitude: 40.71,    // New York
    value: 100
  }
]);

You can also mix: use sourceId for one end and explicit coordinates for the other.

Waypoints

To route a flow through intermediate geographic points (e.g., around land masses or through shipping lanes), use the waypoints array:

sankeySeries.data.setAll([
  {
    sourceId: "GB",
    targetId: "JP",
    value: 40,
    waypoints: [
      { longitude: 30, latitude: 65 },
      { longitude: 90, latitude: 55 }
    ]
  }
]);
sankeySeries.data.setAll([
  {
    sourceId: "GB",
    targetId: "JP",
    value: 40,
    waypoints: [
      { longitude: 30, latitude: 65 },
      { longitude: 90, latitude: 55 }
    ]
  }
]);

The flow will pass smoothly through each waypoint using cubic bezier curves with Catmull-Rom style tangent averaging.

Configuring bands

Appearance

Flow bands are MapPolygon elements, configured via mapPolygons.template:

sankeySeries.mapPolygons.template.setAll({
  fill: am5.color(0xff6b35),
  fillOpacity: 0.6,
  strokeOpacity: 0,
  tooltipText: "{sourceId} > {targetId}: {value}"
});
sankeySeries.mapPolygons.template.setAll({
  fill: am5.color(0xff6b35),
  fillOpacity: 0.6,
  strokeOpacity: 0,
  tooltipText: "{sourceId} > {targetId}: {value}"
});

Band width

The maxWidth setting controls the maximum band width in geographic degrees. The thickest band (highest value) will be this wide; others are proportional.

var sankeySeries = chart.series.push(
  am5map.MapSankeySeries.new(root, {
    polygonSeries: polygonSeries,
    maxWidth: 3
  })
);
let sankeySeries = chart.series.push(
  am5map.MapSankeySeries.new(root, {
    polygonSeries: polygonSeries,
    maxWidth: 3
  })
);

Curve shape

The controlPointDistance setting (0 to 0.5) controls how pronounced the S-curve is. Higher values create longer straight sections before the curve transitions. Default is 0.5.

var sankeySeries = chart.series.push(
  am5map.MapSankeySeries.new(root, {
    polygonSeries: polygonSeries,
    controlPointDistance: 0.3    // less S-curve
  })
);
let sankeySeries = chart.series.push(
  am5map.MapSankeySeries.new(root, {
    polygonSeries: polygonSeries,
    controlPointDistance: 0.3    // less S-curve
  })
);

You can also set separate values for the source and target ends:

var sankeySeries = chart.series.push(
  am5map.MapSankeySeries.new(root, {
    polygonSeries: polygonSeries,
    controlPointDistanceSource: 0.5,  // long straight departure
    controlPointDistanceTarget: 0.2     // shorter approach
  })
);
let sankeySeries = chart.series.push(
  am5map.MapSankeySeries.new(root, {
    polygonSeries: polygonSeries,
    controlPointDistanceSource: 0.5,  // long straight departure
    controlPointDistanceTarget: 0.2     // shorter approach
  })
);

These can also be overridden per individual link in the data:

sankeySeries.data.setAll([
  { sourceId: "CN", targetId: "US", value: 300, controlPointDistance: 0.2 },
  { sourceId: "CN", targetId: "DE", value: 200, controlPointDistanceSource: 0.4 }
]);
sankeySeries.data.setAll([
  { sourceId: "CN", targetId: "US", value: 300, controlPointDistance: 0.2 },
  { sourceId: "CN", targetId: "DE", value: 200, controlPointDistanceSource: 0.4 }
]);

Orientation

By default, bands use horizontal S-curves (departing and arriving east/west). The orientation setting can change this:

ValueDescription
"horizontal"Bands depart and arrive horizontally (default). Bands stack vertically.
"vertical"Bands depart and arrive vertically (north/south). Bands stack horizontally.

Sorting

When multiple bands share an endpoint, they are stacked vertically. The autoSort setting (default true) sorts them by the latitude of their target, so bands heading north appear on top and bands heading south appear on the bottom. This prevents overlapping.

var sankeySeries = chart.series.push(
  am5map.MapSankeySeries.new(root, {
    polygonSeries: polygonSeries,
    autoSort: true   // default
  })
);
let sankeySeries = chart.series.push(
  am5map.MapSankeySeries.new(root, {
    polygonSeries: polygonSeries,
    autoSort: true   // default
  })
);

Antimeridian handling

By default, bands take the shortest path, crossing the ±180° longitude line if needed (e.g., China to US goes east across the Pacific). Set antimeridian to "long" to force bands to always go the long way around:

var sankeySeries = chart.series.push(
  am5map.MapSankeySeries.new(root, {
    polygonSeries: polygonSeries,
    antimeridian: "long"   // never cross ±180°
  })
);
let sankeySeries = chart.series.push(
  am5map.MapSankeySeries.new(root, {
    polygonSeries: polygonSeries,
    antimeridian: "long"   // never cross ±180°
  })
);

This only applies to links without explicit waypoints.

Multi-level flows

MapSankeySeries supports multi-level Sankey flows, where the same country can be both a target and a source (an intermediate node). At intermediate nodes, incoming and outgoing bands share a unified stacking range so they align properly:

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 }
]);
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 }
]);

Loops are also supported - a flow can go from a downstream node back to an upstream node.

Endpoint nodes

Each unique endpoint (from or to) gets a node shape drawn on top of the bands. Nodes are configured via nodes.mapPolygons.template:

sankeySeries.nodes.mapPolygons.template.setAll({
  fill: am5.color(0x8b5e3c),
  fillOpacity: 0.9,
  stroke: am5.color(0xffffff),
  strokeWidth: 1.5
});
sankeySeries.nodes.mapPolygons.template.setAll({
  fill: am5.color(0x8b5e3c),
  fillOpacity: 0.9,
  stroke: am5.color(0xffffff),
  strokeWidth: 1.5
});

Node type

The nodeType setting controls the shape:

ValueDescription
"circle"A circle covering all flows at the endpoint (default)
"bar"A rectangular bar like in a traditional Sankey diagram. Bar orientation is perpendicular to the flow direction.
var sankeySeries = chart.series.push(
  am5map.MapSankeySeries.new(root, {
    polygonSeries: polygonSeries,
    nodeType: "bar",
    nodeWidth: 1.5   // bar width in degrees
  })
);
let sankeySeries = chart.series.push(
  am5map.MapSankeySeries.new(root, {
    polygonSeries: polygonSeries,
    nodeType: "bar",
    nodeWidth: 1.5   // bar width in degrees
  })
);

Node padding

The nodePadding setting adds extra degrees to node size, helping avoid subpixel gaps between bands and the node shape:

var sankeySeries = chart.series.push(
  am5map.MapSankeySeries.new(root, {
    polygonSeries: polygonSeries,
    nodePadding: 0.3   // default
  })
);
let sankeySeries = chart.series.push(
  am5map.MapSankeySeries.new(root, {
    polygonSeries: polygonSeries,
    nodePadding: 0.3   // default
  })
);

Bullets

You can add animated bullets that travel along the flow paths, similar to regular Sankey charts. Bullets are positioned using locationX (0 = source, 1 = target):

sankeySeries.bullets.push(function() {
  return am5.Bullet.new(root, {
    locationX: 0,
    autoRotate: true,
    sprite: am5.Circle.new(root, {
      radius: 3,
      fill: am5.color(0xffffff)
    })
  });
});
sankeySeries.bullets.push(function() {
  return am5.Bullet.new(root, {
    locationX: 0,
    autoRotate: true,
    sprite: am5.Circle.new(root, {
      radius: 3,
      fill: am5.color(0xffffff)
    })
  });
});

Animating bullets

To animate bullets along the flow paths, animate the locationX property on each bullet. Use a sankeySeries.events.on("datavalidated", ...) handler to ensure bullets exist before animating them:

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
        });
      });
    }
  });
});
sankeySeries.events.on("datavalidated", function() {
  am5.array.each(sankeySeries.dataItems, function(dataItem) {
    let 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 are automatically hidden when they are on the back side of the globe (orthographic projection), using the same visibility check as MapPointSeries.

Custom bullet shapes

You can use any sprite as a bullet, including SVG paths:

sankeySeries.bullets.push(function() {
  return am5.Bullet.new(root, {
    locationX: 0,
    autoRotate: true,
    sprite: am5.Graphics.new(root, {
      svgPath: "M-3,-2 C-3,-4 0,-5 2,-4 C4,-3 4,0 2,2 C0,4 -3,3 -3,-2 Z",
      fill: am5.color(0x3c1e0e),
      scale: 0.6,
      centerX: am5.p50,
      centerY: am5.p50
    })
  });
});
sankeySeries.bullets.push(function() {
  return am5.Bullet.new(root, {
    locationX: 0,
    autoRotate: true,
    sprite: am5.Graphics.new(root, {
      svgPath: "M-3,-2 C-3,-4 0,-5 2,-4 C4,-3 4,0 2,2 C0,4 -3,3 -3,-2 Z",
      fill: am5.color(0x3c1e0e),
      scale: 0.6,
      centerX: am5.p50,
      centerY: am5.p50
    })
  });
});

Settings summary

SettingTypeDefaultDescription
polygonSeriesMapPolygonSeriesReference series for resolving country IDs to centroids
maxWidthnumber5Maximum band width in geographic degrees
controlPointDistancenumber0.5Bezier control point distance (0–0.5)
controlPointDistanceSourcenumberControl point distance at source end
controlPointDistanceTargetnumberControl point distance at target end
orientationstring"horizontal""horizontal" or "vertical"
resolutionnumber50Sample points per bezier segment
autoSortbooleantrueSort bands by target latitude to prevent overlap
nodeTypestring"circle""circle" or "bar"
nodeWidthnumber1Bar width in degrees (only for "bar" nodeType)
nodePaddingnumber0.3Extra padding on node shapes in degrees
antimeridianstring"short""short" for shortest path crossing ±180°, "long" to avoid it
sourceIdFieldstring"sourceId"Data field name for source polygon ID
targetIdFieldstring"targetId"Data field name for target polygon ID
sourceLongitudeFieldstring"sourceLongitude"Data field name for source longitude
sourceLatitudeFieldstring"sourceLatitude"Data field name for source latitude
targetLongitudeFieldstring"targetLongitude"Data field name for target longitude
targetLatitudeFieldstring"targetLatitude"Data field name for target latitude
waypointsFieldstring"waypoints"Data field name for waypoints array

Data item properties

PropertyTypeDescription
sourceIdstringSource polygon ID (ISO alpha-2 code)
targetIdstringTarget polygon ID
sourceLongitudenumberSource longitude (if not using sourceId)
sourceLatitudenumberSource latitude (if not using sourceId)
targetLongitudenumberTarget longitude (if not using targetId)
targetLatitudenumberTarget latitude (if not using targetId)
valuenumberNumeric value controlling band thickness
waypointsarrayArray of { longitude, latitude } for routing
controlPointDistancenumberPer-link bezier control point distance
controlPointDistanceSourcenumberPer-link source end distance
controlPointDistanceTargetnumberPer-link target end distance

Full example

The following example shows a complete multi-level coffee supply chain visualization with animated bullets, a globe/map toggle, and coffee-themed styling:

See the Pen Map Sankey Chart: Global Coffee Supply Chain by amCharts (@amcharts) on CodePen.

Related class references