In previous two episodes I did the same D3 app without any tooling, and then with Parcel. Let's do it again, but using Svelte, and using D3 only for data crunching, not for DOM manipulation.
D3 will still do parts of the interface like drawing axes and lines on the graph, but it will hand that data over to Svelte with which then do the DOM changes.
Create a new Svelte app
To start an app with Svelte and D3 we can do this:
$ npx degit sveltejs/template myapp
$ cd myapp
$ npm install d3
This includes some extra stuff I don't care much for, so I deleted a bunch of irrelevant files, and cleaned up syntax of what's left by removing some nasty semicolons.
public/index.html
This is coming from the boilerplate, with some minor cleanup. The most imporant part was switch from absolute to relative URLs so it works on GitHub Pages:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="./build/bundle.css">
<script defer src="./build/bundle.js"></script>
</head>
<body>
</body>
</html>
src/main.js
This is coming from the boilerplate, with some minor cleanup:
import App from "./App.svelte"
let app = new App({target: document.body})
export default app
src/App.svelte
This component is responsible for loading the data, and then calling our actual graph drawing code.
It also does some global styling. This could arguably be put into separate static CSS file instead.
<script>
import * as d3 from "d3"
import Graph from "./Graph.svelte"
let parseRow = ({date,tank}) => ({date: new Date(date), tank: +tank})
let loadData = async () => {
let url = "./russia_losses_equipment.csv"
let data = await d3.csv(url, parseRow)
data.unshift({date: new Date("2022-02-24"), tank: 0})
return data
}
let dataPromise = loadData()
</script>
{#await dataPromise then data}
<Graph {data} />
{/await}
<style>
:global(body) {
margin: 0;
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
</style>
src/Graph.svelte
And finally our main component:
<script>
import * as d3 from "d3"
export let data
let xScale = d3.scaleTime()
.domain(d3.extent(data, d => d.date))
.range([0, 600])
let yScale = d3.scaleLinear()
.domain(d3.extent(data, d => d.tank))
.range([400, 0])
let pathData = d3.line()
.x(d => xScale(d.date))
.y(d => yScale(d.tank))
(data)
let xAxis, yAxis
$: {
d3.select(xAxis).selectAll("*").remove()
d3.select(xAxis).call(d3.axisBottom(xScale))
}
$: {
d3.select(yAxis).selectAll("*").remove()
d3.select(yAxis).call(d3.axisLeft(yScale))
}
</script>
<h1>Russian Tank Losses</h1>
<svg>
<g class="graph">
<path d={pathData}/>
</g>
<g class="x-axis" bind:this={xAxis}></g>
<g class="y-axis" bind:this={yAxis}></g>
</svg>
<style>
svg {
height: 600px;
width: 800px;
}
.graph {
transform: translate(100px, 100px);
}
path {
fill: none;
stroke: red;
stroke-width: 1.5;
}
.x-axis {
transform: translate(100px, 500px);
}
.y-axis {
transform: translate(100px, 100px);
}
</style>
There's a lot going on here:
- we don't need to deal with
async
stuff, asdata
we get is already properly prepared, and all we need to do is display it - we create scales just as before, there's nothing Svelte specific here
- line graph nicely separates D3 part from Svelte part. The syntax is quite unusual, as we construct a function with
d3.line().x(d => xScale(d.date)).y(d => yScale(d.tank))
then call it with(data)
. Then we pass this data to<path d={pathData}/>
- we can handle all static visual attributes with CSS
- axes are awkward - each axis is a lot of SVG elements, so D3 needs some DOM access here. Svelte is fairly flexible with this, but it might be better to move this mess to a separate component like
<Axis position="bottom" scale={xScale} x=100 y=500 />
Story so far
I deployed this on GitHub Pages, you can see it here.
Coming next
In the next episode, we'll add some functionality to the app. The end goal is to try to figure out how long until Russia runs out of tanks, but that might take longer than an episode.
Top comments (0)