A plane flies back and forth along a route from Paris to Toronto, Los Angeles and Havana. Drag a city and the route and the plane follow it.
Any point on a map can sit on a line and move along it, turning with the line as it goes. That makes a route easy to follow: the eye tracks the plane instead of reading a still path. The route takes the shortest way over the globe, which is why it curves on a flat map.
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 // invisible 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: zoomed to the route
autoHome: true,
homeGeoPoint: { longitude: -58, latitude: 37 }, // the home view centers on the North Atlantic...
homeZoomLevel: 2.6, // ...zoomed in 2.6 times
// dragging sideways turns the world around, dragging up and down moves it
panX: "rotateX",
panY: "translateY",
projection: am5map.geoEqualEarth() // the Equal Earth projection
}));
// 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; it brings back the view of the route
zoomControl.homeButton.set("visible", true);
// Create series for background fill
// https://www.amcharts.com/docs/v5/charts/map-chart/map-polygon-series/#Background_polygon
var backgroundSeries = chart.series.push(am5map.MapPolygonSeries.new(root, {
name: "Background Series",
// the map fits the countries, not this rectangle around the whole world
affectsBounds: false
}));
backgroundSeries.mapPolygons.template.setAll({
fill: root.interfaceColors.get("alternativeBackground"), // a color that contrasts with the background
fillOpacity: 0, // transparent oceans; raise it to tint them
strokeOpacity: 0 // no outline
});
// Add background polygon
// https://www.amcharts.com/docs/v5/charts/map-chart/map-polygon-series/#Background_polygon
backgroundSeries.data.push({
geometry: am5map.getGeoRectangle(90, 180, -90, -180) // a rectangle around 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 graticule series: grid lines every 10 degrees
// https://www.amcharts.com/docs/v5/charts/map-chart/graticule-series/
var graticuleSeries = chart.series.push(am5map.GraticuleSeries.new(root, {
step: 10
}));
graticuleSeries.mapLines.template.setAll({
stroke: root.interfaceColors.get("alternativeBackground"), // a color that contrasts with the background...
strokeOpacity: 0.08 // ...very faint
});
// 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 // low-detail world countries
}));
// 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, {
name: "Line Series"
}));
lineSeries.mapLines.template.setAll({
stroke: root.interfaceColors.get("alternativeBackground"), // a color that contrasts with the background...
strokeOpacity: 0.3 // ...for a faint route line
});
// Create point series for markers
// https://www.amcharts.com/docs/v5/charts/map-chart/map-point-series/
var pointSeries = chart.series.push(am5map.MapPointSeries.new(root, {
name: "City Series"
}));
pointSeries.bullets.push(function() {
var circle = am5.Circle.new(root, {
radius: 7, // a 7px dot for each city
tooltipText: "{title}", // the city's name
cursorOverStyle: "pointer", // a hand pointer: the cities can be dragged
tooltipY: 0, // the tooltip points at the dot's center
fill: colors.getIndex(10),
stroke: root.interfaceColors.get("background"), // an outline in the background color...
strokeWidth: 2, // ...2px wide
draggable: true, // drag a city to move it
// a black shadow, a little blurred
shadowColor: am5.color(0x000000),
shadowBlur: 4,
shadowOffsetX: 1,
shadowOffsetY: 2,
shadowOpacity: 0.4
});
// a dragged city takes the coordinates under it as it moves, so the route and the plane follow
circle.events.on("dragged", function(event) {
var dataItem = event.target.dataItem;
var geoPoint = chart.invert({ x: circle.x(), y: circle.y() }); // the map coordinates under the dot
dataItem.setAll({
longitude: geoPoint.longitude,
latitude: geoPoint.latitude
});
});
return am5.Bullet.new(root, {
sprite: circle
});
});
pointSeries.data.setAll([
{ id: "paris", title: "Paris", latitude: 48.8567, longitude: 2.351 },
{ id: "toronto", title: "Toronto", latitude: 43.8163, longitude: -79.4287 },
{ id: "la", title: "Los Angeles", latitude: 34.3, longitude: -118.15 },
{ id: "havana", title: "Havana", latitude: 23, longitude: -82 }
]);
// the route connects the cities by their ids
lineSeries.set("pointSeries", pointSeries);
lineSeries.data.setAll([
{ id: "route", pointIds: ["paris", "toronto", "la", "havana"] }
]);
// The plane's color, in a template so the satellite view can change it
var planeTemplate = am5.Template.new({
fill: root.interfaceColors.get("text") // in the text color, so it shows in light and dark mode
});
// a series with a single point: the plane on the route
var planeSeries = chart.series.push(am5map.MapPointSeries.new(root, {
name: "Plane Series"
}));
planeSeries.bullets.push(function(root, series, dataItem) {
var container = am5.Container.new(root, {});
var plane = container.children.push(am5.Graphics.new(root, {
// the plane's outline, as an SVG path
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.06, // the path is drawn big; shrink it to 6%
centerY: am5.p50, // centered on its point on the route
centerX: am5.p50,
// a soft black shadow on the ground, in light and dark mode
shadowColor: am5.color(0x000000),
shadowBlur: 10,
shadowOffsetX: 4,
shadowOffsetY: 6,
shadowOpacity: 0.5
}, planeTemplate));
// The plane turns with the route; flip it around when it heads back.
// The listener goes when the plane does.
var prevPosition;
plane.addDisposer(dataItem.on("positionOnLine", function(value) {
if (prevPosition < value) {
plane.set("rotation", 0); // heading forward
}
if (prevPosition > value) {
plane.set("rotation", -180); // turned around
}
prevPosition = value;
}));
return am5.Bullet.new(root, { sprite: container });
});
// the plane sits on the start of the route
planeSeries.data.setAll([
// autoRotate turns the plane along the line
{ id: "plane", lineId: "route", positionOnLine: 0, autoRotate: true }
]);
// Fly back and forth along the route
planeSeries.getDataItemById("plane").animate({
key: "positionOnLine",
to: 1, // to the end of the route...
duration: 10000, // ...and back, in 10 seconds
loops: Infinity, // over and over
easing: am5.ease.yoyo(am5.ease.linear) // out and back at a steady speed
});
// The image credit
var credit = chart.children.push(am5.Label.new(root, {
text: "Imagery: NASA Earth Observatory",
fontSize: 12, // 12px text
fill: am5.color(0xffffff), // white, over the dark picture
fillOpacity: 0.6,
x: am5.p100, // at the right edge...
centerX: am5.p100, // ...measured from the label's right end...
dx: -10, // ...10px in from it
y: 10, // 10px from the top
visible: false // shown with the satellite view only
}));
// 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,
// the route and the plane turn white, the credit shows and the map sits in near-black space
satelliteSeries.on("visible", function(visible) {
if (visible) {
// NASA's 2048px picture of the Earth
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); // the near-black space behind the map
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"));
lineSeries.mapLines.template.set("stroke", visible ? am5.color(0xffffff) : root.interfaceColors.get("alternativeBackground"));
planeTemplate.set("fill", visible ? am5.color(0xffffff) : root.interfaceColors.get("text"));
});
// 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": -58,
"latitude": 37
},
"homeZoomLevel": 2.6,
"panX": "rotateX",
"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"
]
},
"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": "route",
"pointIds": [
"paris",
"toronto",
"la",
"havana"
],
"__parse": true
}
]
},
{
"data-4": [
{
"id": "paris",
"title": "Paris",
"latitude": 48.8567,
"longitude": 2.351
},
{
"id": "toronto",
"title": "Toronto",
"latitude": 43.8163,
"longitude": -79.4287
},
{
"id": "la",
"title": "Los Angeles",
"latitude": 34.3,
"longitude": -118.15
},
{
"id": "havana",
"title": "Havana",
"latitude": 23,
"longitude": -82
}
]
},
{
"data-5": [
{
"id": "plane",
"lineId": "route",
"positionOnLine": 0,
"autoRotate": true
}
]
},
{
"template-0": {
"type": "Template",
"settings": {
"fill": "@root.interfaceColors.get('text')"
}
}
},
{
"series-0": {
"type": "MapPolygonSeries",
"settings": {
"name": "Background Series",
"affectsBounds": false
},
"properties": {
"data": "#data-0",
"mapPolygons": {
"properties": {
"template": {
"settings": {
"fill": "@root.interfaceColors.get('alternativeBackground')",
"fillOpacity": 0,
"strokeOpacity": 0
}
}
}
}
}
}
},
{
"series-1": {
"type": "MapRasterSeries",
"settings": {
"visible": false,
"affectsBounds": false
}
}
},
{
"series-2": {
"type": "GraticuleSeries",
"settings": {
"step": 10
},
"properties": {
"mapLines": {
"properties": {
"template": {
"settings": {
"stroke": "@root.interfaceColors.get('alternativeBackground')",
"strokeOpacity": 0.08
}
}
}
}
}
}
},
{
"series-3": {
"type": "MapPolygonSeries",
"settings": {
"name": "Polygon Series",
"geoJSON": "am5geodata_worldLow"
}
}
},
{
"series-5": {
"type": "MapPointSeries",
"settings": {
"name": "City Series"
},
"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,
"draggable": true,
"shadowColor": {
"type": "Color",
"value": "#000000"
},
"shadowBlur": 4,
"shadowOffsetX": 1,
"shadowOffsetY": 2,
"shadowOpacity": 0.4
}
}
}
}
]
}
}
},
{
"series-4": {
"type": "MapLineSeries",
"settings": {
"name": "Line Series",
"pointSeries": "#series-5"
},
"properties": {
"data": "#data-3",
"mapLines": {
"properties": {
"template": {
"settings": {
"stroke": "@root.interfaceColors.get('alternativeBackground')",
"strokeOpacity": 0.3
}
}
}
}
}
}
},
{
"series-6": {
"type": "MapPointSeries",
"settings": {
"name": "Plane Series"
},
"properties": {
"data": "#data-5",
"bullets": [
{
"type": "Bullet",
"settings": {
"sprite": {
"type": "Container",
"children": [
{
"type": "Graphics",
"settings": {
"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.06,
"centerY": {
"type": "Percent",
"value": 50
},
"centerX": {
"type": "Percent",
"value": 50
},
"shadowColor": {
"type": "Color",
"value": "#000000"
},
"shadowBlur": 10,
"shadowOffsetX": 4,
"shadowOffsetY": 6,
"shadowOpacity": 0.5
},
"properties": {
"template": "#template-0"
}
}
],
"settings": {
"animations": [
{
"key": "positionOnLine",
"from": 0,
"to": 1,
"duration": 5000,
"loops": 0,
"yoyo": true,
"target": "dataItem"
}
]
}
}
}
}
]
}
}
}
]
}
