A question I often get is this: "How do you build a zoomable dataviz component?"
Well, you use d3.zoom. That gives you zoom
events for pinch-to-zoom and the mousewheel. Detects panning too. Just like your users expect from everything else that zooms.
Then what?
Then you have a choice to make. Do you want to zoom your whole component like it was an image, or do you want to zoom the space between your datapoints? The first looks pretty, the second gives users a chance to see more detail.
In a side-by-side comparison, the two zoom effects look like this 👇
Both scatterplots use the same random data. Left side zooms like an image, right side zooms the space between datapoints. It even works on a phone, look.
So how do you make that?
You'll need: - 2 React components - 2 D3 scales - 1 D3 zoom - 1 D3 random number generator - 1 line of HTML - 5 lines of CSS - some event hooks - a sprinkle of state - a few props
Here we go 🤘
See the Pen Two different zooms with D3 and React by Swizec Teller (@swizec) on CodePen.
<Chart /> component talks to d3.zoom
Our <Chart />
component renders two scatterplots and talks to d3.zoom
to zoom them. This way we can use a single zoom behavior for the entire SVG, which makes the scatterplots zoom in unison.
I also found it more reliable than attaching d3.zoom
to individual <g>
elements, but couldn't figure out why. I think it assumes internally that it's working on a whole SVG element.
const random = d3.randomNormal(5, 1);
class Chart extends React.Component {
constructor(props) {
super(props);
this.state = {
data: d3.range(200).map((_) => [random(), random()]),
zoomTransform: null,
};
this.zoom = d3
.zoom()
.scaleExtent([-5, 5])
.translateExtent([
[-100, -100],
[props.width + 100, props.height + 100],
])
.extent([
[-100, -100],
[props.width + 100, props.height + 100],
])
.on("zoom", this.zoomed.bind(this));
}
componentDidMount() {
d3.select(this.refs.svg).call(this.zoom);
}
componentDidUpdate() {
d3.select(this.refs.svg).call(this.zoom);
}
zoomed() {
this.setState({
zoomTransform: d3.event.transform,
});
}
render() {
const { zoomTransform } = this.state,
{ width, height } = this.props;
return (
<svg width={width} height={height} ref="svg">
<scatterplot
data={this.state.data}
x={0}
y={0}
width={width / 2}
height={height}
zoomtransform={zoomTransform}
zoomtype="scale"
></scatterplot>
<scatterplot
data={this.state.data}
x={width / 2}
y={0}
width={width / 2}
height={height}
zoomtransform={zoomTransform}
zoomtype="detail"
></scatterplot>
</svg>
);
}
}
Our chart component breaks down into 4 parts:
- We use the
constructor
to generate random[x, y]
coordinate pairs and ad3.zoom
behavior.scaleExtent
defines min and max scaling factor – from-5
to5
– andtranslateExtent
andextent
define movement boundaries. How much do we allow our chart to move around while zooming? We use100px
in every direction. - In
componentDidMount
andcomponentDidUpdate
, we call our zoom behavior on the rendered SVG. This attaches touch, drag, and scroll events to the DOM. D3 normalizes them into a singlezoom
event for us. - The
zoomed
function is our zoom event callback. We update component state withd3.event.transform
, which is where D3 puts the information we need to zoom our chart. - Our
render
method draws two<Scatterplot />
components inside an<svg>
element and gives them some props.
<Scatterplot /> component draws datapoints and zooms itself
The <Scatterplot />
component follows the full integration approach I outline in React+D3v4. We have D3 stuff in an updateD3
function and we call it when props change to update the internal states of D3 objects.
One complication we run into is that we use the same scatterplot component for two different types of zoom. That means some bloat, but it's manageable.
class Scatterplot extends React.Component {
constructor(props) {
super(props);
this.updateD3(props);
}
componentWillUpdate(nextProps) {
this.updateD3(nextProps);
}
updateD3(props) {
const { data, width, height, zoomTransform, zoomType } = props;
this.xScale = d3.scaleLinear()
.domain([0, d3.max(data, ([x, y]) => x)])
.range([0, width]),
this.yScale = d3.scaleLinear()
.domain([0, d3.max(data, ([x, y]) => y)])
.range([0, height]);
if (zoomTransform && zoomType === "detail") {
this.xScale.domain(zoomTransform.rescaleX(this.xScale).domain());
this.yScale.domain(zoomTransform.rescaleY(this.yScale).domain());
}
}
get transform() {
const { x, y, zoomTransform, zoomType } = this.props;
let transform = "";
if (zoomTransform && zoomType === "scale") {
transform = `translate(${x + zoomTransform.x}, ${y + zoomTransform.y}) scale(${zoomTransform.k})`;
}else{
transform = `translate(${x}, ${y})`;
}
return transform;
}
render() {
const { data } = this.props;
return (
<g transform={this.transform} ref="scatterplot">
{data.map(([x, y]) => <circle cx={this.xScale(x)} cy={this.yScale(y)} r={4}>)}
</circle></g>
)
}
}
Much like the <Chart />
component, you can think of <Scatterplot />
as having 4 parts:
constructor
andcomponentWillUpdate
callupdateD3
with fresh props to update internal D3 stateupdateD3
sets up two linear scales for us.xScale
translates between data values and horizontal coordinates,yScale
translates between data values and vertical coordinates- The third part is split between the bottom of
updateD3
andget transform
. It handles zooming.
Inside updateD3
we zoom the space between datapoints by changing our scale's domains. zoomTransform.rescaleX
takes a scale and returns a changed scale. We take its domain and update xScale
. Same for yScale
. This updates both the scatterplot's positioning and spacing between datapoints.
This will never make intuitive sense to me, but it works.
get transform
also handles zooming. It creates an SVG transform
attribute which we use to position and scale a scatterplot. We use translate()
to move a chart into position and scale()
to make it bigger or smaller depending on the factor zoomTransform
gives us.
Even if we're not zooming, we still translate()
the chart so that we can move it around the page and show two scatterplots side by side.
- The fourth part is our
render
method. It creates a grouping element, walks through our data and renders circles.
You can play with this example on CodePen.
See the Pen Two different zooms with D3 and React by Swizec Teller (@swizec) on CodePen.
To learn more about putting React and D3v4 together, check out my new book, React+D3v4
Continue reading about The two ways to build a zoomable dataviz component with d3.zoom and React
Semantically similar articles hand-picked by GPT-4
- Livecoding #34: A Map of Global Migrations, Part 3
- Declarative D3 charts with React 16.3
- Tooltips and state across various d3 charts in a React dashboard!
- JavaScript’s most popular dataviz library
- How to drive React state with D3 transitions for complex animation
Learned something new?
Read more Software Engineering Lessons from Production
I write articles with real insight into the career and skills of a modern software engineer. "Raw and honest from the heart!" as one reader described them. Fueled by lessons learned over 20 years of building production code for side-projects, small businesses, and hyper growth startups. Both successful and not.
Subscribe below 👇
Software Engineering Lessons from Production
Join Swizec's Newsletter and get insightful emails 💌 on mindsets, tactics, and technical skills for your career. Real lessons from building production software. No bullshit.
"Man, love your simple writing! Yours is the only newsletter I open and only blog that I give a fuck to read & scroll till the end. And wow always take away lessons with me. Inspiring! And very relatable. 👌"
Have a burning question that you think I can answer? Hit me up on twitter and I'll do my best.
Who am I and who do I help? I'm Swizec Teller and I turn coders into engineers with "Raw and honest from the heart!" writing. No bullshit. Real insights into the career and skills of a modern software engineer.
Want to become a true senior engineer? Take ownership, have autonomy, and be a force multiplier on your team. The Senior Engineer Mindset ebook can help 👉 swizec.com/senior-mindset. These are the shifts in mindset that unlocked my career.
Curious about Serverless and the modern backend? Check out Serverless Handbook, for frontend engineers 👉 ServerlessHandbook.dev
Want to Stop copy pasting D3 examples and create data visualizations of your own? Learn how to build scalable dataviz React components your whole team can understand with React for Data Visualization
Want to get my best emails on JavaScript, React, Serverless, Fullstack Web, or Indie Hacking? Check out swizec.com/collections
Did someone amazing share this letter with you? Wonderful! You can sign up for my weekly letters for software engineers on their path to greatness, here: swizec.com/blog
Want to brush up on your modern JavaScript syntax? Check out my interactive cheatsheet: es6cheatsheet.com
By the way, just in case no one has told you it yet today: I love and appreciate you for who you are ❤️