--- date: "2021-07-11" title: "Making an interactive, responsive d3 chart in Gatsby" summary: "This blogpost explores how to use d3 inside Gatsby, and shows how to create a simple interactive chart" lang: "en" show: true imgsrc: "/assets/interactive_d3.gif" categories: - Data Visualization - TypeScript - Gatsby --- import BasicComponent from './basicComponent' import StaticPlot from './staticPlot' import StaticPlotWithAxes from './staticPlotWithAxes' import AddingASlider from './addingASlider' import WindowResizing from './windowResizing' import FinalChart from './final' Making an interactive, responsive d3 chart in Gatsby In this blogpost, we will construct the chart of the probability mass function of a Poisson distribution where the user can change the mean parameter $\lambda$. This chart is interactive and responsive. I built it using [d3, a JavaScript library for making data visualizations](https://d3js.org/). To adapt it to my blog and [Gatsby](https://www.gatsbyjs.com/), I had to learn a bit of [React](https://reactjs.org/) in the process. This blogpost explains the process from the ground-up: we first build the plot as a React component, then we add the input slider for the mean, and we wrap up by dynamically changing the plot's width and height according to screen size. If you want to see the code for the final result, [here it is](https://github.com/miguelgondu/blog_gatsby/blob/master/src/pages/blogposts/2021-11-07/final.tsx). I didn't know anything about React before I started making this post, but I did know a little bit of node, d3 and Gatsby. I expect this blogpost to be friendly to people that have some experience with basic NodeJS development (e.g. installing packages using `npm`), with blog developing in Gatsby and with making simple plots on d3. This blogpost has two main references. The first is Chapter 9 of Elijah Meeks' book *D3.js in action* (and [here](https://medium.com/noteableio/interactive-applications-with-react-d3-f76f7b3ebc71)'s a free teaser of it), and the second is [this tutorial video](https://www.youtube.com/watch?v=Bdeu-BFisJU) on mixing d3 and Gatsby by [Jason Lengstorf](https://www.youtube.com/channel/UCnty0z0pNRDgnuoirYXnC5A) and [Swizec Teller](https://twitter.com/Swizec). This interactive plot may seem like a simple example, but it could easily be replaced with more elaborate interactive plots (like the ones showcased in [d3 examples](https://observablehq.com/@d3/gallery)). React components in mdx Gatsby is built on top of React, which is an open-source library for building user interfaces. React uses reusable components that swallow information through their `props` (or by querying some API) and maintain a certain `state`. Our plot doesn't really swallow information, but it does update its state according to user interaction. When developing blogs in Gatsby, it is very common to write posts in **Markdown React** (or `.mdx`). This is a special version of Markdown that allows us to use React components, and this blogpost is an example of one (see the raw file [here](https://raw.githubusercontent.com/miguelgondu/blog_gatsby/master/src/pages/blogposts/2021-11-07/interactive_d3_in_gatsby.mdx)). So, we will write our plot as a React component in a `chart.tsx` file, and place it in the same folder as this blogpost. That way, we can import it by just saying ```tsx // blogpost.mdx import PoissonPlot from './chart' // ... the rest of your blogpost // The place you want the plot to be // ... the rest of your blogpost ``` Now, we need to create and export the React component `PoissonPlot` in `./chart.tsx`. Let's start with some boilerplate. ```ts import React, { Component } from 'react' interface FigureProps { mean: number } class PoissonPlot extends Component { constructor(props: FigureProps) { super(props) // Nothing yet. } render() { return ( <>
I'm a React component.

) } } export default PoissonPlot; ``` Maybe the TypeScript interface for FigureProps is an overkill, but let's leave it like that. If we render it in this state we get: It's not much, but it's a first step! Making the plot in d3 Let's plot our probability mass function inside this component using d3. The function we want to plot is: $$ y(x;\lambda) = \exp(-\lambda)\frac{\lambda^x}{x!}. $$ Let's fix $\lambda=15$ for now. To implement this function we need exponentials (available using the internal `Math` library) and factorials (which we can implement by hand): ```tsx // Somewhere in chart.tsx import * as d3 from 'd3' interface Point { x: number, y: number } function factorial(k: number) { // I hadn't manually implemented a factorial since 2013. let res = 1; for (let i = 1; i <= k; i++) { res *= i; } return res } function poissonPMF(k: number, mean: number) { return ( Math.exp(-mean) * Math.pow(mean, k) / factorial(k) ) } function getPoissonPoints(maxX: number, mean: number): Array { return d3.range(maxX).map((k) => ({x: k, y: poissonPMF(k, mean)})) } ``` And here we used our first d3 function. `d3.range(n)` returns an array `[0, ..., n-1]` that we can then use to create an array of `Point`s with the formula we implemented. We will focus on plotting the cloud of points that results from `getPoissonPoints(25, 15)` for now, and then dynamically change the mean according to user input. Rendering an SVG plot using d3 Usually, a d3 visualization is constructed by letting it select and enter into HTML elements all around the webpage, but this is in conflict with how React uses and maintains a version of the DOM. The usual practice when integrating d3 and React is to let React deal with all DOM manipulations, and use d3 as a mapping library that generates all the necessary drawings in SVG. So what we are going to do is to start the state with the data from `getPoissonPoints(25, 15)`, and display them as dots in an SVG plot using d3. We will need to initialize some axes `x` and `y` and use them to render the dots: ```tsx // Some updates to chart.tsx interface State { mean: number, data: Array, figureWidth: number, figureHeight: number } class PoissonPlot extends Component { state: State x: d3.ScaleLinear; y: d3.ScaleLinear; constructor(props: FigureProps) { super(props) this.state = { "mean": 15, "data": getPoissonPoints(25, 15), "figureWidth": 400, "figureHeight": 400 } // Initializing the axes this.x = d3.scaleLinear() .range([ 45, this.state.figureWidth-10]) .domain([0, Math.max(...this.state.data.map((d: Point) => d.x))]) this.y = d3.scaleLinear() .range([this.state.figureHeight-45, 5]) .domain([0, 0.22]) } render() { let Circle = ({x, y}) => { return } let Line = ({x, y}) => { let style = { stroke: "#04aa6d", strokeWidth: "3px" } return } return ( <>
{/* For each datapoint, render a line and then a circle. */} {this.state.data.map( (d: Point) => ( <> ) )}

) } } export default PoissonPlot; ``` And this is the result of rendering this component: Getting there! The figure is definitely too tall, and we are still missing the axes. Let's add those. I don't understand how to add axes It turns out that adding the usual d3 axes to this plot is non-trivial. I used [`d3blackbox`](https://github.com/Swizec/d3blackbox) (as discussed in the video I linked to, [minute 15:46](https://youtu.be/Bdeu-BFisJU?t=945)). We build an `Axes` object inside our render function that will call the usual `d3.select(something).call(d3.axisBottom(axis))` using a black-box. I'm completely lost in the details, but we need to do it this way because of how React handles DOM manipulations. So, inside the `render` function add something like ```tsx // Somewhere inside the render. let Axis = ({x, y, scale, axisType}) => { let ref; if (axisType == 'y') { // Render y axis ref = useD3( element => d3.select(element).call(d3.axisLeft(scale)) ) } else { // Render x axis ref = useD3( element => d3.select(element).call(d3.axisBottom(scale)) ) } return } ``` And let's add that to our return ```tsx return ( <>
{/* For each datapoint, render a line and then a circle. */} {this.state.data.map( (d: Point) => ( <> ) )} {/* Add x and y axes */}

) ``` Here's the result of rendering it now. Making it interactive Now that we have the core plot working, the next step is to add interactivity with a slider that governs the `mean` attribute of the state, to recompute the data every time there's a user interaction and to re-render the plot accordingly. We need to start maintaining and updating the state according to changes in the component. Let's add an `` tag to our component, and have it change the state whenever its value changes. ```tsx // Some changes to the component in chart.tsx class PoissonPlot extends Component { state: State x: d3.ScaleLinear; y: d3.ScaleLinear; constructor(props: FigureProps) { super(props) // Binding the changing function to have access // to all our attributes and methods. this.handleChange = this.handleChange.bind(this); // All the other stuff about state and axes // ... } handleChange(event) { // A function that is called every time the // slider value changes this.setState( { mean: event.target.value, data: getPoissonPoints(25, event.target.value) } ) } // Mounting the component. componentDidMount () {} render() { // All the stuff about circles, lines and axes // ... return ( <>
{/* everything that was inside the svg before */}

) } } export default PoissonPlot; ``` If we render it, now we get Now we are talking! The default slider is ugly, but we can change it later into [whatever w3schools recommends](https://www.w3schools.com/howto/howto_js_rangeslider.asp). Making it responsive Notice that the figure has a static width and height. In what remains, we will adapt this width and height dynamically to cover about 4/5s of the screen, but bounded at 500px width and height. We will also add some style to have it be in the center of the page. At mount time, we will add an event listener for every time the window is resized, which executes a function that adapts these two attributes in the state. We will also add a method that computes what the figure width and height should be, given the current screen size: ```tsx // Some changes in the component class PoissonPlot extends Component { state: State x: d3.ScaleLinear; y: d3.ScaleLinear; constructor(props: FigureProps) { // A new constructor that resizes super(props) this.handleChange = this.handleChange.bind(this); this.onResize = this.onResize.bind(this); // Get initial width and height let figSpecs = this.getFigureWidthAndHeight() // initialize state and axes this.state = { "mean": 15, "data": getPoissonPoints(25, 15), "figureWidth": figSpecs["figureWidth"], "figureHeight": figSpecs["figureHeight"] } this.x = d3.scaleLinear() .range([ 45, figSpecs["figureWidth"]-10]) .domain([0, 25]) this.y = d3.scaleLinear() .range([figSpecs["figureHeight"]-45, 5]) .domain([0, 0.22]) } // handleChange and some other stuff // A new function getFigureWidthAndHeight() { let maxHeight = 500; let maxWidth = 500; let proportion = 4/5; // We need to do this screen width and height // stuff because Gatsby needs something // that works on node alone, without a screen. // See https://github.com/gatsbyjs/gatsby/issues/12427 let screenHeight let screenWidth if (typeof window !== `undefined`) { screenHeight = Math.min(window.innerHeight, maxHeight) screenWidth = Math.min(window.innerWidth, maxWidth) } let figureWidth = screenWidth * proportion let figureHeight = screenHeight * proportion return {"figureWidth": figureWidth, "figureHeight": figureHeight} } // A new function that resizes the figure onResize() { let figSpecs = this.getFigureWidthAndHeight() this.setState(figSpecs) // Update the axes. this.x = d3.scaleLinear() .range([ 45, figSpecs["figureWidth"]-10]) .domain([0, 25]) this.y = d3.scaleLinear() .range([figSpecs["figureHeight"]-45, 5]) .domain([0, 0.22]) } // A new component mount componentDidMount () { window.addEventListener('resize', this.onResize, false); this.onResize() } // Some changes in render render() { // ...Definitions of Circle, Line and Axis // What's new: let figContainerStyle = { textAlign: "center" } return ( <>
{/* This is a new div with centered contents */}
{/* The input... */}
{/* The svg... */}

) } } ``` With these changes, we are almost done: Putting it all together Let's add a couple of divs with the title and the current mean value. Let's also add some css for our slider. Adding the divs is quite trivial, we just put a couple of divs in the fig container, and define their style somewhere inside the `render`: ```tsx // Put this inside the fig container div
Poisson distribution
Mean: {this.state.mean}
// and this by the other container style. let textContainerStyle = { textAlign: "center", lineHeight: 1.6, fontSize: "xx-large" } ``` And finally, you can copy and paste [w3school's range sliders CSS](https://www.w3schools.com/howto/howto_js_rangeslider.asp) into your CSS files (which, in my case, resides in `/styles/global.css`). Finally, add the `className="slider"` attribute to the input tag. If we do all that, we get our final chart! Conclusion In this blogpost we made a d3 chart inside a React component. The way this usually works is by letting React handle DOM updates, while D3 maps data into SVG elements. This component reacts to user interaction, and updates the data it is maintaining and re-renders the chart with this new specifications. We also made the width and height dynamically change according to the window's shape and its resizes! Which means that, in theory, this plot should adapt its size according to screen resizes.