Bars with Moving Bullets
A bar chart with a photo on each bar. Point at a bar and the photo slides along it to the end. Here, step counts for six friends, the bars darker the more they walked.
About this chart
When to add motion and faces
Photos make a ranking personal, and a small movement on hover invites people to explore it. It suits a team leaderboard or a social post more than a report, where the motion adds nothing to the numbers.
Good for
- Team leaderboards and challenges
- Rankings of people, products or brands
- Social posts and presentations
Think twice when
- More than about ten bars: the photos crowd each other
- Reports and printouts: nothing moves on paper
- Values close together: label them, the bars alone won’t tell them apart
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. 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.
// 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)
]);
var data = [
{
name: "Monica",
steps: 45688,
pictureSettings: {
src: "https://www.amcharts.com/wp-content/uploads/assets/timeline/monica.jpg"
}
},
{
name: "Joey",
steps: 35781,
pictureSettings: {
src: "https://www.amcharts.com/wp-content/uploads/assets/timeline/joey.jpg"
}
},
{
name: "Ross",
steps: 25464,
pictureSettings: {
src: "https://www.amcharts.com/wp-content/uploads/assets/timeline/ross.jpg"
}
},
{
name: "Phoebe",
steps: 18788,
pictureSettings: {
src: "https://www.amcharts.com/wp-content/uploads/assets/timeline/phoebe.jpg"
}
},
{
name: "Rachel",
steps: 15465,
pictureSettings: {
src: "https://www.amcharts.com/wp-content/uploads/assets/timeline/rachel.jpg"
}
},
{
name: "Chandler",
steps: 11561,
pictureSettings: {
src: "https://www.amcharts.com/wp-content/uploads/assets/timeline/chandler.jpg"
}
}
];
// Create chart
// https://docs.amcharts.com/v5/charts/xy-chart/
var chart = root.container.children.push(
am5xy.XYChart.new(root, {
panX: false, // the plot doesn't pan when dragged
panY: false,
paddingLeft:0, // the names sit at the chart's left edge
paddingRight:30, // room for a photo at the end of the longest bar
wheelX: "none", // the mouse wheel doesn't zoom or pan
wheelY: "none"
})
);
// Create axes
// https://docs.amcharts.com/v5/charts/xy-chart/axes/
var yRenderer = am5xy.AxisRendererY.new(root, {
minorGridEnabled:true
});
yRenderer.grid.template.set("visible", false); // no grid lines across the rows
var yAxis = chart.yAxes.push(
am5xy.CategoryAxis.new(root, {
categoryField: "name",
renderer: yRenderer,
paddingRight:40 // room between the names and the bars, for the photos
})
);
var xRenderer = am5xy.AxisRendererX.new(root, {
minGridDistance:80, // at least 80px between labels; on narrow screens some are skipped
minorGridEnabled:true // fainter grid lines between the labeled ones
});
var xAxis = chart.xAxes.push(
am5xy.ValueAxis.new(root, {
min: 0, // the bars start at zero
renderer: xRenderer
})
);
// Add series
// https://docs.amcharts.com/v5/charts/xy-chart/series/
var series = chart.series.push(
am5xy.ColumnSeries.new(root, {
name: "Steps",
xAxis: xAxis,
yAxis: yAxis,
valueXField: "steps",
categoryYField: "name",
sequencedInterpolation: true, // the bars grow one after another
// finds the lowest and highest steps, which the heat rules below color by
calculateAggregates: true,
// let the photos stick out past the plot area's edges
maskBullets: false,
tooltip: am5.Tooltip.new(root, {
dy: -30, // 30px higher, above the photo
pointerOrientation: "vertical", // it points up or down at the bar
labelText: "{valueX}" // the steps
})
})
);
series.columns.template.setAll({
strokeOpacity: 0, // no outline
cornerRadiusBR: 10, // fully rounded ends
cornerRadiusTR: 10,
cornerRadiusBL: 10,
cornerRadiusTL: 10,
maxHeight: 50, // bars no thicker than 50px
fillOpacity: 0.8 // a little see-through
});
var currentlyHovered; // the bar whose photo is out at its end
// on hover, the photo slides from the start of its bar to the end, and back when the pointer leaves
series.columns.template.events.on("pointerover", function(e) {
handleHover(e.target.dataItem);
});
series.columns.template.events.on("pointerout", function(e) {
handleOut();
});
// slide the hovered bar's photo to its end, after sending the last one back
function handleHover(dataItem) {
if (dataItem && currentlyHovered != dataItem) {
handleOut();
currentlyHovered = dataItem;
var bullet = dataItem.bullets[0];
bullet.animate({
key: "locationX",
to: 1, // to the end of the bar
duration: 600, // in 0.6 seconds
easing: am5.ease.out(am5.ease.cubic)
});
}
}
// slide the photo back to the start of its bar
function handleOut() {
if (currentlyHovered) {
var bullet = currentlyHovered.bullets[0];
bullet.animate({
key: "locationX",
to: 0,
duration: 600,
easing: am5.ease.out(am5.ease.cubic)
});
}
}
var circleTemplate = am5.Template.new({}); // one template for all the circles, so the heat rule can color them
series.bullets.push(function(root, series, dataItem) {
var bulletContainer = am5.Container.new(root, {});
var circle = bulletContainer.children.push(
am5.Circle.new(
root,
{
radius: 34 // a colored ring around the photo
},
circleTemplate
)
);
var maskCircle = bulletContainer.children.push(
am5.Circle.new(root, { radius: 27 }) // the photo is cut to this circle
);
// only containers can be masked, so we add image to another container
var imageContainer = bulletContainer.children.push(
am5.Container.new(root, {
mask: maskCircle
})
);
var image = imageContainer.children.push(
am5.Picture.new(root, {
templateField: "pictureSettings", // the photo's src from each data item's pictureSettings
centerX: am5.p50, // centered on the bullet
centerY: am5.p50,
width: 60, // a 60px photo, cut to the circle above
height: 60
})
);
return am5.Bullet.new(root, {
locationX: 0, // the photo starts at the beginning of the bar
sprite: bulletContainer
});
});
// heatrule: the series' color from the theme, a light tint for the fewest steps and the full color for the most
var heatMax = series.get("fill");
var heatMin = am5.Color.lighten(heatMax, 0.6);
series.set("heatRules", [
{
dataField: "valueX",
min: heatMin, // the fewest steps get the light tint...
max: heatMax, // ...the most, the full color
target: series.columns.template,
key: "fill"
},
{
dataField: "valueX",
min: heatMin,
max: heatMax,
target: circleTemplate, // the rings get the same colors
key: "fill"
}
]);
series.data.setAll(data);
yAxis.data.setAll(data);
var cursor = chart.set("cursor", am5xy.XYCursor.new(root, {}));
cursor.lineX.set("visible", false); // no cursor lines
cursor.lineY.set("visible", false);
// the row under the cursor counts as hovered too, even where the pointer misses the bar
cursor.events.on("cursormoved", function() {
var dataItem = series.get("tooltip").dataItem;
if (dataItem) {
handleHover(dataItem)
}
else {
handleOut();
}
})
// Make stuff animate on load
// https://docs.amcharts.com/v5/concepts/animations/
series.appear();
chart.appear(1000, 100);
{
"type": "XYChart",
"settings": {
"panX": false,
"panY": false,
"paddingLeft": 0,
"paddingRight": 30,
"wheelX": "none",
"wheelY": "none",
"cursor": {
"type": "XYCursor",
"properties": {
"lineX": {
"settings": {
"visible": false
}
},
"lineY": {
"settings": {
"visible": false
}
}
}
}
},
"properties": {
"series": [
"#series-0"
],
"xAxes": [
"#axis-0"
],
"yAxes": [
"#axis-1"
]
},
"refs": [
{
"data-0": [
{
"name": "Monica",
"steps": 45688,
"pictureSettings": {
"src": "https://www.amcharts.com/wp-content/uploads/assets/timeline/monica.jpg"
},
"__parse": true
},
{
"name": "Joey",
"steps": 35781,
"pictureSettings": {
"src": "https://www.amcharts.com/wp-content/uploads/assets/timeline/joey.jpg"
},
"__parse": true
},
{
"name": "Ross",
"steps": 25464,
"pictureSettings": {
"src": "https://www.amcharts.com/wp-content/uploads/assets/timeline/ross.jpg"
},
"__parse": true
},
{
"name": "Phoebe",
"steps": 18788,
"pictureSettings": {
"src": "https://www.amcharts.com/wp-content/uploads/assets/timeline/phoebe.jpg"
},
"__parse": true
},
{
"name": "Rachel",
"steps": 15465,
"pictureSettings": {
"src": "https://www.amcharts.com/wp-content/uploads/assets/timeline/rachel.jpg"
},
"__parse": true
},
{
"name": "Chandler",
"steps": 11561,
"pictureSettings": {
"src": "https://www.amcharts.com/wp-content/uploads/assets/timeline/chandler.jpg"
},
"__parse": true
}
]
},
{
"template-0": {
"type": "Template"
}
},
{
"template-1": {
"type": "Template",
"settings": {
"strokeOpacity": 0,
"cornerRadiusBR": 10,
"cornerRadiusTR": 10,
"cornerRadiusBL": 10,
"cornerRadiusTL": 10,
"maxHeight": 50,
"fillOpacity": 0.8
}
}
},
{
"axis-0": {
"type": "ValueAxis",
"settings": {
"min": 0,
"renderer": {
"type": "AxisRendererX",
"settings": {
"minGridDistance": 80,
"minorGridEnabled": true
}
}
}
}
},
{
"axis-1": {
"type": "CategoryAxis",
"settings": {
"categoryField": "name",
"renderer": {
"type": "AxisRendererY",
"settings": {
"minorGridEnabled": true
},
"properties": {
"grid": {
"properties": {
"template": {
"settings": {
"visible": false
}
}
}
}
}
},
"paddingRight": 40
},
"properties": {
"data": "#data-0"
}
}
},
{
"series-0": {
"type": "ColumnSeries",
"settings": {
"name": "Steps",
"xAxis": "#axis-0",
"yAxis": "#axis-1",
"valueXField": "steps",
"categoryYField": "name",
"sequencedInterpolation": true,
"calculateAggregates": true,
"maskBullets": false,
"tooltip": {
"type": "Tooltip",
"settings": {
"dy": -30,
"pointerOrientation": "vertical",
"labelText": "{valueX}"
}
},
"heatRules": [
{
"dataField": "valueX",
"min": {
"type": "Color",
"value": "#b3b9d9"
},
"max": "@root.interfaceColors.get('primaryButton')",
"target": "#template-1",
"key": "fill",
"__parse": true
},
{
"dataField": "valueX",
"min": {
"type": "Color",
"value": "#b3b9d9"
},
"max": "@root.interfaceColors.get('primaryButton')",
"target": "#template-0",
"key": "fill",
"__parse": true
}
]
},
"properties": {
"data": "#data-0",
"columns": {
"properties": {
"template": "#template-1"
}
},
"bullets": [
{
"type": "Bullet",
"settings": {
"locationX": 0,
"sprite": {
"type": "Container",
"children": [
{
"type": "Circle",
"settings": {
"radius": 34
},
"properties": {
"template": "#template-0"
}
},
{
"type": "Circle",
"settings": {
"radius": 27
}
},
{
"type": "Container",
"settings": {
"mask": {
"type": "Circle",
"settings": {
"radius": 27
}
}
},
"children": [
{
"type": "Picture",
"settings": {
"templateField": "pictureSettings",
"centerX": {
"type": "Percent",
"value": 50
},
"centerY": {
"type": "Percent",
"value": 50
},
"width": 60,
"height": 60
}
}
]
}
]
}
}
}
]
}
}
}
]
}



