React for Data Visualization [FULL COURSE] - Part 8

s30e146 - Christmas movies at the box office - horizontal bar chart

Challenge

Christmas movies are the best movies. How much do they make at the box office? Show the power distribution curve with a vertical barchart.

Dataset

My Solution

We built this one with React hooks because we can. Not a class-based component in sight :v:

Styled components for styling, D3 for scales and data loading and parsing, hooks to hook it all together.

Loading data with React hooks

I looked around for a good data loading hook. None could be found. So we made our own :muscle:

Not that hard as it turns out. You need a dash of useState to save the data you load, a bit of useEffect to run data loading on component mount aaaand … that’s it. Goes in your App function.

function App() {
  const [data, setData] = useState(null);

  useEffect(
    () => {
      d3.tsv("/data.tsv", d => {
        const year = Number(d.movie.match(/\((\d+)\)/)[1]);
        return {
          movie: d.movie.replace(/\(\d+\)/, ""),
          year: year,
          per_year: Number(d.box_office) / (2018 - year),
          box_office: Number(d.box_office)
        };
      }).then(setData);
    },
    [!data]
  );

The useState hook takes a default value, and always returns current state - data - and a setter - setData .

useEffect runs our function on every component render. After committing to the DOM, I believe. We use d3.tsv to load and parse our christmas movie dataset, use a parsing function to transform each row into an object with all the info we need, then call setData when he have it.

Each datapoint holds

  • a movie name
  • the year a movie was produced parsed from the movie name with a regex
  • the per_year revenue of the movie as a fraction
  • the total box_office revenue

Switch display modes with React hooks

Movie box office revenue follows a pretty clear power law distribution. The highest grossing movie or two make a lot more than the next best. Which makes way more than next one down the list, etc.

But how does age factor into this?

Home Alone has had 28 years to make its revenue. Daddy’s Home 2 is only a year old.

I decided to add a button to switch modes. From total box_office to per_year revenue. And boy does it change the story. Altho maybe I’m being unfair because how long are theater runs anyway? :thinking:

Driving that logic with React hooks looks like this :point_down:

const [perYear, setPerYear] = useState(false)
const valueFunction = perYear ? d => d.per_year : d => d.box_office

// ...

<Button onClick={() => setPerYear(!perYear)}>
  {perYear ? "Show Total Box Office" : "Show Box Office Per Year"}
</Button>

A useState hook gives us current state and a setter. We use the state, perYear , to define a value accessor function, and a butto’s onClick method to toggle the value.

We’re going to use that value accessor to render our graph. Makes the switch feel seamless.

Render

First you need this bit in your App function. It renders <VerticalBarchart> in an SVG, if data exists.

<Svg width="800" height="600" showKevin={perYear}>
  {data && (
    <VerticalBarchart
      data={data}
      width={600}
      height={600}
      value={valueFunction}
    />
  )}
</Svg>

That data && ... is a common trick. The return value of true && something is something, return value of false && something is nothing. Means when data is defined, we render, otherwise we don’t.

Oh and Svg is a styled SVG component. Gets a nice gif background when showKevin is set to true :stuck_out_tongue:

The VerticalBarchart itself is a functional component. We said no classes right?

const VerticalBarchart = ({ data, width, height, value }) => {
  const yScale = d3
    .scaleBand()
    .paddingInner(0.1)
    .domain(data.map(d => d.movie))
    .range([0, height]);
  const widthScale = d3
    .scaleLinear()
    .domain([0, d3.max(data, value)])
    .range([0, width]);

  return (
    <g>
      {data.map(d => (
        <React.Fragment key={d.movie}>
          <Bar
            x={0}
            y={yScale(d.movie)}
            height={yScale.bandwidth()}
            width={widthScale(value(d))}
          />
          <Label x={10} y={yScale(d.movie) + yScale.bandwidth() / 2}>
            {d.movie}
          </Label>
        </React.Fragment>
      ))}
    </g>
  );
};

We can define our D3 scales right in the render function. Means we re-define them from scratch on every render and sometimes that’s okay. Particularly when data is small and calculating domains and ranges is easy.

Once we have a scaleBand for the vertical axis and a scaleLinear for widths, it’s a matter of iterating over our data and rendering styled <Bar> and <Label> components.

Notice that we use the value accessor function every time we need the value of a datapoint. To find the max value for our domain and to grab each individual width.

Makes our chart automatically adapt to flicking that perYear toggle :ok_hand:

s30e147 - What Americans want for Christmas - horizontal stack chart

Challenge

Different ages want different things. Create a horizontal stack chart showing what everyone wants for Christmas.

Dataset

My Solution

Today’s challenge is a perfect example of how chroma-js automatically makes your dataviz beautiful. Best magic trick I ever learned from Shirley Wu.

We used Susie Lu’s d3-legend for the color legend, D3’s stack layout to calculate coordinates for stacking those bar charts, D3 axis for the axis, and the rest was React. Similar code to the bar chart in Christmas movies at the box office.

Load the data

We begin once more by loading the data. If you’ve been following along so far, this code will look familiar.

  componentDidMount() {
    d3.tsv("/data.tsv", d => ({
      category: d.category,
      young: Number(d.young),
      mid: Number(d.mid),
      old: Number(d.old)
    })).then(data => this.setState({ data }));
  }

d3.tsv loads our tab separated values file with data, a parsing function turns each line into nice objects we can use, and then we save it into component local state.

An axis and a legend

Building axes and legends from scratch is not hard, but it is fiddly and time consuming and fraught with tiny little traps for you to fall into. No time for that on a daily challenge!

d3blackbox to the rescue!

const VerticalAxis = d3blackbox((anchor, props) => {
  const axis = d3.axisLeft().scale(props.scale);
  d3.select(anchor.current).call(axis);
});

const Legend = d3blackbox((anchor, props) => {
  d3.select(anchor.current).call(
    legend
      .legendColor()
      .scale(props.scale)
      .title('Age group')
  );
});

Here you can see just how flexible the blackbox rendering approach I teach in React for Data Visualization can be. You can take just about any D3 code and turn it into a React component.

Means you don’t have to write your own fiddly stuff :ok_hand:

d3blackbox ensures our render functions are called on every component render and creates a positionable grouping, <g> , SVG element for us to move around.

Each category’s barchart

You can think a stacked bar chart as a series of barcharts. Each category gets its own.

const BarChart = ({ entries, y, width, marginLeft, color }) => (
  <React.Fragment>
    {entries.map(([min, max], i) => (
      <rect
        x={marginLeft + width(min)}
        width={width(max) - width(min)}
        y={y(y.domain()[i])}
        height={y.bandwidth()}
        key={y.domain()[i]}
        fill={color}
      >
        <title>
          {min}, {max}
        </title>
      </rect>
    ))}
  </React.Fragment>
);

These barchart subcomponents are fully controled components. They help us clean up the rendering and don’t need any logic of their own.

Takes a list of entries to render, a y scale for vertical positioning, a width scale to calculate widths, some margin on the left for the big axis, and a color to use.

Renders a React Fragment with a bunch of rectangles. Loop over the entries, return a positioned rectangle for each.

Our entries are pairs of min and max values as calculated by the stack layout. We use them to decide the horizontal, x position of our rectangle, and its width. Using the width scale both times. That takes care of proper sizing for us.

That key prop is a little funny though.

The y scale is an ordinal scale. Its domain is a list of categories, which means we can get the name of each bar’s category by picking the right index out of that array. Perfect for identifying our elements :smiley:

A stack chart built with React and D3

Here’s how all of that ties together :point_down:

class StackChart extends React.Component {
  y = d3
    .scaleBand()
    .domain(this.props.data.map(d => d.category))
    .range([0, this.props.height])
    .paddingInner(0.1);
  stack = d3.stack().keys(['young', 'mid', 'old']);
  color = chroma.brewer.pastel1;
  colorScale = d3
    .scaleOrdinal()
    .domain(['🧒 18 to 29 years', '🙍‍♂️ 30 to 59 years', '🧓 60 years or older'])
    .range(this.color);

  render() {
    const { data } = this.props;

    const stack = this.stack(data);

    const width = d3
      .scaleLinear()
      .domain([0, d3.max(stack[2], d => d[1])])
      .range([0, 400]);

    return (
      <g>
        <VerticalAxis scale={this.y} x={220} y={0} />
        {this.stack(data).map((entries, i) => (
          <BarChart
            entries={entries}
            y={this.y}
            key={i}
            marginLeft={223}
            color={this.color[i]}
            width={width}
          />
        ))}
        <Legend scale={this.colorScale} x={500} y={this.props.height - 100} />
      </g>
    );
  }
}

Okay that’s a longer code snippet :sweat_smile:

D3 setup

In the beginning, we have some D3 objects.

  1. A y band scale. Handles vertical positioning, sizing, spacing, and all
  2. A stack generator with hardcoded keys. We know what we want and there’s no need to be fancy
  3. A color list. Chroma’s brewer.pastel1 looked Best
  4. A colorScale with a more verbose domain and our list of colors as the range

Having a separate list of colors and color scale is important. Our individual bars want a specific color, our legend wants a color scale. They use different domains and unifying them would be fiddly. Easier to keep apart.

render

We do a little cheating in our render method. That stack should be generated in a componentDidUpdate of some sort and so should the width linear scale.

But our data is small so it’s okay to recalculate all this every time.

The stack generator creates a list of lists of entries. 3 lists, one for each category (age group). Each list contains pairs of numbers representing how they should stack.

Like this

[
  [[0, 5], [0, 10]],
  [[5, 7], [10, 16]],
  [[13, 20], [26, 31]]
]

Entries in the first list all begin at 0 . Second list begins where the previous list ends. Third list begins where the second list ended. Stacking up as far as you need.

Your job is then to take these numbers, feed them into some sort of scale to help with sizing, and render.

That was our <BarChart> sub component up above. It takes each list, feeds its values into a width scale, and renders.

Making sure we render 3 of them, one for each age group, is this part:

return (
  <g>
    <VerticalAxis scale={this.y} x={220} y={0} />
    {stack.map((entries, i) => (
      <BarChart
        entries={entries}
        y={this.y}
        key={i}
        marginLeft={223}
        color={this.color[i]}
        width={width}
      />
    ))}
    <Legend scale={this.colorScale} x={500} y={this.props.height - 100} />
  </g>
);

Starts by rendering an axis, followed by a loop through our stack, rendering a <BarChart> for each, and then the <Legend> component neatly positioned to look good.

A beautiful chart pops out.

Beautiful chart

Today you learned :face_with_monocle:

  • chroma-js exist and is amazing
  • d3-legend for easy legends
  • d3blackbox still saving the day
  • D3 stack generator

:nerd_face:

s30e148 - Christmas carols and their words - a word cloud

Challenge

Christmas carols are a time honored tradition. Draw a heatmap of their most popular words.

Dataset

My Solution

Building these word clouds kicked my ass. Even had to ask the three wise men for help.

So apparently combining useState and useMemo and promises makes your JavaScript crash hard.

What am I doing wrong? @ryanflorence @kentcdodds @dan_abramov ? I thought useMemo was supposed to only run once and not infinite loop on me pic.twitter.com/29OVRXxhuz

— Swizec Teller (@Swizec) December 8, 2018

Turns out that even though useMemo is for memoizing heavy computation, this does not apply when said computation is asynchronous. You have to use useEffect .

At least until suspense and async comes in early 2019.

Something about always returning the same Promise, which confuses useMemo and causes an infinite loop when it calls setState on every render. That was fun.

There’s some computation that goes into this one to prepare the dataset. Let’s start with that.

Preparing word cloud data

Our data begins life as a flat text file.

Angels From The Realm Of Glory

Angels from the realms of glory
Wing your flight over all the earth
Ye, who sang creations story
Now proclaim Messiah's birth
Come and worship, come and worship
Worship Christ the newborn King
Shepherds in the fields abiding
Watching over your flocks by night
God with man is now residing

And so on. Each carol begins with a title and an empty line. Then there’s a bunch of lines followed by an empty line.

We load this file with d3.text , pass it into parseText , and save it to a carols variable.

const [carols, setCarols] = useState(null);

useEffect(() => {
  d3.text('/carols.txt')
    .then(parseText)
    .then(setCarols);
}, [!carols]);

Typical useEffect / useState dance. We run the effect if state isn’t set, the effect fetches some data, sets the state.

Parsing that text into individual carols looks like this

function takeUntilEmptyLine(text) {
  let result = [];

  for (
    let row = text.shift();
    row && row.trim().length > 0;
    row = text.shift()
  ) {
    result.push(row.trim());
  }

  return result;
}

export default function parseText(text) {
  text = text.split('\n');

  let carols = { 'All carols': [] };

  while (text.length > 0) {
    const title = takeUntilEmptyLine(text)[0];
    const carol = takeUntilEmptyLine(text);

    carols[title] = carol;
    carols['All carols'] = [...carols['All carols'], ...carol];
  }

  return carols;
}

Our algorithm is based on a takeUntil function. It takes lines from our text until some condition is met.

Basically:

  1. Split text into lines
  2. Run algorithm until you run out of lines
  3. Take lines until you encounter an empty line
  4. Assume the first line is a title
  5. Take lines until you encounter an empty line
  6. This is your carol
  7. Save title and carol in a dictionary
  8. Splat carrol into the All carols blob as well

We’ll use that last one for a joint word cloud of all Christmas carols.

Calculating word clouds with d3-cloud

With our carols in hand, we can build a word cloud. We’ll use the wonderful d3-cloud library to handle layouting for us. Our job is to feed it data with counted word frequencies.

Easiest way to count words is with a loop

function count(words) {
  let counts = {};

  for (let w in words) {
    counts[words[w]] = (counts[words[w]] || 0) + 1;
  }

  return counts;
}

Goes over a list of words, collects them in a dictionary, and does +1 every time.

We use that to feed data into d3-cloud .

function createCloud({ words, width, height }) {
  return new Promise(resolve => {
    const counts = count(words);

    const fontSize = d3
      .scaleLog()
      .domain(d3.extent(Object.values(counts)))
      .range([5, 75]);

    const layout = d3Cloud()
      .size([width, height])
      .words(
        Object.keys(counts)
          .filter(w => counts[w] > 1)
          .map(word => ({ word }))
      )
      .padding(5)
      .font('Impact')
      .fontSize(d => fontSize(counts[d.word]))
      .text(d => d.word)
      .on('end', resolve);

    layout.start();
  });
}

Our createCloud function gets a list of words, a width, and a height. Returns a promise because d3-cloud is asynchronous. Something about how long it might take to iteratively come up with a good layout for all those words. It’s a hard problem. :exploding_head:

(that’s why we’re not solving it ourselves)

We get the counts, create a fontSize logarithmic scale for sicing, and invoke the D3 cloud.

That takes a size , a list of words without single occurrences turned into { word: 'bla' } objects, some padding, a font size method using our fontSize scale, a helper to get the word and when it’s all done the end event resolves our promise.

When that’s set up we start the layouting process with layout.start()

Animating words

Great. We’ve done the hard computation, time to start rendering.

We’ll need a self-animating <Word> componenent that transitions itself into a new position and angle. CSS transitions can’t do that for us, so we’ll have to use D3 transitions.

class Word extends React.Component {
  ref = React.createRef();
  state = { transform: this.props.transform };

  componentDidUpdate() {
    const { transform } = this.props;
    d3.select(this.ref.current)
      .transition()
      .duration(500)
      .attr('transform', this.props.transform)
      .on('end', () => this.setState({ transform }));
  }

  render() {
    const { style, children } = this.props,
      { transform } = this.state;

    return (
      <text
        transform={transform}
        textAnchor="middle"
        style={style}
        ref={this.ref}
      >
        {children}
      </text>
    );
  }
}

We’re using my Declarative D3 transitions with React approach to make it work. You can read about it in detail on my main blog.

In a nutshell:

  1. Store the transitioning property in state
  2. State becomes a sort of staging area
  3. Take control of rendering in componentDidUpdate and run a transition
  4. Update state after transition extends
  5. Render text from state

The result are words that declaratively transition into their new positions. Try it out.

Putting it all together

Last step in the puzzle is that <WordCloud> component that was giving me so much trouble and kept hanging my browser. It looks like this

export default function WordCloud({ words, forCarol, width, height }) {
  const [cloud, setCloud] = useState(null);
  useEffect(() => {
    createCloud({ words, width, height }).then(setCloud);
  }, [forCarol, width, height]);

  const colors = chroma.brewer.dark2;

  return (
    cloud && (
      <g transform={`translate(${width / 2}, ${height / 2})`}>
        {cloud.map((w, i) => (
          <Word
            transform={`translate(${w.x}, ${w.y}) rotate(${w.rotate})`}
            style={{
              fontSize: w.size,
              fontFamily: 'impact',
              fill: colors[i % colors.length],
            }}
            key={w.word}
          >
            {w.word}
          </Word>
        ))}
      </g>
    )
  );
}

A combination of useState and useEffect makes sure we run the cloud generating algorithm every time we pick a different carol to show, or change the size of our word cloud. When the effect runs, it sets state in the cloud constant.

This triggers a render and returns a grouping element with its center in the center of the page. d3-cloud creates coordinates spiraling around a center.

Loop through the cloud data, render a <Word> component for each word. Set a transform, a bit of style, the word itself.

And voila, a declaratively animated word cloud with React and D3 :v:

With your powers combined I got it working! Thanks guys :smiley: pic.twitter.com/7qKr6joeRC

— Swizec Teller (@Swizec) December 8, 2018

Original data from Drew Conway

s30e149 - Will you buy a christmas tree? - a pie chart

Challenge

Not everyone buys a Christmas tree. :christmas_tree: Draw a donut chart of people’s thoughts.

Dataset

My Solution

This donut chart build was short and sweet. D3 has all the ingredients we need, Chroma’s got the colors, d3-svg-legend has nice legend stuff. Oh and we used it as an excuse to update my d3blackbox library so it actually exports the hooks version.

Thought it did, had it in the docs, published version didn’t have it. 20 day old issue report on GitHub. Oops :sweat_smile:

You can see data loading in the Codesandbox above. Here’s the fun stuff

React and D3 pie chart tutorial with React hooks

Pie charts and donut charts are the same. If there’s a hole in the middle it’s a donut, otherwise it’s a pie. You should always make donuts because donuts are delicious and easier to read due to intricacies around area size perception.

Our code fits in a functional React component

const TreeDonut = ({ data, x, y, r }) => {};

Takes data , x,y coordinates for positioning, and r for the total radius.

We begin with a bunch of D3 objects. Scales, pie generators, things like that.

const pie = d3.pie().value(d => d.percentage);
const arc = d3
  .arc()
  .innerRadius(90)
  .outerRadius(r)
  .padAngle(0.01);
const color = chroma.brewer.set1;
const colorScale = d3
  .scaleOrdinal()
  .domain(data.map(d => d.answer))
  .range(color);

Here’s what they do:

  1. The d3.pie() generator takes data and returns everything you need to create a pie chart. Start and end angles of each slice and a few extras.
  2. The d3.arc() generator creates path definitions for pie slices. We define inner and outer radiuses and add some padding.
  3. We take the color list from one of Chroma’s pre-defined colors.
  4. We’ll use colorScale for the legend. Maps answers from our dataset to their colors

Next thing we need is some state for the overlay effect. It says which slice is currently selected.

const [selected, setSelected] = useState(null);

Hooks make this way too easy. :stuck_out_tongue: We’ll use setSelected to set the value and store it in selected .

Then we render it all with a loop.

return (
  <g transform={`translate(${x}, ${y})`}>
    {pie(data).map(d => (
      <path
        d={arc
          .outerRadius(selected === d.index ? r + 10 : r)
          .innerRadius(selected === d.index ? 85 : 90)(d)}
        fill={color[d.index]}
        onMouseOver={() => setSelected(d.index)}
        onMouseOut={() => setSelected(null)}
      />
    ))}
    <Legend x={r} y={r} colorScale={colorScale} />
  </g>
);

A grouping element positions our piechart from the center out.

Inside that group, we iterate over the output of our pie() generator and render a <path> for each entry. Its shape comes from the arc generator.

We update inner and outer radius on the fly depending on whether the current slice is highlighted. This creates the become-bigger-on-mouse-over effect. We drive it with mouse event callbacks and the setSelected method.

setSelected stores the current selected index in selected . This triggers a re-render. The selected slice shows as bigger.

Perfect :ok_hand:

PS: The legend component with hooks is a piece of cake

d3-svg-legend does it all for us. We use useD3 from my d3blackbox to make it work.

const Legend = function({ x, y, colorScale }) {
  const ref = useD3(anchor => {
    d3.select(anchor).call(d3legend.legendColor().scale(colorScale));
  });

  return <g transform={`translate(${x}, ${y})`} ref={ref} />;
};

Lets us render any D3 code into an anchor element and wrap it in a React component. Behind the scenes useD3 is a combination of useRef and useEffect .

Enjoy :v:

s30e150 - What goes in Chrstmas stockings - a piechart with tooltips

Challenge

Ever seen Christmas stockings? They get stuffed with all sorts of stuff. Build a donut chart of what’s what and add a mouse hover effect that shows what a slice represents.

Dataset

My Solution

Tooltips … tooltips are hard. So simple in theory yet organizing it into sensible code will wreck your mind. :exploding_head:

Your goal is to build this:

  1. A tooltip component
  2. Some way to store tooltip position and content
  3. Ability to change that on mouse over

Mousing over a thing - slice of the donut chart in this case - updates positioning and content. This triggers a tooltip re-render. Tooltip shows up where you need saying what you want.

So how do you organize that in a way that makes sense?

:thinking:

You can watch me flounder around trying different solutions in the stream above. In the end we went with a combination of state in the App component and React Context shared between everyone else.

We’re using React hooks because hooks are the hot new kid on the block and learning new coding paradigms is fun.

Managing and sharing tooltip state

const [tooltip, setTooltip] = useState({
  show: false,
  x: 0,
  y: 0,
  content: '',
  orientLeft: false,
});

Our tooltip state holds a show flag, tooltip coordinates, and content . orientLeft is the nascent beginnings of a fuller tooltip API. The tooltip component is going to consume this context and use it to render itself.

To make changing this state easier, we sneakily include setTooltip in the object passed into React Context itself.

<TooltipContext.Provider value={{ ...tooltip, setTooltip }}>

Now any consumer can change values in context. Whoa

The component

Our <Tooltip> component doesn’t do much on its own. It’s a wrapper that handles positioning, visibility, and supports a nascent orientation API. We can use orientLeft to align our tooltip left or right. A fuller API would also have top/bottom and a bunch of similar features.

const Tooltip = ({ width, height, children }) => {
  const { x, y, show, orientLeft } = useContext(TooltipContext);

  return (
    <g
      transform={`translate(${orientLeft ? x - width : x}, ${y})`}
      style={{ visibility: show ? 'visible' : 'hidden' }}
    >
      <foreignObject width={width} height={height}>
        {children}
      </foreignObject>
    </g>
  );
};

useContext takes the TooltipContext object and returns its current value on every render. We use destructuring to get at the parts we need: coordinates, show flag, orientation.

Tooltip then renders a <g> grouping element with positioning based on the orientation, and visibility based on the flag. Inside it wraps children in a sized foreignObject element. This allows us to embed HTML inside SVG.

HTML is better for tooltip content than SVG because HTML supports text automatic layouting. Set a width and the browser will figure out what to do with long strings. Don’t get that with SVG.

The Tooltip.js file also exports a React Context.

const TooltipContext = React.createContext({
  show: false,
  x: 0,
  y: 0,
  orientLeft: false,
  content: '',
});

// ...
export default Tooltip;
export { TooltipContext };

Makes it easier to share the same context between different consumers.

Render Tooltip in App

Rendering our tooltip happens in the main App component. It also holds tooltip state that gets passed into React Context.

import Tooltip, { TooltipContext } from "./Tooltip";

// ...

function App() {
  const [tooltip, setTooltip] = useState({
    show: false,
    x: 0,
    y: 0,
    content: "",
    orientLeft: false
  });

  return (
      <TooltipContext.Provider value={{ ...tooltip, setTooltip }}>
        <svg width="800" height="600">
          {* // where you put tooltip triggerers *}

          <Tooltip width={150} height={60}>
            <TooltipP>{tooltip.content}</TooltipP>
          </Tooltip>
        </svg>
      </TooltipContext.Provider>
  );
}

We import tooltip and its context, then useState to create a local tooltip state and its setter. Pass both of those in a common object into a <TooltipContext.Provider .

That part took me a while to figure out. Yes with React hooks you still need to render Providers. Hooks are consumer side.

Render our Tooltip as a sibling to all the other SVG stuff. Any components that want to render a tooltip will share the same one. That’s how it usually works.

<TooltipP> is a styled component by the way.

const TooltipP = styled.p`
  background: ${chroma('green')
    .brighten()
    .hex()};
  border-radius: 3px;
  padding: 1em;
`;

Nice green background, rounded corners, and a bit of padding.

I am no designer

I am no designer :sweat_smile:

Trigger tooltips from donuts

Donut code itself is based on code we built for the Will you buy a Christmas tree? donut chart.

We split it into the main donut component and a component for each slice, or <Arc> . Makes it easier to calculate coordinates for tooltips. Means we ca handle slice highlighted state locally in its own component.

const Arc = ({ d, r, color, offsetX, offsetY }) => {
  const [selected, setSelected] = useState(false);
  const tooltipContext = useContext(TooltipContext);

  const arc = d3
    .arc()
    .outerRadius(selected ? r + 10 : r)
    .innerRadius(selected ? r - 80 : r - 75)
    .padAngle(0.01);

  const mouseOver = () => {
    const [x, y] = arc.centroid(d);

    setSelected(true);
    tooltipContext.setTooltip({
      show: d.index !== null,
      x: x + offsetX + 30,
      y: y + offsetY + 30,
      content: d.data.stuffer,
      orientLeft: offsetX < 0,
    });
  };

  const mouseOut = () => {
    setSelected(null);
    tooltipContext.setTooltip({ show: false });
  };

  return (
    <path
      d={arc(d)}
      fill={color}
      onMouseOver={mouseOver}
      onMouseOut={mouseOut}
      style={{ cursor: 'pointer' }}
    />
  );
};

Here you can see a downside of hooks: They can lead to pretty sizeable functions if you aren’t careful.

We create a selected flag and its setter with a useState hook and we hook into our tooltip context with useContext . We’ll be able to use that setTooltip method we added to show a tooltip.

Then we’ve got that const arc stuff. It creates an arc path shape generator. Radius depends on selected status.

All that is followed by our mouse eve handling fucntions.

const mouseOver = () => {
  const [x, y] = arc.centroid(d);

  setSelected(true);
  tooltipContext.setTooltip({
    show: d.index !== null,
    x: x + offsetX + 30,
    y: y + offsetY + 30,
    content: d.data.stuffer,
    orientLeft: offsetX < 0,
  });
};

mouseOver is the active function. Mouse over an arc and it calculates its center, sets the arc to selected , and pushes necessary info into tooltip state. This triggers a re-render of the tooltip component and makes it show up.

Technically it triggers a re-render of our whole app because it’s tied to App state. You could split that out in a bigger app. Or rely on React being smart enough to figure out the smallest possible re-render.

Deselecting the arc happens in a mouseOut function

const mouseOut = () => {
  setSelected(false);
  tooltipContext.setTooltip({ show: false });
};

Set selected to falls and hide the tooltip.

With all that defined, rendering our arc is a matter of returning a path with some attributes.

return (
  <path
    d={arc(d)}
    fill={color}
    onMouseOver={mouseOver}
    onMouseOut={mouseOut}
    style={{ cursor: 'pointer' }}
  />
);

Use the arc generator to create the shape, fill it with color, set up mouse events, add a dash of styling.

Render a donut :doughnut:

We did all the complicated state and tooltip stuff in individual arcs. The donut component uses a pie generator and renders them in a loop.

const StockingDonut = ({ data, x, y, r }) => {
  const pie = d3.pie().value(d => d.percentage);

  const color = chroma.brewer.set3;
  return (
    <g transform={`translate(${x}, ${y})`}>
      {pie(data).map(d => (
        <Arc
          d={d}
          color={color[d.index]}
          r={r}
          key={d.index}
          offsetX={x}
          offsetY={y}
        />
      ))}
    </g>
  );
};

d3.pie takes our data and returns all the info you need to build a donut. Start angles, end angles, stuff like that.

Render a grouping element that centers our donut on (x, y) coordiantes, render <Arc> s in a loop.

Make sure to pass offsetX and offsetY into each arc. Arcs are positioned relatively to our donut center, which means they don’t know their absolute position to pass into the tooltip context. Offsets help with that.

:v:

And that’s how you make tooltips in SVG with React hooks. Same concepts and complications apply if you’re using normal React state or even Redux or something.

You need a global way to store info about the tooltip and some way to trigger it from sibling components.

PS: A neat way to useData

Yeah I’ve been doing that pattern a lot.

const [state, setState] = useState(null)
useEffect(() => doStuff().then(setState), [!state])

— Swizec Teller (@Swizec) December 8, 2018

Got tired of the useState / useEffect dance when loading data with hooks. Built a new hook called useData . That’s a neat feature of hooks; you can make new ones.

function useData(loadData) {
  const [data, setData] = useState(null);

  useEffect(() => {
    loadData(setData);
  }, [!data]);

  return data;
}

Takes a loadData function, sets up useState for the data, uses an effect to load it, gives you setData so you can return the value, and returns the final value to your component.

You use it like this :point_down:

function App() {
  const data = useData(setData =>
    d3
      .tsv("/data.tsv", d => ({
        stuffer: d.stuffer,
        percentage: Number(d.percentage)
      }))
      .then(setData)
  );

Much cleaner I think :ok_hand:

Might be cleaner to take a promise and handle setData internally. Hmm … :thinking:

Thinking I might open source this, but it needs a few more iterations.

s30e151 - When Americans buy Christmas presents - a curved line chart

Challenge

My girlfriend likes to buy her presents early, I wait until the last minute. What do other people do? Create a way to visualize the last few weeks of the year and rank them by popularity.

Dataset

My Solution

Well that was fun. What do you when you have a sparse dataset? A dataset with so few data points positioned so weirdly that it’s almost certain to hide important trends?

You try to fix it.

With science! Math! Code!

Or … well … you can try. We tried a few things until we were forced to admit defeat in the face of mathematics beyond our station. Or at least beyond mine.

Our dataset and what’s wrong with it

We’re visualizing results of a poll asking people When do you start christmas shopping? . The poll had over 4000 responses. We get just the result.

Before October end  39
November before Thanksgiving    21
November after Thanksgiving 27
December    11
Janaury 2
February    0

39% start shopping before October ends, 21% in November before thanksgiving, 27% right after thanksgiving, and so on.

You can imagine these are long timespans. Ranging from basically a whole year to just a week in length. That’s a problem for us because it makes the datapoints hard to compare.

Of course Before October end is overrepresented: It’s got almost four times as long to accumulate its result as the rest of the time periods combined.

A simple presentation

We start exploring with a line chart. Borrowing a lot of code from the Money spent on Christmas challenge.

That’s our data plotted as a curved line. Circles represent the actual datapoints we’ve got. Curves are a a good first approach to show that our data might not be all that exact.

We borrow axis implementation from Money spent on Christmas using my d3blackbox library.

const BottomAxis = d3blackbox((anchor, props) => {
  const axis = d3.axisBottom().scale(props.scale);
  d3.select(anchor.current).call(axis);
});

const LeftAxis = d3blackbox((anchor, props) => {
  const axis = d3
    .axisLeft()
    .scale(props.scale)
    .tickFormat(d => `${d}%`);
  d3.select(anchor.current).call(axis);
});

Each axis implementation renders an anchor element and injects a pure D3 rendered axis on every update. No need to fiddle with building our own.

A <Datapoints> component keeps our main code cleaner. Renders those tiny little circles.

const Circle = styled.circle`
  fill: none;
  stroke: black;
  r: 3px;
`;

const Datapoints = ({ data, x, y }) => (
  <g>
    {data.map(d => (
      <Circle cx={x(d.descriptor)} cy={y(d.percentage)}>
        <title>{d.descriptor}</title>
      </Circle>
    ))}
  </g>
);

Takes data, an x scale and a y scale. Walks through data in a loop, renders circles with a title. Makes it so you can mouse over a circle and if you do it just right a browser native tooltip appears.

The LineChart component brings all of this together and uses a D3 line generator for a single path definition.

const Line = styled.path`
  fill: none;
  stroke: ${chroma("green").brighten(1.5)};
  stroke-width: 2px;
`;

class LineChart extends React.Component {
  height = 500

  x = d3.scalePoint()
    .domain(this.props.data.map(d => d.descriptor)
    .range([0, 600]),
  y = d3
    .scaleLinear()
    .domain([0, d3.max(this.props.data, d => d.percentage)])
    .range([this.height, 0])

  line = d3
    .line()
    .x(d => this.x(d.percentage))
    .y(d => this.y(d.descriptor))
    .curve(d3.curveCatmullRom.alpha(0.5))

  render() {
    const { data, x, y } = this.props;

    return (
      <g transform={`translate(${x}, ${y})`}>
        <Line d={this.line(data)} />
        <Datapoints data={data} x={this.x} y={this.y} />
        <BottomAxis scale={this.x} x={0} y={this.height} />
        <LeftAxis scale={this.y} x={0} y={0} />
      </g>
    )
  }
}

Sets up a horizontal x point scale, a vertical y scale with an inverted range, a line generator with a curve, then renders it all.

Nothing too crazy going on here. You’ve seen it all before. If not, the Money spent on Christmas article focuses more on the line chart part.

Is it realistic?

So how realistic does this chart look to you? Does it represent the true experience?

Yes according to the data most people start shopping before the end of October. And it’s true, very many start some time in November, with a moderate spike around Black Friday and Cyber Monday.

Does the variation in time period hide important truths?

:thinking:

Making an approximation

All of the above is true. And yet it hides an important fact.

39% before October end is a huge percentage. But it might mean August, last day of October, or even March. Who knows? The data sure don’t tell us.

And that week after thanksgiving? It’s got more starting shoppers than all of the rest of November combined. Even though it’s just 1 week versus 3 weeks.

We can tease out these truths :point_right: normalize our data by week.

Assume each datapoint spreads uniformly over its entire period, and a different picture comes out.

October doesn’t look so hot anymore, November looks better, January is chilly, but then that Black Friday and Cyber Monday. Hot damn. Now that is a spike in shopping activity!

See how much stronger that spike looks when you normalize data by time period? Wow.

You can do it with a little elbow grease

We have to construct a fake dataset with extra points in between the original data. Because our dataset is small, we could do this manually with just a bit of maths.

Goes in getDerivedStateFromProps

static getDerivedStateFromProps(props, state) {
    // Basic goal:
    // Split "Before October end" into 4 weekly datapoints
    // split "November before Thanksgiving" into 3 weekly datapoints
    // split "November after Thanksgiving" into 1 weekly datapoint
    // split "December" into 4 weekly datapoints
    // split "January" into 4 weekly datapoints
    const { data } = props,
      { x, xDescriptive } = state;

    const approximateData = [
      ...d3.range(4).map(_ => data[1].percentage / 4),
      ...d3.range(3).map(_ => data[1].percentage / 3),
      ...d3.range(1).map(_ => data[2].percentage / 1),
      ...d3.range(4).map(_ => data[3].percentage / 4),
      ...d3.range(4).map(_ => data[4].percentage / 4)
    ];

    x.domain(d3.range(approximateData.length));
    // Manually define range to match number of fake datapoints in approximateData
    xDescriptive.range([
      x(0),
      x(4),
      x(4 + 3),
      x(4 + 3 + 1),
      x(4 + 3 + 1 + 4),
      x(4 + 3 + 1 + 4 + 4 - 1)
    ]);

    return {
      approximateData,
      x,
      xDescriptive
    };
  }

We take props and state , then split every datapoint into its appropriate number of weeks. Our approach is roughly based on the idea of a running average. You could make a more generalized algorithm for this, but for a small dataset it’s easier to just do it like this.

So the whole of October, that first datapoint, becomes 4 entries with a fourth of the value each. November turns into 3 with thirds. And so on.

Since we want to keep the original labeled axis and datapoints, we have to use two different horizontal scales. One for the approximate dataset, one for the original.

We put them in state so we can set them up in getDerivedStateFromProps .

state = {
  x: d3.scalePoint().range([0, 600]),
  xDescriptive: d3
    .scaleOrdinal()
    .domain(this.props.data.map(d => d.descriptor)),
};

x is a point scale with a range. Its domain comes from our approximate dataset. One for each entry based on the index.

xDescriptive works much like our old horizontal point scale. But because we have to spread it over more datapoints that it never receives, it needs to be an ordinal scale.

Ordinal scales map inputs directly into outputs. Like a dictionary. Our domain becomes every descriptor from the dataset, the range we define manually to line up with output from the x scale.

Rendering is still the same, we just gotta be careful which scale we pass into which element.

render() {
    const { data, x, y } = this.props,
      { approximateData } = this.state;

    return (
      <g transform={`translate(${x}, ${y})`}>
        <Line d={this.line(approximateData)} />
        <Datapoints data={data} x={this.state.xDescriptive} y={this.y} />
        <BottomAxis scale={this.state.xDescriptive} x={0} y={this.height} />
        <LeftAxis scale={this.y} x={0} y={0} />
      </g>
    );
  }

<Line> gets the approximate data set, <Datapoints> gets the original dataset with the descriptive horizontal scale. The <BottomAxis> gets the descriptive scale, and the <LeftAxis> stays the same.

End result is a chart that tells a more accurate story overlayed with the original data.

Attempting a more sophisticated solution

One thing still bothers me though. I bet you those weekly distributions aren’t uniform.

You’re less likely to start Christmas shopping in the first week of October than you are in the last week of October. Just like you’re less likely to start at the beginning of November than towards the end.

December should be the inverse. You’re more likely to start in the first week than you are the day before Christmas.

Know what I mean?

It just doesn’t seem to fit real world experience that those weeks would have even probabilities.

And that’s our clue for next steps: You can fit a probability distribution over your weeks, then generate random datapoints that fit the distribition to make a nice smooth curve.

A sort of Monte Carlo approach. Commonly used for integration, fitting complex lines to probabilities, and stuff like that.

Monte Carlo methods (or Monte Carlo experiments) are a broad class of computational algorithms that rely on repeated random sampling to obtain numerical results. Their essential idea is using randomness to solve problems that might be deterministic in principle. They are often used in physical and mathematical problems and are most useful when it is difficult or impossible to use other approaches.

Is it difficult or impossible to use other approaches? I’m not sure.

There’s different ways to fit a polygon to a set of known numbers. Our curve approach did that actually.

Not sure we can do more than that with normal mathematics.

Unfortunately we were unable to implement a monte carlo method to approximate more datapoints. We tried. It didn’t produce good results.

The line kept being random, my math wasn’t good enough to fit those random numbers to a probability distribution and it was just a mess. But a promising mess.

Basic idea goes something like this:

  1. Define a probability distribution (less likely week 1, more likely week 4)
  2. Pick random numbers
  3. Keep going until the sum of your points adds up to the known value
  4. Voila, in theory

You can watch me flounder around with this before I finally gave up in the stream above.

See you tomorrow :v:

s30e152 - When people buy candy - animated barchart with easing

Challenge

Candy is delicious. When do people buy it most? Visualize the data in a fun way

Dataset

My Solution

Did you know Americans buy Eight hundred million dollars worth of candy on Easter? That’s crazy. Absolutely bonkers. Even the normal baseline of $300,000,000 /week throughout the year is just staggering. :lollipop:

What better way to visualize it than candy falling from the sky into the shape of a bar chart?

The basic idea behind that visualization goes like this:

  1. Load and parse data
  2. Scale for horizontal position
  3. Scale for vertical height
  4. Render each bar in a loop
  5. Divide height by 12
  6. Render that many emojis
  7. Create a custom tween transition to independently animate horizontal and vertical positionioning in a declarative and visually pleasing way

:stuck_out_tongue:

The basics

Let’s start with the basics and get them out of the way. Bottom up in the Codesandbox above.

const FallingCandy = ({ data, x = 0, y = 0, width = 600, height = 600 }) => {
  const xScale = d3
    .scalePoint()
    .domain(data.map(d => d.week))
    .range([0, width]);
  const yScale = d3
    .scaleLinear()
    .domain([250, d3.max(data, d => d.sales)])
    .range([height, 0]);

  return (
    <g transform={`translate(${x}, ${y})`}>
      {data.map(d => (
        <CandyJar
          x={xScale(d.week)}
          y={height}
          height={height - yScale(d.sales)}
          delay={d.week * Math.random() * 100}
          type={d.special}
          key={d.week}
        />
      ))}
      <BottomAxis scale={xScale} x={0} y={height} />
      <LeftAxis scale={yScale} x={0} y={0} />
    </g>
  );
};

The <FallingCandy> component takes data, positioning, and sizing props. Creates two scales: A point scale for horizontal positioning of each column, a vertical scale for heights.

Render a grouping element to position everything, walk through the data and render a <CandyJar> component for each entry. Candy jars need coordinates, a height, some delay for staggered animations, and a type.

Type tells them which emoji to render. Makes it so we can have special harts on Valentine’s day, bunnies on Easter, jack-o-lanterns on Halloween, and Christmas trees on Christmas.

I know this works because when my girlfriend saw it this morning she was like “Whaaat why so much candy on Easter?” . Didn’t even have to tell her what the emojis mean :muscle:

We’ll talk about the animation staggering later. I’ll explain why it has to be random as well.

The axes

Using our standard approach for axes: use d3blackbox to render an anchor element, then take over with D3 and use an axis generator.

const BottomAxis = d3blackbox((anchor, props) => {
  const scale = props.scale;
  scale.domain(scale.domain().filter((_, i) => i % 5 === 0));

  const axis = d3
    .axisBottom()
    .scale(props.scale)
    .tickFormat(d => `wk ${d}`);
  d3.select(anchor.current).call(axis);
});

const LeftAxis = d3blackbox((anchor, props) => {
  const axis = d3
    .axisLeft()
    .scale(props.scale)
    .tickFormat(d => `${d} million`);
  d3.select(anchor.current).call(axis);
});

We have to filter the scale’s domain for <BottomAxis> because point scales are ordinal. That means there’s no generalized way to interpolate values in between other values, so the axis renders everything.

That looks terrible. Instead, we render every 5th tick.

Both axes get a custom tickFormat so they’re easier to read.

The

Candy jars are just columns of emojis. There’s not much logic here.

const CandyJar = ({ x, y, height, delay, type }) =>
  d3
    .range(height / 12)
    .map(i => (
      <Candy
        x={x}
        y={y - i * 12}
        type={type}
        delay={delay + i * Math.random() * 100}
        key={i}
      />
    ));

Yes, we could have done this in the main <FallingCandy> component. Code feels cleaner this way.

Create a counting array from zero to height/12 , the number of emojis we need, walk through the array and render <Candy> components for each entry. At this point we add some more random delay. I’ll tell you why in a bit.

The animated component

All that animation happens in the Candy component. Parent components are blissfully unaware and other than passing a delay prop never have to worry about the details of rendering and animation.

That’s the beauty of declarative code. :ok_hand:

Our plan is based on my Declarative D3 transitions with React 16.3+ approach:

  1. Move coordinates into state
  2. Render emoji from state
  3. Run transition on componentDidMount
  4. Update state when transition ends

We use component state as a sort of staging area for transitionable props. D3 helps us with what it does best - transitions - and React almost always knows what’s going on so it doesn’t get confused.

Have had issues in the past with manipulating the DOM and React freaking out at me.

class Candy extends React.Component {
  state = {
    x: Math.random() * 600,
    y: Math.random() * -50,
  };
  candyRef = React.createRef();

  componentDidMount() {
    const { delay } = this.props;

    const node = d3.select(this.candyRef.current);

    node
      .transition()
      .duration(1500)
      .delay(delay)
      .ease(d3.easeLinear)
      .attrTween('y', candyYTween(this.state.y, this.props.y))
      .attr('x', this.props.x)
      .on('end', () => this.setState({ y: this.props.y }));
  }

  get emoji() {
    // return emoji based on this.props.type
  }

  render() {
    const { x, y } = this.state;

    return (
      <text x={x} y={y} style={{ fontSize: '12px' }} ref={this.candyRef}>
        {this.emoji}
      </text>
    );
  }
}

We initate the <Candy> component in a random location off screen. Too high up to be seen, somewhere on the visualization horizontally. Doesn’t matter where.

I’ll show you why random soon.

We create a ref as well. D3 will need that to get access to the DOM node.

Then we have componentDidMount which is where the transition happens.

Separate, yet parallel, transitions for each axis

  componentDidMount() {
    const { delay } = this.props

    const node = d3.select(this.candyRef.current)

    node
      .transition()
      .duration(1500)
      .delay(delay)
      .ease(d3.easeLinear)
      .attrTween('y', candyYTween(this.state.y, this.props.y))
      .attr('x', this.props.x)
      .on('end', () => this.setState({ y: this.props.y }))
  }

Key logic here is that we d3.select() the candy node, start a transition on it, define a duration, pass the delay from our props, disable easing functions, and specify what’s transitioning.

The tricky bit was figuring out how to run two different transitions in parallel.

D3 doesn’t do concurrent transitions, you see. You have to run a transition, then the next one. Or you have to cancel the first transition and start a new one.

Of course you can run concurrent transitions on multiple attributes. But only if they’re both the same transition.

In our case we wanted to have candy bounce vertically and fly linearly in the horizontal direction. This was tricky.

I mean I guess it’s okay with a bounce in both directions? :face_with_monocle:

No that’s weird.

You can do it with a tween

First you have to understand some basics of how transitions and easing functions work.

They’re based on interpolators. An interpolator is a function that calculates in-between values between a start and end value based on a t argument. When t😮 , you get the initial value. When t=1 you get the end value.

const interpolate = d3.interpolate(0, 100);

interpolate(0); // 0
interpolate(0.5); // 50
interpolate(1); // 1

Something like that in a nutshell. D3 supports much more complex interpolations than that, but numbers are all we need right now.

Easing functions manipulate how that t parameter behaves. Does it go from 0 to 1 linearly? Does it bounce around? Does it accelerate and slow down?

When you start a transition with easeLinear and attr('x', this.props.😆 you are essentially creating an interpolator from the current value of x to your desired value, and the t parameter changes by an equal amount on every tick of the transition.

If you have 1500 milliseconds to finish the transition (your duration), that’s 90 frames at 60fps. Means your t adds 0.01 on every tick of the animation.

We can use that to create a custom tween for the vertical coordinate, y .

function candyYTween(oldY, newY) {
  const interpolator = d3.interpolate(oldY, newY);
  return function() {
    return function(t) {
      return interpolator(d3.easeBounceOut(t));
    };
  };
}

candyYTween takes the initial and new coordinates, creates an interpolator, and returns a function. This function returns a parametrized function that drives our transition. For every t we return the value of our interpolator after passing it through the easeBounceOut easing function.

We’re basically taking a linear parameter, turning it into a bouncy paramater, and passing that into our interpolator. This creates a bouncy effect without affecting the x coordinate in the other transition.

I don’t know why we need the double function wrap, but it didn’t work otherwise.

So why all the randomness?

Randomness makes our visualization look better. More natural.

Here’s what it looks like without any Math.random()

Here’s why adding randomness to your animations matters :point_down:

This chart of candy buying habits in the US is not random at all. Delay based purely on array index. pic.twitter.com/pTTWxovaSp

— Swizec Teller (@Swizec) December 13, 2018

Randomness on the CandyJar level.

Here we add randomness to the column delay. pic.twitter.com/ZPfQzInXvi

— Swizec Teller (@Swizec) December 13, 2018

Randomness on the CandyJar and Candy level.

Adding a random delay to each individual emoji makes it even better :face_with_monocle: pic.twitter.com/Xn49KRbcCy

— Swizec Teller (@Swizec) December 13, 2018

Randomness in the start position as well.

And when you add a random start point as well, that’s when you unlock true beauty :ok_hand:#ReactVizHoliday Day 9 was fun like that.
Check it out here :point_right: https://t.co/Yh62OVG3pW pic.twitter.com/5N2gQJtfUX

— Swizec Teller (@Swizec) December 13, 2018

You decide which looks best :v:

s30e153 - A responsive stack chart of smartphone market share

Challenge

Smartphones, magnificent little things. But there’s only 4 kinds. Draw a responsive stackchart of their marketshare.

Dataset

My Solution

We’ve built stackcharts before, on the What do Americans want for Christmas day. That means we can focus on teh responsive part today.

Although I still had to build the full stack chart from scratch and my jetlagged brain struggled. Sorry viewers. You might want to skip the first several minutes of the stream :sweat_smile:

How to make a responsive chart with React and D3

There’s two parts to making responsive charts and data visualizations:

  1. Build your chart so it conforms to a width and height
  2. Use CSS to resize your SVG based on viewport size
  3. React to window size changes
  4. Read SVG size
  5. Pass it into your chart

We’ll go from the outside-in.

Dynamically sized SVG

There’s a few ways you can render your SVG so it resizes based on available space. Flexbox, css grid, old school CSS tricks.

The easist is a 100% width.

<svg width="100%" height="400" ref={this.svgRef}>
  {data && (
    <ResponsiveStackChart
      data={data}
      keys={['android', 'ios', 'blackberry', 'microsoft']}
      width={width}
      height={height}
    />
  )}
</svg>

Our SVG always occupies the full width of its parent div - the whole page in our case. It contains a <ResponsiveStackChart> that accepts width, height, and data.

Those four come from state.

const { data, width, height } = this.state;

You could track different widths for different charts, do some layouting, things like that. We don’t need those complications because this is a small example.

Listen to window size changes

Now that we have a dynamic SVG, we have to read its size every time the window size changes. That happens when users resize their browser (never), or when they turn their phone (sometimes).

In reality this part almost never happens. People rarely resize their browsers and only turn their phones if you give them a reason to. But it’s a nice touch when we’re talking about responsive :smiley:

We add a listener to the resize window event in componentDidMount and remove it in componentWillUnmount . Both in the main App componenet.

componentDidMount() {
    // data loading

    this.measureSVG();
    window.addEventListener("resize", this.measureSVG);
}

componentWillUnmount() {
    window.removeEventListener("resize", this.measureSVG);
}

measureSVG is where the next bit happens.

Measure SVG element size

A useful DOM method engineers often forget exists is getBoundingClientRect . Tells you the exact size of a DOM node. Great for stuff like this :ok_hand:

measureSVG = () => {
  const { width, height } = this.svgRef.current.getBoundingClientRect();

  this.setState({
    width,
    height,
  });
};

Take the bounding client rect of our SVG element, read out its width and height, save it to state. This triggers a re-render of our app, passes new sizing props into the chart, and the chart resizes itself.

A chart that listens to its width and height

Now that we’ve got dynamic always accurate width and height, we have to listen to them.

Best way to do that is with D3 scales that you keep up to date. We use the dynamic full integration approach from my React for Data Visualization course.

That means:

  1. Scales go into state
  2. Scales update their domain and range in getDerivedStateFromProps
class ResponsiveStackChart extends React.Component {
  state = {
    xScale: d3
      .scaleBand()
      .domain(this.props.data.map(d => d.date))
      .range([0, 600]),
    yScale: d3.scaleLinear().range([0, 600])
  };
  stack = d3.stack().keys(this.props.keys);
  color = chroma.brewer.Paired;

  static getDerivedStateFromProps(props, state) {
    let { xScale, yScale } = state;

    xScale.domain(props.data.map(d => d.date)).range([0, props.width]);
    yScale.range([0, props.height - 50]);

    return {
      ...state,
      xScale,
      yScale
    };
  }

We define default state for our xScale and yScale . Both assume the chart is going to be 600x600 pixels. xScale has a domain with every identifier in our dataset, the month/year, and yScale will get its domain in the render function. I’ll explain why.

getDerivedStateFromProps runs every time our component updates for any reason. A good place to update our scales so they fit any new into from props.

We redefine their ranges to match the width and height props. If we are careful to always rely on scales to position and size elements on our chart, the chart will automatically resize.

The stack layout

To avoid calculating the stack layout multiple times, we do it in the render method. Need its data for rendering and for the yScale domain.

render() {
    const { data, height } = this.props,
      { yScale, xScale } = this.state;
    const stack = this.stack(data);

    yScale.domain([0, d3.max(stack[stack.length - 1].map(d => d[1]))]);

The stack generator returns an array of arrays. At the top level we have an array for every key in our dataset. Inside is an array of tuples for each datapoint. The touples hold a min and max value that tells us where a datapoint starts and ends.

We use d3.max to find the highest value in the stack data and feed it into yScale’s domain so it can proportionally size everything when we render.

:ok_hand:

An axis with dynamic number of tricks

The last step is making our axis look good at every size. We have to make sure ticks don’t overlap and their number adapts to available space.

const BottomAxis = d3blackbox((anchor, props) => {
  const scale = props.scale,
    tickWidth = 60,
    width = scale.range()[1],
    tickN = Math.floor(width / tickWidth),
    keepEveryNth = Math.floor(scale.domain().length / tickN);

  scale.domain(scale.domain().filter((_, i) => i % keepEveryNth === 0));

  const timeFormat = d3.timeFormat('%b %Y');
  const axis = d3
    .axisBottom()
    .scale(props.scale)
    .tickFormat(timeFormat);
  d3.select(anchor.current).call(axis);
});

This is quite mathsy. The idea works like this:

  1. Decide how much room you want for each tick - tickWidth
  2. Read the width from scale.range - width
  3. Use division to decide how many ticks fit - tickN
  4. Some more division to decide every Nth tick you can keep - keepEveryNth

Then we filter the scale’s domain and keep only every keepEveryNth element.

Only reason we need this is because we’re using a band scale, which is an ordinal scale. Means D3 can’t easily interpolate datapoints and figure these things out on its own.

The result is a perfectly responsive chart :point_down:

A responsive #react and #d3 stackchart. #ReactVizHoliday 10

:point_right: https://t.co/8a8r5ifhyz pic.twitter.com/kMWgUAZB4J

— Swizec Teller (@Swizec) December 16, 2018