Animations Along Lines
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.
About this map
Moving along a map line
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.
Good for
- Flight, shipping and delivery routes
- The order of stops on a trip
- Live tracking of a vehicle or a parcel
Think twice when
- Many routes at once: moving markers crowd, so keep the lines still
- When each leg happens: a timeline says it better
- Print: the motion is lost
For developers
Get the code.
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.
For projects that use npm:
How it reads:
Using Claude? Install the amCharts plugin first, so it works from our docs and examples.
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.How it reads:
"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.Using Claude? Install the amCharts plugin first, so it works from our docs and examples.
// Create root element
// https://docs.amcharts.com/v5/getting-started/#root-element
var root = am5.Root.new("chartdiv");
// Set themes
// https://docs.amcharts.com/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://docs.amcharts.com/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://docs.amcharts.com/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://docs.amcharts.com/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://docs.amcharts.com/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://docs.amcharts.com/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://docs.amcharts.com/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://docs.amcharts.com/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://docs.amcharts.com/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://docs.amcharts.com/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": "@colors.getIndex(10)",
"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"
}
]
}
}
}
}
]
}
}
}
]
}
More map tools.

SVG Map Generator
Any region, any projection
Pick a map and projection, simplify it and paint regions, then export SVG or PNG.
Pixel Map Generator
Dotted and pixel-art maps
Turn any map into dots, squares, hexagons or diamonds for posters and slides.

SVG Maps
427 maps, free to download
World, continents, countries and regions, with every region as its own named path.



