Writing the Script - Part 4

To get started with our draw function, we're going to need to…

1) Prepare our data
2) Create the axes for our graph,
3) Draw our bars


Preparing Our Data

First, we'll make our data simple.

Currently, the data value is in one array, and the data name is in another. We'll build an array of objects that contain both pieces of information.

var finaldata = [];		
for(var q=0;q<data.length;q++)
{
	finaldata.push({name: this.columns[q].name,value: parseFloat(data[q])});						
}

This gives us an array of objects, each with a name and a value property.


Create an X Axis

D3.js uses scale objects to define your different axes. These are used to translate values into coordinates.

In a bar-chart, we'll need two axes. We're going to have our temperature on the horizontal or X axis, and split the different sensors up into bars across vertical or Y axis.

The X axis is a simple, linear scale. But first, we'll need to know where the scale starts and finishes.

var minmax = d3.extent(finaldata,d => d.value);	

This function returns an array containing the minimum and maximum of the 'value' attribute inside our array.

Next, we'll need to know how big our image is going to be. For these, we can use the margin property of our Report object.

var width = this.margin.right - this.margin.left;
var height = this.margin.bottom - this.margin.top;

This gives us the width and height we have to work with inside our image.

Next, we turn this information - the width of our image and the maximum value on the chart - into our X axis. The domain of a d3 scale is it's measured value range, while the range is the coordinate range.

var x = d3.scaleLinear().domain([0,minmax[1]*1.1]).range([0,width]);

This creates a scale that converts our value to a position on the X axis. We've also added a bonus 10% to the range, so that there's room between the longest bar and the edge of the screen

Next, we can draw that axis onto our SVG file, with the code below…

this.svgbase.append("g").call(d3.axisBottom(x));

This code…

* Adds a 'group' (an invisible SVG element that combines multiple child objects) to the SVG file.
* Calls the 'd3.axisBottom' function to draw the x axis

There's only one remaining problem - if we ran the code, we'd find that the X axis is at the top of the screen, and we'd like to put it at the bottom.

Fortunately, you can add a transform to any SVG element (particularly groups) to move them around the image. We can add a transform attribute to re-position our axis with the following change…

this.svgbase.append("g").call(d3.axisBottom(x))
          .attr("transform","translate(0," + height + ")");

This sets the transform attribute of the group we created. By translating the object by zero in the X direction and the height of the image in the Y direction, we push it down to the bottom of the Infographic.


Create a Y Axis

The Y axis isn't quite as simple as a linear scale - we have a set of distinct things (or temperature sensors).

For this, we can use a d3 band scale, used for exactly these situations.

var y = d3.scaleBand().range([0,height]).domain(finaldata.map(d => d.name)).padding(.1);

The 'map' function creates an array of column names. This new scale will take the name of our data channel and convert it into a position on the Y axis. We'll add a 10% padding between each bar.

We can now draw that Y axis.

this.svgbase.append("g").call(d3.axisLeft(y));

The append function adds a new SVG 'group' object, then calls the axisLeft function, which does the actual drawing.


Draw The Bars

It can take a little bit of getting used to how d3.js works. It's called data driven documents for a reason.

Instead of you creating a loop and manually drawing each of your bars, you let the data do that for you.

You use the selectAll function to choose all of the bars that already exist in your image (the first time you do this, there will be none at all).

Then you can use the join function, which will create, update or destroy the bars as needed.

this.svgbase.selectAll(".bar").data(finaldata)
   .join(
      enter => enter.append("rect")
                     .attr("class","bar element")
                     .attr("x",function (d) { return x(0); })
                     .attr("y",function (d) { return y(d.name); })
                     .attr("width", function(d) { return x(d.value);})
                     .attr("height",y.bandwidth)
                     .attr("fill","cyan")
                     .attr("name",function(d) { return d.name; })
		     .attr("value",function(d) { return d.value; })					
		     .attr("units"," Deg C")
		     .call(this.tip)
   );

This can look really daunting the first time you see it. But once you're used to the way d3 works, it's actually not anything like as complex as it seems.

The selectAll function gathers all of the objects that match the selector - in this case, all of the objects of the class 'bar'.

Initially, this will be an empty list.

The data command tells d3 that it should be performing the following commands (ie. join) on every item in the finaldata array - the one that contains the name and value for each of our bars.

The join function looks to see if we already have a bar it can use in the list we got from selectAll.

For each bar, there are three things that could happen. It could enter the scene (creating a new object), update (modify an existing object to show new values) or exit the scene (deleting an object).

In this case, we've given instructions for append. When a new bar is needed, it creates a new rect (rectangle) element. We then use the attr function we used earlier to set all of the attributes of that object - the X, Y, Width, Height, Class, Name, Value and Units attributes.

The attr function takes two parameters - the attribute you want to set, and the value you want to set it to. But there's a trick here. If the value is a function, it will get called only when the value is actually needed, and it will be passed a parameter - the array value that caused the join to happen. In this case, the parameter to each of the functions will be an object with the name and value for the bar.

Let's look at this line-by-line, remembering that when you see functions, the 'd' parameter is the object that has both the bars name and value.

LineMeaning
.attr(“class”,“bar element”)Sets the class for the bar
.attr(“x”,function (d) { return x(0); })Sets the X coordinate (fixed at 0)
.attr(“y”,function (d) { return y(d.name); })Sets the Y coordinate, using the Y scale
.attr(“width”, function(d) { return x(d.value);})Sets the width of the rectangle
.attr(“height”,y.bandwidth)The bandScale has a function to return the size of each band
.attr(“fill”,“cyan”)Sets the bars colour
.attr(“name”,function(d) { return d.name; })The name of the bar (for tooltips)
.attr(“value”,function(d) { return d.value; })
.attr(“units”,“ Deg C”)The units of measurement (for tooltips)
.call(this.tip)Registers the bar for tooltips

So assuming you only had one temperature sensor, your finaldata array would look like this…

{
   "name": "Area Sensor 1.Temperature - Air",
   "value": 22.3
}

..and the following steps would happen.

1) The image would be searched for any objects of class 'bar'.
2) D3 would then combine that list with the array of the bars we want to create
3) Since there are no bars yet, the enter function would be called.
4) A rect object would be created for the bar
5) Each of the attributes would be set - during which, each of the functions would be called
6) Each function is sent the object as the first parameter - so y(d.name) would translate the name 'Area Sensor 1.Temperature - Air' into a Y coordinate on the image.
7) Finally, the bar would be marked as interactive, so tooltips appear when you hover over it.


Writing the Script - Part 5