Creating Visualizations with D3

(click for print version)


Eamonn Maguire

Tweet @antarticdesign | GitHub @eamonnmag | eamonn[at]protonmail.ch

Slides and all code from here - https://github.com/eamonnmag/vis-course

Outline


Some basics


  • What is D3?
  • Getting started with HTML, CSS, JS, and SVG
  • Debugging

More Advanced


  • D3.js - Fundamentals
  • Loading Data
  • Processing Data
  • Zooming, Brushing, Linking
  • Multidimensional Data Vis
  • Network Vis
  • Geo Vis

Introducing D3

D3 (Data-Driven Documents):

  • A JavaScript library that utilizes SVG, HTML, and CSS for creating data visualization on the web
  • Created by Mike Bostock, Vadim Ogievetsky, and Jeff Heer
  • With D3, you can:
    • Easily load your data into the browser
    • Inspect and manipulate your data through the Document Object Model (DOM)
    • Include interaction and animation in your visualization easily
  • Download the latest version of D3 (http://d3js.org/)

Getting Started: HTML

HTML (Hypertext Markup Language):

  • Is used to structure content for web browsers
  • Saved as plain text file with .html extension
  • Many different revisions of the HTML standard
  • HTML5 is the current revision
  • The DOCTYPE declaration defines the document type:
    • <!DOCTYPE html> is the doctype for HTML5
  • "Mark up" content to specify semantic structure by adding tags to create elements

HTML Elements and Attributes

  • Elements are entities for structuring the HTML documents
  • Most of the elements are defined with a start tag, an end tag, and the content in between
<element>Content in between</element> <!-- Element with closing
                        tag -->
  • For example:
<h1>Hello World!</h1>
  • Elements can have attributes to provide extra information about them
  • The attributes are defined in the start tag and are in name/value pairs: name = "value"
  • For example:
<h1 class="myStyle">Hello World!</h1>

Example: HTML

<!DOCTYPE html>
<html>
<!-- The content between the <html> and </html> describes the web page -->
<head>
    <!-- <head></head> handles all the head elements -->
    <!-- <head></head> also provides the information about the page -->
    <!-- Tags that can be inside <head> element: <title>, <style>, <meta>, <link>,
    <script>, <noscript>, <base> -->
    <!-- <title></title> gives the title of the page -->
    <title>Visual Analytics</title>
</head>
<body>
    <!-- The content between the <body> and </body> is the visible page content -->
    <h1>Visual Analytics</h1>
    <p class="desc">Examples of <em>visualisation</em>:</p>
    <!-- Valid class or id: Must start with a letter and can be followed by letter, digits, hypens, and underscores -->
    <div id="iAmUnique1" class="desc">
        <ul>
            <li>Parallel coordinates</li>
            <li>Scatter plots</li>
        </ul>
    </div>
</body>
</html>

Example: HTML

Getting Started: Developer Tools

  • Nowadays, every modern-day browser has built-in developer tools
  • Examples: Chrome, Firefox, Safari, etc.
  • Can view source code of an HTML page by right-click on the page then select View page source (e.g. Chrome and Safari)
  • See the DOM by the inspecting the element
  • For example in Chrome or Safari, right-click on the page then select Inspect element

Example: Developer Tools

Getting Started: DOM

DOM (Document Object Model):

  • Hierarchical structure of HTML
  • Each pair of bracketed tags is an element
  • Refer to elements' relative relationships to each other in terms of: parent, child, sibling, ancestor and descendant
  • In the previous example:
    • body is the parent element to its children - h1, p and div (which are siblings to each other)
    • And all elements are descendants of html

Example: DOM

Getting Started: CSS

CSS (Cascading Style Sheets):

  • The visual representation of the document structure
  • Consists of selectors and properties
selector {
    property: value;
}

selectorA, selectorB {
    property: value;
}
  • For example:
p {
    font-size: 24px;
}

h1, .desc {
    color: #6d4aff;
    font-weight: bold;
}

CSS-Style Selectors

Type selectors: match DOM elements with the same name

p /* Selects all paragraphs */

Descendant selectors: match elements contained by another elements

div p /* Selects p elements contained in a div */

Class selectors: match elements of any type assigned to a specific class


.desc /* Selects elements with class "desc" */
.desc.highlight /* Selects hightlighted desc */

ID selectors: match the element with the specific ID

#iAmUnique1 /* Selects element with ID "iAmUnique1" */

Combination: combined to target specific elements

div.desc /* Selects divs with class "desc" only */

Referencing Styles

Three ways to apply CSS styles rules to HTML document:

  • Embed the CSS in the HTML

<head>
    <style type="text/css">
        h1 { font-size: 16px; }
    </style>
</head>
  • For example:
<!DOCTYPE html>
<html>
    <head>
        <title>Visual Analytics</title>
        <!-- Embedding the CSS in the HTML -->
        <style type="text/css">
            h1 { font-size: 16px; }
        </style>
    </head>

    <body>
        <h1>Visual Analytics</h1>
    </body>
</html>

Referencing Styles

Three ways to apply CSS styles rules to HTML document:

  • Reference an external stylesheet from the HTML

<head>
    <link rel="stylesheet" href="style.css">
</head>
  • For example:
<!DOCTYPE html>
<html>
    <head>
        <title>Visual Analytics</title>
        <!-- Reference an external stylesheet from the HTML -->
        <link rel="stylesheet" href="style.css">
    </head>
    <body>
        <h1>Visual Analytics</h1>
    </body>
</html>

Referencing Styles

Three ways to apply CSS styles rules to HTML document:

  • Attach inline styles
<h1 style="color: red; font-size: 24px;">An example of inline style</h1>
  • For example:
<!DOCTYPE html>
<html>
    <head>
        <title>Visual Analytics</title>
    </head>
    <body>
        <!-- Attach inline styles -->
        <h1 style="color: red; font-size: 24px;">Visual Analytics</h1>
    </body>
</html>

Getting Started: SVG

SVG (Scalable Vector Graphics):

  • A text-based image format that is defined using markup code same as HTML
  • You can include SVG code directly inside any HTML document or
  • Insert it dynamically into the DOM using JavaScript
  • SVG is XML-based therefore all elements must have a closing tag:
<element></element> <!-- Element with closing tag -->
<element/> <!-- Element with self-closing tag -->
            
  • Coordinate system starts at the top-left corner
0,0

SVG Elements and Shapes

  • An SVG image begins with <svg> element
  • The width and height of the SVG image can be defined by the width and height attributes
  • A simple SVG example:

<svg width="100%" height="100%">
<rect x="85" y="5" width="40" height="40" fill="rgba(255, 0, 0, 1.0)"></rect>
</svg>

SVG Elements and Shapes

  • Some of the pre-defined shapes in SVG:
    • Rectangle <rect>
    • Circle <circle>
    • Ellipse <ellipse>
    • Line <line>
    • Polyline <polyline>
    • Polygon <polygon>
    • Path <path>
  • SVG also renders text using <text> element
  • You can group together your SVG shapes using <g> element

Styling SVG Elements and Shapes

  • Default style is black fill with no stroke
  • You can use SVG properties such as fill, stroke and opacity to style your SVG elements and shapes
  • For example using inline styles:
<rect x="85" y="5" width="40" height="40" fill="red" stroke="blue" stroke-width="5" />
  • Or through the CSS approach:
<rect x="85" y="5" width="40" height="40" class="mySvgStyle" />
.mySvgStyle {
    fill: red;
    stroke: blue;
    stroke-width: 5;
}

Example: SVG


<svg width="800" height="220">
    <!-- The order when the elements are called determines the ordering of the objects -->
    <rect x="250" y="10" width="500" height="200" stroke-width="1" stroke="black" fill="white"></rect>
    <!-- <g> groups the shapes together allowing you to transform and style the whole group as a single shape -->
    <g transform="scale(3) translate(100, 10)">
        <circle cx="25" cy="25" r="20" fill="rgba(127, 201, 127, 0.7)" stroke="rgba(127, 201, 127, 0.5)" stroke-width="5"></circle>
        <ellipse cx="65" cy="25" rx="35" ry="15" fill="rgba(190, 174, 212, 0.7)" stroke="rgba(190, 174, 212, 0.5)" stroke-width="5"></ellipse>
        <rect x="85" y="5" width="40" height="40" fill="rgba(253, 192, 134, 0.7)" stroke="rgba(253, 192, 134, 0.5)" stroke-width="5"></rect>
    </g>
</svg>

Getting Started: JavaScript

  • Scripting language for manipulating the DOM after a page has been loaded in the browser
  • Type the code directly in the JavaScript console or
  • Load the script to the browser using a web page
  • Two ways to reference the scripts:
    • Directly in the HTML, between two script tags
    <body>
        <script type="text/javascript">
            alert("Hello, world!");
        </script>
    </body>
    • Stored in a separate file with a .js suffix, and then referenced in the HTML
    <head>
        <title>Page Title</title>
        <script type="text/javascript" src="script.js"></script>
    </head>

Example: JavaScript

/* Declaring a variable of an array of objects */
var myChocolate = [
    {"type": "milk", "cocoa_butter": 15, "quantity": 13 },
    {"type": "sweet", "cocoa_butter": 18, "quantity": 8 }];

/* A function to loop through the array and display the cocoa butter content */
function displayCocoaButter() {
    var index;
    // Loop through the array of objects
    for (index = 0; index < myChocolate.length; index++) {
        /* Create a <p> element */
        var _p = document.createElement("p");
        /* Get the cocoa butter content and add it to the <text> node */
        var _text = document.createTextNode("Cocoa butter content: " + myChocolate[index].cocoa_butter);
        /* Add the created <text> node to the <p> element */
        _p.appendChild(_text);
        /* Add the created <p> element to <body> */
        document.body.appendChild(_p);
    }
}

Example: JavaScript

JavaScript Code Style tips

Generally, I write my code using the 'module' pattern.

/* visualization module */
var visualization = (function(){
    /* Everything here is private */
    var draw_circles = function(options){// do stuff};
    var draw_rectangles = function(options){// do stuff};

    /* Everything in the return is public, so this is your API */
    return {
        do_vis: function(placement, data_url, options) {
            // my code
        }
    }
})()

/* Call the method */
visualization.do_vis('#myCanvas', '/api/genomes/get?id=1', {'width':100, 'height':100});
/* No one can access your private methods, so the only method you need to ensure always works and stays consistent are your public methods */

D3.js Fundamentals

Drawing shapes using D3


var svg = d3.select(placement).append("svg")
    .attr("width", width)
    .attr("height", height)
    .append("g");

svg.append("circle")
    .style("fill", "#6d4aff")
    .attr("cx", 40)
    .attr("cy", 50)
    .attr("r", 15);

svg.append("path")
    .attr("d", d3.symbol().size(200).type(d3.symbolCross))
    .style("fill", "#6d4aff")
    .attr("transform", "translate(90,50)");
    ....

Rectangles

var svg = d3.select("div_id")
                    .append("svg")
                    .attr('width',200)
                    .attr('height',200);
svg.append("rect")
    .attr('width', 60)
    .attr('height', 30)
    .attr('x', 10)
    .attr('y', 30)
    .style('fill', 'white');

Lines

See the full list at D3 Wiki | Curves

  • linear
  • basis
  • basis-open
  • basis-closed
  • step-before
  • step-after
  • bundle

var line = d3.line()
    .curve(d3.curveCardinal) // there are many different interpolators
    .x(function (d) {
        return d.x;
    })
    .y(function (d) {
        return d.y;
    });

var svg = d3.select("#canvas-id").append("rect")
    .attr('width', 200)
    .attr('height', 200');

svg.append("path")
    .attr("d", line([{x:0, y:10}, {x:20, y:10}, {x:40, y:40}, {x:60, y:60}]))
    .style({'stroke': 'white', 'stroke-width': 2, 'stroke-linecap': 'round'});

                

D3: Data Joins

Through this piece of code, we can draw a circle. But just one.

var svg = d3.select("body").append("svg")
    .attr("width", 400)
    .attr("height", 400)
    .append("g");

svg.append("circle")
    .attr("r", 20)
    .attr("cx", 30)
    .attr("cy", 40)
    .style("fill", "#6d4aff");

But what if we want to draw a circle for every data point?


var svg = d3.select("body").append("svg")
    .attr("width", 400)
    .attr("height", 400)
    .append("g");

var data = [{x:160, y:190}, {x:30, y:200}, {x:300, y:100}];

svg.selectAll("data-circle").data(data)
    .enter().append("circle")
    .attr("r", 10)
    .attr("cx", function(d) {
        return d.x;
    }).attr("cy", function(d) {
        return d.y;
    }).style("fill", "#6d4aff");
    

svg.selectAll("data-circle")
    .data(data)
    .enter()
    .append("circle")
    .attr("r", 10)
    .attr("cx", function(d) {
        return d.x;
    })
    .attr("cy", function(d) {
        return d.y;
    })
    .style("fill", "#6d4aff");
        
svg.selectAll("circle")

How can I select something that doesn't yet exist?

In D3, we are saying "I want all circles to correspond to data" and there will be one circle per data item.

We set the radius of the circle with a value of 10.

.attr("r", 10)

Here we take the data, enter it and append circles items for each data item.

.data(data).enter()
var data = [{x:160, y:190}, {x:30, y:200}, {x:300, y:100}];
            // d.y accesses the y variable of each data item d
            attr("cy", function(d) { return d.y;})

We can access each individual data item property using a function which can pass through a data element d.

The Enter/Update/Exit Pattern

Enter-Exit-Update

Enter/update pattern


var svg = d3.select(placement).append("svg")
    .attr("width", width)
    .attr("height", height)
    .append("g");

update([{x:30, y:60}]);


function update(data) {
    var rect = enter_svg.selectAll("rect")
        .data(data);

    rect.enter().append("rect")
        .style("fill", "#fff")
        .attr("height", 20)
        .attr("width", 0)
        .transition()
           .attr("width", 25);

    rect.attr("x", function(d) { return d.x; })
        .attr("y", function(d) { return d.y; });

    rect.exit().attr("width", 25).transition()
        .attr("width", 0)
        .remove();
}
        
We want to:
  • tell d3 to assign data to rectangle objects;
  • enter our data;
  • update the data that's changed; and
  • exit the join

How does D3 know what changed?


var data = [{id: 1, x: 160, y: 190}, {id: 2, x: 30, y: 200}, {id: 3, x: 200, y: 100}];

var svg = d3.select(placement + " svg g");
    var rect = svg.selectAll("rect")
        .data(data, function(d) {
            return d.id;
        });

    rect.enter().append("rect")
        .style("fill", "#fff")
        .attr("height", 20)
        .attr("width", 0)
        .transition()
            .attr("width", 25);

    rect.attr("x", function (d) {
        return d.x;
    })
    .attr("y", function (d) {
        return d.y;
    });

    rect.exit().attr("width", 25).transition()
        .attr("width", 0)
        .remove();

        

In the previous example, d3 just looks at the index of the items in the array.

This is fine, but not enough for more complex use cases where we normally want to provide our own index.

An example: Plotting some data

var data = [
      {x: 10.0, y: 9.14},
      {x: 15.0, y: 18.14},
      {x: 13.0, y: 28.74},
      {x: 49.0, y: 35.77},
      {x: 11.0, y: 9.26},
      {x: 23.0, y: 18.10},
      {x: 43.0, y: 16.13},
      {x: 65.0, y: 13.10},
      {x: 12.0, y: 19.13},
      {x: 30.0, y: 70.26},
      {x: 25.0, y: 40.74}
      ];

Data is just an array of dictionary objects.

Let's plot it...

First step: plot the data

var svg = d3.select(placement).append("svg")
    .attr("width", width)
    .attr("height", height)
    .append("g");

svg.selectAll("circle")
    .data(data)
    .enter().append("circle")
    .attr("class", "dot")
    .attr("cx", function (d) {
        return d.x;
    })
    .attr("cy", function (d) {
        return d.y;
    })
    .attr("r", 5);

Fantastic...but this wouldn't work well if we had X/Y coordinates greater than our canvas size.

Exercise 1

Create a function to load in data from an array, and display circles to represent each point.

Bonus Point - Allow for updates to the displayed data (pass in a new array).

Enter Scales...

“Scales are functions that map from an input domain to an output range.” Mike Bostock

  1. Quantitative (see here for details)
    1. Linear
      var myScale = d3.scaleLinear().domain([0, 1000]).range([0, 100]);
      myScale(100); // will output 10
    2. Log
      var myScale = d3.scaleLog().domain([1, 1000]).range([0, 100]);
      myScale(100); // will output 66.67
    3. Power (creates)
      var myScale = d3.scalePow().domain([1, 1000]).range([0, 100]).exponent(0.5);
      myScale(100); // will output 29.39
    4. Identity
      var myScale = d3.scalePow().domain([1, 1000]).range([0, 100]).exponent(0.5);
      myScale(100); // will output 29.39

Other functions

myScale.nice(); // rounds the first and last value of the domain
myScale.clamp([true]); // ensures that values passed through larger than the domain keep within the range.
myScale.invert(); // for a value in the range, outputs the equivalent domain values
  1. Quantize

    Much like linear scales but with a discrete range (see here for details).

    var myScale = d3.scaleQuantize().domain([0, 1]).range(['b', 'i', 'o', 'v', 'i', 's']);
    myScale('0'); // will output 'b'
    
    myScale.invertExtent('s'); // returns [0.8333, 1]
    
  2. Quantile

    Same as quantize, but input domain is assumed to be discrete (see here for details).

    var myScale = d3.scaleQuantile().domain([0, 1]).range(['b', 'i', 'o', 'v', 'i', 's']);
        myScale('0'); // will output 'b'
    
        myScale.invertExtent('s'); // returns [0.8333, 1]
    

  3. Threshold See here for more info.
    var myScale = d3.scaleThreshold().domain([0, 1]).range(['a', 'b', 'c']);
    myScale(-1) === 'a';
    myScale(0) === 'b';
    myScale(1) === 'c';
    
    myScale.invertExtent('a'); // returns [undefined, 0]
    
  1. Ordinal (see here for details)
    1. e.g. colors d3.scaleOrdinal(d3.schemeCategory10) creates a scale mapping Strings etc. to a colour e.g.
      var colors = d3.scaleOrdinal(d3.schemeCategory10);
      var colourForMe = colors("Visual Analytics"); // will output 
    2. or define your your range
      var myScale = d3.scaleOrdinal().domain(["d", "proton"]).range(["3", "mail"]);
      myScale("d"); // will output "3"

Bringing scales in to our plot...

var margin = {top: 20, right: 20, bottom: 20, left: 30};
var xScale = d3.scaleLinear()
    .domain(d3.extent(data, function (d) {
        return d.x;
}))
.range([0, width - margin.left - margin.right]);

var yScale = d3.scaleLinear()
    .domain(d3.extent(data, function (d) {
        return d.y;
    }))
.range([height - margin.top - margin.bottom, 0]);

// Now, modify the X and Y positions of
// the circle using the scale...
svg.selectAll("circle")
    .data(data).enter().append("circle")
    .attr("class", "dot")
    .attr("cx", function (d) {
        return xScale(d.x);
    })
    .attr("cy", function (d) {
        return yScale(d.y);
    }).attr("r", 5);

Wouldn't it be great if we now knew something about the value range...

Exercise 2

Add scales to your plot from Exercise 1.

Enter Axes...

var xAxis = d3.axisBottom(xScale).tickPadding(4);

var yAxis = d3.axisLeft(yScale).tickPadding(10);

svg.append("g")
    .attr("class", "x axis")
    .attr("transform", "translate(0," + yScale.range()[0] + ")")
    .call(xAxis);

svg.append("g")
    .attr("class", "y axis")
    .call(yAxis);

Exercise 3

Add axes to your plot from Exercise 2.

Loading Data

Get my data in to D3 from external files.

Loading Data

  • D3 has a built-in method to help load CSV and JSON files
  • Example of loading CSV data
  • 
        d3.csv("chocolate.csv").then(function(csvData) {
        console.log(csvData);
    });
  • Example of loading JSON data
  • d3.json("chocolate.json").then(function(jsonData) {
        console.log(jsonData);
    });
  • Note: d3.csv() and d3.json() are asynchronous methods, where the rest of your code is executed as your browser waits for the file to be downloaded.

    A callback, represented by the function call is what is executed when the data has been retrieved successfully.

chocolates.json

{ "chocolates": [{
    "name": "Dairy Milk",
    "price": 45,
    "rating": 2
    }, ...
]}

This is how we get access to it...

// this is how we process it
d3.json("assets/data/chocolate.json").then(function (data) {
    chocolates = data.chocolates;
}

Apply This Function

function loadAndDisplayData(placement, w, h) {
    width = w; height = h;

    d3.select(placement).html("");

    d3.json("assets/data/chocolate.json").then(function (data) {
        data = data.chocolates;

        var svg = d3.select(placement).append("svg").attr("width", width).attr("height", height).append("g")
            .attr("transform", "translate(" + margins.left + "," + margins.top + ")");

        var xScale = d3.scaleLinear()
            .domain(d3.extent(data, function (d) {
                return d.price;
            }))
            .range([0, width - margins.left - margins.right]);

        var yScale = d3.scaleLinear()
            .domain(d3.extent(data, function (d) {
                return d.rating;
            }))
            .range([height - margins.top - margins.bottom, 0]);

        var colors = d3.scaleOrdinal(d3.schemeCategory10);
        

        var xAxis = d3.axisBottom(xScale).tickPadding(2);
        var yAxis = d3.axisLeft(yScale).tickPadding(2);

        svg.append("g").attr("class", "x axis").attr("transform", "translate(0," + yScale.range()[0] + ")").call(xAxis);
        svg.append("g").attr("class", "y axis").call(yAxis);

        svg.append("text").attr("fill", "#414241").attr("text-anchor", "end")
        .attr("x", width / 2).attr("y", height - 35).text("Price in pence (£)");

        var chocolate = svg.selectAll("g.node").data(data, function (d) { return d.name; });

        var chocolateEnter = chocolate.enter().append("g").attr("class", "node")
            .attr('transform', function (d) {
                return "translate(" + xScale(d.price) + "," + yScale(d.rating) + ")";
        });

        chocolateEnter.append("circle").attr("r", 5).attr("class", "dot")
            .style("fill", function (d) {
                return colors(d.manufacturer);
            });

        chocolateEnter.append("text").style("text-anchor", "middle").attr("dy", -10)
            .text(function (d) {
                return d.name;
            });
    }
}

Giving us this

You can access all the code for this here, with commentary!

Now that we have our base, we can do all other sorts of exciting stuff! Like include mouse overs, zoom, brushing and animation!

Exercise 5

Modify your code from exercise 4 to load this file as your data source.

Mouse Events

I want to click on things...

Mouse Event Types

myItem.on("mouseover",function (d) {
    // do something on mouseover
    }).on("mouseout", function (d) {
    // do something on mouseout
    }).on("click", function (d) {
    // do something on click
    }).on("mousemove", function(d) {
    // do something on mouse move
    }).on("mousedown", function(d) {
    // do something on mouse down
    }).on("mouseup", function(d) {
    // do something on mouse up (a mouse 'click' is a 'mousedown' and 'mouseup' event)
    })

Attaching mouse events to our plot...

var chocolateEnter = chocolate.enter().append("g").attr("class", "node")
    .attr('transform', function (d) {
    return "translate(" + x(d.price) + "," + (height + 100) + ")";
    });
    ...

For each node, we have a circle and a text item.

// add a circle
    chocolateEnter.append("circle")
    .attr("r", 5)
    .attr("class", "dot")
    .style("fill", function (e, d) {
    return colors(d.manufacturer);
    });

    // add text
    chocolateEnter.append("text")
    .style("text-anchor", "middle")
    .attr("dy", -10)
    .text(function (e, d) {
    return d.name;
    })

Attaching mouse events to our plot...

chocolateEnter.on("mouseover",function (d) {
    d3.select(this).style("stroke-width", "1px").style("stroke", "white");
    }).on("mouseout", function (d) {
    d3.select(this).style("stroke", "none");
    }).on("click", function(d) {
    alert("Hi, you clicked on " + d.name);
    });

See here for example source code

Exercise 5

Add mouse events to your plot from Exercise 4.

Zooming

Show me things, close up...


Zooming is a complicated thing to do in most frameworks. In D3, it's pretty easy once you start thinking about things in terms of transforms and scales.


const xAxis = d3.axisBottom(xScale).tickPadding(2);
const yAxis = d3.axisLeft(yScale).tickPadding(2);

const gX = svg.append("g").attr("class", "x axis").attr("transform", "translate(20," + y.range()[0] + ")").call(xAxis);
const gY = svg.append("g").attr("class", "y axis").attr("transform", "translate(20,0)").call(yAxis);

const handleZoom = function(e) {
// Then we have to tell the axes to update...
gX.call(xAxis.scale(e.transform.rescaleX(xScale)));
gY.call(yAxis.scale(e.transform.rescaleY(yScale)));

// And we need to tell the chocolate nodes where to go.
g.selectAll(".chocolatenode").attr('transform', function(d) {
    let _x = e.transform.rescaleX(x)(d.price);
    let _y = e.transform.rescaleY(y)(d.rating);
    return `translate(${_x}, ${_y}) scale(${e.transform.k})`
})
};
var zoom = d3.zoom().on("zoom", handleZoom);
                

Then, we call it from our SVG component.

 svg = d3.select(placement).append("svg")
    .attr("width", width)
    .attr("height", height)
    .append("g")
    .attr("transform", "translate(" + margins.left + "," + margins.top + ")")
    .call(zoom);

Zoooommmm...

Exercise 6

Add zooming to your plot from Exercise 5.

Brushing

Select items and tell me more...

What is brushing

A method of selection in visualizations...

How to add it...

brush = d3.brush()
    // When the brushing event is started, this function is called
    .on("brushstart", function() {
    console.log("Resetting selected var");
    selected = {};
    })
    // whilst brushing is happening, this function is called
    .on("brush", brushed)
    // when finished, brushend is called
    .on("brushend", function() {
    console.log("Selected");
    // output the keys of the selection
    console.log(Object.keys(selected))
    });

Now, let's add the brush to the container...

svg.append("g")
    .attr("class", "brush")
    .call(brush);

The extent from D3 returns back a 2D array with the top left to bottom right coordinates

var brushed = function() {
    var extent = brush.extent();
    d3.selectAll("g.chocolatenode").select("circle").style("fill", function (d) {
    d.selected = (d.x > x(extent[0][0]) && d.x < x(extent[1][0])) && (d.y < y(extent[0][1]) && d.y > y(extent[1][1]));
    if(d.selected) {
        selected[d.name] = d;
    }
    return d.selected ? "#6d4aff" : colors(d.manufacturer);
    });
}

This code grabs the extent from the brush and calculates which elements are within the bounds.

Exercise 7

Add brushing to your plot from Exercise 6.

Multidimensional Visualization

Parallel Coordinates


Simply uses some of the techniques already shown here - scales, and brushing. Let's check the code.

Most of the work has been done for you..., there are already a few parallel coordinate implementations that can be used out of the box.


You can use my code directly which deals with rendering of multiple inter-related parallel coordinates, or use this one which is very nice!

Scatter plot Matrices

Let's check the code.

Network Visualization

Graph Layouts

Look cool, but get cluttered very quickly, and don't scale well.

Creating Graphs

Data given as an array of nodes and links.

{
    "nodes": [
    {
        "id": 0,
        "name": "Myriel",
        "group": 1,
        "showLabel": true
    },
    {
        "id": 1,
        "name": "Napoleon",
        "group": 1,
        "showLabel": true,
        "nodetype": "cross"
    }
],
"links": [
    {
        "source": 0,
        "target": 1,
        "value": 10
    }
]
}

View the code to create this graph.

Your Choice of Layouts

There are a number of layouts that D3 gives you for free.

  • Force directed layout: this is what you've seen so far. Nodes have resistance to other nodes calculated based on the weight on the link and 'charge'.
  • Cluster: for dendrogram layouts.
  • Tree: for nice tree layouts.
  • Treemap: recursive partitioning of a space to match the tree layout with space filled by the value of sub nodes.
  • Pack: for sunburst style layouts.

Hive Plots

Useful for tidying up clutter, but can be difficult to interpret initially.

The idea is that each axis has a different meaning, e.g. all nodes with just outgoing connections, all nodes with just incoming connections, and nodes with both, e.g. HIV Plot

View the code to create this graph.

Linking Visualizations

  • We have enable brushing in our last plot, which means we've enabled selection.

  • Now we can get more information about these data points and visualise more.

  • Overview first, details on demand...key infovis paradigm from Ben Shneiderman.

Let's plot chocolate prices over a year for selected data.

...
.on("end", function () {
    plot_detailed(placement, selected, 300, 60);
});
plot_detailed: function (placement, selection, width, height) {
    d3.selectAll(".link_detail_plot").remove();
    Object.keys(selection).forEach((key) => {
        var svg = d3.select(placement).append("svg")
                .attr('width', width + 30)
                .attr('height', height + 30)
                .attr('id', "detail-" + selection)
                .attr('class', 'link_detail_plot')
                .append("g").attr('transform', 'translate(20,20)');

        // let's generate some random data.
        var dataset = [];
        for (var i = 0; i < 12; i++) {
            var newNumber = Math.random() * 60;
            dataset.push({x: i, y: newNumber});
        }


        // our X and Y scales
        var xScale = d3.scaleLinear()
            .domain(d3.extent(dataset, function (d) {
                return d.x
        }))
        .range([0, width]);


        var yScale = d3.scaleLinear()
            .domain([0, d3.max(dataset, function (d) {
                return d.y
            })])
        .range([height, 0]);

        // now add all our bars...
        svg.selectAll("rect").data(dataset).enter().append("rect").style("fill", colors(selection)).attr('x', 0).transition().attr('height', function (d, i) {
            return height - yScale(d.y);
        }).attr('width', 10).attr('x', function (d, i) {
            return xScale(d.x);
        }).attr('y', function (d) {
            return yScale(d.y)
        });

    svg.append("text").text(selection).attr({'x':5, y: 10})

    })
}

For more high-performance systems, it can be useful to use something like crossfilter with d3 to provide fast grouping functions.


There are utility libraries out there such as dc.js to provide plots etc. on top of crossfilter and d3.js. e.g. this is a project I just handed over.

That's the Fundamentals covered on D3.

There are obviously more complex things such as polar layouts, and maps which are beyond a 90 minute introduction, but this is a starting point.

There are a huge number of examples online. Check them out. The best way to learn about D3 is to play with it.

Observable with D3.js

Further Reading

  • Helder de Rocha, Learn D3.js: Create interactive data-driven visualizations for the web with the D3.js library, Packt 2019
  • Scott Murray, Interactive Data Visualization for the Web: An Introduction to Designing with D3, O'Reilly 2017
  • Philip Janert, D3 for the Impatient: Interactive Graphics for Programmers and Scientists, O'Reilly 2019
  • Claus Wilke, Fundamentals of Data Visualization: A Primer on Making Informative and Compelling Figures, O'Reilly 2019

Also check out the following links...

Short Advice

  • Don't overcomplicate things, the best solution is often the simplest.
  • It's always best to draw out what you want to do and build up.
  • Colors:
    • Colour brewer is pretty good...
    • I like FlatUIColors
    • Try to avoid too much colour in your interface if your visualization uses that channel a lot.
  • Fonts:
    • Clean fonts such as Lato, Roboto, Sans serif pro are free from Google Fonts, use them.
  • Looks are important. Use resources to improve the look of your web page such as:
    • Bootstrap
    • MaterialiseCSS
    • SemanticUI
  • Stackoverflow is great for resolving D3 problems. And there are tonnes of examples online

Next Steps

The next step is creating your own visualizations with D3.js starting with the concepts you've learnt today.


We will have a short sprint of 2 weeks where you can all go off and visualize your own data for work or personal projects.


I'm here to help over those 2 weeks and you can ask anything - I will try to help :)


At the end of the two weeks we'll have a show and tell with everyone's (those who wish to particate) projects'