SVG Sparklines with no dependencies

In one of our projects, we used Highcharts to display a sparkline. However, when I began to explore the bundle contents to improve our Largest Contentful Paint (LCP), I noticed that Highcharts was fairly large - around 200kb before being gzipped.

Previously, I had used Pygal to generate SVG charts on the server side. To my delight, I discovered that it supported sparklines. However, after giving it a whirl, I found that the resulting SVG was larger than I had hoped. It was bloated with a lot of unnecessary CSS and JS. I tried wrangling the library to fit my needs for some time, but eventually decided to explore other options.

I then turned my attention to the SVG produced by Highcharts. Interestingly, I noticed two visible path elements - one for the area below the line and one for the line itself. With the assistance of ChatGPT, I managed to gain insight into the structure of the path data and asked it to write a function that would generate these paths.

To my surprise, the function worked flawlessly on the first attempt! Of course, I had to make a few tweaks to support both the line and the area below it, and to handle any edge cases that might come up. So, in the end, the lesson learned was that sometimes it's best to roll up your sleeves and get down to creating exactly what you need.

import React, { FC } from 'react';

const WIDTH = 150;
const HEIGHT = 25;

interface SparklineProps {
  data: number[];
}
/*

The calculation HEIGHT - ((value - minimum) / (maximum - minimum)) * HEIGHT is used to normalize and scale the values of the data set within a certain range. In this case, it is scaling the values within the range of the height of the SVG canvas. Here's how it works:

1. (value - minimum) / (maximum - minimum): This part of the equation normalizes the value. This means it adjusts the value to be on a scale from 0 to 1, where 0 corresponds to the minimum value in the dataset and 1 corresponds to the maximum value. This is achieved by subtracting the minimum value from the current value (making the minimum value now 0), and then dividing by the range of the dataset, which is (maximum - minimum).

2. The normalized value is then multiplied by the HEIGHT. This scales the normalized value to the range of the height of the SVG canvas. This value now represents a position on the y-axis of the SVG canvas, but from the top down (because in SVG coordinates, 0,0 is at the top left).

3. Finally, HEIGHT - ... is used to invert the value. This is done because SVG coordinates start from the top left corner, meaning higher y-values are lower on the canvas. This step ensures that higher data values are plotted higher on the canvas.

*/
export const generateSparkline = (data: number[], area = false): string => {
  // We don't want to display single point sparklines
  if (data.length <= 1) {
    return '';
  }

  let minimum = Math.min(...data);
  let maximum = Math.max(...data);

  // Prevent division by zero when values are constant
  if (minimum === maximum) {
    maximum += 1;
  }
  let deltaXincrementPerDataPoint = WIDTH / (data.length - 1); // Calculate the x-axis increment for each data point

  const path = data.map((value, i) => {
    // Scale the value to fit within the height
    let scaledValue =
      HEIGHT - ((value - minimum) / (maximum - minimum)) * HEIGHT;

    let mode: 'M' | 'L';
    if (i === 0 && !area) {
      // For line path move to the first point and start there
      mode = 'M';
    } else {
      // in every other case draw line
      mode = 'L';
    }

    return `${mode}${i * deltaXincrementPerDataPoint} ${scaledValue}`;
  });

  if (area) {
    path.unshift(`M0 ${HEIGHT}`); // Start at the bottom left
    path.push(`L${WIDTH} ${HEIGHT}`); // Line to the last point
  }
  return path.join(' ');
};

const Sparkline: FC<SparklineProps> = ({ data }) => {
  let areaPath = generateSparkline(data, true);
  let linePath = generateSparkline(data);

  return (
    <svg
      version="1.1"
      xmlns="http://www.w3.org/2000/svg"
      width={WIDTH}
      height={HEIGHT}
      viewBox={`0 0 ${WIDTH} ${HEIGHT}`}
    >
      <path fill="#47C1BF1A" d={areaPath}></path>
      <path
        fill="none"
        d={linePath}
        stroke="#47C1BF"
        strokeWidth="1"
        strokeLinejoin="round"
        strokeLinecap="round"
      ></path>
    </svg>
  );
};

export default Sparkline;