Swizec Teller - a geek with a hatswizec.com

Senior Mindset Book

Get promoted, earn a bigger salary, work for top companies

Senior Engineer Mindset cover
Learn more

    Tooltips and state across various d3 charts in a React dashboard!

    One of the most common burning questions readers ask 👉 "How do I add tooltips to my charts?" followed by "How do I sync them between multiple charts?"

    So we played around and got that working on a stream 👇

    Using hooks because why not. It's fun to experiment with the future :)

    We built a simple dashboard with 3 scatterplots because scatterplots are easy to build. Filled with random data because real small datasets are hard to find.

    Scatterplots with synchronized tooltips

    Scatterplots with synchronized tooltips

    Here's a CodeSandbox, try it out 👇

    First, you need a context and some state

    We're using context to share state between charts. No prop passing, just state that everyone has access to. It's like a Redux or MobX store without the setup.

    A couple lines is all we need:

    // src/useTooltip.js
    
    import React, { useState } from "react";
    
    const tooltipContext = React.createContext();
    
    function useTooltip() {
      const [tooltip, setTooltip] = useState(false);
    
      return { tooltip, setTooltip };
    }
    
    export { useTooltip, tooltipContext };
    

    Not sure yet how I feel about this state pattern with hooks, need to play around some more. Here's the idea:

    1. Create a tooltipContext
    2. Create a custom useTooltip hook with 1 piece of state, the tooltip value
    3. Return value and setter from the hook
    4. Export hook and context

    Might look cleaner to export context in the useTooltip hook itself? 🤔

    Either way, the idea here is that every component has access to the same tooltip state. It tells them whether to display a tooltip and its context. Any component can take over, usually on mouse over, and call setTooltip to set a new tooltip.

    If your system has just 1 mouse cursor, like most computers do, this approach works great. If you want to support multiple simultaneous mouse over events, you could run into trouble.

    Second, you need a scatterplot

    Like I said, we used scatterplots because they're quick to build. Take some data, pick two dimensions, iterate and draw circles.

    // src/Scatterplot.js
    
    import { tooltipContext } from "./useTooltip";
    
    export default ({
      x,
      y,
      width,
      height,
      data,
      xDimension,
      yDimension,
      padding = 10
    }) => {
      const { tooltip, setTooltip } = useContext(tooltipContext);
    
      const xScale = d3
        .scaleLinear()
        .domain(d3.extent(data, xDimension))
        .range([padding, width - padding]);
      const yScale = d3
        .scaleLinear()
        .domain(d3.extent(data, yDimension))
        .range([padding, height - padding]);
    
      return (
        <g transform="{`translate(${x}," ${y})`}="">
          {data.map(d => (
            <circle key={d.id} cx={xScale(xDimension(d))} cy={yScale(yDimension(d))} r={3} onmouseover="{()" ==""> setTooltip(d)}
              onMouseOut={() => setTooltip(false)}
            />
          ))}
          {tooltip && (
            <tooltip x={xScale(xDimension(tooltip))} y={yScale(yDimension(tooltip))} info={tooltip}>
          )}
        </tooltip></circle></g>
      );
    };
    

    We use the useContext hook to get access to our tooltip state. Here's where using useTooltip would be nicer but I couldn't quite figure out how to make it work.

    That gives us tooltip and setTooltip. I'll show you how they get into our context in a bit.

    Then we set up two D3 scales: One for horizontal coordinates, xScale, one for vertical coordinates, yScale.

    We return a grouping element using the transform prop to position at (x, y), iterate over our data, and render circles.

    Each circle gets a position based on our scale and a passed-in value accessor. xDimension or yDimension. This lets us re-use the same scatterplot for random looks into our data you'll see. onMouseOver we set the tooltip to current datapoint, onMouseOut we reset it back to false.

    Then we render the ``component.

    Third, you need a``

    The ``component takes coordinates and content. Renders them in an SVG foreignObject so we can leverage HTML's built-in text formatting abilities.

    Reflowing text in SVG yourself is painful 😉

    // src/Scatterplot.js
    
    const Tooltip = ({ x, y, info }) => (
      <foreignobject x="{x" +="" 10}="" y="{y" width={100} height={50}>
        <div>
          <strong>{info.name}</strong>
    
            {info.state} {info.zipCode}
    
          <p style={{ color: info.color }}>
            {info.number1.toFixed(2)}, {info.number2.toFixed(2)}
          </p>
        </div>
      </foreignobject>
    );
    

    This one is very dataset specific. We know each info has a name, a zipCode, and two numbers. Display them in some nice format and that's that.

    Each of our scatterplots now has its own instance of a tooltip. They're all driven by the same state in context, but they're different tooltips.

    This is crucial.

    Fourth, you need an App to hold it all together

    Our App component holds everything together. Generates data, renders scatterplots, sets up a context provider. Everything.

    Looks like this

    function App() {
      // Generate random data on first load
      const data = useMemo(
        () =>
          range(100).map(i => ({
            id: i,
            number1: Math.random(),
            number2: Math.random(),
            name: Faker.name.findName(),
            zipCode: Faker.address.zipCode(),
            state: Faker.address.state(),
            color: Faker.internet.color()
          })),
        []
      );
    
      // bucketize states into 50 buckets
      const stateToNumber = scaleOrdinal()
        .domain(data.map(d => d.state))
        .range(range(50));
    
      // alphabetize letters into 25 buckets
      const alphabet = scaleOrdinal()
        .domain("abcdefghijklmnoprstuvwxyz".split(""))
        .range(range(25));
    
      const state = useTooltip();
    
      return (
        <div class="App">
          <h1>React & D3 dashboard with synced tooltips</h1>
          <h2>For simplicity, it's scatterplots ✌️</h2>
          <tooltipcontext class="provider" value={state}>
            <svg width="800" height="600">
              <scatterplot x={0} y={0} width={200} height={200} data={data} xdimension="{d" ==""> d.number1}
                yDimension={d => d.number2}
              />
    
              <scatterplot x={250} y={0} width={200} height={200} data={data} xdimension="{d" ==""> d.number1}
                yDimension={d => stateToNumber(d.state)}
              />
    
              <scatterplot x={0} y={250} width={200} height={200} data={data} xdimension="{d" ==""> stateToNumber(d.state)}
                yDimension={d => alphabet(d.name[0])}
              />
            </scatterplot></scatterplot></scatterplot></svg>
          </tooltipcontext>
        </div>
      );
    }
    

    So that's a long ass function. This is our hook-based future I'm afraid. Surely patterns will develop to clean this stuff up?

    But also we were just playing around. No need to make it clean :)

    We start with some fake data. useMemo makes sure we calculate it once and keep it in memory through any re-renders. Makes our dataviz look more consistent.

    Two ordinal scales help us turn state names and zip codes into linear numbers. Easier than changing our scatterplots to support ordinal data.

    The useTooltip hook from earlier gives us a state object that we'll feed into our tooltipContext. Again a good opportunity to make this easier to use, but need to play around some more.

    For the rendering, we wrap our dashboard with tooltipContext.Provider, give it our state, render an SVG element, and plop down three scatterplots.

    This is where making scatterplots flexible comes handy. We're reusing the same component, giving it different params, and it gives us different insights into the same dataset.

    The result, a dashboard with synchronized tooltips.

    Scatterplots with synchronized tooltips

    Scatterplots with synchronized tooltips

    ✌️

    In other news

    I DID IT I FINISHED EDITING ALL EIGHT HOURS OF VIDEO FOR React For Data Visualization \o/

    https://twitter.com/Swizec/status/1089782724510965760
    https://twitter.com/Swizec/status/1089782724510965760

    Only took 4 months, heh 😅

    But now we know. This is a good datapoint for the future.

    The process started in August with research, writing, running online workshops to test the material and flow. Well, the real process started in April 2015 with the first React + D3 book …

    Fingers crossed it doesn't take me 4 years to produce the next full video course 🤞 I think the 5 month flow is doable.

    Right now we've got 94 videos in 27 sections and 133 lessons. Today I'm recording a couple explainer intro videos to give context and ease onboarding and then we're off to the races launching this thing.

    Thanks everyone for your patience. Watch your emails for more info coming soon :)

    Oh also I started ramping up for another marathon because I'm dumb

    https://twitter.com/Swizec/status/1089614535047110656
    https://twitter.com/Swizec/status/1089614535047110656

    A few TIL lessons from the week

    These days I post a cool tip in the form of a TodayILearned on twitter every day. Here are some good ones from this week. Click on them, they're threads 👇

    https://twitter.com/Swizec/status/1087591170174115845
    https://twitter.com/Swizec/status/1087591170174115845
    https://twitter.com/Swizec/status/1087973312288952322
    https://twitter.com/Swizec/status/1087973312288952322
    https://twitter.com/Swizec/status/1088683833321390081
    https://twitter.com/Swizec/status/1088683833321390081
    https://twitter.com/Swizec/status/1089384771510128640
    https://twitter.com/Swizec/status/1089384771510128640

    This one has some cool CompSci fundamentals ^

    https://twitter.com/Swizec/status/1089746524748140544
    https://twitter.com/Swizec/status/1089746524748140544

    This one is an epic mystery with a weird resolution

    A few cool things

    And as always on a Monday, some cool stuff I discovered this week 👇

    Script-8 which is a whole 8-bit computer built in JavaScript. Great for making retro games.

    dmaydan published this neat maze builder and solver that renders on canvas. If you find that cool, you'll enjoy Bostock's epic maze generation algorithms article.

    This is C++ but I really liked it: A KABOOM animation in 180 lines of C++. Neat effect and good writeup on how to get there.

    And I think this is a cool idea, but I haven't had time to listen to the episodes. A React learning course compiled out of podcasts

    Hope you have a great week. and to that person who asked Do you even read these? in the typeform below: Yes I do but I can't reply.

    Cheers,
    ~Swizec

    Published on January 28th, 2019 in Front End, Livecoding, Technical

    Did you enjoy this article?

    Continue reading about Tooltips and state across various d3 charts in a React dashboard!

    Semantically similar articles hand-picked by GPT-4

    Senior Mindset Book

    Get promoted, earn a bigger salary, work for top companies

    Learn more

    Have a burning question that you think I can answer? Hit me up on twitter and I'll do my best.

    Who am I and who do I help? I'm Swizec Teller and I turn coders into engineers with "Raw and honest from the heart!" writing. No bullshit. Real insights into the career and skills of a modern software engineer.

    Want to become a true senior engineer? Take ownership, have autonomy, and be a force multiplier on your team. The Senior Engineer Mindset ebook can help 👉 swizec.com/senior-mindset. These are the shifts in mindset that unlocked my career.

    Curious about Serverless and the modern backend? Check out Serverless Handbook, for frontend engineers 👉 ServerlessHandbook.dev

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

    Want to get my best emails on JavaScript, React, Serverless, Fullstack Web, or Indie Hacking? Check out swizec.com/collections

    Did someone amazing share this letter with you? Wonderful! You can sign up for my weekly letters for software engineers on their path to greatness, here: swizec.com/blog

    Want to brush up on your modern JavaScript syntax? Check out my interactive cheatsheet: es6cheatsheet.com

    By the way, just in case no one has told you it yet today: I love and appreciate you for who you are ❤️

    Created by Swizec with ❤️