Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / XHTML

A Gauge Chart - Step & Step

4.78/5 (4 votes)
1 Dec 2016CPOL4 min read 23.6K   285  
This is a gauge chart by D3 step by step.

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'.

HTML
<!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.

JavaScript
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);
        
    // Add a rect as the background
    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)');
    };
        
    // Create a 1/2 circle arc and put it at the center
    addArc(createArc(-90, 90), c.gaugeColor);    
};
    
window.addEventListener('load', function() {
    // Test the createGauge 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. 

JavaScript
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.

JavaScript
let r = (c.width - 2*c.margin)/2;

The center of the arc is also adjusted to include the margins into the consideration.

JavaScript
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.

JavaScript
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.

JavaScript
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.

JavaScript
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.

JavaScript
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.

JavaScript
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
        };
    }
    
// Add the initial indicator
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.

JavaScript
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.

JavaScript
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;
        
    };
    
// Add the 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.

JavaScript
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.

JavaScript
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'
    };
    
    // Merge the external config to the default config
    c = config? jmerge(c, config): c;
    
    // Rest of the code ...
};

When displaying the chart, we can try to give it some external configuration parameters to override the default configuration.

JavaScript
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.

JavaScript
let self = this;
self.update = function(value) {
        
        // Update the indicator
        let data = getIndicatorData(value);
        indicator.attr('d', createArc(data.sa, data.ea))
        
        // Update the label
        valueText.text(value);
    };

We can then make a call to the "update" method to change the value of the gauge.

JavaScript
let gauge = new Gauge({
        margin: 10,
        label: {
            unit: 'Hours',
            labelText: {
                text: 'GOING-ON'
            }
        }
    });
    
// Test the update function
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)