LogoDTreeLabs

Composable charts using Nivo in React

Gaurav SinghBy Gaurav Singh in ReactJavaScriptNivo on March 10, 2022

Nivo is a popular charting library for the charts in ReactJS. It provides a rich set of DataViz components, built on top of D3 and ReactJS. But, it does not provide a straightforward way to compose multiple charts into one. Let's take an example of a bar chart and line chart combined in a single chart. In Nivo, we can either create a Bar chart or Line chart. A chart combining both of them is not supported by default.

In this article we will learn how to create composable charts using Nivo in ReactJS by customizing the chart component.

A discussion on a github issue of the Nivo repository indicates that merging two or more charts is not possible. But, Nivo has introduced a concept of layers property which allows us to pass extra layers and also to change the order of elements. This is supported for the Bar, ScatterPlot and Line charts. Layers define the order of the components which will be rendered on the canvas.

From the official documentation:

{Layers} defines the order of layers, available layers are: grid, axes, bars, markers, legends, annotations. The markers layer is not available in the canvas flavor. You can also use this to insert extra layers to the chart, this extra layer must be a function which will receive the chart computed data and must return a valid SVG element.

In simpler words, if we create a function that returns an SVG then we can superimpose this on top of the Nivo chart by appending the SVG in the layers. We can create a composed chart in following steps:

  1. Create a bar chart.
  2. Create circles on top of the bars.
  3. Generate a line

1. Create bar chart

Let's first create a Bar chart.

import React from "react";
import ReactDOM from "react-dom";
import { ResponsiveBar } from "@nivo/bar";

const barColor = "#bd7a0f";

const data = [
  { x: "0", v: 2.7 },
  { x: "1", v: 3.3 },
  { x: "2", v: 3.8 },
  { x: "3", v: 4.3 },
  { x: "4", v: 1.7 },
  { x: "5", v: 2.2 },
  { x: "6", v: 5.5 },
  { x: "7", v: 6.0 }
];

const App = () => (
  <div className="App">
    <ResponsiveBar
      width={500}
      height={400}
      data={data}
      keys={["v"]}
      maxValue={6.6}
      padding={0.6}
      margin={{
        top: 10,
        right: 10,
        bottom: 36,
        left: 36
      }}
      indexBy="x"
      enableLabel={false}
      colors={[barColor]}
      borderRadius={2}
      axisLeft={{
        tickValues: 7
      }}
    />
  </div>
);

ReactDOM.render(<App />, document.getElementById("root"));

This will render the chart with the bars. Attaching image below for the reference.

2. Create circles on top of the bars

As we have discussed earlier, we can append svg elements to the chart by appending then to the layers. Let's check this in action:

const ScatterCircle = ({ bars, xScale, yScale }) => {
  return (
    <>
      {bars.map((bar) => (
        // Render the circle SVG in chart using Bars co-ordinates.
        <circle
          key={`point-${bar.data.data.x}`}
          // Scale x-cordinate of the circle to the center of bar
          cx={xScale(bar.data.index) + bar.width / 2}
          // Scale y-cordinate of the circle to top of the bar
          cy={yScale(bar.data.data.v + 0.2)}
          r={3}
          fill="black"
          stroke="black"
          style={{ pointerEvents: "none" }}
        />
      ))}
    </>
  );
};

const App = () => (
  <div className="App">
    <ResponsiveBar
      width={500}
      height={400}
      data={data}
      keys={["v"]}
      maxValue={6.6}
      padding={0.6}
      margin={{
        top: 10,
        right: 10,
        bottom: 36,
        left: 36
      }}
      indexBy="x"
      enableLabel={false}
      colors={[barColor]}
      borderRadius={2}
      axisLeft={{
        tickValues: 7
      }}
      /* Add scatter comp to the layers. Layers define the order in which elements 
      will be placed on the chart. In this case first grid, second axes, third bars
       and lastly Scatter will be rendered. */
      layers={["grid", "axes", "bars", ScatterCircle]}
    />
  </div>
);

Outcome:

3. Generate the line

We are using d3-shape to generate the line. We are using line() for generating the coordinates for the line and curveCatmullRom for smoothness of the line.

import { line, curveCatmullRom } from "d3-shape";

const ScatterCircle = ({ bars, xScale, yScale }) => {
  return (
    <>
      {bars.map((bar) => (
        // Render the circle SVG in chart using Bars co-ordinates.
        <circle
          key={`point-${bar.data.data.x}`}
          // Scale x-cordinate of the circle to the center of bar
          cx={xScale(bar.data.index) + bar.width / 2}
          // Scale y-cordinate of the circle to top of the bar
          cy={yScale(bar.data.data.v + 0.2)}
          r={3}
          fill="black"
          stroke="black"
          style={{ pointerEvents: "none" }}
        />
      ))}
    </>
  );
};

const Line = ({ bars, xScale, yScale }) => {
  const lineGenerator = line()
    .x((bar) => xScale(bar.data.index) + bar.width / 2)
    .y((bar) => yScale(bar.data.data.v + 0.2))
    .curve(curveCatmullRom.alpha(0.5));

  return (
    <path
      d={lineGenerator(bars)}
      fill="none"
      stroke={lineColor}
      style={{ pointerEvents: "none", strokeWidth: "2" }}
    />
  );
};

const App = () => (
  <div className="App">
    <ResponsiveBar
      width={500}
      height={400}
      data={data}
      keys={["v"]}
      maxValue={6.6}
      padding={0.6}
      margin={{
        top: 10,
        right: 10,
        bottom: 36,
        left: 36
      }}
      indexBy="x"
      enableLabel={false}
      colors={[barColor]}
      borderRadius={2}
      axisLeft={{
        tickValues: 7
      }}
      /* Add Line component to the layers. Line component will be placed on the component at last. */
      layers={["grid", "axes", "bars", ScatterCircle, Line]}
    />
  </div>
);

Outcome:

This is a fairly simple process to generate the the composable charts. We have to create a main component like Bar chart and layers attribute provides the composable structure to add elements to the Chart. This approach has some limitations as well:

  1. Manage the scaling and placement of elements on the chart canvas explicitly.
  2. No default tooltips for the generated SVG components.

Please refer codesandbox for the full demo.

References:

  1. Extra Layers Code sandbox
  2. Nivo Bar Chart Library
  3. D3-Shape
  4. Compose chart issue in Nivo

You can connect with us on Twitter for your feedback or suggestions.