Shirley Wu
Use D3 to calculate data
React to render visualizations
D3's learning curve:
enter-update-exit
==
React's virtual DOM
(let React do the hard work!)
Data Visualization
for React Developers
↓
Intro to D3
↓
Building Custom
Data Visualizations
Basic chart types
and when to use them
↓
The making of
a chart with SVG
↓
Going from data
to SVG shapes
↓
Using React to
render SVG and Canvas
↓
Exceptions and
finishing touches
Bar chart
For categorical comparisons
Domain: categorical
Range: quantitative
Histogram
For categorical distributions
Domain: quantitative bins
Range: frequency of quantitative bin
Scatterplot
For correlation
2 attributes and the relationship between their quantitative values
Line chart
For temporal trends
Domain: temporal
Range: quantitative
Tree
For hierarchy
Parent-child relations
Multiple tiers of category
Node-link diagram
For connection
Relationship between entities
Chloropleth
For spatial trends
Domain: spatial regions
Range: quantitative
Best for:
Not good for:
(Source: Datawrapper)
Pie chart
For hierarchical part-to-whole
Best for:
Not good for:
(Source: Datawrapper)
rect
x: x-coordinate of top-left
y: y-coordinate of top-left
width
height
circle
cx: x-coordinate of center
cy: y-coordinate of center
r: radius
text
x: x-coordinate
y: y-coordinate
dx: x-coordinate offset
dy: y-coordinate offset
text-anchor: horizontal text alignment
Hi!
path
d: path to follow
Moveto, Lineto, Curveto, Arcto
0,0
x
y
25,50
125,100
365 <rect />'s
2 <path />'s
365 <path />'s
Play with the SVG elements!
(Observable notebook)
One of the (many) things
D3 is good for!
rect
x: x-coordinate of top-left
y: y-coordinate of top-left
width
height
d3.scaleLinear()
.domain([min, max]) // input
.range([min, max]); // output
scale: mapping from
data attributes (domain)
to display (range)
date → x-value
value → y-value
value → opacity
etc.
// get min/max
var width = 800;
var height = 600;
var data = [
{date: new Date('01-01-2015'), temp: 0},
{date: new Date('01-01-2017'), temp: 3}
];
var min = d3.min(data, d => d.date);
var max = d3.max(data, d => d.date);
// or use extent, which gives back [min, max]
var extent = d3.extent(data, d => d.temp);
var xScale = d3.scaleTime()
.domain([min, max])
.range([0, width]);
var yScale = d3.scaleLinear()
.domain(extent)
.range([height, 0]);
Quantitative | Continuous domain Continuous range |
scaleLinear scaleLog scaleTime |
Continuous domain Discrete range |
scaleQuantize | |
Categorical | Discrete domain Discrete range |
scaleOrdinal |
Discrete domain Continuous range |
scaleBand |
path
d: path to follow
Moveto, Lineto, Curveto, Arcto
var data = [
{date: '2007-3-24', value: 93.24},
{date: '2007-3-24', value: 95.35},
{date: '2007-3-24', value: 98.84},
{date: '2007-3-24', value: 99.92},
{date: '2007-3-24', value: 99.80},
{date: '2007-3-24', value: 99.47},
…
];
var line = d3.line()
.x((d) => { return xScale(new Date(d.date)); })
.y((d) => { return yScale(d.value); });
line(data)
Input: array of objects
Output: string that can be used in path's d attribute
M5,19.0625L13,21.875L21,12.03125
L29,12.03125L37,23.28125L45,26.09375
L53,7.8125L61,5L69,12.03125L77,10.625
L85,13.4375L93,21.875L101,14.84375
L109,13.4375L117,21.875L125,19.0625
L133,21.875L141,17.65625L149,13.4375L157,16.25
L165,16.25L173,14.84375L181,19.0625L189,19.0625
L197,16.25L205,16.25L213,13.4375
L221,12.03125L229,10.625L237,10.625L245,12.03125
path
d: path to follow
Moveto, Lineto, Curveto, Arcto
var pie = {
"data": 1, "value": 1,
"startAngle": 6.050474740247008,
"endAngle": 6.166830023713296,
};
var arc = d3.arc()
.innerRadius(0)
.outerRadius(100)
.startAngle(d => d.startAngle)
.endAngle(d => d.endAngle);
arc(pie);
// M-23.061587074244123,-97.30448705798236A100,100,0,0,1,-11.609291412523175,-99.32383577419428L0,0Z
Let's break down the D3 API to just what we care about ✌️
I usually end up using native array functions or lodash for these
(it depends on what you feel comfortable with!)
But some functions are really helpful for getting data ready for D3's scale/shape/layout functions
I use this a lot for getting data into what SVG needs to draw
These are really great dataviz-specific interactions that I use frequently
What we'll use React for
Division of responsibilities:
Chart component
Gets passed in raw data as prop
Translates raw data to screen space
Renders the calculated data
Manages state for interactions that don’t require redrawing of the chart (hover, click)
Root component
Manages updates to raw data
Manages state for interactions that require redrawing of charts (filter, aggregate, sort, etc.)
Where to calculate data:
getDerivedStateFromProps
Pro: simple and straightforward
Con: asynchronous, race conditions if not careful
Render
Pro: very straightforward
Con: will recalculate on every lifecycle
componentDidMount & componentDidUpdate
Pro: no race condition
Con: less straightforward
Assumes:
Main takeaway:
Draw a bar chart!
Draw a radial chart!
componentDidUpdate(nextProps, nextState) {
// prevents infinite loop
if (this.props.someId !== nextProps.someId) {
this.calculateData();
}
}
componentDidMount() {
// Make sure component is rendered first
if (this.SVG.current) {
this.calculateData();
}
}
calculateData() {
...
this.setState({data})
}
(Adapted from code by Jhon Paredes)
Functions where D3 needs access to the DOM
**Never ever let D3 and React manage same parts of the DOM!
OR BUGS!!
Axes are very important in making the data readable, and D3 makes it easy.
const yAxis = d3.axisLeft()
.scale(yScale);
<g ref='group' />
d3.select(this.refs.group)
.call(yAxis);
Create axes for the bar chart!
*It works, it's performant, but the code is ugly. I don't highly recommend it.
// in componentDidUpdate
d3.select(this.refs.bars)
.selectAll('rect')
.data(this.state.bars)
.transition()
.attr('y', d => d.y)
.attr('height', d => d.height)
.attr('fill', d => d.fill);
// in render
<g ref='bars'>
{this.state.bars.map((d, i) =>
(<rect key={i} x={d.x} width='2' />))}
</g>
In componentDidUpdate:
Make sure React doesn't manage the attributes D3 is transitioning!
// in componentDidMount
this.brush = d3.brush()
.extent([[0, 0], [width ,height]])
.on('end', () => ...);
d3.select(this.refs.brush)
.call(this.brush);
// in render
<g ref='brush' />
In componentDidMount:
// in render
<canvas ref=’canvas’
style={{width: `${width}px`, height: `${ height}px`}}
// retina screen friendly
width={2 * width} height={2 * height} />
// in componentDidMount
ctx = this.refs.canvas.getContext('2d')
Performant because only one DOM element that we're "drawing" shapes on
ctx.fillRect(x, y, width, height)
// or ctx.strokeRect(x, y, width, height)
ctx.beginPath()
ctx.arc(x, y, radius, startAngle, endAngle, anticlockwise)
ctx.fill() // or ctx.stroke()
ctx.beginPath()
// moveTo, lineTo, bezierCurveTo
ctx.fill() // or ctx.stroke()
Render the bar chart with canvas!
Render the radial chart with canvas!
Thank you to
my beta-testers 💕
Carol
Juan
Jhon
Sam
Hector
Alexander