Drawing with Data
So far every position was a number we typed by hand. Now we hook the shapes up to real data: loading a dataset, mapping each row to a simple shape, and letting the data itself decide where that shape sits on the screen.
From there we make the charts interactive — highlighting points on hover, filtering the data on the fly, and responding to what the reader does. Together these are everything we need to start building the final project.
1. Draw a Shape with Data
Up to now we typed every number ourselves. Let’s load some real data.
We pull the data in with an import. It loads the file once and gives the list a name we can use everywhere - data:
<script>
import data from '$assets/data/spotify-2010s.csv';
console.log(data);
</script>console.log(...) displays a value to the browser’s console so we can look at it (open it with F12, then the Console tab).
// log the whole dataset
console.log(data);
// log the first row
console.log(data[0]);
// log the title of the first row
console.log(data[0]["title"]); // "Hey, Soul Sister"So far these values only appear in the console. To put one on the page, we wrap it in curly braces { }. Whatever sits between the braces is run as JavaScript and its result is dropped into the HTML:
<circle r={row["popularity"]} /> <!-- use as radius value -->
<text>{row["title"]}</text> <!-- use as text content -->src/exercises/4-draw-a-shape-with-data.svelte2. Drawing an Entire Dataset
We drew one song. With 139 of them, we don’t want to copy that markup 139 times - we want to repeat it once per row. Svelte’s {#each} block does exactly that:
{#each data as row, i}
<circle r={row["popularity"]} />
{/each}row is the song for this turn of the loop, and i is its index - its position in the list (0, 1, 2, …).
Now lets map our data values to a more acceptable range, we will use a scale for this.
const radius = scaleLinear()
.domain([0, 100]) // popularity runs from 0 to 100
.range([0, 95]); // 0 -> invisible, 100 -> biggest dotA scale is just a mapping: every value in the domain lands on a matching value in the range, in proportion. Drag the handles below to feel how an input maps to an output — and edit the four numbers to reshape the mapping:
scale(40) = 200
Now size each circle with r={radius(row["popularity"])}. Fill in the cells in the exercise:
src/exercises/5-drawing-an-entire-dataset.svelte3. Styling your Shapes
To colour our shapes based on genre we use an **object** as a lookup table - think of it like a **dictionary**: you look up a word (the genre) and get back its definition (the colour). Each line is a **key** (the genre) paired with a **value** (the colour):let genreColors = {
'Dance pop': '#E75B2B',
Pop: '#DB1E24',
'Hip-hop/R&B': '#F9B806',
'Acoustic/Folk': '#3091B9',
Other: '#E032A3',
EDM: '#53DC41'
};To look one up, we put the key in square brackets:
genreColors['Dance pop']; // "#E75B2B"
genreColors[row["genre"]]; // the colour for this row's genreNow do it in your exercise:
src/exercises/5-drawing-an-entire-dataset.svelte4. Adding Interactivity
Same file as before. The {#each} just draws whatever list we hand it - so to change the chart, we change the list, not the drawing.
1. A list we can reshape. A few variables hold the chosen options, and a reactive modifiedData filters and sorts data from them:
let sizeMetric = 'popularity';
let sortMetric = 'popularity';
let selectedGenre = 'all';
$: modifiedData = data
.filter((row) => selectedGenre === 'all' || row["genre"] === selectedGenre)
.sort((a, b) => b[sortMetric] - a[sortMetric]);Loop over modifiedData instead of data (keep it keyed for later) and size each disc by the chosen metric. Leave size at gridSize(data.length) so the canvas doesn’t resize as you filter:
{#each modifiedData as row, i (row["title"] + row["artist"] + row["year"])}
<g class="tile" transform={cellPosition(i)}>
...
<circle cx="100" cy="100" r={radius(row[sizeMetric])} fill="#351D13" />
...
</g>
{/each}Change any of those variables by hand and the grid redraws itself - that’s the whole idea.
2. Play around with the controls. bind:value ties each <select> to its variable, so a dropdown sets it. Try the dropdowns in the demo:
<div class="mb-4 flex flex-wrap items-center gap-5 text-sm">
<label class="flex items-center gap-2 font-medium">
Size by
<select bind:value={sizeMetric} class="rounded border border-gray-300 px-2 py-1">
<option value="popularity">Popularity</option>
<option value="energy">Energy</option>
<option value="danceability">Danceability</option>
</select>
</label>
<label class="flex items-center gap-2 font-medium">
Sort by
<select bind:value={sortMetric} class="rounded border border-gray-300 px-2 py-1">
<option value="popularity">Popularity</option>
<option value="energy">Energy</option>
<option value="danceability">Danceability</option>
</select>
</label>
<label class="flex items-center gap-2 font-medium">
Genre
<select bind:value={selectedGenre} class="rounded border border-gray-300 px-2 py-1">
<option value="all">All
</option>
<option value="Dance pop">Dance pop</option>
<option value="Pop">Pop</option>
<option value="Hip-hop/R&B">Hip-hop/R&B</option>
<option value="Acoustic/Folk">Acoustic/Folk</option>
<option value="EDM">EDM</option>
<option value="Other">Other</option>
</select>
</label>
</div>3. Smooth the jumps. Without easing, the tiles snap straight to their new spots - a bit choppy. Two CSS transitions, one for transform (the move) and one for r (the resize), make it glide:
g {
transition: all 0.5s cubic-bezier(0.22, 1, 0.36, 1);
}
circle {
transition: r 0.5s cubic-bezier(0.68, -0.6, 0.32, 1.6);
}4. Hover animations. While we’re in CSS we can add a little polish. When the chart is hovered we dim every tile, then bring the one under the cursor back to full strength and lift it with a shadow - so a single record stands out from the crowd:
/* if the svg is hovered then make all g elements inside it 0.5 opacity */
svg:hover g {
opacity: 0.5;
}
/* if the svg is hovered AND the g inside it is ALSO hovered then apply opacity 1 + drop shadow */
svg:hover g:hover {
opacity: 1;
filter: drop-shadow(0 6px 12px rgba(0, 0, 0, 0.45));
}Now make it yours - paste these pieces into the grid from the last section and try it live:
src/exercises/5-drawing-an-entire-dataset.svelte