Using JavaScript and D3 to Graph a Value in a Range

A project came up recently where I needed to display a value on a range of values. You can imagine something like the display on an old thermostat: a range of values are displayed with a marker to indicate the current value.

The prior solution involved an image of the range of numbers with an image of the dot floated over it using CSS and JavaScript. It was a clever solution but once the site moved to a responsive design, it became difficult to maintain since the browser was scaling everything.

When the opportunity came up to rewrite the graph, I chose D3.

If you’re not familiar with it, D3 stands for Data-Driven Documents. It’s a library for building custom data visualizations. Anything where you have a document that needs to react to a set of data. You could use it for common charts and graphs but those things aren’t built into the library so it would probably be overkill. D3 is meant more for custom graphs, charts and visualizations.

Since I was unable to find a library with the necessary graph, I thought this would be a great opportunity to try out D3.

Example


D3 works by taking an HTML or SVG element, binding data to that element, and then transforming or manipulating it to react to that data. Elements may be added dynamically to the page or may be hard coded.

Here is a link to the full code repository:
https://github.com/gwmccull/d3-graph-value-in-range

Adding SVG to your page

In the example above, the only element that is coded into the HTML of the page is:

<svg id="graph-value-range" width="480" height="100" viewBox="0 0 1000 200" preserveAspectRatio="xMinYMin"></svg>

Everything else in the graph, has been dynamically added to the SVG container element using D3.

I’ll start by talking about this SVG container element and then dive into the code that builds the graph. The SVG contains the following pieces:

  • D3 works using CSS selectors. The id makes it easy to find the SVG element on the page.
  • The width & height are the sizes of the SVG element in pixels. You may not need these based on your application. However, if you don’t supply them, IE likes to automatically assign a height of 150px to your SVG element. I believe that SVG is considered a block level element, so if you don’t supply a width, the browser will make it take up the whole line. If your graph contains scaling and non-scaling elements (for example, text labels in a responsive graph), then you’ll want to fix the width & height so that you can calculate the scaling factor correctly (the width in this example could be 100%).
  • The viewBox is the coordinate system for your SVG element that allows you to position things accurately in the element. The first two digits, 0 0, are the X,Y starting coordinates. The last two, 1000 200, are the X,Y ending coordinates.
  • preserveAspectRatio instructs the browser on how to scale the SVG if it is resized. It’s useful for responsive graphs but not really needed here. This attribute has a lot of different options.

Setting Up For D3

The next portion of code is used to set up a bunch of variables that will be used by D3:

var ticks = [
    { value: 1, type: 'major'},
    { value: 2, type: 'minor'},
    { value: 3, type: 'minor'},
    { value: 4, type: 'minor'}, 
    { value: 5, type: 'major'}
];
 
var marker = {};
marker.width = 30;
marker.height = 30;
 
var tick = {};
tick.major = {};
tick.major.height = 40;
tick.minor = {};
tick.minor.height = 20;
tick.lineWidth = 8;
 
var graph = {};
graph.totalWidth = 1000;
graph.width = graph.totalWidth - marker.width;
graph.markerCount = ticks.length;
 
tick.increment = graph.width / (graph.markerCount - 1);

ticks is an array that will be used to create the tick marks below the graph. D3 operates mostly on arrays so this type of construct is common. If there were a lot, you could generate this data with a loop. marker represents the arrow on graph that shows the value. tick represents the size of each tick mark. graph is the information about the graph itself.

I like to set up all of my values using variables to make it easier to change up the graph and to perform the math needed to calculate the exact position of an element.

The next line is also important:

var svg = d3.select('#graph-value-range');

This line uses a CSS selector passed to the D3 select function to find the SVG element on the page and save a reference to it. I’ll be using this variable a lot to allow me to add elements to the SVG element.

Creating The Graph

The next few lines aren’t too exciting, so I’ll skip to the meat of the code. This is the function that draws the graph:

function renderGraph() {
    var valueRange = svg.append('g')
            .attr('id', 'value-range');
 
    for (var ii = 1; ii < ticks.length; ii++) {
        valueRange.append('line')
                .data([ii])
                .attr('class', 'base-line')
                .attr('y1', 0)
                .attr('y2', 0)
                .attr('x1', wrapCalcPosition.call(this, 0))
                .attr('x2', wrapCalcPosition.call(this, 1))
                .attr('stroke', wrapGetColor.call(this, 0.5))
                .attr('stroke-width', tick.lineWidth)
                .attr('transform', 'translate(0,' + (tick.major.height - (tick.lineWidth / 2)) + ')');
    }
 
    valueRange.selectAll('.tick')
            .data(ticks)
            .enter()
            .append('line')
            .attr('class', 'tick')
            .attr('y1', function(d) {
                return tick.major.height - tick[d.type].height;
            })
            .attr('y2', tick.major.height)
            .attr('x1', calcPosition)
            .attr('x2', calcPosition)
            .attr('stroke', getColor)
            .attr('stroke-width', tick.lineWidth);
 
    valueRange.selectAll('.label')
            .data(ticks)
            .enter()
            .append('text')
            .attr('class', 'label')
            .attr('y', tick.major.height * 2)
            .attr('x', calcPosition)
            .attr('text-anchor', 'middle')
            .attr('font-size', '40px')
            .attr('fill', getColor)
            .text(function(d) {
                return d.value;
            })
 
}

I start off here by appending a g tag to the SVG element. The g tag is used to group other elements together. It’s useful in cases where you might need to add a bunch of elements and then move them as a unit.

Next I use a for loop to add the horizontal lines that appear between the tick marks. First, I use the data function to bind the looping variable, ii, to the line segment. I do this so that the data element will be available later for the other chained functions. attr and other functions can take a function as their 2nd argument. If the 2nd is a function, then D3 will attempt to pass two variables to that function, d and i. d represents the data that was bound to this element using the data function. i represents the index of the element in the array that D3 is operating on. In this example, since I’m binding a single element array to my selected element, the index would always be 0.

Note, it’s important that you always ensure that the data that you are binding is in an array. If you try to pass an integer, D3 will silently fail.

I create the lines with a Y position of 0, and then use a translate to move all of the lines into the correct Y position. Alternatively, I could have used the math formula to calculate the Y position and skipped the translate altogether.

The other unusual thing you’ll note here is the functions, wrapCalcPosition and wrapGetColor. As I mentioned before, D3 allows you to pass a function as the 2nd argument to attr. Typically, that is done by creating an anonymous inline function. However, it is considered bad practice to create anonymous functions in a loop. To prevent that, I’ve created a function that returns a function. I’ll talk more about this construct below.

After the for loop is the code to create the tick marks and labels. Both are similar so I’ll talk about them together.

In both cases, I’m using the variable that references my g tag and appending some stuff to it. Since I’m binding an array of data with multiple elements, I’ll be applying each of the chained functions to each of the elements in the array. So imagine all of those functions running for every single tick mark and label.

The tick mark code shows a good example of creating an anonymous function that receives the data, d, that is bound to the element. In this case, it’s appropriate, because the anonymous function is only being created once. Although, I could have used the same pattern as the wrapCalcPosition.call() code from before.

You’ll also note that the calcPosition and getColor functions are referenced but not called (calling it would look like this: calcPosition()). Here I’m taking advantage of the fact that D3 will automatically pass the data element to the function when it calls it as the first argument. Later, I’ll show the definition of the calcPosition function where it receives that data as the first argument.

As an aside, many of the attributes that I set on these SVG elements (for example, text-anchor and font-size) may also be set using CSS.

And that’s pretty much it for the graph.

Adding The Marker

Next up, here is how we render the moving arrow:

function renderMarker(value) {
    svg.select('#marker').remove();
 
    svg.append('polygon')
            .data(value)
            .attr('id', 'marker')
            .attr('points', (marker.width / 2 * -1) + ',0 ' + (marker.width / 2) + ',0 ' + 0 + ',' + marker.height)
            .attr('fill', function(d) {
                return getColor({
                    value: d
                })
            })
            .attr('transform', function(d) {
                return 'translate(' + calcPosition({value: d}) + ',' + (tick.major.height - marker.height - tick.lineWidth) + ')';
            })
}

The first thing I do with the marker is I select the marker from the SVG variable and remove it. I’ve added a random number generator to this project to automatically change the data around. But a realistic project might also have data that changes, either due to the data being returned asynchronously from a data service or because the data is modified over time. If I didn’t remove the old marker, D3 would insert a new marker each time and the graph would quickly get cluttered with a bunch of markers.

This line is also useful if your graph contains some scalable and some non-scalable elements in a responsive graph. Otherwise, when the size of the window is changed, you’d see your graph elements appear to be smeared across the page.

If you know that your data is available at render time, it will never change during the life of the page, and the scaling issue doesn’t apply, this line isn’t needed.

Next, I append the triangle to the SVG element as a polygon.

I created the triangle so that it is centered at 0 on the X axis. This allows me to use the same function for calculating the position as I used for the tick marks. I then use a translate to move it into the correct position.

Helper Functions

Finally, I have the helper functions that are used throughout this example:

function calcPosition(d) {
    return (tick.increment * (d.value - 1)) + (marker.width / 2);
}
 
function wrapCalcPosition(increment) {
    return function(d) {
        return calcPosition({
            value: d + increment
        });
    };
}
 
function getColor(d) {
    var color;
    if (d.value < 2) {
        color = 'blue';
    } else if (d.value <= 4) {
        color = 'purple';
    } else{
        color = 'red';
    }
 
    return color;
}
 
function wrapGetColor(increment) {
    return function(d) {
        return getColor({
            value: d + increment
        });
    };
}

calcPosition calculates the horizontal position of something based on it’s data element. I made sure that the data element comes in as the first argument so that I could take advantage of how D3 calls functions with implicit arguments. You’ll not that I subtract 1 from the data and then add half of the marker width. The value range goes from 1 to 5 in this example but it’s easier to calculate the position if I treat it like it goes from 0 to 4. I add back half of the marker width to account for the padding on the left side of the graph. This padding prevents the marker from getting cut off when it’s value is 1.

wrapCalcPosition is a wrapper that allows me to use .call() to pass in the correct context. The wrapper function returns a function. .call() is run on the wrapper function and passes in the increment argument. D3 then receives the returned function and executes it with the implicit d and i arguments that I mentioned earlier. I can then take advantage of JavaScript’s closure feature to call calcPosition with the correct arguments.

The same more or less applies to getColor and wrapGetColor.

getColor is pretty much the only place in this example where I have hard coded values that would need to be edited if I changed the range of values for the graph. If I wasn’t sure about the range, I could probably write an algorithm that would determine the colors. For example, the top 20% could be red, the bottom 20% might be blue, and the rest would be purple.

If getColor needed to do a bunch more work besides just determining the color (for example, if it also needed to change the font size or text anchor), then I could have it return the name of a class. Since I used .call() to call wrapGetColor with a context of this, the this of wrapGetColor refers to the element that D3 is operating on. We can take advantage of that by applying the returned class directly to the element using the following: d3.select(this).classed(className, true)

Mixing Scalable And Non-Scalable Elements In A Responsive SVG

The page I originally built this graph for had a responsive design. The width of the SVG graph varied by the browser width. Since Scalable is built into the name of SVG (Scalable Vector Graphics), you’d think this wouldn’t be a problem. And, for the most part, it’s not. When the size of the graphic changes, the browser is easily able to scale the SVG element and everything within. All of the graphical elements stay sharp and correctly placed.

However, some things you might not want to scale. For example, if text scales too small, it becomes difficult to read. If you have an SVG graph that can be scaled to a small size but you don’t want the labels on the graph to get to small, then you have to start manually moving and scaling individual pieces of the graph.

I don’t have an example that I can provide that demonstrates this but I can try to walk through the concepts.

To start, I determined the “normal” size of the graph. It was the width, in pixels, of the graph as I designed it. Then I created a function that used D3 to select the SVG elements, get it’s width and return the ratio of the designed width to the current width. This is the scaling factor.

Then I used that scaling factor to cancel out the browser scaling on elements that were not designed to supposed to scale (ie, the labels and the marker arrow). Additionally, I had to apply the scaling factor to the Y position of all of the elements to prevent the graph from moving up and covering the label above the graph.

All of this was made much easier by smart use of the g tag to group scaling and non-scaling elements and then scaling the entire group.

Well, I hope that helps everyone. Leave comments if you have questions.

Leave a Comment