JR
Use OS setting

Pages

No results for 'undefined'

Blog Posts

No results for 'undefined'
Powered by Algolia

Gatsby Interactive Plots

Aug 30, 20192 min readJS, Tutorial, Web Dev, Science

Contents

This is a guide on how to get interactive 2d and 3d plots displayed alongside regular markdown content by combining Gatsby, Plotly and MDX. I’m assuming you already have a Gatsby site up and running.

Setting up MDX

The first step is to equip your existing site with MDX-support. Gatsby already provides an official and as always excellent guide on how to do that. The process has gotten much simpler in recent months and by now is pretty much reduced to adding mdx and gatsby-plugin-mdx to your dependencies

yarn add gatsby-plugin-mdx @mdx-js/mdx @mdx-js/react

followed by including it in your gatsby-config.js

gatsby-config.js
module.exports = {
  plugins: [
    // ....
    `gatsby-plugin-mdx`,
  ],
}

That’s the most basic setup. Just like that, you can start adding .mdx files to your src/pages folder and they’ll be automatically converted to HTML pages including any React components you import. Once you’ve started getting the hang of MDX’s capabilities you’ll probably want to look into how to create MDX pages programmatically in gatsby-node.js (but that’s not the subject of this post).

Setting up Plotly

Next we’ll need to install a plotting library. I went with Plotly because

  • it’s been around for a while, it’s consistently added new features over that period and hence has accumulated extensive functionality in both 2d and 3d data visualization.
  • it provides official React support.
  • it has multiple language bindings including Python and R.
  • it seems like the library most suited to scientific plotting in the JavaScript ecosystem.
  • they’ve really fleshed out their docs over the last year or two.

Plotly offers loads of customization options, perhaps at the expense of being a little more verbose than other options. If you’re more interested in financial data visualization where (I think) people use more standardized plots, you might want to take a look at Highcharts instead which also offers official React support. However, they seem to be 2d only.

To start using Plotly, we need to install it along with react-loadable

yarn add plotly.js react-plotly.js react-loadable

The reason we need react-loadable is that react-plotly.js as yet doesn’t support server-side rendering (SSR). They rely on several browser APIs such as the global document variable, bounding boxes and WebGL contexts which don’t exist in node, making it hard to work making SSR hard to implement on their side. In any case, react-loadable allows us to turn the <Plot /> component (the default export of react-plotly.js) into a lazily-loading version of itself. That means it will not be rendered during Gatsby’s build cycle. Instead it renders client-side once the page has loaded where document is available. The LazyPlot component looks as follows:

src/components/Loadable/index.js
import React, { useState } from 'react'
import Loadable from 'react-loadable'
import { SpinnerDiv } from './styles'

export const Spinner = () => {
  const [active, setActive] = useState(true)
  return (
    <SpinnerDiv active={active} onClick={() => setActive(!active)}>
      {Array(4).fill(<div />)}
    </SpinnerDiv>
  )
}

export const LazyPlot = Loadable({
  loader: () => import('react-plotly.js'),
  loading: () => <Spinner />,
})

I threw in a cool little <Spinner /> courtesy of Tobias Ahlin which I refactored to use styled-components and have dark mode support. It is displayed while the plot is still loading to let the user know more content is about to appear in the spinner’s place. This is what looks like in action. (You can click it to pause the animation if it disturbs your reading.)

Here’s the corresponding <SpinnerDiv /> styled component:

src/components/Loadable/styles.js
import styled from 'styled-components'

// Adapted from https://tobiasahlin.com/spinkit.
export const SpinnerDiv = styled.div`
  margin: 2em auto;
  width: 2em;
  height: 2em;
  position: relative;
  transform: rotateZ(45deg);
  div {
    float: left;
    width: 50%;
    height: 50%;
    position: relative;
    transform: scale(1.1);
  }
  div:before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: ${props => props.theme.textColor};
    ${props => props.active && `animation: foldCube 2.4s infinite linear both`};
    transform-origin: 100% 100%;
  }
  ${[2, 4, 3]
    .map(
      (el, idx) => `div:nth-child(${el}) {
        transform: scale(1.1) rotateZ(${90 * (idx + 1)}deg);
      }
      div:nth-child(${el}):before {
        animation-delay: ${0.3 * (idx + 1)}s;
      }`
    )
    .join(`\n`)}
  @keyframes foldCube {
    0%,
    10% {
      transform: perspective(140px) rotateX(-180deg);
      opacity: 0;
    }
    25%,
    75% {
      transform: perspective(140px) rotateX(0deg);
      opacity: 1;
    }
    90%,
    100% {
      transform: perspective(140px) rotateY(180deg);
      opacity: 0;
    }
  }

And that’s it for the setup. We’re now ready to produce some actual plots.

Demos

Frontend Framework Popularity

Let’s start simple with a 2d plot.

Frontend framework popularity over time measured by Google search frequency. Source: Google Trends

I was particularly surprised how simple it was to add dark mode support to this plot. All it took was the three highlighted lines in the following component.

frameworkPopularity.js
import React from 'react'
import { withTheme } from 'styled-components'
import { LazyPlot } from '../../../src/components/Loadable'

const colors = [`red`, `green`, `blue`, `orange`]

// prettier-ignore
const months = [`2012/01`, `2012/02`, `2012/03`, `2012/04`, `2012/05`, `2012/06`, `2012/07`, `2012/08`, `2012/09`, `2012/10`, `2012/11`, `2012/12`, `2013/01`, `2013/02`, `2013/03`, `2013/04`, `2013/05`, `2013/06`, `2013/07`, `2013/08`, `2013/09`, `2013/10`, `2013/11`, `2013/12`, `2014/01`, `2014/02`, `2014/03`, `2014/04`, `2014/05`, `2014/06`, `2014/07`, `2014/08`, `2014/09`, `2014/10`, `2014/11`, `2014/12`, `2015/01`, `2015/02`, `2015/03`, `2015/04`, `2015/05`, `2015/06`, `2015/07`, `2015/08`, `2015/09`, `2015/10`, `2015/11`, `2015/12`, `2016/01`, `2016/02`, `2016/03`, `2016/04`, `2016/05`, `2016/06`, `2016/07`, `2016/08`, `2016/09`, `2016/10`, `2016/11`, `2016/12`, `2017/01`, `2017/02`, `2017/03`, `2017/04`, `2017/05`, `2017/06`, `2017/07`, `2017/08`, `2017/09`, `2017/10`, `2017/11`, `2017/12`, `2018/01`, `2018/02`, `2018/03`, `2018/04`, `2018/05`, `2018/06`, `2018/07`, `2018/08`]

// prettier-ignore
const data = {
  React: [2, 2, 3, 3, 3, 2, 2, 2, 3, 3, 3, 3, 2, 3, 3, 3, 2, 2, 3, 3, 3, 4, 4, 3, 4, 4, 5, 5, 5, 5, 6, 7, 7, 10, 10, 10, 13, 18, 18, 19, 20, 21, 24, 25, 27, 28, 29, 29, 32, 39, 39, 41, 42, 43, 41, 43, 41, 47, 49, 50, 55, 65, 68, 68, 71, 79, 76, 83, 73, 80, 74, 66, 74, 82, 88, 89, 94, 95, 98, 100],
  AngularJS: [1, 1, 1, 2, 3, 3, 4, 4, 4, 6, 8, 9, 11, 13, 17, 17, 20, 22, 25, 24, 23, 30, 33, 35, 39, 41, 45, 49, 53, 53, 58, 51, 48, 52, 58, 61, 60, 61, 69, 74, 67, 67, 65, 58, 57, 53, 61, 62, 59, 59, 64, 56, 59, 51, 53, 51, 51, 54, 57, 62, 55, 55, 55, 51, 52, 47, 46, 41, 34, 37, 41, 38, 37, 38, 38, 35, 35],
  Angular: [2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 5, 5, 6, 6, 8, 7, 9, 9, 12, 12, 11, 13, 15, 15, 16, 17, 20, 20, 21, 20, 24, 22, 20, 24, 27, 26, 29, 30, 33, 35, 33, 34, 31, 30, 29, 29, 32, 36, 33, 37, 40, 35, 36, 38, 36, 38, 36, 42, 48, 52, 49, 49, 55, 48, 55, 49, 52, 51, 44, 48, 53, 53, 54, 56, 54, 60, 56],
  Vue: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 1, 2, 3, 3, 3, 3, 3, 4, 5, 5, 6, 6, 8, 8, 8, 9, 10, 10, 12, 12, 12, 12, 12, 14, 14, 16, 16, 16, 18, 18, 19]
}

export default withTheme(({ theme }) => (
  <LazyPlot
    data={Object.keys(data).map((key, index) => ({
      x: months,
      y: data[key],
      type: `scatter`,
      mode: `lines+markers`,
      name: key,
      marker: { color: colors[index] },
    }))}
    layout={{
      margin: { t: 0, r: 0, l: 35 },
      paper_bgcolor: `rgba(0, 0, 0, 0)`,
      plot_bgcolor: `rgba(0, 0, 0, 0)`,
      font: {
        color: theme.textColor,
        size: 16,
      },
      // The next 3 directives make the plot responsive.
      autosize: true,
    }}
    style={{ width: `100%` }}
    config={{
      displayModeBar: false,
      showTips: false,
    }}
  />
))

Saddle

Next, let’s aim a little higher and visualize the saddle point of x2y2x^2 - y^2 at (0,0)(0,0).

The surface x2y2x^2 - y^2 plotted over the domain [10,10]2[-10,10]^2.

saddle.js
import React from 'react'
import { withTheme } from 'styled-components'
import { LazyPlot } from '../../../src/components/Loadable'

const [points, middle] = [21, 10]
const range = Array.from(Array(points), (e, i) => i - middle)
const data = range.map(x => range.map(y => x * x - y * y))

export default withTheme(({ theme }) => (
  <LazyPlot
    data={[
      {
        z: data,
        type: `surface`,
        contours: {
          z: {
            show: true,
            usecolormap: true,
            highlightcolor: `white`,
            project: { z: true },
          },
        },
      },
    ]}
    layout={{
      margin: { t: 0, r: 0, l: 35 },
      paper_bgcolor: `rgba(0, 0, 0, 0)`,
      plot_bgcolor: `rgba(0, 0, 0, 0)`,
      font: {
        color: theme.textColor,
        size: 16,
      },
      autosize: true,
    }}
    style={{ width: `100%`, height: `30em` }}
    useResizeHandler
    config={{
      displayModeBar: false,
      showTips: false,
    }}
  />
))

Trimodal Distribution

Here’s another surface, this one a trimodal distribution consisting of three unnormalized Gaussians.

exp[120(x2+y2)]+exp{110[(x10)2+y2]}+exp{110[(x7)2+(y7)2]}\exp[-\frac{1}{20}(x^2 + y^2)] + \exp\{-\frac{1}{10}[(x-10)^2 + y^2]\} + \exp\{-\frac{1}{10}[(x-7)^2 + (y-7)^2]\}.

triModal.js
import React from 'react'
import { withTheme } from 'styled-components'
import { LazyPlot } from './Loadable'

const [points, middle] = [51, 25]
const range = Array.from(Array(points), (e, i) => 0.5 * (i - middle))
const data = range.map(x =>
  range.map(
    y =>
      Math.exp(-0.05 * (x * x + y * y)) +
      0.7 * Math.exp(-0.1 * (Math.pow(x - 10, 2) + y * y)) +
      0.5 * Math.exp(-0.1 * (Math.pow(x + 7, 2) + Math.pow(y - 7, 2)))
  )
)

export default withTheme(({ theme }) => (
  <LazyPlot
    data={[
      {
        z: data,
        x: range,
        y: range,
        type: `surface`,
        contours: {
          z: {
            show: true,
            usecolormap: true,
            highlightcolor: `white`,
            project: { z: true },
          },
        },
        showscale: false,
      },
    ]}
    layout={{
      margin: { t: 0, r: 0, l: 35 },
      paper_bgcolor: `rgba(0, 0, 0, 0)`,
      plot_bgcolor: `rgba(0, 0, 0, 0)`,
      font: {
        color: theme.textColor,
        size: 16,
      },
      autosize: true,
    }}
    style={{ width: `100%`, height: `30em` }}
    useResizeHandler
    config={{
      displayModeBar: false,
      showTips: false,
    }}
  />
))

Conclusion

I’m still getting the hang of Plotly’s APIs and it usually takes some fiddling to get things looking the way I want but in exchange, the possibilities seem endless. What a great time to be developing for the web!

Questions or cool ideas for more plots to add to the demo? Let me know in the comments.

© 2019 - Janosh RiebesellThis site is open source
Powered byGatsbyGithubNetlify