Tweet @antarticdesign | GitHub @eamonnmag | eamonn[at]protonmail.ch
Slides and all code from here - https://github.com/eamonnmag/vis-course
D3 (Data-Driven Documents):
HTML (Hypertext Markup Language):
<!DOCTYPE html> is the doctype for HTML5<element>Content in between</element> <!-- Element with closing
tag -->
<h1>Hello World!</h1>
<h1 class="myStyle">Hello World!</h1>
<!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>
DOM (Document Object Model):
body is the parent element to its children - h1,
p and
div (which are siblings to each other)
html
CSS (Cascading Style Sheets):
selector {
property: value;
}
selectorA, selectorB {
property: value;
}
p {
font-size: 24px;
}
h1, .desc {
color: #6d4aff;
font-weight: bold;
}
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 */
Three ways to apply CSS styles rules to HTML document:
<head>
<style type="text/css">
h1 { font-size: 16px; }
</style>
</head>
<!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>
Three ways to apply CSS styles rules to HTML document:
<head>
<link rel="stylesheet" href="style.css">
</head>
<!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>
Three ways to apply CSS styles rules to HTML document:
<h1 style="color: red; font-size: 24px;">An example of inline style</h1>
<!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>
SVG (Scalable Vector Graphics):
<element></element> <!-- Element with closing tag -->
<element/> <!-- Element with self-closing tag -->
<svg> element<svg width="100%" height="100%">
<rect x="85" y="5" width="40" height="40" fill="rgba(255, 0, 0, 1.0)"></rect>
</svg>
<rect><circle><ellipse><line><polyline><polygon><path><text> element<g> elementfill, stroke and
opacity to
style your SVG elements and shapes
<rect x="85" y="5" width="40" height="40" fill="red" stroke="blue" stroke-width="5" />
<rect x="85" y="5" width="40" height="40" class="mySvgStyle" />
.mySvgStyle {
fill: red;
stroke: blue;
stroke-width: 5;
}
<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>
script tags<body>
<script type="text/javascript">
alert("Hello, world!");
</script>
</body>
<head>
<title>Page Title</title>
<script type="text/javascript" src="script.js"></script>
</head>
/* 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);
}
}
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 */
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)");
....
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');
See the full list at D3 Wiki | Curves
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'});
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.
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();
}
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.
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...
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.
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).
“Scales are functions that map from an input domain to an output range.” Mike Bostock
var myScale = d3.scaleLinear().domain([0, 1000]).range([0, 100]);
myScale(100); // will output 10
var myScale = d3.scaleLog().domain([1, 1000]).range([0, 100]);
myScale(100); // will output 66.67
var myScale = d3.scalePow().domain([1, 1000]).range([0, 100]).exponent(0.5);
myScale(100); // will output 29.39
var myScale = d3.scalePow().domain([1, 1000]).range([0, 100]).exponent(0.5);
myScale(100); // will output 29.39
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
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]
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]
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]
var colors = d3.scaleOrdinal(d3.schemeCategory10);
var colourForMe = colors("Visual Analytics"); // will output
var myScale = d3.scaleOrdinal().domain(["d", "proton"]).range(["3", "mail"]);
myScale("d"); // will output "3"
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...
Add scales to your plot from Exercise 1.
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);
Add axes to your plot from Exercise 2.
Get my data in to D3 from external files.
d3.csv("chocolate.csv").then(function(csvData) {
console.log(csvData);
});
d3.json("chocolate.json").then(function(jsonData) {
console.log(jsonData);
});
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.
{ "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;
}
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;
});
}
}
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!
Modify your code from exercise 4 to load this file as your data source.
I want to click on things...
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)
})
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;
})
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
Add mouse events to your plot from Exercise 4.
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...
Add zooming to your plot from Exercise 5.
Select items and tell me more...
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.
Add brushing to your plot from Exercise 6.
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!
Let's check the code.
Look cool, but get cluttered very quickly, and don't scale well.
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.
There are a number of layouts that D3 gives you for free.
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.
...
.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.
Also check out the following links...
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'