DEV Community

Tomasz Wegrzanowski
Tomasz Wegrzanowski

Posted on

Open Source Adventures: Episode 32: Better Russian Tank Losses Graphs with D3 and Svelte

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:

<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>
Enter fullscreen mode Exit fullscreen mode

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:

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

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.

let xAxis = d3.axisBottom()
  .scale(xScale)
  .ticks(d3.timeThursday)
  .tickFormat(d3.timeFormat("%b %d"))
Enter fullscreen mode Exit fullscreen mode

Trend line

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

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

src/Graph.svelte

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

<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>
Enter fullscreen mode Exit fullscreen mode

Story so far

All the code is on GitHub.

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)