One dial, two scales: the inner one runs to 160 and the outer one to 240, each with its own needle and readout in matching colors.
Two scales on one dial save room when two readings belong together, like the same speed in two units or two related sensors. Matching colors tie each needle to its scale and readout. With more than two readings, give each its own gauge.
Built with amCharts 5, a JavaScript library. The code includes your theme and renderer (canvas or SVG), but not the changes you make with the controls.
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."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)
]);
// Create chart
// https://www.amcharts.com/docs/v5/charts/radar-chart/
var chart = root.container.children.push(am5radar.RadarChart.new(root, {
panX: false, // the gauge doesn't pan when dragged
panY: false,
startAngle: 180, // the arc starts on the left...
endAngle: 360, // ...and goes over the top to the right
radius: am5.percent(90) // the dial fills 90% of the space
}));
// Colors
var colors = am5.ColorSet.new(root, {
// skip every other color, so the two scales differ more
step: 2
});
// Measurement #1
// Axis
var color1 = colors.next();
var axisRenderer1 = am5radar.AxisRendererCircular.new(root, {
// the inner scale: 10px inside the outer one, with its ticks and labels on its inner side
radius: -10,
stroke: color1, // the axis line in the first color
strokeOpacity: 1, // fully opaque
strokeWidth: 6, // a thick 6px arc
inside: true
});
axisRenderer1.grid.template.setAll({
forceHidden: true // no grid lines across the dial
});
axisRenderer1.ticks.template.setAll({
stroke: color1, // ticks in the axis color
visible: true, // ticks are hidden by default
length: 10, // 10px long
strokeOpacity: 1,
inside: true // pointing inward
});
axisRenderer1.labels.template.setAll({
radius: 15, // 15px away from the arc
inside: true // on the arc's inner side
});
var xAxis1 = chart.xAxes.push(am5xy.ValueAxis.new(root, {
maxDeviation: 0, // the scale can't be panned past its ends
min: 0, // the inner scale runs from 0...
max: 160, // ...to 160
strictMinMax: true, // exactly, not rounded out to nicer numbers
renderer: axisRenderer1
}));
// Label
// the first readout, a number in a colored box
var label1 = chart.seriesContainer.children.push(am5.Label.new(root, {
fill: am5.color(0xffffff), // white text
width: 100, // a fixed 100px box
centerX: am5.percent(50), // centered on its position...
textAlign: "center", // ...with the text centered in the box
centerY: am5.percent(50), // ...and vertically
fontSize: "2em", // twice the chart's text size
text: "0", // the readout before the first value arrives
background: am5.RoundedRectangle.new(root, { // a rounded box behind the number...
fill: color1 // ...in the first scale's color
})
}));
// Add clock hand
// the hand is the bullet of an axis range: animating the range's value turns the hand
var axisDataItem1 = xAxis1.makeDataItem({
value: 0, // the hand starts at 0
fill: color1,
name: "Measurement #1"
});
var clockHand1 = am5radar.ClockHand.new(root, {
pinRadius: 14, // the round pin at the center, 14px
radius: am5.percent(98), // the hand reaches almost to the scale
bottomWidth: 10 // 10px wide at the base, tapering to the tip
});
clockHand1.pin.setAll({
fill: color1 // the pin...
});
clockHand1.hand.setAll({
fill: color1 // ...and the hand in the first scale's color
});
var bullet1 = axisDataItem1.set("bullet", am5xy.AxisBullet.new(root, {
sprite: clockHand1
}));
xAxis1.createAxisRange(axisDataItem1);
// hide the range's own grid line and tick, so only the hand shows
axisDataItem1.get("grid").set("forceHidden", true);
axisDataItem1.get("tick").set("forceHidden", true);
// Measurement #2
// Axis
var color2 = colors.next();
// the outer scale, with its ticks and labels outside the arc
var axisRenderer2 = am5radar.AxisRendererCircular.new(root, {
stroke: color2, // the axis line in the second color
strokeOpacity: 1,
strokeWidth: 6 // a thick 6px arc
});
axisRenderer2.grid.template.setAll({
forceHidden: true // no grid lines across the dial
});
axisRenderer2.ticks.template.setAll({
stroke: color2,
visible: true, // ticks are hidden by default
length: 10, // 10px long
strokeOpacity: 1
});
axisRenderer2.labels.template.setAll({
radius: 15 // 15px outside the arc
});
var xAxis2 = chart.xAxes.push(am5xy.ValueAxis.new(root, {
maxDeviation: 0,
min: 0, // the outer scale runs from 0...
max: 240, // ...to 240
strictMinMax: true, // exactly, not rounded out to nicer numbers
renderer: axisRenderer2
}));
// Label
// the second readout, in the second scale's color
var label2 = chart.seriesContainer.children.push(am5.Label.new(root, {
fill: am5.color(0xffffff),
width: 100,
centerX: am5.percent(50),
textAlign: "center",
centerY: am5.percent(50),
fontSize: "2em",
text: "0",
background: am5.RoundedRectangle.new(root, {
fill: color2
})
}));
// Add clock hand
// the second hand, on the outer scale
var axisDataItem2 = xAxis2.makeDataItem({
value: 0,
fill: color2,
name: "Measurement #2"
});
var clockHand2 = am5radar.ClockHand.new(root, {
pinRadius: 10, // smaller than the first pin, so both pins show
radius: am5.percent(98),
bottomWidth: 10
});
clockHand2.pin.setAll({
fill: color2
});
clockHand2.hand.setAll({
fill: color2
});
var bullet2 = axisDataItem2.set("bullet", am5xy.AxisBullet.new(root, {
sprite: clockHand2
}));
xAxis2.createAxisRange(axisDataItem2);
// hide the range's own grid line and tick, as for the first hand
axisDataItem2.get("grid").set("forceHidden", true);
axisDataItem2.get("tick").set("forceHidden", true);
// Keep the readouts in proportion to the dial, so they fit on small screens too
chart.plotContainer.events.on("boundschanged", function () {
var radius = chart.getPrivate("radius", 0); // the dial's radius in pixels
var scale = Math.min(1, radius / 300); // shrink the readouts on a dial smaller than 300px
// left of the center, a little above it
label1.setAll({ x: -radius * 0.24, y: -radius * 0.14, scale: scale });
label2.setAll({ x: radius * 0.24, y: -radius * 0.14, scale: scale }); // right of the center
});
// Animate values
setInterval(function () {
var value1 = Math.round(Math.random() * 160); // a random value on the inner scale
axisDataItem1.animate({
key: "value",
to: value1,
duration: 1000, // the hand takes a second to turn
easing: am5.ease.out(am5.ease.cubic) // slowing down as it arrives
});
label1.set("text", value1); // the readout jumps to the new value
var value2 = Math.round(Math.random() * 240); // a random value on the outer scale
axisDataItem2.animate({
key: "value",
to: value2,
duration: 1000,
easing: am5.ease.out(am5.ease.cubic)
});
label2.set("text", value2);
}, 2000) // every 2 seconds
// Make stuff animate on load
chart.appear(1000, 100);