===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 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**. ^Line^Meaning^ |.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; })|The value of the bar (for tooltips) |.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 Script5|Writing the Script - Part 5]]