Click here to Skip to main content
13,146,742 members (79,795 online)
Click here to Skip to main content
Add your own
alternative version

Stats

5.6K views
130 downloads
5 bookmarked
Posted 1 Dec 2016

A Gauge Chart - Step & Step

, 1 Dec 2016
Rate this:
Please Sign up or sign in to vote.
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'.

<!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);
        
    // 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. 

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

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

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'
    };
    
    // 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.

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) {
        
        // 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.

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)

Share

About the Author

Dr. Song Li
United States United States
I have been working in the IT industry for some time. It is still exciting and I am still learning. I am a happy and honest person, and I want to be your friend.

You may also be interested in...

Comments and Discussions

 
Questionway to learn java Pin
Ema Cruz2-Dec-16 22:44
memberEma Cruz2-Dec-16 22:44 
AnswerRe: way to learn java Pin
Dr. Song Li4-Dec-16 14:08
mvpDr. Song Li4-Dec-16 14:08 
GeneralRe: way to learn java Pin
Ema Cruz4-Dec-16 17:08
memberEma Cruz4-Dec-16 17:08 
QuestionNice Pin
Sacha Barber2-Dec-16 0:40
mvpSacha Barber2-Dec-16 0:40 
AnswerRe: Nice Pin
Dr. Song Li2-Dec-16 10:47
mvpDr. Song Li2-Dec-16 10:47 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.170915.1 | Last Updated 1 Dec 2016
Article Copyright 2016 by Dr. Song Li
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid