Skip to content

Contact sales

By filling out this form and clicking submit, you acknowledge our privacy policy.

Make D3.js Charts Responsive in React Apps

Learn how to use ViewBox to scale SVG to be responsive to different screen layouts.

Oct 8, 2020 • 6 Minute Read

Introduction

Responsive web design is an approach that suggests that design and development should cater to the user experience of accessing a webpage on different screen sizes and orientations. Normally, you would achieve this with CSS media queries. However, when using D3.js with SVG, there are some additional considerations.

In this guide, you will learn how to use ViewBox to scale SVG to be responsive to different screen layouts. This guide builds on a previous guide, Using D3.js Inside a React App, that covered how to set up a simple bar chart in D3.js using static in-memory data. Here you will extend that bar chart to change its size and layout based on screen size.

The ViewBox SVG Property

In the previous guide, Using D3.js Inside a React App, you manually defined the height and width and of the bar chart to 500 pixels. When you resize the screen, the size of the chart doesn't change.

SVG was designed to render images in a scalable, resizable way. To achieve this, it offers a property called ViewBox to facilitate defining "user space," and all child elements are positioned relative inside this ViewBox. The ViewBox property has four properties, but the last two are the most relevant for charts—width and height. When you specify the ViewBox it causes the SVG element to fill the size of its parent.

Update Bar Chart to Use ViewBox

There are very few changes required to make your <BarChart /> component use ViewBox. The code to do this is demonstrated below:

      import { useD3 } from "./hooks/useD3";
import React from "react";
import * as d3 from "d3";

const height = 500;
const width = 500;
function BarChart({ data }) {
  const ref = useD3(
    (svg) => {
      const margin = { top: 20, right: 30, bottom: 30, left: 40 };

      const x = d3
        .scaleBand()
        .domain(data.map((d) => d.year))
        .rangeRound([margin.left, width - margin.right])
        .padding(0.1);

      const y1 = d3
        .scaleLinear()
        .domain([0, d3.max(data, (d) => d.sales)])
        .rangeRound([height - margin.bottom, margin.top]);

      const xAxis = (g) =>
        g.attr("transform", `translate(0,${height - margin.bottom})`).call(
          d3
            .axisBottom(x)
            .tickValues(
              d3
                .ticks(...d3.extent(x.domain()), width / 40)
                .filter((v) => x(v) !== undefined)
            )
            .tickSizeOuter(0)
        );

      const y1Axis = (g) =>
        g
          .attr("transform", `translate(${margin.left},0)`)
          .style("color", "steelblue")
          .call(d3.axisLeft(y1).ticks(null, "s"))
          .call((g) => g.select(".domain").remove())
          .call((g) =>
            g
              .append("text")
              .attr("x", -margin.left)
              .attr("y", 10)
              .attr("fill", "currentColor")
              .attr("text-anchor", "start")
              .text(data.y1)
          );

      svg.select(".x-axis").call(xAxis);
      svg.select(".y-axis").call(y1Axis);

      var tooltip = d3.select('.tooltip-area')
        .style('opacity', 0);

      svg
        .select(".plot-area")
        .attr("fill", "steelblue")
        .selectAll(".bar")
        .data(data)
        .join("rect")
        .attr("class", "bar")
        .attr("x", (d) => x(d.year))
        .attr("width", x.bandwidth())
        .attr("y", (d) => y1(d.sales))
        .attr("height", (d) => y1(0) - y1(d.sales))
        .append('title')
        .text((d) => `Sales were ${d.sales} in ${d.year}`);
    },
    [data.length]
  );

  return (
    <>
      <svg
        ref={ref}
        viewBox={`0 0 ${height} ${width}`}
        style={{
          height: "100%",
          marginRight: "0px",
          marginLeft: "0px",
        }}
      >
        <g className="plot-area" />
        <g className="x-axis" />
        <g className="y-axis" />
        <g className="tooltip-area">
          <text className="tooltip-area__text">aas</text>
        </g>
      </svg>
    </>
  );
}

export default BarChart;
    

The width and heigh property has been removed from the <svg />. It has been replaced with ViewBox="0 0 500 500". This means we have set the user space height and width to 500. Note that these values do not represent pixels. If something is drawn at coordinates 250, 250, it will be drawn in the middle of the <svg />, which is dependent on its parent.

Test the Responsiveness

To change the layout of your chart, you can experiment by creating multiple charts and repositioning with Flexbox.

To do this, update App.js to create several charts and reposition them with CSS based on screen size.

      <div className="App">
      <header className="App-header">
        <button onClick={() => setData([...data, {year: 2011, sales: 1, efficiency: 100}])}>test</button>
        {loading && <div>loading</div>}
        <div className="chart-container">
          {!loading && <BarChart data={data} />}
          {!loading && <BarChart data={data} />}
        </div>
      </header>
    </div>
    

The snippet above creates two <BarChart /> components to make it easier to understand how they can be positioned in different screen layouts.

On mobile, you may want to stack them on top of each other. However, on a desktop, you may want to place them side by side.

      .chart-container {
  display: flex;
  flex-direction: column;
}

@media (min-width: 900px) {
  .chart-container {
    flex-direction: row;
  }
}
    

The CSS above uses a media query to change the flex-direction, allowing you to position the charts.

Conclusion

D3.js provides a rich set of capabilities for visualizing data. Combining these skills with CSS is essential for building an experience that caters to different platforms and screen sizes. You can learn more about building responsive web apps by reading this article: Why Mobile First.