Data Visualization for React Developers
Introduction
Workshop Goal
- Use D3 to calculate data
- React to render visualizations
WHY?
- D3’s learning curve
- enter - update - exit == React virtual DOM(let React do the hard work!)
Agenda
- 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
The Basic Chart Types
Introduction to Different Data Types
Data types
- Categorical(genres)
- Ordinal(t-shirt sizes)
- Quantitative(temperatures)
- Temporal(dates)
- Spatial(cities)
Basic Charts and When to Use Them
- 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 special trends
- Domain: spatial regions
- Range: quantitative
- Best for:
- Regional patterns
- Only one variable
- Relative data(normalize for population)
- Not good for:
- Subtle difference in data
- Best for:
- Bar chart
Pie Charts: Do’s and Do not’s
- Basic Charts and When to Use Them
- Pie chart
- For hierarchical part-to-whole
- Best for:
- When values are around 25%, 30%, or 75%
- 3 or 4 values
- Not good for:
- Comparing fine differences
- Multiply totals
- Pie chart
Course Demonstration Review
The Masking of a Chart
Introduction to SVG
- SVG Elements
rect | circle | text | path |
---|---|---|---|
x: x-coordinate of top-left | cx: x-coordinate of center | x: x-coordinate | d: path to follow |
y: y-coordinate of top-left | cy: y-coordinate of center | y: y-coordinate | Moveto, Lineto, CurveTo, Arcto |
width | r: radius | dx: x-coordinate offset | |
height | dy: y-coordinate offset | ||
text-anchor: horizontal text | |||
alignment |
Weather Data Chart Examples
Making of a bar chart
- 365
<rect />
‘s - x: date
- y: high temp
- height: low - high temp
- fill: average temp
- 365
Making of a line chart
- 2
<path />
‘s - d: line commands that connect points
- For each point
- x: date
- y: temperature
- fill: red for high temp blue for low temp
- 2
Making of a radial chart
- 365
<path />
‘s - d: line + curve commands to make slices
- for each slice
- angle: day
- inner radius: low temp
- outer radius: high temp
- fill: average temp
- 365
Exercise: Data to SVG Shapes
Going from Data to SVG Shapes
Going from data to SVG shapes
- One of the(many) things D3 is good for!
Data to SVG:
scales
1
2
3d3.scaleLinear()
.domain([min, max]) // input
.range([min, max]) // outputscale: mapping from data attributes(domain) to display(range)
- date -> x-value
- value -> y-value
- value -> opacity
- etc.
Data to SVG:
scales
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22// get min/max
const width = 800
const height = 600
const data = [
{date: new Date('01-01-2015'), temp: 0},
{date: new Date('01-01-2017'), temp: 3},
]
const min = d3.min(data, d => d.date)
const max = d3.max(data, d => d.date)
// or use extent, which gives back [min, max]
const extent = d3.extent(data, d => d.temp)
const xScale = d3.scaleTime()
.domain([min, max])
.range([0, width])
const yScale = d3.scaleLinear()
.domain(extent)
.range([height, 0])
Data to SVG:
- Scales I use often
- Quantitative
- Continuous domain Continuous range
- scaleLinear
- scaleLog
- scaleTime
- Continuous domain Discrete range
- scaleQuantize
- Continuous domain Continuous range
- Catetorical
- Discrete domain Discrete range
- scaleOrdinal
- Discrete domain Continuous range
- scaleBand
- Discrete domain Discrete range
Exercise: Going from Data to SVG
- Starter notebook
- Full notebook
solution
1 | const extent = d3.extent(data, ({date}) => date) |
Creating Line Chart
Data to SVG:
- line chart
- path
- d: path to follow
- MoveTO, LineTo, CurveTo, ArcTo
- line chart
Data to SVG:
d3.line()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15var data = [
{date: '2007-3-24', value: 93.24},
{date: '2007-3-25', value: 95.35},
{date: '2007-3-26', value: 98.84},
{date: '2007-3-27', value: 99.92},
{date: '2007-3-28', value: 99.80},
{date: '2007-3-29', value: 99.47},
…
];
var line = d3.line()
.x((d) => { return xScale(new Date(d.date)); })
.y((d) => { return yScale(d.value); });
line(data)
Exercise: Creating Line Chart
solution
1 | lineChartData = { |
Creating a Radial Chart
Data to SVG:
- Radial Chart
- path
- d: path to follow
- MoveTo, LineTo, CurveTo, ArcTo
- Radial Chart
Data to SVG:
- d3.arc()
1 | const pie = { |
Exercise: Radial Chart
solution
1 | radialChartData = { |
Rendering with React
Breaking Down D3 API
Some functions are really helpful for getting data ready for D3’s scale/shape/layout functions
D3 Manipultations and Interactions
React Renders
React renders: Architecture
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.)
- Chart component
Where to calculate data:
- getDerivedStateFromPorps
- 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
- getDerivedStateFromPorps
Assumes:
- React manages state(no redux or similar)
- Multiple charts that all share some part of the data or state
Main takeaway:
- D3 calculations can go anywhere(that makes sense for your project) as long as React can access it in its render function
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18componentDidUpdate(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})
}
Exercise: React and Bar Chart
Exercise: React and Radial Chart
The Finishing Touches
The Three Exceptions
Functions where D3 needs access to the DOM
- Axis
- Transitions
- Brush
- Zoom
D3 renders
- Instantiate D3 function in componentDidMount
- Create
<g />
container in render - Place D3 code in componentDidMount and/or componentDidUpdate
- Never ever let D3 and React manage same parts of the DOM! OR BUGS!!
Axes, Legends & Annotations
- D3 renders: axes
- Axes are very important in making the data readable, and D3 makes it easy
1 | // 1. create axisLeft or axisBottom at beginning of React lifecylcle and set corresponding scale |
Exercise: Axes
Transitions
D3 renders: transitions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// in componentDidUpdate
d3.select(this.refs.bars)
.selectAll('rect') // select elements to transition
.data(this.state.bars) // bind data
.transition() // call transition
.attr('y', d => d.y) // set the attributes to transition
.attr('height', d => d.height)
.attr('fill', d => d.fill)
// in render
<g ref="bars">
{this.state.bars.map((d, i) => {
// make sure React doesn't manage the attributes D3 is transitioning!
return <rect key={i} x={d.x} width="2" />
})}
</g>It works, it’s performant, but the code is ugly. Don’t highly recommend it
Brush
D3 renders: brush
1 | // in componentDidMount |
Exercise: Brush
Additional Resources
Readability: legends
Readability: annotations
React Framework
Canvas
Large datasets: canvas
1
2
3
4
5
6
7
8
9// in render
<canvas ref="canvas"
styel={{ width: `${width}px`, height: `${height}px` }}
// retina screen friendly
width={ 2 * width } height={ 2 * height }
/>
// in componentDidMount
ctx = this.refs.canvas.getContext('2d')Performance because only one DOM element that we’re “drawing” shapes on
Large datasets: canvas
- Rect
- ctx.fillRect(x, y, width, height)
- or ctx.strokeRect(x, y, width, height)
- ctx.fillRect(x, y, width, height)
- Circle
- ctx.beginPath()
- ctx.arc(x, y, radius, startAngle, endAngle, anticlockwise)
- ctx.fill()
- or ctx.stroke()
- Line
- ctx.beginPath()
- moveTo, lineTo, bezierCurveTo
- ctx.fill()
- or ctx.stroke()
- ctx.beginPath()
- Rect