Building graph axes with React (and d3.js)


This post explains how to build axes from d3 scales for a chart. It relies on the tick() method to compute the tick positions and use react for the rendering. The code of the BottomAxis and LeftAxiscomponents is provided, together with some reproducible examples.

This minimal example uses scaleLinear() to compute the scales, ticks() to compute tick positions and react to render the axes.

Bottom Axis

The code snippet below builds a AxisBottom component. It is very much inspired from this blogpost by Amelia Wattenberger. I've just changed a few things, notably passing a scale as input instead of a range and a domain.

The logic mainly relies on the ticks() method of a d3 scale. It takes a target number of ticks as input, find the most appropriate way to build smart ticks based on this target, and returns an array with all the tick positions.

What follows is just some svg drawings based on those tick positions.

const TICK_LENGTH = 6;

export const AxisBottom = ({ xScale, pixelsPerTick }) => {
  const range = xScale.range();

  const ticks = useMemo(() => {
    const width = range[1] - range[0];
    const numberOfTicksTarget = Math.floor(width / pixelsPerTick);

    return xScale.ticks(numberOfTicksTarget).map((value) => ({
      value,
      xOffset: xScale(value),
    }));
  }, [xScale]);

  return (
    <>
      {/* Main horizontal line */}
      <path
        d={["M", range[0], 0, "L", range[1], 0].join(" ")}
        fill="none"
        stroke="currentColor"
      />

      {/* Ticks and labels */}
      {ticks.map(({ value, xOffset }) => (
        <g key={value} transform={`translate(${xOffset}, 0)`}>
          <line y2={TICK_LENGTH} stroke="currentColor" />
          <text
            key={value}
            style={{
              fontSize: "10px",
              textAnchor: "middle",
              transform: "translateY(20px)",
            }}
          >
            {value}
          </text>
        </g>
      ))}
    </>
  );
};

Left

Exactly the same idea than for the bottom axis above

const TICK_LENGTH = 6;

export const AxisLeft = ({ yScale, pixelsPerTick }) => {
  const range = yScale.range();

  const ticks = useMemo(() => {
    const height = range[0] - range[1];
    const numberOfTicksTarget = Math.floor(height / pixelsPerTick);

    return yScale.ticks(numberOfTicksTarget).map((value) => ({
      value,
      yOffset: yScale(value),
    }));
  }, [yScale]);

  return (
    <>
      {/* Main vertical line */}
      <path
        d={["M", 0, range[0], "L", 0, range[1]].join(" ")}
        fill="none"
        stroke="currentColor"
      />

      {/* Ticks and labels */}
      {ticks.map(({ value, yOffset }) => (
        <g key={value} transform={`translate(0, ${yOffset})`}>
          <line x2={-TICK_LENGTH} stroke="currentColor" />
          <text
            key={value}
            style={{
              fontSize: "10px",
              textAnchor: "middle",
              transform: "translateX(-20px)",
            }}
          >
            {value}
          </text>
        </g>
      ))}
    </>
  );
};

Margins

The bottom and left axes are not displays at the border of the main chart component. Some margins are computed by the viz component. It is important to understand that a chart is composed by:

  • the global chart area, often specified by the width and height properties of the chart components.
  • the "bounds" area, i.e. the area located inside the x and y axis. It is calculated by substracting the margins

Usage

Once you have the bottom and left axis component described above you just need to call them properly. You need to compute the bounds area by substracting the margins to the total svg area.

Don't forget to add an additional translation to the bottom axis to render it... at the bottom.

0246810

This axis is rendered without using d3.js to render.

Alternative: the d3 way

If you're a d3.js afficionados and want to deal with as little react as possible, you can still use the good old axisBottom() and axisLeft() methods of d3 and wrap them in auseEffect() hook.

Here is an example below:

This axis is rendered using d3. The d3 necessary functions are called from a useEffect




General Knowledge

Contact

👋 Hey, I'm Yan and I'm currently working on this project!

Feedback is welcome ❤️. You can fill an issue on Github, drop me a message on Twitter, or even send me an email pasting yan.holtz.data with gmail.com. You can also subscribe to the newsletter to know when I publish more content!

    Contact

    👋 Hey, I'm Yan and I'm currently working on this project!

    Feedback is welcome ❤️. You can fill an issue on Github, drop me a message on Twitter, or even send me an email pasting yan.holtz.data with gmail.com. You can also subscribe to the newsletter to know when I publish more content!