Flight routes drawn as lines from London to eleven cities, with the destinations as dots. Origin switches to the routes from Vilnius.
Lines between places show connections: routes, trade, migration or messages. Lines that follow the curve of the Earth, as these do, show the shortest path, which is why the route to New York bends north. Start them from one place, as here, or keep them few, or they turn into a tangle.
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. Chart 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 // clear until the satellite view turns it on
}),
minZoomLevel: 0.5, // zoom out to half the fitted size at most
// go to the home view once the map is fitted: the area of the selected origin's routes, London's to start with
autoHome: true,
homeGeoPoint: { longitude: -18, latitude: 43 }, // the home view centers here...
homeZoomLevel: 3, // ...at zoom level 3
panX: "translateX", // drag the map sideways...
panY: "translateY", // ...or up and down to move it
// hold shift and drag to zoom into a box
boxZoom: "shift",
projection: am5map.geoEqualEarth() // a world map that keeps the countries' areas true to size
})
);
// 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, {}));
// the home button is hidden by default
zoomControl.homeButton.set("visible", true);
// true while the map is drawn as a globe, which turns to the routes instead of panning
var globe = false;
// 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"), // dark on a light background, light on a dark one
fillOpacity: 0.05, // barely there
strokeOpacity: 0 // no outline
});
waterSeries.data.push({
geometry: am5map.getGeoRectangle(90, 180, -90, -180) // north, east, south and west edges: the whole world
});
// 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, {
geoJSON: am5geodata_worldLow
})
);
// latitude and longitude lines, every 10 degrees by default
var graticuleSeries = chart.series.push(am5map.GraticuleSeries.new(root, {}));
graticuleSeries.mapLines.template.setAll({
stroke: root.interfaceColors.get("alternativeBackground"), // dark on a light background, light on a dark one
strokeOpacity: 0.08 // very faint
});
// Create line series for trajectory lines
// https://www.amcharts.com/docs/v5/charts/map-chart/map-line-series/
var lineSeries = chart.series.push(am5map.MapLineSeries.new(root, {}));
lineSeries.mapLines.template.setAll({
stroke: root.interfaceColors.get("alternativeBackground"), // contrasting with the background...
strokeOpacity: 0.6 // ...a little see-through
});
// Create point series for markers
// https://www.amcharts.com/docs/v5/charts/map-chart/map-point-series/
var originSeries = chart.series.push(
am5map.MapPointSeries.new(root, { idField: "id" }) // so a city can be found by its id (getDataItemById)
);
// a clickable dot for each origin city
originSeries.bullets.push(function () {
var circle = am5.Circle.new(root, {
radius: 7, // bigger than the destination dots
tooltipText: "{title}", // the city's name on hover
cursorOverStyle: "pointer", // a hand pointer: the dot can be clicked
tooltipY: 0, // the tooltip points at the top of the dot
fill: colors.getIndex(10), // a palette color...
stroke: root.interfaceColors.get("background"), // ...with an outline in the background color...
strokeWidth: 2 // ...2px wide
});
circle.events.on("click", function (e) { // a click shows this city's routes
selectOrigin(e.target.dataItem.get("id"));
});
return am5.Bullet.new(root, {
sprite: circle
});
});
// destination series
var destinationSeries = chart.series.push(am5map.MapPointSeries.new(root, {}));
destinationSeries.bullets.push(function () {
var circle = am5.Circle.new(root, {
radius: 5, // smaller than the origin dots
tooltipText: "{title}",
tooltipY: 0,
fill: colors.getIndex(10),
stroke: root.interfaceColors.get("background"),
strokeWidth: 2
});
return am5.Bullet.new(root, {
sprite: circle
});
});
var originCities = [
{
id: "london",
title: "London",
destinations: [
"vilnius",
"reykjavik",
"lisbon",
"moscow",
"belgrade",
"ljubljana",
"madrid",
"stockholm",
"bern",
"kyiv",
"new york"
],
geometry: { type: "Point", coordinates: [-0.1262, 51.5002] },
// the area its routes cover
homeGeoPoint: { longitude: -18, latitude: 43 }, // the home view for this origin...
homeZoomLevel: 3 // ...and how far to zoom in
},
{
id: "vilnius",
title: "Vilnius",
destinations: [
"london",
"brussels",
"prague",
"athens",
"dublin",
"oslo",
"moscow",
"bratislava",
"belgrade",
"madrid"
],
geometry: { type: "Point", coordinates: [25.2799, 54.6896] },
homeGeoPoint: { longitude: 15, latitude: 49 },
homeZoomLevel: 5.5
}
];
var destinationCities = [
{
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.506, 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.948] }
},
{
id: "kyiv",
title: "Kyiv",
geometry: { type: "Point", coordinates: [30.5367, 50.4422] }
},
{
id: "new york",
title: "New York",
geometry: { type: "Point", coordinates: [-74, 40.43] }
}
];
originSeries.data.setAll(originCities);
destinationSeries.data.setAll(destinationCities);
// The selected origin's routes give the home view; a globe turns to face them and zooms in half as far
function setHome() {
var area = originSeries.getDataItemById(currentId).dataContext;
if (globe) {
chart.set("rotationX", -area.homeGeoPoint.longitude);
chart.set("rotationY", -area.homeGeoPoint.latitude);
}
chart.set("homeGeoPoint", area.homeGeoPoint);
chart.set("homeZoomLevel", globe ? area.homeZoomLevel / 2 : area.homeZoomLevel);
}
// Show the routes from an origin, "london" or "vilnius", and go to their area; a click on an origin calls it
function selectOrigin(id, first) {
currentId = id;
var dataItem = originSeries.getDataItemById(id);
var dataContext = dataItem.dataContext;
setHome();
if (!first) {
chart.goHome(); // a picked origin flies to its area; the first view comes from autoHome
}
var destinations = dataContext.destinations;
var lineSeriesData = [];
var originLongitude = dataItem.get("longitude");
var originLatitude = dataItem.get("latitude");
am5.array.each(destinations, function (did) {
var destinationDataItem = destinationSeries.getDataItemById(did);
// a destination can also be the other origin city
if (!destinationDataItem) {
destinationDataItem = originSeries.getDataItemById(did);
}
lineSeriesData.push({
geometry: { // a line from the origin to the destination
type: "LineString",
coordinates: [
[originLongitude, originLatitude],
[
destinationDataItem.get("longitude"),
destinationDataItem.get("latitude")
]
]
}
});
});
lineSeries.data.setAll(lineSeriesData);
}
var currentId = "london";
// draw the first origin's routes once all the cities have their positions
destinationSeries.events.on("datavalidated", function () {
selectOrigin(currentId, true);
});
// The image credit
var credit = chart.children.push(am5.Label.new(root, {
text: "Imagery: NASA Earth Observatory",
fontSize: 12, // small text...
fill: am5.color(0xffffff), // ...in white, over the dark satellite picture...
fillOpacity: 0.6, // ...a little see-through
x: am5.p100, // at the chart's right edge...
centerX: am5.p100, // ...aligned by its own right edge
dx: -10, // 10px in from the edge
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 routes 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"));
// white routes show over the dark picture
lineSeries.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",
"boxZoom": "shift",
"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"
]
},
"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": [
{
"geometry": {
"type": "LineString",
"coordinates": [
[
-0.1262,
51.5002
],
[
25.2799,
54.6896
]
],
"__parse": true
},
"__parse": true
},
{
"geometry": {
"type": "LineString",
"coordinates": [
[
-0.1262,
51.5002
],
[
-21.8952,
64.1353
]
],
"__parse": true
},
"__parse": true
},
{
"geometry": {
"type": "LineString",
"coordinates": [
[
-0.1262,
51.5002
],
[
-9.1355,
38.7072
]
],
"__parse": true
},
"__parse": true
},
{
"geometry": {
"type": "LineString",
"coordinates": [
[
-0.1262,
51.5002
],
[
37.6176,
55.7558
]
],
"__parse": true
},
"__parse": true
},
{
"geometry": {
"type": "LineString",
"coordinates": [
[
-0.1262,
51.5002
],
[
20.4781,
44.8048
]
],
"__parse": true
},
"__parse": true
},
{
"geometry": {
"type": "LineString",
"coordinates": [
[
-0.1262,
51.5002
],
[
14.506,
46.0514
]
],
"__parse": true
},
"__parse": true
},
{
"geometry": {
"type": "LineString",
"coordinates": [
[
-0.1262,
51.5002
],
[
-3.7033,
40.4167
]
],
"__parse": true
},
"__parse": true
},
{
"geometry": {
"type": "LineString",
"coordinates": [
[
-0.1262,
51.5002
],
[
18.0645,
59.3328
]
],
"__parse": true
},
"__parse": true
},
{
"geometry": {
"type": "LineString",
"coordinates": [
[
-0.1262,
51.5002
],
[
7.4481,
46.948
]
],
"__parse": true
},
"__parse": true
},
{
"geometry": {
"type": "LineString",
"coordinates": [
[
-0.1262,
51.5002
],
[
30.5367,
50.4422
]
],
"__parse": true
},
"__parse": true
},
{
"geometry": {
"type": "LineString",
"coordinates": [
[
-0.1262,
51.5002
],
[
-74,
40.43
]
],
"__parse": true
},
"__parse": true
}
]
},
{
"data-4": [
{
"id": "london",
"title": "London",
"destinations": [
"vilnius",
"reykjavik",
"lisbon",
"moscow",
"belgrade",
"ljubljana",
"madrid",
"stockholm",
"bern",
"kyiv",
"new york"
],
"geometry": {
"type": "Point",
"coordinates": [
-0.1262,
51.5002
],
"__parse": true
},
"homeGeoPoint": {
"longitude": -18,
"latitude": 43
},
"homeZoomLevel": 3,
"__parse": true
},
{
"id": "vilnius",
"title": "Vilnius",
"destinations": [
"london",
"brussels",
"prague",
"athens",
"dublin",
"oslo",
"moscow",
"bratislava",
"belgrade",
"madrid"
],
"geometry": {
"type": "Point",
"coordinates": [
25.2799,
54.6896
],
"__parse": true
},
"homeGeoPoint": {
"longitude": 15,
"latitude": 49
},
"homeZoomLevel": 5.5,
"__parse": true
}
]
},
{
"data-5": [
{
"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": "new york",
"title": "New York",
"geometry": {
"type": "Point",
"coordinates": [
-74,
40.43
],
"__parse": true
},
"__parse": true
}
]
},
{
"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": {
"geoJSON": "am5geodata_worldLow"
}
}
},
{
"series-3": {
"type": "GraticuleSeries",
"properties": {
"mapLines": {
"properties": {
"template": {
"settings": {
"stroke": "@root.interfaceColors.get('alternativeBackground')",
"strokeOpacity": 0.08
}
}
}
}
}
}
},
{
"series-4": {
"type": "MapLineSeries",
"properties": {
"data": "#data-3",
"mapLines": {
"properties": {
"template": {
"settings": {
"stroke": "@root.interfaceColors.get('alternativeBackground')",
"strokeOpacity": 0.6
}
}
}
}
}
}
},
{
"series-5": {
"type": "MapPointSeries",
"settings": {
"idField": "id"
},
"properties": {
"data": "#data-4",
"bullets": [
{
"type": "Bullet",
"settings": {
"sprite": {
"type": "Circle",
"settings": {
"radius": 7,
"tooltipText": "{title}",
"cursorOverStyle": "pointer",
"tooltipY": 0,
"fill": {
"type": "Color",
"value": "#dc8c67"
},
"stroke": "@root.interfaceColors.get('background')",
"strokeWidth": 2
}
}
}
}
]
}
}
},
{
"series-6": {
"type": "MapPointSeries",
"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
}
}
}
}
]
}
}
}
]
}
