Introduction
This is a gauge chart by D3 step by step.
Background
Many Javascript libraries support gauge charts in the web pages, most noticeably the well known C3.js library. Although highly configurable, I found it difficult to make the chart to look like the following by C3.js, so I decided to implement a gauge chart by myself.
For simplicity, I used the D3.js for the SVG manipulations. It is simple, but it is nice to keep a record on how to create the gauge chart step by step.
The attached is a Java Maven project. I have tested it on Tomcat 7. if you want run it, you do not have to use Tomcat, because all the files are simply static HTML files. But I would recommend you run the files through a web server to avoid browser security checks. You will also need the internet access to load the files, because the D3.js library is linked to a CDN.
step-0-start-from-a-half-circle.html
The following HTML content will be used to test the gauge. For simplicity, the SVG gauge will be appended directly to the 'body'.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>step-0-start-from-a-half-circle.html</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.11/d3.min.js"></script>
</head>
<body>
<div><a href="index.html">Go back</a></div>
</body>
</body>
</html>
As the "0th" step, the only goal is to add an arc (half-circle) to the web page. Because we only need a half-circle, the size of the SVG is set to 200 x 100.
let d2r = function(d) {
return d * (Math.PI / 180);
};
let Gauge = function() {
let c = {
element: 'body',
width: 200,
height: 100,
thickness: 30,
gaugeColor: '#dde9f7',
backgroundColor: '#f5f5f5'
};
let svg = d3.select(c.element).append('svg')
.attr('width', c.width).attr('height', c.height);
svg.append('rect').attr('x', 0).attr('y', 0)
.attr('width', c.width).attr('height', c.height).attr('fill', c.backgroundColor);
let r = c.width/2;
let createArc = function(sa, ea) {
return d3.svg.arc()
.outerRadius(r)
.innerRadius(r - c.thickness)
.startAngle(d2r(sa))
.endAngle(d2r(ea));
};
let addArc = function(arc, color) {
return svg.append('path')
.attr('d', arc).attr('fill', color)
.attr('transform', 'translate('
+ r + ',' + r + '), scale(1, 1)');
};
addArc(createArc(-90, 90), c.gaugeColor);
};
window.addEventListener('load', function() {
let gauge = new Gauge();
});
- With the help of the D3.js, adding an arc to the SVG is a simple call to the "d3.svg.arc()" function;
- Besides adding the arc with "startAngle = -90 degree" and "endAngle = 90 degree", a rectangle is also added to the SVG to serve as the background of the chart.
You should have noticed the JSON object "let c= {...}". All the configurable parameters are defined in this object. In the later parts of this example, this object will also be used to take input configurable parameters.
step-1-add-margin.html
Although not always necessary, it should be nice to add some margins to the chart.
let c = {
element: 'body',
width: 200,
height: 100,
margin: 4,
thickness: 30,
gaugeColor: '#dde9f7',
backgroundColor: '#f5f5f5'
};
In order that the margin to be configurable, it is added to the configuration object. The outer radius of the arc is then calculated as the following to leave the room for the margins.
let r = (c.width - 2*c.margin)/2;
The center of the arc is also adjusted to include the margins into the consideration.
let addArc = function(arc, color) {
return svg.append('path')
.attr('d', arc).attr('fill', color)
.attr('transform', 'translate(' + (c.margin + r)
+ ',' + (c.margin + r) + '), scale(1, 1)');
};
You can see that the margins at the left, the top, and the right sides are added nicely. You should also see that there is no margin at the bottom. It is because I will chop and scale this arc and put labels at the bottom in the later steps.
step-2-scale-and-chop-the-arc.html
In order to achieve the desired look, I need to chop and scale the arc.
let c = {
element: 'body',
width: 200,
height: 100,
margin: 4,
yscale: 0.75,
chopAngle: 60,
thickness: 30,
gaugeColor: '#dde9f7',
backgroundColor: '#f5f5f5'
};
The "yscale" and "chopAngle" is added to the configuration object. In oder that the arc keeps the same left, top, and right margins, the radius of the arc needs to be calculated as the following to take the chop angle into consideration.
let ir = (c.width - 2*c.margin)/2;
let r = ir/Math.sin(d2r(c.chopAngle));
The center of the arc needs to take the scale into consideration, and the chop angle needs to be specified when drawing the arc.
let addArc = function(arc, color) {
return svg.append('path')
.attr('d', arc).attr('fill', color)
.attr('transform', 'translate(' + (c.margin + ir)
+ ',' + (c.margin + r*c.yscale) + '), scale(1, ' + c.yscale + ')');
};
addArc(createArc(-1*c.chopAngle, c.chopAngle), c.gaugeColor);
step-3-add-the-indicator.html
For the gauge chart to display a value, we need to add the value indicator.
let c = {
element: 'body',
width: 200,
height: 100,
margin: 4,
yscale: 0.75,
chopAngle: 60,
thickness: 30,
value: {
minValue: 0,
maxValue: 100,
initialvalue: 60
},
gaugeColor: '#dde9f7',
indicatorColor: '#4281a4',
backgroundColor: '#f5f5f5'
};
The configuration object now has the value object, which has the minimum, maximum, and the initial value of the gauge.
let getIndicatorData = function(value) {
let min = c.value.minValue;
let max = c.value.maxValue;
return {
sa: c.chopAngle - 2*c.chopAngle*value/(max - min),
ea: c.chopAngle
};
}
let data = getIndicatorData(c.value.initialvalue);
let indicator = addArc(createArc(data.sa, data.ea), c.indicatorColor);
In this gauge, the value represents "how much left/remaining", so the indicator occupies the right side of the gauge. If you want the gauge value to represent "how much used/spent", you can modify the "getIndicatorData()" function to achieve your desired result.
step-4-add-the-labels.html
Normally a gauge chart needs some labels to show the value in text.
let c = {
element: 'body',
width: 200,
height: 100,
margin: 4,
yscale: 0.75,
chopAngle: 60,
thickness: 30,
value: {
minValue: 0,
maxValue: 100,
initialvalue: 60
},
label: {
yoffset: 10,
unit: 'unit.',
labelText: {
text: 'REMAINING',
yoffset: 12
}
},
gaugeColor: '#dde9f7',
indicatorColor: '#4281a4',
backgroundColor: '#f5f5f5'
};
The "label" object in the configuration object has the information for us to create the labels for the chart.
let createLabel = function(config) {
let c = {
x: config.width/2,
y: config.height/2 + config.label.yoffset,
unit: config.label.unit,
label: {
text: config.label.labelText.text,
yoffset: config.label.labelText.yoffset
},
value: config.value.initialvalue
};
let text = svg.append('text')
.attr("x", c.x)
.attr("y", c.y)
.attr("text-anchor", "middle")
.attr("alignment-baseline", "middle")
.attr("font-family", "sans-serif")
.attr("font-size", "25px");
let label = text.append('tspan')
.attr("font-size", "25px")
.text(c.value);
text.append('tspan')
.attr('font-size', '10px').text(c.unit)
.attr('fill', '#333333');
text = svg.append('text')
.attr("x", c.x)
.attr("y", c.y + c.label.yoffset)
.attr("text-anchor", "middle")
.attr("alignment-baseline", "central")
.attr("font-family", "sans-serif")
.attr('fill', '#a8a8a8')
.attr("font-size", "12px");
text.text(c.label.text);
return label;
};
let valueText = createLabel(c);
step-5-external-configuration.html
In order to allow the "Gauge()" function to take external configuration parameters, I created the following method to merge the external configuration object with the default configuration object.
var jmerge = function(t, s) {
if (t === null || typeof t === 'undefined'
|| s === null || typeof s === 'undefined') { return s;}
if ((typeof t !== 'object') || (typeof s !== 'object')) { return s; }
if ((t instanceof Array) || (s instanceof Array)) { return s; }
if ((t instanceof String) || (s instanceof String)) { return s; }
for (var key in s) {
if (s.hasOwnProperty(key)) {
if (!t.hasOwnProperty(key)) { t[key] = s[key]; }
else { t[key] = jmerge(t[key], s[key]); }
}
}
return t;
}
The "jmerge" function recursively merges the "s" object into the "t" object and returns the merged result. It is not a perfect Javascript merge function, but it should be good enough to serve the purpose of this example. With the help of the "jmerge" function, the "Gauge" function now takes an external configuration object.
let Gauge = function(config) {
let c = {
element: 'body',
width: 200,
height: 100,
margin: 4,
yscale: 0.75,
chopAngle: 60,
thickness: 30,
value: {
minValue: 0,
maxValue: 100,
initialvalue: 60
},
label: {
yoffset: 10,
unit: 'unit.',
labelText: {
text: 'REMAINING',
yoffset: 12
}
},
gaugeColor: '#dde9f7',
indicatorColor: '#4281a4',
backgroundColor: '#f5f5f5'
};
c = config? jmerge(c, config): c;
};
When displaying the chart, we can try to give it some external configuration parameters to override the default configuration.
let gauge = new Gauge({
margin: 10,
label: {
unit: 'Hours',
labelText: {
text: 'GOING-ON'
}
}
});
step-6-update-the-value.html
In order that we can update the gauge display with the real-time data, I added the "update" method to the "Gauge" construction function.
let self = this;
self.update = function(value) {
let data = getIndicatorData(value);
indicator.attr('d', createArc(data.sa, data.ea))
valueText.text(value);
};
We can then make a call to the "update" method to change the value of the gauge.
let gauge = new Gauge({
margin: 10,
label: {
unit: 'Hours',
labelText: {
text: 'GOING-ON'
}
}
});
gauge.update(40);
Points of Interest
- This is a gauge chart by D3 built by 6 baby steps;
- I hope you like my postings and I hope this note can help you one way or the other.
History
First Revision - 12/1/2016