Today's Game Plan:
d3 background
svg basics
editing svgs with d3
mapping with d3
Will be working with d3.v4 in this demo
Chrome devtools -- other browsers are fine if you are familiar with them
Slides are online (hit S) to show speaker view and see the embarassingly large amount of notes I require
=
D ata
D riven
D ocuments
D3 = Data Driven Documents
Document = DOM (Document Object Model)
Open source lib that uses html, js, css, svg to transform web elements based on data
Not a wizard, but a tool that gives you the means to create vizualizations
Bostock leaves the NYTimes to work on opensource projects fulltime in 2015
1. D3 is hard
2. D3 has a crazy dedicated community
--> Today we will focus on the basics so you can read/understand
general concepts in d3, so you can interact with the d3 community.
SVG
SVG Elements
SVG Attributes
SVG = Scaleable Vector Graphic
Similar to flash, accept instead of being compiled and run in a viewer, it uses open web standards
... which means it can be used with CSS and animated with JS
XML based -> Looks like HTML, but is made for graphics
... so it is much faster and more flexible, but nonetheless you can pretty much just cut and past svg code into html docs
But it is not a standin for the internet ->
Pretty much anything can be drawn in SVG format
... in fact, you can export into svg format in Illustrator and Drop the contents of the file right into an HTML doc
Basic SVG Shapes
In this next section we are going to go through some examples of SVG shapes so you can get a taste of what the general syntax for svg
svg-basics / circles
codepen.io/eslivinski/pen/bqRZex
To define a circle you first need to give the SVG where the circle lives a width and height
Then you need to give the circle a radius (r) and a position within the svg (cx & cy or transform)
Point out which is which
The position of a circle is relative to its center
Open up developer tools and modify the attributes
svg-basics / rectangles
codepen.io/eslivinski/pen/qrjGdw
To define a rectangle you need to give it a width, height, and position
The first rectangle uses x and y to give the position (similar to what we did for the circles)
The second uses the transform attribute and translates (moves) it to (600, 10) (x,y)
ANY svg element can use transform generally doesn't make a difference
The position of rectangle is relative to its top-right corner
Stroke position is always aligned so the middle of the stroke is in the middle of the svg element
The position of the svg element does not include the stroke
svg-basics / text
Hello World
Hooray SVGs
Hello World
Hooray SVGs
codepen.io/eslivinski/pen/JWJqGz
To define text you need to give a position and you will probably want to give it text too
In this example you can see that just like rectangles, position can be set with x/y or transform
Also in this example you can see an example of what I mentioned earlier about using CSS with svgs
Modifying SVGs with d3.js
modifying-svgs / general-ideas
// SELECT ITEMS TO MANIPULATE
var circles = d3.selectAll('circle');
var circles = d3.selectAll('circle');
// DEFINE PROPERTIES AND ATTRIBUTES TO MODIFY
circles.attr('stroke', 'black');
circles.attr('stroke-width', 1);
var circles = d3.selectAll('circle');
// CHAINING METHODS
circles
.attr('stroke', 'black')
.attr('stroke-width', 1)
.attr('radius', 100)
.attr('cy', 100);
I want to quickly go over some general concepts in the "d3-approach" to modifying svgs and JS in general,
as it is pretty different than the way most JS libs do things.
Selecting items to manipulate. (Defining our selection)
d3 gives us 2 methods to do this (select and selectAll) -- we will go over in more detail later
because we have 3 circles here I am use selectAll
Define properties/ attrs to modify
use the attr method to change an attribute
1st param = the attr we want to modify
2nd param = the value we want to set this property to (can be a static value or a function that returns a value)
Chaining methods
Typically we will see a d3 with a long list of methods stacked up
We can write d3 in this way because each method is returning the selection
modifying-svgs / d3.select, d3.attr
// STEP #1: Identify the element wish to modify
var circle = d3.select('circle');
// STEP #2: Use the attr method to change the element's fill
// from blue to orange
circle.attr('fill', 'orange');
codepen.io/eslivinski/pen/zZzQoy
Need to tell d3 what we are interested in modifying - here we are using .select, b/c we only have one circle
Similar to document.querySelector or the $ in jQuery -> returns a the objects that match that css selector
Show in devtools what comes back from a selection
modifying-svgs / d3.selectAll
// Select will only grab the first element matches the selection
d3.select('circle')
.attr('fill', 'orange');
// selectAll will grab all elements that match the description
d3.selectAll('circle')
.attr('fill', 'purple');
codepen.io/eslivinski/pen/GWEarq
What happens if we have two elements that match the css selector?
==> Only modify the first one
==> Need to use selectAll instead
Show in devtools what comes back from a selection
Selectors
Select Me
Practice Selecting This
Select Me
// Pure Javscript
document.querySelector('foo#bar');
// Jquery
$('foo#bar');
// d3
d3.select('foo#bar');
animating-svgs / d3.transition
// STEP #1: Make the circle bigger
// Attr changes called after .transition will happen
// gradually
d3.select('circle')
.transition()
.attr('r', 100);
// STEP #2: Make the circle purple as it moves to the right
// If multiple attr changes occur after .transition they
// happen at the same time
d3.select('circle')
.transition()
.attr('cx', 500)
.attr('fill', "purple");
// STEP #3: Make the circle hot pink then make it smaller
// Separating groups of attr changes with .transition
// method calls will create stepped animations
d3.select('circle')
.transition()
.attr('fill', '#E91E63')
.transition()
.attr('r', 10);
codepen.io/eslivinski/pen/jBwoaz
To make our changes occur as smooth animations, rather than abrupt updates, we can .transiton
Adding the .transition method makes any subsequent .attr calls in the code block animate
If there is more than one .attr call in the code block, the changes will happen simultaneously
Separating groups of .attr calls with .transition, will create stepped animations
data-binding / d3.selection.data
var my_data = [30];
d3.select('circle')
.data(my_data);
codepen.io/eslivinski/pen/zZzQRX
Data binding is where d3 gets its name
Process of attaching data to elements and using that data to determin their appearence/behavior
To bind data we use the .data method, which passes an array of data to our selection
Right click circle > inspect element > open properties tab (in elements) > circle > __data__ === 30
So what?
"Horray you ran some code that allows you to dig through the elements panel and see the number 30"
Data-binding can help us define:
How elements should look on the page
Which elements should be on the page
data-binding / dynamic-attributes
var my_data = [30]
d3.select('circle')
.data(my_data)
.transition()
.attr('r', function(d, i) {
// whenever a function is passed into a method of
// a d3 attribute function the first parameter (d)
// always represents the data bound to that element
// and the second (i) always represents the index
// of that element in the selection
return d;
});
codepen.io/eslivinski/pen/NpgVYX
Select the circle -> what we want to modify
Use .data to pass in my_data => attach my_data to the selection
Transition => animate my changes
Update radius attr
Instead of passing in an static value, we are passing in a function that will recieve 2 variables and return our attr value
First Param(d) => the data attached to the element
Second Param(i) => the elements index in the selection
Params don't have to be called d and i, since we are defining the function, we can call them what ever we want, all that matters is the order
Function must return the value for attr
open the code in codepen and modify the data
data-binding / complex-data-models
var my_data = [
{ stat_1: 100, stat_2: 50 }
];
d3.select('circle')
.data(my_data)
.transition()
.attr('r', function(d, i) {
return d.stat_1;
})
.attr('fill', function(d, i) {
if (d.stat_2 >= 100) {
return 'red';
} else {
return 'blue';
}
});
codepen.io/eslivinski/pen/NpgVMX
This example uses a data object rather than an integer
Radius value is set based on the value of stat_1
The color of the circle is set based on the value of stat_2
open the code in codepen and modify the data
data-binding / d3.selection.data.enter
// Create elements by adding an additional data object
var my_data = [
{ stat_1: 40, stat_2: 10 },
{ stat_1: 30, stat_2: 250 },
{ stat_1: 130, stat_2: 40 }
];
// Have to adjust to our selection to first select the
// container then select the child elements we are
// interested in and to use selectAll
// even though there is currently only one circle
var circles = d3.select('svg')
.selectAll('circle')
.data(my_data);
// Only Happens to the new circles
var newCircles = circles.enter()
.append('circle');
// Update the definition of circles var to include
// the new circles
circles = circles.merge(newCircles)
.attr('cy', 100)
.attr('cx', function(d, i) {
return (i+1) * 200;
})
.transition()
.attr('r', function(d, i) {
return d.stat_1;
})
.attr('fill', function(d, i) {
if (d.stat_2 >= 100) {
return 'red';
} else {
return 'blue';
}
});
codepen.io/eslivinski/pen/bqRyzL
What happens if there are more items in our array then on the page?
By making some adjustments to our code we can actually add el to the page to match the array
.select('svg').selectAll('circle')
Like .attr, the select/selectAll are chainable
When we use select followed by selectAll we are effectively defining a parent and its children
This is necessary if we are going to add new items because we have to tell d3 where to put them
.enter().append
After binding datat to circles
, we are defububg another var newCircles = circles.enter().append('circle')
What this is saying is for all the items in the array that don't don't have a matching element in the DOM, add a new circle to the svg
Specifically, .enter()
selects the elements that are going to be added & ...
.append
adds a new element. -- the value passed into the append method defines what kind of element to add
Open dev tools to show new elements
We could do other stuff in this code block too, but it would only happen to our new circles
circles = circles.merge(newCircles)
Here we are updating our definition of circles to include the newCircles we just added, then setting attr for them
.attr('cx', function(d, i) { return (i+1) * 200; })
js arrays are "zero indexed", meaining var firstItem = someArray[0]; var secondItem = someArray[1];
if we remember that the "i" param passed into our attr function = index, then we see that this evaluates to [200, 400, 600]
this is a very common way to structure code in d3 when arranging items
data-binding / d3.selection.data.exit
var my_data = [
{ stat_1: 150, stat_2: 300 },
{ stat_1: 30, stat_2: 115 }
];
var circles = d3.select('svg')
.selectAll('circle')
.data(my_data);
// Get rid of the extra circle
circles.exit()
.remove();
// Won't be needed because the data is getting smaller
// But we will include it for good measure
var newCircles = circles.enter()
.append('circle');
circles.merge(newCircles)
.attr('cy', 100)
.attr('cx', function(d, i) {
return (i+1) * 200;
})
.transition()
.attr('r', function(d, i) {
return d.stat_1;
})
.attr('fill', function(d, i) {
if (d.stat_2 >= 100) {
return 'red';
} else {
return 'blue';
}
});
codepen.io/eslivinski/pen/YZQbMp
What happens if there are more el in the DOM than in the array?
Again, by making some adjustments to our code we can actually add el to the page to match the array
circles.exit().remove();
Similar to .enter()
, .exit()
selects all the items on the page that don't have matching items in the array and therefore are presumeably leaving
.remove()
deletes the elments in this subset
You might notice that I left the code we added in the last step.
This is because, together these to codeblocks exemplify the 'insert, update, delete' code structure of d3.
This is a pretty advanced concept (horray you did it!), but it allows us to basically throw anything into the array and see it represented in our svg.
data-binding / selecting-things-that-don't-exist
var my_data = [
{ stat_1: 40, stat_2: 10 },
{ stat_1: 30, stat_2: 250 },
{ stat_1: 130, stat_2: 40 }
];
// This looks like craziness, because there are no circles in the svg
// but we are essentially creating a placeholder for circles
// and handling the possibility that there might be circles
var circles = d3.select('svg')
.selectAll('circle')
.data(my_data);
var newCircles = circles.enter()
.append('circle');
circles.exit()
.remove();
circles = circles.merge(newCircles)
.attr('cy', 100)
.attr('cx', function(d, i) {
return (i+1) * 200;
})
.transition()
.attr('r', function(d, i) {
return d.stat_1;
})
.attr('fill', function(d, i) {
if (d.stat_2 >= 100) {
return 'red';
} else {
return 'blue';
}
});
codepen.io/eslivinski/pen/MmWYXy
To really prove the utility of the insert/update/delete structure lets try one more example
Why select things that don't exist?
Takes care of the possibility that they might exist -- we might not know what the data will look like
Create elements with data already bound to them
codepen.io/eslivinski/pen/WpOqeK
Defining the SVG
Unlike the other examples we have gone through, we are starting from complete scratch and need to define the SVG and all the things we want to put in it.
Similar to what we did when we added the circles in a previous example we create the SVG by selecting the parent container where we want the SVG inserted (d3.select("body")
)...
...then add the svg using .append("svg")
...finally, we set the width and the height attrs to create space for our map.
Projection
In cartography a projection is the way you go from a round globe to a flat map (the way you peel the orange)...
In d3 a projection is the exact same thing, but that strategy for going from round to flat is expressed as a js function
Function takes an array of coords ([lng, lat]) and returns an array of coords within our svg
Lots of built in projections, plus the ability to add custom definitions.
Path
Seeing as our projection function only takes one array of coords at a time, we will want to use a path generator to more efficiently project geojson polygons and lines.
Path generators take in geojsons and return projected coordinates expressed in an svg path (ie. d)
Data URL
To make things easy for codepen I am hosting the geojson of the US on github
If you open the url in your browser you will see it is just a geojson
D3.JSON
Since we are loading this json file from somewhere outside of this JS file we need to request the file using d3.json
D3.json takes 2 params
Url of our data
Callback function (What to do when d3 is done going out and grabbing our dataset)
2 param (err and data)
err - if all goes according to plan, we don't have to worry about this, but if there is an issue loadingour dataset
d3 will send that issue back in this 1st param.
data - the data we were looking for
We have to work with our data within the scope of this callback function, because there is no telling how long it will take
our request to actually retrieve the data (like all AJAX calls d3.json is handled asynchronously)
Defining states
var states = svg.selectAll("path").data(geojson.features).enter().append("path")
Using our "selecting things that don't exist" strategy to bind our geojson data to our states
Drawing states
.attr("d", path)
We want to use our geo path function to define the d property of our states. Since attr accepts a function and
will pass the states' data into the first param of that function we can write it like this.
.attr("d", function(d) { return path(d); })
works too.
Styling states
Adding the states class, which will define the stroke color.
Setting the fill property based on the the states' properties.stat_1
Drawing complex SVG shapes / SVG Paths
SVG Paths