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.
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>
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!