Creating a Gradient-Encoded Line Chart in Svelte with D3

In this tutorial, we’ll learn how to build a gradient-encoded line chart using Svelte 5 and D3.js. The gradient will not only enhance the visual appeal of the chart but also encode temperature data directly into the line’s styling.

Temperature (°F) ↑

What You’ll Learn

  • Importing and setting up D3 modules in a Svelte project.
  • Using scales to map data values to visual properties.
  • Generating an SVG for the line.
  • Adding a vertical color gradient that fills the line.

Full Code Preview

<script>
  import { scaleTime, scaleLinear, scaleSequential } from 'd3-scale';
  import { extent, ticks } from 'd3-array';
  import { interpolateTurbo } from 'd3-scale-chromatic';
  import { line, curveStep } from 'd3-shape';
  import { timeFormat } from 'd3-time-format';
  import { csv } from 'd3-fetch';
  import { onMount } from 'svelte';

  let data = $state([]);

  onMount(async () => {
    try {
      const rawData = await csv('/data/temperature.csv'); // Change the path if needed
      data = rawData.map((d) => ({
        date: new Date(d.date),
        temperature: d.temperature
      }));
      console.log(rawData, data);
    } catch (error) {
      console.error('Error loading CSV file:', error);
    }
  });
  // Chart dimensions
  const width = 928;
  const height = 500;
  const margin = { top: 20, right: 30, bottom: 30, left: 40 };

  // Scales
  let xScale = $derived(
    scaleTime()
      .domain(extent(data, (d) => d.date))
      .range([margin.left, width - margin.right])
  );

  let yScale = $derived(
    scaleLinear()
      .domain(extent(data, (d) => d.temperature))
      .nice()
      .range([height - margin.bottom, margin.top])
  );

  let colorScale = $derived(scaleSequential(yScale.domain(), interpolateTurbo));

  // Line generator
  let lineGenerator = $derived(
    line()
      .curve(curveStep)
      .defined((d) => !isNaN(d.temperature))
      .x((d) => xScale(d.date))
      .y((d) => yScale(d.temperature))
  );

  // Gradient stops
  let gradientStops = $derived(ticks(0, 1, 10));
  $effect(() => console.log(gradientStops));
  // Axis ticks
  let xTicks = $derived(xScale.ticks(width / 80));
  let yTicks = $derived(yScale.ticks(5));
</script>

<svg
  {width}
  {height}
  viewBox={`0 0 ${width} ${height}`}
  style="max-width: 100%; height: auto;">
  <!-- X-axis -->
  <g transform={`translate(0, ${height - margin.bottom})`}>
    {#each xTicks as tick}
      <text
        class="fill-gray-400"
        x={xScale(tick)}
        y={6}
        dy="0.71em"
        font-size="14px"
        text-anchor="middle">
        {timeFormat('%b')(tick)}
      </text>
    {/each}
    <line
      x1={margin.left}
      x2={width - margin.right}
      stroke="currentColor"
      stroke-opacity="0.1" />
  </g>

  <!-- Y-axis -->
  <g transform={`translate(${margin.left}, 0)`}>
    {#each yTicks as tick}
      <text
        class="fill-gray-400"
        x={-6}
        y={yScale(tick)}
        dy="0.32em"
        font-size="14px"
        text-anchor="end">
        {tick}
      </text>
      <line
        x1={0}
        x2={width - margin.left - margin.right}
        y1={yScale(tick)}
        y2={yScale(tick)}
        stroke="currentColor"
        stroke-opacity="0.1" />
    {/each}
    <!-- Y Axis Label -->
    <text
      x={-26}
      y={yScale(yScale.domain()[1])}
      class="fill-gray-400"
      font-size="14px"
      text-anchor="start">
      Temperature (°F) ↑
    </text>
  </g>

  <!-- Gradient -->
  <defs>
    <linearGradient
      id="temperature-gradient"
      gradientUnits="userSpaceOnUse"
      x1="0"
      y1={height - margin.bottom}
      x2="0"
      y2={margin.top}>
      {#each gradientStops as stop, i}
        <stop offset={stop} stop-color={colorScale.interpolator()(stop)} />
      {/each}
    </linearGradient>
  </defs>
  <!-- Line -->
  <path
    opacity="1"
    d={lineGenerator(data)}
    fill="none"
    stroke="url(#temperature-gradient)"
    stroke-width="1.5"
    stroke-linejoin="round"
    stroke-linecap="round" />
</svg>

Parsing and Loading Data

  • You can download the data here

We use d3-fetch to load CSV data and process it for use in our chart. The dates are parsed into JavaScript Date objects, and temperature values, initially read as strings, are converted to numbers.

By default, d3.csv treats all values in the CSV as strings. This creates issues when we try to compute the domain for the y-axis scale using extent(data, d => d.temperature). Without conversion, the temperature values are treated as strings and sorted lexicographically, which leads to incorrect scaling or errors in the chart.

To fix this, we convert the temperature values to numbers using +d.temperature. Here’s the essential block:

onMount(async () => {
  const rawData = await csv('/data/temperature.csv');
  data = rawData.map((d) => ({
    date: parseDate(d.date),
    temperature: +d.temperature // Convert string to number
  }));
});

Creating the Scales

Scales are critical for mapping data values to visual properties.

  • xScale maps dates to horizontal positions on the chart.
  • yScale maps temperatures to vertical positions.
  • colorScale is a sequential color scale (interpolateTurbo) that defines the gradient’s color progression based on temperature values.
let xScale = $derived(
  scaleUtc()
    .domain(extent(data, (d) => d.date))
    .range([margin.left, width - margin.right])
);
let yScale = $derived(
  scaleLinear()
    .domain(extent(data, (d) => d.temperature))
    .nice()
    .range([height - margin.bottom, margin.top])
);
let colorScale = $derived(scaleSequential(yScale.domain(), interpolateTurbo));

Generating the Line

To draw the temperature trend line, we use D3’s line() generator with curveStep for a stepped appearance.

let lineGenerator = $derived(
  line()
    .curve(curveStep)
    .defined((d) => !isNaN(d.temperature))
    .x((d) => xScale(d.date))
    .y((d) => yScale(d.temperature))
);

Adding the Gradient

The vertical gradient is defined using an SVG <linearGradient> inside <defs> element and is applied to the line.

What is <defs>?

<defs> is a container for graphical elements that you want to define once and reuse multiple times. The elements inside <defs> are not rendered directly in the SVG. Instead, they can be referenced using an id attribute in other elements (e.g., lines, shapes). In this example, the <linearGradient> with the ID temperature-gradient is stored within <defs> and later applied to the chart elements using stroke or fill attributes.

<defs>
  <linearGradient
    id="temperature-gradient"
    gradientUnits="userSpaceOnUse"
    x1="0"
    y1={height - margin.bottom}
    x2="0"
    y2={margin.top}>
    {#each gradientStops as stop, i}
      <stop offset={stop} stop-color={colorScale.interpolator()(stop)} />
    {/each}
  </linearGradient>
</defs>

Key Points:

  • gradientUnits="userSpaceOnUse": Aligns the gradient with the chart’s coordinate system, ensuring it maps correctly to the chart dimensions.
  • x1, y1, x2, y2: Define the start (x1, y1) and end (x2, y2) points of the gradient. Here, the gradient transitions vertically, spanning the y-axis.
  • gradientStops: Dynamically generates 10 evenly spaced stops (using d3.ticks), with each stop assigned a color from the colorScale. The colorScale maps the y-axis domain (e.g., temperature values) to the gradient’s range of colors. This ensures that any y-value corresponds to one of the gradient colors..

Adding gradient to the Line Path

Finally, we apply the gradient to the line itself using the stroke attribute:

<path
  d={lineGenerator(data)}
  fill="none"
  stroke="url(#temperature-gradient)"
  stroke-width="1.5"
  stroke-linejoin="round"
  stroke-linecap="round" />

Gradient Example Explanation

This is similar to applying a gradient as a background to a rect element. Instead of drawing a line, we can fill a rect with the same gradient. This allows us to visualize how the gradient would appear across the entire chart area:

<rect
  opacity="1"
  x={margin.left}
  y="0"
  fill="url(#temperature-gradient)"
  height={height - margin.bottom}
  width={width - margin.left - margin.right}></rect>
°F

Adding Axes

Both x-axis and y-axis ticks are rendered with simple loops using D3-generated ticks.

<!-- X-axis -->
<g transform={`translate(0, ${height - margin.bottom})`}>
  {#each xTicks as tick}
    <text
      x={xScale(tick)}
      y={6}
      dy="0.71em"
      font-size="14px"
      text-anchor="middle">
      {timeFormat('%b')(tick)}
    </text>
  {/each}
</g>

<!-- Y-axis -->
<g transform={`translate(${margin.left}, 0)`}>
  {#each yTicks as tick}
    <text x={-6} y={yScale(tick)} dy="0.32em" font-size="10" text-anchor="end">
      {tick}
    </text>
  {/each}
</g>

Final Result

The completed chart includes:

  • A stepped line encoding temperature trends.
  • A gradient fill below the line for enhanced visual impact.
  • X and Y axes for context.

By following these steps, you can create a visually appealing and functional gradient-encoded line chart!

Join our Discord to share your charts, ask questions, or collaborate with fellow developers!