Linechart and Scatterplot with D3

In this tutorial we do our first linesplot and scatterplot. In addition to the D3 links mentioned on the first page, the D3 Tips and Tricks by D3Noob and Scott Murray's D3 tutorials are also very helpful. We use the structure from D3Noob's Linechart and Scatterplot tutorials here. Before we build our Linechart and Scatterplot, we will need to learn about the component building blocks, that is sets of methods and function chunks that we use across any D3 we write. First up, we learn these.

Embedding D3 in an HTML File

By now, we know that any new project we do, goes in as a separate page in our folder structure, as before. For this project, insert the following line inside the <head></head> tag of your HTML document just uder the <title></title> tag:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Lines plot</title>
    <script src="https://d3js.org/d3.v4.min.js"></script>
    <style> 
    </style>
  </head>
  <body>
    <script>
    </script>
  </body>
</html>
				

Note that we are using D3 version 4, which is a new version and the syntax of some of its methods differ from most of the online tutorials that are written using older versions (v3 and v2).

So, now after including the d3 link, we expect to have access to d3 methods inside the document. To check, copy the above code and save it as .html document and run it on a localhost on a Chrome browser. Then, open the JavaScript console and type d3, the console will return an object. It means that d3 is embedded in your document now. Now, we can access all D3 methods using the d3.methodName syntax inside the <script></script> tag of your HTML document.

Selecting Elements Using D3

Recall that one of the main strengths of JavaScript is that we can alter DOM elements after the page has already loaded. So, to do this, we will need to select elements, to change them. We use the D3.select() method to select a single element from the DOM (say the body, or a single paragraph element p). select returns a reference/selection to the first matching element in the DOM of that type. Similarly, the method D3.selectAll() returns all matching references to a certain type. For example, selectAll("h3") will select all the h3 elements.

To select element/elements from the DOM, pass the name of its HTML tag, id name or class name between two quotation marks inside the brackets of select/selectAll methods:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Lines plot</title>
    <script src="https://d3js.org/d3.v4.min.js"></script>
    <style> 
    </style>
  </head>
  <body>
    
  	<h1>Heading</h1>
  	<p class="paragraph">Hello, world</p>
  	<p id="end">See you, world</p>
  	
    <script>
    
    	var test1 = d3.select("h1");
    	var test2 = d3.select(".paragraph");
    	var test3 = d3.select("#end");
    
    </script>
  </body>
</html>
				

Save the code above and run it on a chrome browser, then, open the console and type test1, test2 and test3 each at a time and click the Return key, an object that represents the selected element will return to the console.

Adding Elements Using D3

We can add an element to the DOM using d3.append() method. It adds an element to a selected DOM element. The added object could be an HTML or SVG item. Check the following example:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Lines plot</title>
    <script src="https://d3js.org/d3.v4.min.js"></script>
    <style> 
    </style>
  </head>
  <body>
    <script>
    
    	var body = d3.select("body");
    	var p = body.append("p");
    	p.text("New paragraph!");
    
    </script>
  </body>
</html>
				

Check the results in the browser as well as the web inspector.

Chaining D3 Methods

As we have seen in the last tutorial, we can "chain methods" and reduce the need for many new variables, by using the output of one method as input into another. We can chain d3 methods into a single line of code to make it fast and easy to read by using the period notation (.) to pass a reference/selection from the previous method to the next method after the (.) . If we apply chaining on the previous code, it will appear like the following:

Note: from here forward we will only mention the code inside the <script></script> tag since the HTML code in other places is repeated. Thus, to check the result of each step, you can add the new code to the base HTML file from previous steps and test it by running the document on the localhost and interacting with it using Chrome Development Tools.

<script>

	d3.select("body").append("p").text("New paragraph!");

</script>
				

Binding Data Using D3

Data visualisation is a process of mapping data to visuals. Data in, visual properties out. Thus, you as a designer decide this mapping and code it in. Higher the number, taller the bar. Higher the number, darker the colour. And so on. With D3, we bind our data input values to elements in the DOM including HTML and SVG elements. Binding is “attaching” or associating data to specific elements so that later you can reference those elements to apply mapping rules on them. Binding typically involves 5 steps. We will explain them using the following example from Scott Murray's book:

<script>

	var dataset = [ 5, 10, 15, 20, 25 ]; 
		d3.select("body").selectAll("p") // step1
			.data(dataset) // step2
			.enter() // step3
			.append("p") // step4
			.text("New paragraph!"); // step5

</script>
				
  1. Making selection: select the DOM elements that you want your data to associate with. The above example finds the body in the DOM and hands its reference off to the next step in the chain. selectAll("p") select all the paragraphs in the DOM. Since none exist yet, it returns an empty selection. It represents the paragraphs that will exist soon.
  2. Data: Counts and parse our data values. Since there are five elements in our data (dataset), everything past this point is executed five times. The data() method loops through a number of times, which is equal to the number of elements in the dataset.
  3. enter(): This method looks at the DOM and then at the data being handed to it. If there are more data values than corresponding DOM elements, then it creates a new placeholder and hands off a reference to this new placeholder to the next step in the chain. That is, to all effects, a "container ghost" of a new "p" element!
  4. append(): takes the placeholder selection created by enter() and use it to insert a DOM element. Then, it hands off a reference to the elements it just created to the next step in the chain.
  5. Assign values to the properties of the added element: e.g. text: take the reference to the newly created element and add a text value to it

When D3 binds data to an element, that data doesn't exist in the DOM, but it does exist in the memory of the element as a _data_ attribute , which can be viewed using the console e.g. console.log(d3.selectAll("p"));

Accessing Data Using D3

Just to create 5 new paragraphs because we have 5 data elements isn't all that interesting. What we really want to do is access the data, so we can do something with it. When chaining methods together, after calling the data() method, you can create an anonymous function that accepts d and i as input. Recall that when we learnt how to make functions in JavaScript, we were storing the function in a variable? Well, an anonymous function is a different type, where we don't need to do that. Instead, we just declare the function directly using its input parameters, and the function body. The data() method will make sure that d is set to the value in your original dataset that corresponds to the current element and i is set to the index of that element. Don't get confused here by the d, remember that because our methods are chained, what goes into text is simply what comes out from the previous methods. Its confusing, almost magical, but as we do more of it, it becomes a whole lot clearer. Hang on there!

<script>

	var dataset = [5, 10, 15, 20, 25];
	d3.select("body").selectAll("p")
	  .data(dataset)
	  .enter()
	  .append("p")
	  .text(function(d, i) {
	    return "the value is: " + d + "and the index is: " + i;
	});

</script>
				

Modifying Elements' Attributes and Properties Using D3

attr() and style() are D3 methods, which allow us to set the attributes of a selected element (HTML/SVG) using attr() and its css properties using style()

There are a number of attributes, which can be set using attr() including: class, id, src, width, alt and others

CSS properties are many including: color, fill, stroke, font-size and others

<script>

  var dataset = [ 5, 10, 15, 20, 25 ];
    d3.select("body").selectAll("p")
      .data(dataset)
      .enter()
      .append("p")
      .text(function(d) {
        return "the value is: " + d;
      })
      .attr("class", "paragraph")
      .style("color", function(d) {
        if (d > 15) {
          return "red";
        } else {
          return "black";
        }
  });

</script>
				

Drawing with D3

The basic concept of drawing using D3 is by selecting a DOM element and append SVG or HTML elements to it and modify their attributes and style properties using the methods we learned earlier.

In the case of using SVG elements, we first Select an existing HTML element (body for example) and append the svg canvas element that we learned about in Week 5 tutorial. Then, we add svg elements (we also learned them in week 5 tutorial) to the svg canvas and then, bind data to them and modify their attributes and styles until we get the shape that we want.

This is just to understand the main concept. Later, we will go through a detailed example.

Reading Data from a File

Download the RentTrendByArea_Inner.csv file from the Black Board and paste it inside your project folder. We will read the data from this file using d3.csv() method:

<script>

  d3.csv("RentTrendByArea_Inner.csv", function(error, data) {
    if (error) {
      console.error(error);
    }
    console.log(data);
    for(var i = 0; i < data.length; i++) {
      console.log(data[i].rent);
    }
  });

</script>
				

The first attribute of the d3.csv method is the .csv file location; the second attribute is a callback function where you can access the data. The callback function, in turn, has two attributes; the first is an error in case there was an error in reading the data from the file. Notice that we handled that error in the next line by outputting it using the console.error method. The second attribute data could be any name. It holds all the data in the .csv file. It takes the form of an array of objects. Each object is a row in the csv table. Each object has properties, which are the headings of all the columns for that row.

Now, we have the data ready, and we can access different rows using the index number of the items inside the array and different columns using the property name of the item/object. Check the result of the console.log in the browser Developing Tools.

Note: we can only access the data inside the d3.csv method or by passing the data from inside this function to an outside function. We will discuss that later in details.

Filtering Data

We can decide which data is accessible using filter method. It follows the syntax:

data.filter(the elements you want to keep);

Inside this method, you can pass a function to access the data and filter them based on conditions:

<script>
  d3.csv("RentTrendByArea_Inner.csv", function(error, data) {
    if (error) {
      console.error(error);
    }

    data = data.filter(function(obj) {
      if(obj.rent < 200) { 
      // do nothing
      } else { 
        return obj;
      }
    });

  });
</script>
				

In the code above, we decided to exclude the elements/objects of the array or the rows in the csv file that have rent values smaller than 200.

Formatting Data

We can go through all data values using forEach() method and format these values based on our needs. This includes changing the number values that have been read as strings to a proper number format or changing the date values that are also read as strings to a proper date format.

To change a number that is read as a string to a number type variable, we can simply add + sign before it.

To change the data from a string to a date type variable, we can use d3.timeParse() method, which returns a function which we can call and pass to it the value that we want to convert:

<script>
  d3.csv("RentTrendByArea_Inner.csv", function(error, data) {
    if (error) {
      console.error(error);
    }

    var parseTime = d3.timeParse("%b-%y"); // to change the value to Month-Year format
    data.forEach(function(d) {
      d.quarter = parseTime(d.quarter);
      d.rent = +d.rent;
      d.income = +d.income;
    });

  });
</script>
				

Now, all the values in the rent and income columns have changed to numbers format and the values in the quarter column have changed to a date format.

Scales

When we map our data to a graph, we need to make sure that the dimensions of the graph fit within the space we are allocating on the page. This means that we will need to scale the original values to suitable dimensions. D3 has predefined scale methods that can help us achieving this purpose.

Mike Bostock's definition of a scale is as follows: "Scales are functions that map from an input domain to an output range." What does this mean?

The black line below shows you the "real" spread of your possible input data values. This is the input domain of the scale and shows us the total range over which our real data values lie. You can see that it is very small visually. Obviously, we cannot plot a graph on a webpage where each data point equals each pixel value! It will create problems when the data range is too small, as in this example, and when the data range is too large (say from 0 to 1 million). So we have to "stretch" or "compress" the data range to a visual map the size of which fits in our canvas or drawing area, but at the same time ensure that the proportional gaps between data points are accurately maintained. What should we do?

15 75 40

The blue line shows our desried length for the graph, which we decide as the information designer. Note that now (75-15=60) is stretched to 3 times the size (190-10=180). We would now like not to have to compute this for each data point, and automate it instead. So, we can turn this into a function which has an input domain of [15,75] and an output domain of [10,190].

10 190 100

When the function eats 15, it spits out 10. If we are doing a bar chart, for example, the length of the smallest bar can be 10 pixels.

Similarly, when the function eats 75, it spits out 190.

If we feed it any value in the middle, it will spit out the relative corresponding position in the output scale. For example, the mid-point on the input domain should give us the mid-point on the output range.

Thus, Scale is a function that map from original/input domain to aim/output range

d3.scaleLinear() method is the most popular. It is chained to .domain([min, max]) method to which we pass the minimum and the maximum values of the original data as an array. It is also chained to .range([min, max]) method, which also takes an array of two elements; the minimum and the maximum values of the output.

We can use d3.extent() method, which returns the minimum and the maximum values of a data set in the form of an array. We can also use d3.max, d3.min to get the maximum and the minimum values of a set of data

<script>

  var height = 500;
  var width = 1000;

  d3.csv("RentTrendByArea_Inner.csv", function(error, data) {
    if (error) {
      console.error(error);
    }
    var parseTime = d3.timeParse("%b-%y"); // to change the value to Month-Year format
    data.forEach(function(d) {
      d.quarter = parseTime(d.quarter);
      d.rent = +d.rent;
      d.income = +d.income;
    });

	  var yScale = d3.scaleLinear()
                   .domain([0, d3.max(data, function(d) { return Math.max(d.rent, d.income); })])
                   .range([height, 0]);
    var xScale = d3.scaleTime()
                   .domain(d3.extent(data, function(d) { return d.quarter; }))
                   .range([0, width]);

  });
</script>
				

In the above d3.scaleLinear() method, we have chosen the minimum value of the domain to be 0 and we used d3.max method to get the highest value between the maximum value in the rent column and the maximum value in the income columns, which we got using Javascript Math.max() method. Inside the .range , we used the height variable, which represents the height of the intended graph, instead of value so that in case we need to change it we only need to modify the variable and not all corresponding values in every function. Originally, it was: .range([0, height]) , but we reversed the order to respond to the fact that as values on the Y-axis increase, their SVG position will move from top towards the bottom and so, by reversing the order, a smaller input to the yScale function will produce a large output and vice versa.

d3.scaleTime() method has a similar syntax, and it is used to scale date values, which is different from d3.scaleLinear() method, which scales regular numeric values in a linear manner.

Axes

axis is a function. Unlike scale, when we call axis, it doesn't return a value. Instead, it generates a visual element including lines, labels and ticks. There are four types of axes:

Between the two brackets of the axis method, we specify the scale that we want to use to scale the values of the axis. Axis will appear in the graph after we call it using .call(axisName) method:

<script>
  var height = 500;
  var width = 1000;
  d3.csv("RentTrendByArea_Inner.csv", function(error, data) {
    if (error) {
      console.error(error);
    }
    var parseTime = d3.timeParse("%b-%y"); // to change the value to Month-Year format
    data.forEach(function(d) {
      d.quarter = parseTime(d.quarter);
      d.rent = +d.rent;
      d.income = +d.income;
    });
	  var yScale = d3.scaleLinear()
                   .domain([0, d3.max(data, function(d) { return Math.max(d.rent, d.income); })])
                   .range([height, 0]);
    var xScale = d3.scaleTime()
                   .domain(d3.extent(data, function(d) { return d.quarter; }))
                   .range([0, width]);

    var svg = d3.select("#infovismain").append("svg")
                .attr("width", "1000px")
                .attr("height", "500px");
    svg.append("g")
       .call(d3.axisBottom(xScale));

  });
</script>
				

In the example above, we appended an SVG element to a div with the id #infovismain. This svg will be the canvas that contains other SVG elements.

We append a group g to the svg to hold the element that we are going to create later (axisBottom) inside the group. Holding created elements inside a group is a good practice, soon, we will know why.

We called the d3.axisBottom using the .call method. We applied the xScale, which we created earlier to the d3.axisBottom.

A Practice Example

The following example produces lines plot and a scatter plot. It uses most of the methods that we learned above.

First, let's set up the HTML file. Create a new project folder. Copy the following HTML code and save it to a file and name it index.html


<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Lines plot</title>
    <script src="https://d3js.org/d3.v4.min.js"></script>
    <style> /* set the CSS */
      .line {
        fill: none;
        stroke: #084594;
        stroke-width: 2px;
      }
      #title {
        background-color:#9ecae1;
        width: 960px;
        height: 45px;
        font-family: Helvetica, Arial, sans-serif;
        margin: 3px;
        padding:5px;
        margin-left: 10px;
      }
      #infovismain {
        background-color: #f7fbff;
        opacity: 1;
        width: 960px;
        height:500px;
        margin: 3px;
        padding: 5px;
        margin-left:10px;
      }
      #legendinfo {
        background-color:#9ecae1;
        width: 960px;
        height: 50px;
        font-family: Helvetica, Arial, sans-serif;
        margin: 3px;
        padding: 5px;
        margin-left: 10px;
      }
      #text {
        background-color: #f7fbff;
        opacity: 1;
        width: 960px;
        height:100%;
        margin: 3px;
        padding: 5px;
        margin-left:10px;
        font-family: Helvetica, Arial, sans-serif;
        text-align: justify;
        line-height: 150%;
      }
    </style>
  </head>
  <body>
    <div id="title">
      <h3>The reducing gap between incomes and rents: Inner Sydney's rent crisis</h3>
    </div>
    <div id="infovismain"></div>

    <script>
     
    </script>

    <div id="legendinfo">
      <p style="font-size:11px">
      Data source for income (red line): 6523.0, Household Income and Wealth, Australia, Equivalised disposable household incomes for NSW, Second income quintile mean incomes. Data before 1993 and after 2014 has been projected on the basis of 1993 and 2014 data, respectively.<br>
      Data source for rents (blue line): Housing New South Wales, Rent and Sales Reports. 
      </p>
    </div>
    <div id="text">
      <p>
      The chart above shows the progressively reducing gaps between lower middle incomes and rent trends in Inner Sydney. The red line shows income data trends over time for the second quintile of equivalised disposable household income in New South Wales. Equivalised disposable household income means the amount of income available to a household after taxes. The second quintile is the second lowest quintile of income as defined by the Australian Bureau of Statistics. The blue line shows the rental trends in inner Sydney from the Rent and Sales data provided by Housing NSW. The chart shows that over time there has been a progressive reduction of the gap between incomes and rents. The gap between the two lines provides an insight into disposable incomes: how much income is left over once rent is paid? The reducing gap shows a sharp reduction of disposable income left over after paying rent over time for the lower earning groups. For lower income earners living in inner Sydney, this implies reduced access to other benefits and amenities such as education, health or recreation.  
      </p>
    </div>
  </body>
</html>

				

This will be the base HTML structure on top of which, we will build our d3 code. Download the RentTrendByArea_Inner.csv file from the Black Board and paste it inside your project folder. You will add the code from the following steps between the <script></script> tag. Run the index.html document on the localhost and check the result after each step by saving the file and refreshing the localhost browser.

<script>

// set the dimensions and margins of the graph
  var margin = {top: 20, right: 20, bottom: 50, left: 50};
  var width = 960 - margin.left - margin.right;
  var height = 500 - margin.top - margin.bottom;

</script>
				

In the code above, we created an object margin that has properties to define the top, right, bottom and left margins. We will use it to define the margin of the graph area.

width and height variables will be used to define the width and height of the graph area.

<script>
// set the dimensions and margins of the graph
  var margin = {top: 20, right: 20, bottom: 50, left: 50};
  var width = 960 - margin.left - margin.right;
  var height = 500 - margin.top - margin.bottom;

  // append the svg object to the #infovismain div of the page
  // appends a 'group' element to 'svg'
  // moves the 'group' element to the top left margin
  var svg = d3.select("#infovismain").append("svg")
              .attr("width", width + margin.left + margin.right)
              .attr("height", height + margin.top + margin.bottom)
              .append("g")
              .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

</script>
				

svg variable is created to represent the SVG canvas in which we will draw the lines and circle and all other graph components later. The width and height of the svg were set with respect to the margin values.

transform is an attribute that may be new to us. It is responsible for moving, rotating and performing other transformations on an element (the group g in this case). translate is a transform operation that moves the element to a specific direction. The syntax is: attr("transform", "translate(horizontalDistance, verticalDistance)"); The transform attribute moves the group to create a margin distance between the elements that will come inside the group g and the outer edges of the svg container svg.

<script>
// set the dimensions and margins of the graph
  var margin = {top: 20, right: 20, bottom: 50, left: 50};
  var width = 960 - margin.left - margin.right;
  var height = 500 - margin.top - margin.bottom;
  // append the svg object to the #infovismain div of the page
  // appends a 'group' element to 'svg'
  // moves the 'group' element to the top left margin
  var svg = d3.select("#infovismain").append("svg")
              .attr("width", width + margin.left + margin.right)
              .attr("height", height + margin.top + margin.bottom)
              .append("g")
              .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

// Read the data from .csv file
      d3.csv("RentTrendByArea_Inner.csv", function(error, data) {
        if (error) {
          console.error(error);
        }
        
      });

</script>
				

Read the data from the .csv file and throw an error in case it happened.

<script>
// set the dimensions and margins of the graph
  var margin = {top: 20, right: 20, bottom: 50, left: 50};
  var width = 960 - margin.left - margin.right;
  var height = 500 - margin.top - margin.bottom;
  // append the svg object to the #infovismain div of the page
  // appends a 'group' element to 'svg'
  // moves the 'group' element to the top left margin
  var svg = d3.select("#infovismain").append("svg")
              .attr("width", width + margin.left + margin.right)
              .attr("height", height + margin.top + margin.bottom)
              .append("g")
              .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// Read the data from .csv file
      d3.csv("RentTrendByArea_Inner.csv", function(error, data) {
        if (error) {
          console.error(error);
        }

        // format the data
        var parseTime = d3.timeParse("%b-%y");
        data.forEach(function(d) {
          d.quarter = parseTime(d.quarter);
          d.rent = +d.rent;
          d.income = +d.income;
        });

      });
</script>
				

Format the data.

<script>
// set the dimensions and margins of the graph
  var margin = {top: 20, right: 20, bottom: 50, left: 50};
  var width = 960 - margin.left - margin.right;
  var height = 500 - margin.top - margin.bottom;
  // append the svg object to the #infovismain div of the page
  // appends a 'group' element to 'svg'
  // moves the 'group' element to the top left margin
  var svg = d3.select("#infovismain").append("svg")
              .attr("width", width + margin.left + margin.right)
              .attr("height", height + margin.top + margin.bottom)
              .append("g")
              .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// Read the data from .csv file
      d3.csv("RentTrendByArea_Inner.csv", function(error, data) {
        if (error) {
          console.error(error);
        }
        // format the data
        var parseTime = d3.timeParse("%b-%y");
        data.forEach(function(d) {
          d.quarter = parseTime(d.quarter);
          d.rent = +d.rent;
          d.income = +d.income;
        });

        // set the Scales
        var xScale = d3.scaleTime()
                       .domain(d3.extent(data, function(d) { return d.quarter; }))
                       .range([0, width]);
        var yScale = d3.scaleLinear()
                       .domain([0, d3.max(data, function(d) { return Math.max(d.rent, d.income); })])
                       .range([height, 0]);

      });
</script>
				

Set the scale.

<script>
// set the dimensions and margins of the graph
  var margin = {top: 20, right: 20, bottom: 50, left: 50};
  var width = 960 - margin.left - margin.right;
  var height = 500 - margin.top - margin.bottom;
  // append the svg object to the #infovismain div of the page
  // appends a 'group' element to 'svg'
  // moves the 'group' element to the top left margin
  var svg = d3.select("#infovismain").append("svg")
              .attr("width", width + margin.left + margin.right)
              .attr("height", height + margin.top + margin.bottom)
              .append("g")
              .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// Read the data from .csv file
      d3.csv("RentTrendByArea_Inner.csv", function(error, data) {
        if (error) {
          console.error(error);
        }
        // format the data
        var parseTime = d3.timeParse("%b-%y");
        data.forEach(function(d) {
          d.quarter = parseTime(d.quarter);
          d.rent = +d.rent;
          d.income = +d.income;
        });
        // set the Scales
        var xScale = d3.scaleTime()
                       .domain(d3.extent(data, function(d) { return d.quarter; }))
                       .range([0, width]);
        var yScale = d3.scaleLinear()
                       .domain([0, d3.max(data, function(d) { return Math.max(d.rent, d.income); })])
                       .range([height, 0]);

        // define the line generator
        var lineGenerator1 = d3.line()
                               .x(function(d) { return xScale(d.quarter); })
                               .y(function(d) { return yScale(d.rent); });
        var lineGenerator2 = d3.line()
                               .x(function(d) {return xScale(d.quarter); })
                               .y(function(d) {return yScale(d.income); }) 

      });
</script>
				

Later, we are going to draw two lines using svg path that we learnt about in week5 tutorial. The points of one path are represented by quarter data on the x-axis and rent data on the y-axis while the points of the other path are represented by the same x-axis (quarter data) with different y-axis (income data).

If you recall from week5 tutorial, the svg path element needs a d attribute, which describes the move-to and line-to points required to draw a line. This d attribute can be generated using the d3.line method, which is chained to .x() to specify data that represent the x-positions of the points and .y() to allocate data of the y-positions of the points. The two variables lineGenerator1 and lineGenerator2 will be used next to draw the paths.

<script>
// set the dimensions and margins of the graph
  var margin = {top: 20, right: 20, bottom: 50, left: 50};
  var width = 960 - margin.left - margin.right;
  var height = 500 - margin.top - margin.bottom;
  // append the svg object to the #infovismain div of the page
  // appends a 'group' element to 'svg'
  // moves the 'group' element to the top left margin
  var svg = d3.select("#infovismain").append("svg")
              .attr("width", width + margin.left + margin.right)
              .attr("height", height + margin.top + margin.bottom)
              .append("g")
              .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// Read the data from .csv file
      d3.csv("RentTrendByArea_Inner.csv", function(error, data) {
        if (error) {
          console.error(error);
        }
        // format the data
        var parseTime = d3.timeParse("%b-%y");
        data.forEach(function(d) {
          d.quarter = parseTime(d.quarter);
          d.rent = +d.rent;
          d.income = +d.income;
        });
        // set the Scales
        var xScale = d3.scaleTime()
                       .domain(d3.extent(data, function(d) { return d.quarter; }))
                       .range([0, width]);
        var yScale = d3.scaleLinear()
                       .domain([0, d3.max(data, function(d) { return Math.max(d.rent, d.income); })])
                       .range([height, 0]);
        // define the line generator
        var lineGenerator1 = d3.line()
                               .x(function(d) { return xScale(d.quarter); })
                               .y(function(d) { return yScale(d.rent); });
        var lineGenerator2 = d3.line()
                               .x(function(d) {return xScale(d.quarter); })
                               .y(function(d) {return yScale(d.income); }) 

        // Add paths
        svg.append("path")
           .data([data])
           .attr("class", "line")
           .attr("d", lineGenerator1)
           .style("stroke", "blue");

        svg.append("path")
           .data([data])
           .attr("class", "line")
           .attr("d", lineGenerator2)
           .style("stroke", "red");

      });
</script>
				

Adding the two paths. First, append the path elements to the svg group. Then, modify the class, d attributes and the stroke style of the generated paths.

<script>
// set the dimensions and margins of the graph
  var margin = {top: 20, right: 20, bottom: 50, left: 50};
  var width = 960 - margin.left - margin.right;
  var height = 500 - margin.top - margin.bottom;
  // append the svg object to the #infovismain div of the page
  // appends a 'group' element to 'svg'
  // moves the 'group' element to the top left margin
  var svg = d3.select("#infovismain").append("svg")
              .attr("width", width + margin.left + margin.right)
              .attr("height", height + margin.top + margin.bottom)
              .append("g")
              .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// Read the data from .csv file
      d3.csv("RentTrendByArea_Inner.csv", function(error, data) {
        if (error) {
          console.error(error);
        }
        // format the data
        var parseTime = d3.timeParse("%b-%y");
        data.forEach(function(d) {
          d.quarter = parseTime(d.quarter);
          d.rent = +d.rent;
          d.income = +d.income;
        });
        // set the Scales
        var xScale = d3.scaleTime()
                       .domain(d3.extent(data, function(d) { return d.quarter; }))
                       .range([0, width]);
        var yScale = d3.scaleLinear()
                       .domain([0, d3.max(data, function(d) { return Math.max(d.rent, d.income); })])
                       .range([height, 0]);
        // define the line generator
        var lineGenerator1 = d3.line()
                               .x(function(d) { return xScale(d.quarter); })
                               .y(function(d) { return yScale(d.rent); });
        var lineGenerator2 = d3.line()
                               .x(function(d) {return xScale(d.quarter); })
                               .y(function(d) {return yScale(d.income); }) 
        // Add paths
        svg.append("path")
           .data([data])
           .attr("class", "line")
           .attr("d", lineGenerator1)
           .style("stroke", "blue");

        svg.append("path")
           .data([data])
           .attr("class", "line")
           .attr("d", lineGenerator2)
           .style("stroke", "red");

           // Add circles
           var circles1 = svg.append("g")
                             .attr("class", "circles1");
           var circles2 = svg.append("g")
                             .attr("class", "circles2");

           circles1.selectAll("circle")
                   .data(data)
                   .enter()
                   .append("circle")
                   .attr("cx", function(d) {
                     return xScale(d.quarter);
                   })
                   .attr("cy", function(d) {
                     return yScale(d.rent);
                   })
                   .attr("r", "2px")
                   .attr("fill", "black");

           circles2.selectAll("circle")
                   .data(data)
                   .enter()
                   .append("circle")
                   .attr("cx", function(d) {
                     return xScale(d.quarter);
                   })
                   .attr("cy", function(d) {
                     return yScale(d.income);
                   })
                   .attr("r", "2px")
                   .attr("fill", "black");

      });
</script>
				

Here, we started by creating two groups circles1 and circles2 so that each will contain the circles located on each of the two paths. The reason we created a group for each is that, as we learned in the Binding Data Using D3 section, enter() method searches for existing elements first and only adds new elements if the number of existing elements doesn't correspond with the number of data. Therefore, if the circles were all in one group, the circles on the first path will be added successfully since there were no circle elements before, but when trying to add the circles to the second path, enter() method will search and find that there are circles already exist in the same group and will not add all the necessary circles for the second path.

The rest of the code follow the same steps in the Binding Data Using D3 section.

<script>
// set the dimensions and margins of the graph
  var margin = {top: 20, right: 20, bottom: 50, left: 50};
  var width = 960 - margin.left - margin.right;
  var height = 500 - margin.top - margin.bottom;
  // append the svg object to the #infovismain div of the page
  // appends a 'group' element to 'svg'
  // moves the 'group' element to the top left margin
  var svg = d3.select("#infovismain").append("svg")
              .attr("width", width + margin.left + margin.right)
              .attr("height", height + margin.top + margin.bottom)
              .append("g")
              .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// Read the data from .csv file
      d3.csv("RentTrendByArea_Inner.csv", function(error, data) {
        if (error) {
          console.error(error);
        }
        // format the data
        var parseTime = d3.timeParse("%b-%y");
        data.forEach(function(d) {
          d.quarter = parseTime(d.quarter);
          d.rent = +d.rent;
          d.income = +d.income;
        });
        // set the Scales
        var xScale = d3.scaleTime()
                       .domain(d3.extent(data, function(d) { return d.quarter; }))
                       .range([0, width]);
        var yScale = d3.scaleLinear()
                       .domain([0, d3.max(data, function(d) { return Math.max(d.rent, d.income); })])
                       .range([height, 0]);
        // define the line generator
        var lineGenerator1 = d3.line()
                               .x(function(d) { return xScale(d.quarter); })
                               .y(function(d) { return yScale(d.rent); });
        var lineGenerator2 = d3.line()
                               .x(function(d) {return xScale(d.quarter); })
                               .y(function(d) {return yScale(d.income); }) 
        // Add paths
        svg.append("path")
           .data([data])
           .attr("class", "line")
           .attr("d", lineGenerator1)
           .style("stroke", "blue");

        svg.append("path")
           .data([data])
           .attr("class", "line")
           .attr("d", lineGenerator2)
           .style("stroke", "red");
           // Add circles
           var circles1 = svg.append("g")
                             .attr("class", "circles1");
           var circles2 = svg.append("g")
                             .attr("class", "circles2");

           circles1.selectAll("circle")
                   .data(data)
                   .enter()
                   .append("circle")
                   .attr("cx", function(d) {
                     return xScale(d.quarter);
                   })
                   .attr("cy", function(d) {
                     return yScale(d.rent);
                   })
                   .attr("r", "2px")
                   .attr("fill", "black");

           circles2.selectAll("circle")
                   .data(data)
                   .enter()
                   .append("circle")
                   .attr("cx", function(d) {
                     return xScale(d.quarter);
                   })
                   .attr("cy", function(d) {
                     return yScale(d.income);
                   })
                   .attr("r", "2px")
                   .attr("fill", "black");

           // Add the X Axis
           svg.append("g")
              .attr("transform", "translate(0," + height + ")")
              .call(d3.axisBottom(xScale));
           // Add the Y Axis
           svg.append("g")
              .call(d3.axisLeft(yScale));

      });
</script>
				

Appending groups to contain the x-axis and the y-axis, move the groups to locate the axes using transform attribute, calling the axes and applying the scales to them

<script>
// set the dimensions and margins of the graph
  var margin = {top: 20, right: 20, bottom: 50, left: 50};
  var width = 960 - margin.left - margin.right;
  var height = 500 - margin.top - margin.bottom;
  // append the svg object to the #infovismain div of the page
  // appends a 'group' element to 'svg'
  // moves the 'group' element to the top left margin
  var svg = d3.select("#infovismain").append("svg")
              .attr("width", width + margin.left + margin.right)
              .attr("height", height + margin.top + margin.bottom)
              .append("g")
              .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// Read the data from .csv file
      d3.csv("RentTrendByArea_Inner.csv", function(error, data) {
        if (error) {
          console.error(error);
        }
        // format the data
        var parseTime = d3.timeParse("%b-%y");
        data.forEach(function(d) {
          d.quarter = parseTime(d.quarter);
          d.rent = +d.rent;
          d.income = +d.income;
        });
        // set the Scales
        var xScale = d3.scaleTime()
                       .domain(d3.extent(data, function(d) { return d.quarter; }))
                       .range([0, width]);
        var yScale = d3.scaleLinear()
                       .domain([0, d3.max(data, function(d) { return Math.max(d.rent, d.income); })])
                       .range([height, 0]);
        // define the line generator
        var lineGenerator1 = d3.line()
                               .x(function(d) { return xScale(d.quarter); })
                               .y(function(d) { return yScale(d.rent); });
        var lineGenerator2 = d3.line()
                               .x(function(d) {return xScale(d.quarter); })
                               .y(function(d) {return yScale(d.income); }) 
        // Add paths
        svg.append("path")
           .data([data])
           .attr("class", "line")
           .attr("d", lineGenerator1)
           .style("stroke", "blue");

        svg.append("path")
           .data([data])
           .attr("class", "line")
           .attr("d", lineGenerator2)
           .style("stroke", "red");
           // Add circles
           var circles1 = svg.append("g")
                             .attr("class", "circles1");
           var circles2 = svg.append("g")
                             .attr("class", "circles2");

           circles1.selectAll("circle")
                   .data(data)
                   .enter()
                   .append("circle")
                   .attr("cx", function(d) {
                     return xScale(d.quarter);
                   })
                   .attr("cy", function(d) {
                     return yScale(d.rent);
                   })
                   .attr("r", "2px")
                   .attr("fill", "black");

           circles2.selectAll("circle")
                   .data(data)
                   .enter()
                   .append("circle")
                   .attr("cx", function(d) {
                     return xScale(d.quarter);
                   })
                   .attr("cy", function(d) {
                     return yScale(d.income);
                   })
                   .attr("r", "2px")
                   .attr("fill", "black");
           // Add the X Axis
           svg.append("g")
              .attr("transform", "translate(0," + height + ")")
              .call(d3.axisBottom(xScale));
           // Add the Y Axis
           svg.append("g")
              .call(d3.axisLeft(yScale));

           // text label for the x axis
           svg.append("text")             
              .attr("transform", "translate(" + (width/2) + " ," + (height + margin.top + 20) + ")")
              .style("text-anchor", "middle")
              .text("Quarter")
              .style("font-family", "sans-serif"); 
           // text label for the y axis
           svg.append("text")
              .attr("transform", "rotate(-90)")
              .attr("y", 0 - margin.left)
              .attr("x",0 - (height / 2))
              .attr("dy", "1em")
              .style("text-anchor", "middle")
              .text("Rent")
              .style("font-family", "sans-serif"); 

      });
</script>
				

Adding svg text elements to act as main labels for the x-axis and the y-axis.

This is the end of the tutorial! We learned very useful basic methods in d3 that we will probably need to use in any d3-based graph.

Nice Job!


Sarkar, S. and Hussein, D.A., 2017, D3 Tutorials for Information Visualisation Design Studio, University of Sydney.
Email: somwrita.sarkar@sydney.edu.au, dhus4848@uni.sydney.edu.au