Open Source Adventures: Episode 32: Better Russian Tank Losses Graphs with D3 and Svelte
Tags: javascript, d3, svelte
Published: 2022-04-07 00:30:39

Before we answer the question when Russian will run out of tanks, let's refactor and improve our code a bit.

We want to achieve the following:

* put awkward axis code in separate component
* extend tank axis slightly so it ends up on a round number
* change ticks on date axis, so they start on Feb 24
* add trend line for overall data
* reduce dead space around the graph

### `src/Axis.svelte`

This component doesn't do any logic, it just bridges D3 DOM manipulation with Svelte, so we have less awkward code in the main component:

```html
<script>
import * as d3 from "d3"
export let axis

let axisNode

$: {
  d3.select(axisNode).selectAll("*").remove()
  d3.select(axisNode).call(axis)
}
</script>

<g bind:this={axisNode}></g>
```

### Rounded numbers on Y scale

We don't need any special code for this, as D3 already comes with a convenient `.nice()` function that pads domain on both sides to nearest round number. As 0 is already as round as it gets, it will only pad a bit on the top, in this case to 700:

```javascript
let yScale = d3.scaleLinear()
  .domain(d3.extent(data, d => d.tank))
  .nice()
  .range([500, 0])
```

### Date formatting on X scale

This is a bit more complicated. The scale is fine, and we do not want to pad it with extra days. It should start Feb 24, and end at the last day we have data for.

We can solve that by manually telling D3 where to place ticks (with `.ticks(d3.timeThursday)` - as Feb 24 is Thursday), and then how to format them (with `.tickFormat(d3.timeFormat("%d %b"))`)

This isn't perfect, as today doesn't have its own tick, but it will do for now.

```javascript
let xAxis = d3.axisBottom()
  .scale(xScale)
  .ticks(d3.timeThursday)
  .tickFormat(d3.timeFormat("%b %d"))
```

### Trend line

Nothing fancy here, just connect first and last datapoint with dashed line.

```javascript
let trendPathData = d3.line()
  .x(d => xScale(d.date))
  .y(d => yScale(d.tank))
  ([data[0], data.at(-1)])
```

### `src/Graph.svelte`

And here's the complete `src/Graph.svelte`. All other files are unchanged from previous episode:

```html
<script>
import * as d3 from "d3"
import Axis from "./Axis.svelte"

export let data

let xScale = d3.scaleTime()
  .domain(d3.extent(data, d => d.date))
  .range([0, 700])

let yScale = d3.scaleLinear()
  .domain(d3.extent(data, d => d.tank))
  .nice()
  .range([500, 0])

let pathData = d3.line()
  .x(d => xScale(d.date))
  .y(d => yScale(d.tank))
  (data)

let trendPathData = d3.line()
  .x(d => xScale(d.date))
  .y(d => yScale(d.tank))
  ([data[0], data.at(-1)])

let xAxis = d3.axisBottom()
  .scale(xScale)
  .ticks(d3.timeThursday)
  .tickFormat(d3.timeFormat("%b %d"))
let yAxis = d3
  .axisLeft()
  .scale(yScale)
</script>

<h1>Russian Tank Losses</h1>
<svg>
  <g class="graph"><path d={pathData}/></g>
  <g class="trendline"><path d={trendPathData}/></g>
  <g class="x-axis"><Axis axis={xAxis}/></g>
  <g class="y-axis"><Axis axis={yAxis}/></g>
</svg>

<style>
svg {
  height: 600px;
  width: 800px;
}
.graph {
  transform: translate(50px, 20px);
}
.graph path {
  fill: none;
  stroke: red;
  stroke-width: 1.5;
}
.trendline {
  transform: translate(50px, 20px);
}
.trendline path {
  fill: none;
  stroke: red;
  stroke-width: 1.5;
  stroke-dasharray: 3px;
}
.x-axis {
  transform: translate(50px, 520px);
}
.y-axis {
  transform: translate(50px, 20px);
}
</style>
```

### Story so far

[All the code is on GitHub](https://github.com/taw/open-source-adventures).

I deployed this on GitHub Pages, [you can see it here](https://taw.github.io/open-source-adventures/episode-32/).

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