DEV Community

Cover image for Building a SVG Line Chart in React
Kelsey Leftwich for Headway

Posted on • Originally published at headway.io

Building a SVG Line Chart in React

This article was originally posted on the Headway blog. Visit us at headway.io to see how we're making waves. 🏄‍♀️

In this article, we're going to build a line chart using SVGs and React. There are thousands of chart packages available via NPM, but I decided to build one from scratch.
‍ 
 

Why build a line chart from scratch?

  • The effort to create this simple chart from scratch would be the about the same amount of effort as customizing a library chart.

  • You don't have to add another dependency to the project.

  • Gain a better understanding of how line charts function.

My suspicion that I could create a chart fairly easily myself was confirmed in seeing that my finished chart component is fewer than 200 lines of code.

Visual aids to stay organized

As I developed this component, I created a series of diagrams to record the various values and expressions needed to render the chart component. A handful of figures - chart width, chart height, and desired padding - are the basis for laying out axes, guides, points, and labels.

Having a visual aid helped me to keep them organized and ensure the formulas to lay out chart components are accurate. I used LucidChart but you can use any charting software or even a paper and pencil.‍
‍ 
 

Setting up the container padding and the X and Y chart axes

Let's start with the container and chart axes. The first diagram consists of a box representing the chart in a container with padding. I labeled the corners of both boxes with expressions of how to calculate the XY coordinates. The expression variables use the container's height, width, and padding as variables. The height and width are passed in as props to the component.

The padding is calculated by adding the font size of the SVG component and the number of digits for the maximum X value from the data and multiplying the product by two.

padding = (font size + max(x) number of digits) * 2

line chart container diagram

With these expressions, I can draw the chart's axes:

const Axis = (points) => (
  <polyline
    fill="none"
    stroke="#ccc"
    strokeWidth=".5"
    points={points}
  />
)

// Axis start point: (padding, height - padding)
// Axis end point: (width - padding, height - padding)
const XAxis = () => (
  <Axis
    points={`${padding},${height - padding} ${width - padding},${height -
      padding}`}
  />
);

// Axis start point: (padding, padding)
// Axis end point: (padding, height - padding)
const YAxis = () => (
  <Axis
    points={`${padding},${padding} ${padding},${height - padding}`}
  />
);
Enter fullscreen mode Exit fullscreen mode

 
 

Plotting points and connecting them with lines

With the chart area set up, we'll want to start plotting points and connecting them with a line. I added a point to my diagram with expressions of how to calculate the XY coordinates for any element from the dataset. The expressions use the element's X and Y values and the container's height, width, and padding.

plotting points diagram

With these expressions we can map the data to coordinates for an SVG polyline:


const data = [
  { label: 'S', x: 0, y: 0 },
  { label: 'M', x: 1, y: 4 },
  { label: 'T', x: 2, y: 3 },
  { label: 'W', x: 3, y: 1 },
  { label: 'TH', x: 4, y: 4 },
  { label: 'F', x: 5, y: 5 },
  { label: 'S', x: 6, y: 4 }
];

/*
    data.map(e => e.x) returns an array of only the x values:
            [0, 1, 2, 3, 4, 5, 6]
    then we spread this array in the Math.max function so the 
        values in the array can be used as arguments for the function

    we repeat these steps for the y values in data
*/
const maximumXFromData = Math.max(...data.map(e => e.x));
const maximumYFromData = Math.max(...data.map(e => e.y));

const points = data.map(element => {
    /*
    map over each element in the data array and 
    calculate the x and y values for the SVG point
    */

    const x = (element.x / maximumXFromData) * chartWidth + padding;
    const y = chartHeight - (element.y / maximumYFromData) * chartHeight + padding;

    return `${x},${y}`;
}).join(' ');
Enter fullscreen mode Exit fullscreen mode

‍ 
 

Making the chart easier to read with guides

To make our chart easier to read, we will add guides. I started by adding horizontal guides to my diagrams. To lay out a guide, it needs a calculated ratio of the index of the guide plus one to the number of desired guides:

ratio = (index of guide + 1) / number of desired guides

The ratio is then used in calculating the y coordinate of the horizontal line:

formula used to calculate y coordinate

The x coordinate for the start of the horizontal guides is the padding value. The x coordinate for the end of the horizontal guides is the width of the canvas minus padding.

horizontal guide diagram


const HorizontalGuides = () => {
    const startX = padding;
    const endX = width - padding;

    /*
        Array(n) creates an array of n length and 
        fill(0) fills the created array with zeros
        so we can map over it
    */
    return new Array(numberOfHorizontalGuides).fill(0).map((_, index) => {
      const ratio = (index + 1) / numberOfHorizontalGuides;

      const yCoordinate = chartHeight - chartHeight * ratio + padding;

      return (
          <polyline
            fill="none"
            stroke={'#ccc'}
            strokeWidth=".5"
            points={`${startX},${yCoordinate} ${endX},${yCoordinate}`}
          />
      );
    });
  };
Enter fullscreen mode Exit fullscreen mode

‍ 
 
I added vertical guides to my chart, using the same ratio calculation used in laying out the horizontal guides. In the vertical guides, the ratio is used to calculate the x coordinate for the guides:

formula used to calculate x coordinate


The y coordinate for the start of the vertical lines is equal to the padding. The y coordinate for the end of the guides is equal to the height minus padding.

adding vertical guides diagram
‍ 
 

Note:

For the vertical guides, the number of guides is set to the number of data points minus one if the number of desired vertical guides is not passed in to the LineChart component as a prop.

‍ 
 


const VerticalGuides = () => {
    /* if numberOfVerticalGuides is null, use the length of the data
         to calculate the number of guides */
  const guideCount = numberOfVerticalGuides || data.length - 1;

  const startY = padding;
  const endY = height - padding;

  return new Array(guideCount).fill(0).map((_, index) => {
    const ratio = (index + 1) / guideCount;

    const xCoordinate = padding + ratio * (width - padding * 2);

    return (
      <>
        <polyline
          fill="none"
          stroke="#ccc"
          strokeWidth=".5"
          points={`${xCoordinate},${startY} ${xCoordinate},${endY}`}
        />
      </>
    );
  });
};
Enter fullscreen mode Exit fullscreen mode

‍ 
 

Adding labels to the guides

To make our guides more useful we can add labels along the axes. I added x-axis labels to the diagram. The x coordinate for the data element's label is calculated by multiplying the ratio of the element's x value to the maximum x value from the dataset by the width of the chart and adding half of the padding value. The y value is calculated by subtracting the padding value from the height value and adding half of the font size value.

adding labels to guides diagram
‍ 
 


const LabelsXAxis = () => {
    const y = height - padding + FONT_SIZE * 2;

    return data.map(element => {
      const x =
        (element.x / maximumXFromData) * chartWidth + padding - FONT_SIZE / 2;
      return (
        <text
          x={x}
          y={y}
          style={{ fill: '#ccc', fontSize: FONT_SIZE, fontFamily: 'Helvetica' }}
        >
          {element.label}
        </text>
      );
    });
  };
Enter fullscreen mode Exit fullscreen mode

‍ 
 

Y-axis labels

Next I added y-axis labels to the diagram. There are as many labels as there are horizontal guides. The x coordinate is equal to the font size variable. To calculate the y coordinate, I calculated the ratio of the index of the label by the number of guides. I multiplied this ratio by the chart height and subtracted the resulting product from the chart height. I then added padding and the font size divided by 2.

adding labels to y axis diagram
‍ 
 


const LabelsYAxis = () => {
    const PARTS = numberOfHorizontalGuides;
    return new Array(PARTS + 1).fill(0).map((_, index) => {
      const x = FONT_SIZE;
      const ratio = index / numberOfHorizontalGuides;

      const yCoordinate =
        chartHeight - chartHeight * ratio + padding + FONT_SIZE / 2;
      return (
        <text
          x={x}
          y={yCoordinate}
          style={{ fill: '#ccc', fontSize: FONT_SIZE, fontFamily: 'Helvetica' }}
        >
          {parseFloat(maximumYFromData * (index / PARTS)).toFixed(precision)}
        </text>
      );
    });
  };
Enter fullscreen mode Exit fullscreen mode

‍ 
 

Finished SVG line chart example

The result is our finished chart!

finished svg line chart example from this lesson

import React from "react";
import LineChart from "./components/LineChart";

const data = [
  { label: "S", x: 0, y: 0 },
  { label: "M", x: 1, y: 400 },
  { label: "T", x: 2, y: 300 },
  { label: "W", x: 3, y: 100 },
  { label: "TH", x: 4, y: 400 },
  { label: "F", x: 5, y: 500 },
  { label: "S", x: 6, y: 400 }
];

function App() {
  return (
    <div style={{ padding: 25, maxWidth: 700 }}>
      <LineChart
        data={data}
        horizontalGuides={5}
        precision={0}
        verticalGuides={6}
      />
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

‍ 
 

Experimenting with properties

Try experimenting with the height, width, and guides properties. And in reviewing the source code, you'll see I've added a prop for setting the precision of the y-axis label.

Change Example 01

line chart example with adjusted height and accuracy


/* Modified height, vertical guides, and precision of y-axis labels*/

<LineChart
  height={200}
  data={data}
  horizontalGuides={5}
  precision={2}
  verticalGuides={1}
/>

Enter fullscreen mode Exit fullscreen mode

Change Example 02

line chart example with adjusted height labels

/* Modified height, width, and guides props */

<LineChart
  height={350}
  width={600}
  data={data}
  horizontalGuides={1}
  precision={0}
  verticalGuides={1}
/>
Enter fullscreen mode Exit fullscreen mode

‍ 
 

Adding Y-axis unit labels and titles

You might remember from math class that a chart needs unit labels on the y-axis and a title. Add these as sibling components or modify the chart component to accept these as props as well.

line chart example with labels and titles


function App() {
  return (
    <div style={styles.chartComponentsContainer}>
      <div/>
      <ChartTitle text="Movements per Day of the Week"/>
      <Label text="Movements" rotate/>
      <div style={styles.chartWrapper}>
        <LineChart
            width={500 }
          height={300}
          data={data}
          horizontalGuides={5}
          precision={2}
          verticalGuides={1}
        />
      </div>
      <div/>
      <Label text="Days of the Week"/>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Happy coding!

Source code

You can find the source code for the project here.
‍ 
 
 

Resources to learn more about SVGs

Once you are familiar with the foundational components of SVG, you can pair that knowledge with CSS styles and animations and powerful tools like Figma to create engaging visualizations.

SVG: Scalable Vector Graphics | MDN

The MDN topic for SVG includes links to documentation, tutorials, and examples including mapping, interactions, animations, and games. It's a great resource to work through in order to learn SVG.

Styling And Animating SVGs With CSS

In her article for Smashing Magazine, Sara Soueidan covers in detail what SVGs are, editing and optimizing SVGs in graphics editors, styling SVGs, and animating them. The last section on making responsive SVGs is especially worth reading.

Top comments (1)

Collapse
 
drews256 profile image
Andrew Stuntz

This is awesome!