Lines from London that draw themselves to ten cities, over and over, all at the same speed, so the long route to New York takes longest.
Motion shows direction without arrows: lines that grow from one end tell readers which way things flow, here out of London. Each line follows an invisible guide, with two points moving along it and the visible line drawn between them. Use it sparingly; constant motion pulls the eye from everything else on the page.
Built with amCharts 5, a JavaScript library. The JavaScript includes your theme and renderer (canvas or SVG); the JSON also follows the changes you make with the controls.
npm install @amcharts/amcharts5 and @amcharts/amcharts5-geodata. See Getting started.
The same chart as a JSON config. Map Maker reads and writes this format, the library's JSON parser turns it into a chart, and the <am5-chart> element puts it on a page with no code."type" is the class to create and "settings" what you would pass to its new() in code; "properties" fill in its parts, lists such as series and axes or templates such as columns.template; "refs" holds objects used in more than one place ("#series-0" stands for the one named series-0); a string starting with "@" takes a value from another object, such as "@series.get('fill')".
A complete page. Save it as an .html file and open it in a browser.
Paste this into an AI coding assistant to build the chart from scratch. More in amCharts & AI.
// Create root element
// https://www.amcharts.com/docs/v5/getting-started/#Root_element
var root = am5.Root.new("chartdiv");
// Set themes
// https://www.amcharts.com/docs/v5/concepts/themes/
root.setThemes([
am5themes_Animated.new(root),
am5themes_Responsive.new(root)
]);
// Colors from the theme, so the map follows the theme
var colors = am5.ColorSet.new(root, {});
// Create the map chart
// https://www.amcharts.com/docs/v5/charts/map-chart/
var chart = root.container.children.push(am5map.MapChart.new(root, {
// near-black space behind the satellite picture, shown only with it
background: am5.Rectangle.new(root, {
fill: am5.color(0x101318),
fillOpacity: 0
}),
minZoomLevel: 0.5, // can zoom out to half the fitted size
// go to the home view once the map is fitted: zoomed to the lines and cities
autoHome: true,
homeGeoPoint: { longitude: -18, latitude: 43 }, // the home view is centered west of Europe...
homeZoomLevel: 3, // ...and zoomed in 3 times
panX: "translateX", // dragging moves the map sideways...
panY: "translateY", // ...and up and down
projection: am5map.geoEqualEarth() // a projection that keeps the countries' areas true
}));
// Add zoom control
// https://www.amcharts.com/docs/v5/charts/map-chart/map-pan-zoom/#Zoom_control
var zoomControl = chart.set("zoomControl", am5map.ZoomControl.new(root, {})); // + and - zoom buttons
// the home button is hidden by default
zoomControl.homeButton.set("visible", true);
// Create series for the water: a faint fill behind the countries
// https://www.amcharts.com/docs/v5/charts/map-chart/map-polygon-series/#Background_polygon
var waterSeries = chart.series.push(am5map.MapPolygonSeries.new(root, {
// the map fits the countries, not this rectangle around the whole world
affectsBounds: false
}));
waterSeries.mapPolygons.template.setAll({
fill: root.interfaceColors.get("alternativeBackground"), // the color that contrasts with the background...
fillOpacity: 0.05, // ...barely there
strokeOpacity: 0 // no outline
});
waterSeries.data.push({
geometry: am5map.getGeoRectangle(90, 180, -90, -180) // a rectangle over the whole globe
});
// Satellite view: NASA's picture of the Earth by day, under the grid lines and the countries. Hidden at first
// (visible: false): make it visible for the satellite view
// https://www.amcharts.com/docs/v5/charts/map-chart/map-raster-series/
var satelliteSeries = chart.series.push(am5map.MapRasterSeries.new(root, {
visible: false,
// the map fits the countries, not the whole picture
affectsBounds: false
}));
// Create main polygon series for countries
// https://www.amcharts.com/docs/v5/charts/map-chart/map-polygon-series/
var polygonSeries = chart.series.push(am5map.MapPolygonSeries.new(root, {
name: "Polygon Series",
geoJSON: am5geodata_worldLow // the world's countries, in low detail
}));
// grid lines every 10 degrees
var graticuleSeries = chart.series.push(am5map.GraticuleSeries.new(root, {
name: "Graticule Series"
}));
graticuleSeries.mapLines.template.setAll({
stroke: root.interfaceColors.get("alternativeBackground"), // contrasting with the background...
strokeOpacity: 0.08 // ...and very faint
});
// Create line series for trajectory lines
// https://www.amcharts.com/docs/v5/charts/map-chart/map-line-series/
// this will be invisible line (note strokeOpacity = 0) along which invisible points will animate
var lineSeries = chart.series.push(am5map.MapLineSeries.new(root, {
name: "Guide Line Series"
}));
lineSeries.mapLines.template.setAll({
stroke: root.interfaceColors.get("alternativeBackground"),
strokeOpacity: 0
});
// this will be visible line. Lines will connect animating points so they will look like animated
var animatedLineSeries = chart.series.push(am5map.MapLineSeries.new(root, {
name: "Animated Line Series"
}));
animatedLineSeries.mapLines.template.setAll({
stroke: root.interfaceColors.get("alternativeBackground"), // contrasting with the background...
strokeOpacity: 0.6 // ...slightly faded
});
// destination series
var citySeries = chart.series.push(
am5map.MapPointSeries.new(root, {
name: "City Series"
})
);
// visible city circles
citySeries.bullets.push(function() {
var circle = am5.Circle.new(root, {
radius: 5, // 5px radius
tooltipText: "{title}", // the city's name on hover
tooltipY: 0, // the tooltip points at the top of the dot
fill: colors.getIndex(10), // a theme color
stroke: root.interfaceColors.get("background"), // a ring in the background color...
strokeWidth: 2, // ...2px wide
// a black shadow, a little blurred
shadowColor: am5.color(0x000000),
shadowBlur: 4,
shadowOffsetX: 1, // shifted 1px right...
shadowOffsetY: 2, // ...and 2px down...
shadowOpacity: 0.4 // ...at 40% opacity
});
return am5.Bullet.new(root, {
sprite: circle
});
});
// invisible series which will animate along invisible lines
var animatedBulletSeries = chart.series.push(
am5map.MapPointSeries.new(root, {
name: "Animated Point Series"
})
);
animatedBulletSeries.bullets.push(function() {
var circle = am5.Circle.new(root, {
radius: 0 // zero size: the point is there but not seen
});
return am5.Bullet.new(root, {
sprite: circle
});
});
var cities = [
{
id: "london",
title: "London",
geometry: { type: "Point", coordinates: [-0.1262, 51.5002] },
},
{
id: "brussels",
title: "Brussels",
geometry: { type: "Point", coordinates: [4.3676, 50.8371] }
}, {
id: "prague",
title: "Prague",
geometry: { type: "Point", coordinates: [14.4205, 50.0878] }
}, {
id: "athens",
title: "Athens",
geometry: { type: "Point", coordinates: [23.7166, 37.9792] }
}, {
id: "reykjavik",
title: "Reykjavik",
geometry: { type: "Point", coordinates: [-21.8952, 64.1353] }
}, {
id: "dublin",
title: "Dublin",
geometry: { type: "Point", coordinates: [-6.2675, 53.3441] }
}, {
id: "oslo",
title: "Oslo",
geometry: { type: "Point", coordinates: [10.7387, 59.9138] }
}, {
id: "lisbon",
title: "Lisbon",
geometry: { type: "Point", coordinates: [-9.1355, 38.7072] }
}, {
id: "moscow",
title: "Moscow",
geometry: { type: "Point", coordinates: [37.6176, 55.7558] }
}, {
id: "belgrade",
title: "Belgrade",
geometry: { type: "Point", coordinates: [20.4781, 44.8048] }
}, {
id: "bratislava",
title: "Bratislava",
geometry: { type: "Point", coordinates: [17.1547, 48.2116] }
}, {
id: "ljubljana",
title: "Ljubljana",
geometry: { type: "Point", coordinates: [14.5060, 46.0514] }
}, {
id: "madrid",
title: "Madrid",
geometry: { type: "Point", coordinates: [-3.7033, 40.4167] }
}, {
id: "stockholm",
title: "Stockholm",
geometry: { type: "Point", coordinates: [18.0645, 59.3328] }
}, {
id: "bern",
title: "Bern",
geometry: { type: "Point", coordinates: [7.4481, 46.9480] }
}, {
id: "kyiv",
title: "Kyiv",
geometry: { type: "Point", coordinates: [30.5367, 50.4422] }
}, {
id: "paris",
title: "Paris",
geometry: { type: "Point", coordinates: [2.3510, 48.8567] }
}, {
id: "new york",
title: "New York",
geometry: { type: "Point", coordinates: [-74, 40.43] }
}];
citySeries.data.setAll(cities);
// Prepare line series data
var destinations = ["reykjavik", "lisbon", "moscow", "belgrade", "ljubljana", "madrid", "stockholm", "bern", "kyiv", "new york"];
// guide lines connect London to each destination
lineSeries.set("pointSeries", citySeries);
lineSeries.data.setAll(destinations.map(function(did) {
return { id: did, pointIds: ["london", did] }; // each guide line runs from London to the destination
}));
// a start and an end point on each guide line
var animatedPoints = [];
am5.array.each(destinations, function(did) {
animatedPoints.push({ id: did + "-start", lineId: did, positionOnLine: 0 }); // at the guide line's start...
animatedPoints.push({ id: did + "-end", lineId: did, positionOnLine: 1 }); // ...and at its end
});
animatedBulletSeries.data.setAll(animatedPoints);
// visible lines connect the start and end points
animatedLineSeries.set("pointSeries", animatedBulletSeries);
animatedLineSeries.data.setAll(destinations.map(function(did) {
return { pointIds: [did + "-start", did + "-end"] };
}));
var londonDataItem = citySeries.getDataItemById("london"); // London, where all the lines start
// this will do all the animations
am5.array.each(destinations, function(did) {
var destinationDataItem = citySeries.getDataItemById(did);
var startDataItem = animatedBulletSeries.getDataItemById(did + "-start");
var endDataItem = animatedBulletSeries.getDataItemById(did + "-end");
var lon0 = londonDataItem.get("longitude");
var lat0 = londonDataItem.get("latitude");
var lon1 = destinationDataItem.get("longitude");
var lat1 = destinationDataItem.get("latitude");
var distance = Math.hypot(lon1 - lon0, lat1 - lat0); // the line's length in degrees, roughly
// longer lines take longer, so all lines move at the same speed
var duration = distance * 100;
animateStart(startDataItem, endDataItem, duration); // start the loop
});
// a loop: the start point runs out (the line shrinks away), then the end point (it grows back)
function animateStart(startDataItem, endDataItem, duration) {
var startAnimation = startDataItem.animate({
key: "positionOnLine", // move the start point along the guide line...
from: 0, // ...from London...
to: 1, // ...to the destination
duration: duration
});
startAnimation.events.on("stopped", function() { // when it gets there, the end point runs out next
animateEnd(startDataItem, endDataItem, duration);
});
}
// the start point jumps back to London and the end point runs out, so the line grows again
function animateEnd(startDataItem, endDataItem, duration) {
startDataItem.set("positionOnLine", 0)
var endAnimation = endDataItem.animate({
key: "positionOnLine",
from: 0,
to: 1,
duration: duration
})
endAnimation.events.on("stopped", function() { // and the loop goes on
animateStart(startDataItem, endDataItem, duration);
});
}
// The image credit
var credit = chart.children.push(am5.Label.new(root, {
text: "Imagery: NASA Earth Observatory",
fontSize: 12, // small...
fill: am5.color(0xffffff), // ...white...
fillOpacity: 0.6, // ...slightly faded text
x: am5.p100, // at the right edge...
centerX: am5.p100, // ...anchored by its right end...
dx: -10, // ...10px in from it
y: 10, // 10px from the top
visible: false // shown only with the satellite picture
}));
// The countries' own look, to go back to
var landTemplate = polygonSeries.mapPolygons.template;
var landLook = {
fillOpacity: landTemplate.get("fillOpacity", 1),
stroke: landTemplate.get("stroke", root.interfaceColors.get("background")),
strokeOpacity: landTemplate.get("strokeOpacity", 1)
};
// The picture loads the first time it shows. Then the countries turn to white outlines over it, the grid lines and
// the animated lines turn white, the credit shows and the map sits in near-black space
satelliteSeries.on("visible", function(visible) {
if (visible) {
satelliteSeries.set("src", "https://cdn.amcharts.com/lib/5/geodata/images/earthDay2048.jpg");
}
credit.set("visible", visible);
chart.get("background").set("fillOpacity", visible ? 1 : 0);
landTemplate.setAll(visible ? { fillOpacity: 0, stroke: am5.color(0xffffff), strokeOpacity: 0.6 } : landLook);
graticuleSeries.mapLines.template.set("stroke", visible ? am5.color(0xffffff) : root.interfaceColors.get("alternativeBackground"));
animatedLineSeries.mapLines.template.set("stroke", visible ? am5.color(0xffffff) : root.interfaceColors.get("alternativeBackground"));
});
// Make stuff animate on load
chart.appear(1000, 100);
{
"type": "MapChart",
"settings": {
"background": {
"type": "Rectangle",
"settings": {
"fill": {
"type": "Color",
"value": "#101318"
},
"fillOpacity": 0
}
},
"minZoomLevel": 0.5,
"autoHome": true,
"homeGeoPoint": {
"longitude": -18,
"latitude": 43
},
"homeZoomLevel": 3,
"panX": "translateX",
"panY": "translateY",
"zoomControl": {
"type": "ZoomControl",
"properties": {
"homeButton": {
"settings": {
"visible": true
}
}
}
},
"projectionName": "geoEqualEarth"
},
"properties": {
"series": [
"#series-0",
"#series-1",
"#series-2",
"#series-3",
"#series-4",
"#series-5",
"#series-6",
"#series-7"
]
},
"children": [
{
"type": "Label",
"settings": {
"text": "Imagery: NASA Earth Observatory",
"fontSize": 12,
"fill": {
"type": "Color",
"value": "#ffffff"
},
"fillOpacity": 0.6,
"x": {
"type": "Percent",
"value": 100
},
"centerX": {
"type": "Percent",
"value": 100
},
"dx": -10,
"y": 10,
"visible": false
},
"index": 2
}
],
"refs": [
{
"data-0": [
{
"geometry": {
"type": "GeoRectangle",
"north": 90,
"east": 180,
"south": -90,
"west": -180
},
"__parse": true
}
]
},
{
"data-1": []
},
{
"data-2": []
},
{
"data-3": [
{
"id": "reykjavik",
"pointIds": [
"london",
"reykjavik"
],
"__parse": true
},
{
"id": "lisbon",
"pointIds": [
"london",
"lisbon"
],
"__parse": true
},
{
"id": "moscow",
"pointIds": [
"london",
"moscow"
],
"__parse": true
},
{
"id": "belgrade",
"pointIds": [
"london",
"belgrade"
],
"__parse": true
},
{
"id": "ljubljana",
"pointIds": [
"london",
"ljubljana"
],
"__parse": true
},
{
"id": "madrid",
"pointIds": [
"london",
"madrid"
],
"__parse": true
},
{
"id": "stockholm",
"pointIds": [
"london",
"stockholm"
],
"__parse": true
},
{
"id": "bern",
"pointIds": [
"london",
"bern"
],
"__parse": true
},
{
"id": "kyiv",
"pointIds": [
"london",
"kyiv"
],
"__parse": true
},
{
"id": "new york",
"pointIds": [
"london",
"new york"
],
"__parse": true
}
]
},
{
"data-4": [
{
"pointIds": [
"reykjavik-start",
"reykjavik-end"
],
"__parse": true
},
{
"pointIds": [
"lisbon-start",
"lisbon-end"
],
"__parse": true
},
{
"pointIds": [
"moscow-start",
"moscow-end"
],
"__parse": true
},
{
"pointIds": [
"belgrade-start",
"belgrade-end"
],
"__parse": true
},
{
"pointIds": [
"ljubljana-start",
"ljubljana-end"
],
"__parse": true
},
{
"pointIds": [
"madrid-start",
"madrid-end"
],
"__parse": true
},
{
"pointIds": [
"stockholm-start",
"stockholm-end"
],
"__parse": true
},
{
"pointIds": [
"bern-start",
"bern-end"
],
"__parse": true
},
{
"pointIds": [
"kyiv-start",
"kyiv-end"
],
"__parse": true
},
{
"pointIds": [
"new york-start",
"new york-end"
],
"__parse": true
}
]
},
{
"data-5": [
{
"id": "london",
"title": "London",
"geometry": {
"type": "Point",
"coordinates": [
-0.1262,
51.5002
],
"__parse": true
},
"__parse": true
},
{
"id": "brussels",
"title": "Brussels",
"geometry": {
"type": "Point",
"coordinates": [
4.3676,
50.8371
],
"__parse": true
},
"__parse": true
},
{
"id": "prague",
"title": "Prague",
"geometry": {
"type": "Point",
"coordinates": [
14.4205,
50.0878
],
"__parse": true
},
"__parse": true
},
{
"id": "athens",
"title": "Athens",
"geometry": {
"type": "Point",
"coordinates": [
23.7166,
37.9792
],
"__parse": true
},
"__parse": true
},
{
"id": "reykjavik",
"title": "Reykjavik",
"geometry": {
"type": "Point",
"coordinates": [
-21.8952,
64.1353
],
"__parse": true
},
"__parse": true
},
{
"id": "dublin",
"title": "Dublin",
"geometry": {
"type": "Point",
"coordinates": [
-6.2675,
53.3441
],
"__parse": true
},
"__parse": true
},
{
"id": "oslo",
"title": "Oslo",
"geometry": {
"type": "Point",
"coordinates": [
10.7387,
59.9138
],
"__parse": true
},
"__parse": true
},
{
"id": "lisbon",
"title": "Lisbon",
"geometry": {
"type": "Point",
"coordinates": [
-9.1355,
38.7072
],
"__parse": true
},
"__parse": true
},
{
"id": "moscow",
"title": "Moscow",
"geometry": {
"type": "Point",
"coordinates": [
37.6176,
55.7558
],
"__parse": true
},
"__parse": true
},
{
"id": "belgrade",
"title": "Belgrade",
"geometry": {
"type": "Point",
"coordinates": [
20.4781,
44.8048
],
"__parse": true
},
"__parse": true
},
{
"id": "bratislava",
"title": "Bratislava",
"geometry": {
"type": "Point",
"coordinates": [
17.1547,
48.2116
],
"__parse": true
},
"__parse": true
},
{
"id": "ljubljana",
"title": "Ljubljana",
"geometry": {
"type": "Point",
"coordinates": [
14.506,
46.0514
],
"__parse": true
},
"__parse": true
},
{
"id": "madrid",
"title": "Madrid",
"geometry": {
"type": "Point",
"coordinates": [
-3.7033,
40.4167
],
"__parse": true
},
"__parse": true
},
{
"id": "stockholm",
"title": "Stockholm",
"geometry": {
"type": "Point",
"coordinates": [
18.0645,
59.3328
],
"__parse": true
},
"__parse": true
},
{
"id": "bern",
"title": "Bern",
"geometry": {
"type": "Point",
"coordinates": [
7.4481,
46.948
],
"__parse": true
},
"__parse": true
},
{
"id": "kyiv",
"title": "Kyiv",
"geometry": {
"type": "Point",
"coordinates": [
30.5367,
50.4422
],
"__parse": true
},
"__parse": true
},
{
"id": "paris",
"title": "Paris",
"geometry": {
"type": "Point",
"coordinates": [
2.351,
48.8567
],
"__parse": true
},
"__parse": true
},
{
"id": "new york",
"title": "New York",
"geometry": {
"type": "Point",
"coordinates": [
-74,
40.43
],
"__parse": true
},
"__parse": true
}
]
},
{
"data-6": [
{
"id": "reykjavik-start",
"lineId": "reykjavik",
"positionOnLine": 0
},
{
"id": "reykjavik-end",
"lineId": "reykjavik",
"positionOnLine": 1
},
{
"id": "lisbon-start",
"lineId": "lisbon",
"positionOnLine": 0
},
{
"id": "lisbon-end",
"lineId": "lisbon",
"positionOnLine": 1
},
{
"id": "moscow-start",
"lineId": "moscow",
"positionOnLine": 0
},
{
"id": "moscow-end",
"lineId": "moscow",
"positionOnLine": 1
},
{
"id": "belgrade-start",
"lineId": "belgrade",
"positionOnLine": 0
},
{
"id": "belgrade-end",
"lineId": "belgrade",
"positionOnLine": 1
},
{
"id": "ljubljana-start",
"lineId": "ljubljana",
"positionOnLine": 0
},
{
"id": "ljubljana-end",
"lineId": "ljubljana",
"positionOnLine": 1
},
{
"id": "madrid-start",
"lineId": "madrid",
"positionOnLine": 0
},
{
"id": "madrid-end",
"lineId": "madrid",
"positionOnLine": 1
},
{
"id": "stockholm-start",
"lineId": "stockholm",
"positionOnLine": 0
},
{
"id": "stockholm-end",
"lineId": "stockholm",
"positionOnLine": 1
},
{
"id": "bern-start",
"lineId": "bern",
"positionOnLine": 0
},
{
"id": "bern-end",
"lineId": "bern",
"positionOnLine": 1
},
{
"id": "kyiv-start",
"lineId": "kyiv",
"positionOnLine": 0
},
{
"id": "kyiv-end",
"lineId": "kyiv",
"positionOnLine": 1
},
{
"id": "new york-start",
"lineId": "new york",
"positionOnLine": 0
},
{
"id": "new york-end",
"lineId": "new york",
"positionOnLine": 1
}
]
},
{
"series-0": {
"type": "MapPolygonSeries",
"settings": {
"affectsBounds": false
},
"properties": {
"data": "#data-0",
"mapPolygons": {
"properties": {
"template": {
"settings": {
"fill": "@root.interfaceColors.get('alternativeBackground')",
"fillOpacity": 0.05,
"strokeOpacity": 0
}
}
}
}
}
}
},
{
"series-1": {
"type": "MapRasterSeries",
"settings": {
"visible": false,
"affectsBounds": false
}
}
},
{
"series-2": {
"type": "MapPolygonSeries",
"settings": {
"name": "Polygon Series",
"geoJSON": "am5geodata_worldLow"
}
}
},
{
"series-3": {
"type": "GraticuleSeries",
"settings": {
"name": "Graticule Series"
},
"properties": {
"mapLines": {
"properties": {
"template": {
"settings": {
"stroke": "@root.interfaceColors.get('alternativeBackground')",
"strokeOpacity": 0.08
}
}
}
}
}
}
},
{
"series-6": {
"type": "MapPointSeries",
"settings": {
"name": "City Series"
},
"properties": {
"data": "#data-5",
"bullets": [
{
"type": "Bullet",
"settings": {
"sprite": {
"type": "Circle",
"settings": {
"radius": 5,
"tooltipText": "{title}",
"tooltipY": 0,
"fill": {
"type": "Color",
"value": "#dc8c67"
},
"stroke": "@root.interfaceColors.get('background')",
"strokeWidth": 2,
"shadowColor": {
"type": "Color",
"value": "#000000"
},
"shadowBlur": 4,
"shadowOffsetX": 1,
"shadowOffsetY": 2,
"shadowOpacity": 0.4
}
}
}
}
]
}
}
},
{
"series-4": {
"type": "MapLineSeries",
"settings": {
"name": "Guide Line Series",
"pointSeries": "#series-6"
},
"properties": {
"data": "#data-3",
"mapLines": {
"properties": {
"template": {
"settings": {
"stroke": "@root.interfaceColors.get('alternativeBackground')",
"strokeOpacity": 0
}
}
}
}
}
}
},
{
"series-7": {
"type": "MapPointSeries",
"settings": {
"name": "Animated Point Series"
},
"properties": {
"data": "#data-6",
"bullets": [
{
"type": "Bullet",
"settings": {
"sprite": {
"type": "Circle",
"settings": {
"radius": 0
}
}
}
}
]
}
}
},
{
"series-5": {
"type": "MapLineSeries",
"settings": {
"name": "Animated Line Series",
"pointSeries": "#series-7"
},
"properties": {
"data": "#data-4",
"mapLines": {
"properties": {
"template": {
"settings": {
"stroke": "@root.interfaceColors.get('alternativeBackground')",
"strokeOpacity": 0.6
}
}
}
}
}
}
}
]
}
