Map with Animated Bubbles
Bubbles on eight countries that grow and shrink as their values change every two seconds, with the value inside each bubble and the country's name beside it.
About this map
Bubbles that update live
Heat rules tie a bubble's size to its value, and when new data arrives, the bubbles animate to their new size instead of jumping. That keeps changes easy to follow on a live dashboard. The values here are random, to show the motion; in a real map they would come from a data feed.
Good for
- Live dashboards: sales, visitors or sensors by country
- A handful of places whose values change
- Values that need a number as well as a size
Think twice when
- Many countries: bubbles overlap, color the countries instead
- Exact comparisons: bubble sizes are hard to judge, a bar chart is easier
- Values that change once a day: a still map is calmer
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:
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.
// the countries that get a bubble; updateData() replaces the values at random
var data = [
{
id: "US",
name: "United States",
value: 100
}, {
id: "GB",
name: "United Kingdom",
value: 100
}, {
id: "CN",
name: "China",
value: 100
}, {
id: "IN",
name: "India",
value: 100
}, {
id: "AU",
name: "Australia",
value: 100
}, {
id: "CA",
name: "Canada",
value: 100
}, {
id: "BR",
name: "Brazil",
value: 100
}, {
id: "ZA",
name: "South Africa",
value: 100
}
];
// 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
}),
minZoomLevel: 0.5, // can zoom out to half the fitted size
// go to the home view once the map is fitted
autoHome: true,
projection: am5map.geoEqualEarth(), // a projection that keeps the countries' areas true
panX: "rotateX", // dragging sideways turns the globe
boxZoom: "shift" // hold Shift and drag to zoom into a box
}));
// Add zoom control
// https://docs.amcharts.com/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://docs.amcharts.com/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://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"), // contrasting with the background...
strokeOpacity: 0.08 // ...and 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, {
geoJSON: am5geodata_worldLow // the world's countries, in low detail
})
);
// Create point series for the bubbles; each sits in the middle of its country
// https://docs.amcharts.com/v5/charts/map-chart/map-point-series/
var bubbleSeries = chart.series.push(
am5map.MapPointSeries.new(root, {
valueField: "value",
calculateAggregates: true, // works out the series' lowest and highest values
polygonIdField: "id"
})
);
// The bubbles and the country names share templates, so one change reaches them all
var circleTemplate = am5.Template.new({
fill: colors.getIndex(9), // a theme color...
fillOpacity: 0.7 // ...slightly see-through
});
var labelTemplate = am5.Template.new({}); // empty for now; settings put here reach every name
// A bubble with the country name next to it
bubbleSeries.bullets.push(function(root, series, dataItem) {
var container = am5.Container.new(root, {});
var circle = container.children.push(
am5.Circle.new(root, {
radius: 20, // a starting size; the heat rule sets the real one
cursorOverStyle: "pointer", // a hand pointer over the bubble
tooltipText: `{name}: [bold]{value}[/]` // the country and its value on hover
}, circleTemplate)
);
var countryLabel = container.children.push(
am5.Label.new(root, {
text: "{name}", // the country's name...
paddingLeft: 5, // ...5px from the bubble's edge...
populateText: true, // ...filled in from the data item
fontWeight: "bold", // bold...
fontSize: 13, // ...13px text...
centerY: am5.p50 // ...centered on the bubble's middle height
}, labelTemplate)
);
// keep the name just outside the bubble as it grows and shrinks
circle.on("radius", function(radius) {
countryLabel.set("x", radius);
});
return am5.Bullet.new(root, {
sprite: container,
dynamic: true // redrawn whenever the series changes, so it keeps up with the data
});
});
// The value in the middle of the bubble
bubbleSeries.bullets.push(function(root, series, dataItem) {
return am5.Bullet.new(root, {
sprite: am5.Label.new(root, {
text: "{value.formatNumber('#.')}", // the value, as a whole number
fill: am5.color(0xffffff), // white text...
populateText: true, // ...filled in from the data item...
centerX: am5.p50, // ...centered in the bubble...
centerY: am5.p50, // ...both ways...
textAlign: "center" // ...and line by line
}),
// redrawn whenever the series' data changes, so the number stays current
dynamic: true
});
});
// The bubble size follows the value. minValue and maxValue must be set for
// the animations to work
// https://docs.amcharts.com/v5/concepts/settings/heat-rules/
bubbleSeries.set("heatRules", [
{
target: circleTemplate,
dataField: "value",
min: 10, // the radius goes from 10px...
max: 50, // ...to 50px...
minValue: 0, // ...for values from 0...
maxValue: 100, // ...to 100
key: "radius"
}
]);
bubbleSeries.data.setAll(data);
// New random values every 2 seconds
updateData(); // the first new values right away
setInterval(function() {
updateData();
}, 2000);
// gives every bubble a random value from 0 to 100
function updateData() {
for (var i = 0; i < bubbleSeries.dataItems.length; i++) {
// setIndex updates the existing data item, so its bubble animates to the new size
bubbleSeries.data.setIndex(i, { value: Math.round(Math.random() * 100), id: data[i].id, name: data[i].name })
}
}
// 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 country names 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"));
labelTemplate.set("fill", visible ? am5.color(0xffffff) : root.interfaceColors.get("text")); // names readable on the picture
});
{
"type": "MapChart",
"settings": {
"background": {
"type": "Rectangle",
"settings": {
"fill": {
"type": "Color",
"value": "#101318"
},
"fillOpacity": 0
}
},
"minZoomLevel": 0.5,
"autoHome": true,
"panX": "rotateX",
"boxZoom": "shift",
"zoomControl": {
"type": "ZoomControl",
"properties": {
"homeButton": {
"settings": {
"visible": true
}
}
}
},
"projectionName": "geoEqualEarth"
},
"properties": {
"series": [
"#series-0",
"#series-1",
"#series-2",
"#series-3",
"#series-4"
]
},
"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": [
{
"value": 18,
"id": "US",
"name": "United States"
},
{
"value": 8,
"id": "GB",
"name": "United Kingdom"
},
{
"value": 9,
"id": "CN",
"name": "China"
},
{
"value": 3,
"id": "IN",
"name": "India"
},
{
"value": 39,
"id": "AU",
"name": "Australia"
},
{
"value": 32,
"id": "CA",
"name": "Canada"
},
{
"value": 96,
"id": "BR",
"name": "Brazil"
},
{
"value": 36,
"id": "ZA",
"name": "South Africa"
}
]
},
{
"template-0": {
"type": "Template",
"settings": {
"fill": "@colors.getIndex(9)",
"fillOpacity": 0.7
}
}
},
{
"template-1": {
"type": "Template"
}
},
{
"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": "GraticuleSeries",
"settings": {
"step": 10
},
"properties": {
"mapLines": {
"properties": {
"template": {
"settings": {
"stroke": "@root.interfaceColors.get('alternativeBackground')",
"strokeOpacity": 0.08
}
}
}
}
}
}
},
{
"series-3": {
"type": "MapPolygonSeries",
"settings": {
"geoJSON": "am5geodata_worldLow"
}
}
},
{
"series-4": {
"type": "MapPointSeries",
"settings": {
"valueField": "value",
"calculateAggregates": true,
"polygonIdField": "id",
"heatRules": [
{
"target": "#template-0",
"dataField": "value",
"min": 10,
"max": 50,
"minValue": 0,
"maxValue": 100,
"key": "radius",
"__parse": true
}
]
},
"properties": {
"data": "#data-3",
"bullets": [
{
"type": "Bullet",
"settings": {
"sprite": {
"type": "Container",
"children": [
{
"type": "Circle",
"settings": {
"radius": 20,
"cursorOverStyle": "pointer",
"tooltipText": "{name}: [bold]{value}[/]"
},
"properties": {
"template": "#template-0"
}
},
{
"type": "Label",
"settings": {
"text": "{name}",
"paddingLeft": 5,
"populateText": true,
"fontWeight": "bold",
"fontSize": 13,
"centerY": {
"type": "Percent",
"value": 50
}
},
"properties": {
"template": "#template-1"
}
}
]
},
"dynamic": true
}
},
{
"type": "Bullet",
"settings": {
"sprite": {
"type": "Label",
"settings": {
"text": "{value.formatNumber('#.')}",
"fill": {
"type": "Color",
"value": "#ffffff"
},
"populateText": true,
"centerX": {
"type": "Percent",
"value": 50
},
"centerY": {
"type": "Percent",
"value": 50
},
"textAlign": "center"
}
},
"dynamic": true
}
}
]
}
}
}
]
}
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.
Go further in the Dojo.
Full apps and games built with amCharts 5.




