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.

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 a d3.zoom behavior. scaleExtent defines min and max scaling factor – from -5 to 5 – and translateExtent and extent define movement boundaries. How much do we allow our chart to move around while zooming? We use 100px in every direction. In componentDidMount and componentDidUpdate , we call our zoom behavior on the rendered SVG. This attaches touch, drag, and scroll events to the DOM. D3 normalizes them into a single zoom event for us. The zoomed function is our zoom event callback. We update component state with d3.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.

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 and componentWillUpdate call updateD3 with fresh props to update internal D3 state updateD3 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 and get 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

Did you enjoy this article? 👎 👍

Published on August 25th, 2017 in d3js, Front End, react, Technical

Learned something new?

Want to become a high value JavaScript expert? Here's how it works 👇 Leave your email and I'll send you an Interactive Modern JavaScript Cheatsheet 📖right away. After that you'll get thoughtfully written emails every week about React, JavaScript, and your career. Lessons learned over my 20 years in the industry working with companies ranging from tiny startups to Fortune5 behemoths. Start with an interactive cheatsheet 📖 Then get thoughtful letters 💌 on mindsets, tactics, and technical skills for your career. "Man, love your simple writing! Yours is the only email I open from marketers 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. 👌" ~ Ashish Kumar Your Name Your Email Your Address Subscribe & Become an expert 💌 Join over 10,000 engineers just like you already improving their JS careers with my letters, workshops, courses, and talks. ✌️

Have a burning question that you think I can answer? I don't have all of the answers, but I have some! Hit me up on twitter or book a 30min ama for in-depth help.

Ready to Stop copy pasting D3 examples and create data visualizations of your own? Learn how to build scalable dataviz components your whole team can understand with React for Data Visualization

Curious about Serverless and the modern backend? Check out Serverless Handbook, modern backend for the frontend engineer.

Ready to learn how it all fits together and build a modern webapp from scratch? Learn how to launch a webapp and make your first 💰 on the side with ServerlessReact.Dev

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 ❤️