Open Source Adventures: Episode 40: Svelte Stores for Russian Losses App
Tags: javascript, d3, svelte
Published: 2022-04-18 14:55:35

State should generally live inside components, but it's not always possible without excessive callbacks. For such situations, Svelte has stores.

Right now the app has 3 components (tank losses, armored vehicle losses, and artillery losses), and some of the sliders are shared and really should move elsewhere. And if we're moving them, we might just as well move all the sliders into a single store.

### `stores.js`

Unfortunately or `derived` stores we need to list dependencies explicitly, as this is `.js` file not `.svelte` file, so we get none of the Svelte automatic reactivity.

```javascript
import { writable, derived } from "svelte/store"

export let lossAdjustment = writable(0)
export let futureIntensity = writable(100)

export let activeTanks = writable(3417)
export let activeArmored = writable(18543)
export let activeArt = writable(5919)

export let storageTanks = writable(10200)
export let storageArmored = writable(15500)
export let storageGood = writable(10)

export let totalTanks = derived(
  [activeTanks, storageTanks, storageGood],
  ([$activeTanks, $storageTanks, $storageGood]) => Math.round($activeTanks + $storageTanks * $storageGood / 100.0)
)
export let totalArmored = derived(
  [activeArmored, storageArmored, storageGood],
  ([$activeArmored, $storageArmored, $storageGood]) => Math.round($activeArmored + $storageArmored * $storageGood / 100.0)
)

export let totalArt = activeArt
```

### `TankForm.svelte`

The other two are analogous.

```html
<script>
import * as d3 from "d3"
import Slider from "./Slider.svelte"
import { lossAdjustment, futureIntensity, activeTanks, storageTanks, storageGood, totalTanks } from "./stores"
</script>

<form>
  <Slider label="Adjustment for losses data" min={-30} max={50} bind:value={$lossAdjustment} format={(v) => d3.format("+d")(v) + "%"} />
  <Slider label="Predicted future war intensity" min={10} max={200} bind:value={$futureIntensity} format={(v) => `${v}%`} />
  <Slider label="Russian tanks at start of war" min={2500} max={3500} bind:value={$activeTanks} format={(v) => v} />
  <Slider label="Russian tanks in storage" min={8000} max={12000} bind:value={$storageTanks} format={(v) => v} />
  <Slider label="Usable tanks in storage" min={0} max={100} bind:value={$storageGood} format={(v) => `${v}%`} />

  <div>
    <span>Total usable tanks</span>
    <span></span>
    <span>{$totalTanks}</span>
  </div>
</form>

<style>
form {
  display: grid;
  grid-template-columns: auto auto auto;
}
form > div {
  display: contents;
}
</style>
```

Instead of `export let`ing various variables, we import them all from stores, and refer to them with a `$`. Svelte handles all the callbacks automatically. The other two are analogous.

### `TankLosses.svelte`

```html
<script>
import TankForm from "./TankForm.svelte"
import LossesGraph from "./LossesGraph.svelte"
import { totalTanks } from "./stores"

export let data

let lossData = data.map(({date, tank}) => ({date, unit: tank}))
</script>

<h1>Russian Tank Losses</h1>
<LossesGraph {lossData} total={$totalTanks} label="tank" />
<TankForm />
```

The component is a lot simpler now, the only thing we need to do is specify which total to use (tank, armored, or artillery).

### `LossesGraph.svelte`

```html
<script>
import * as d3 from "d3"
import Graph from "./Graph.svelte"
import { lossAdjustment, futureIntensity } from "./stores"

export let lossData, total, label

let adjust = (data, adjustmentLoss) => data.map(({date, unit}) => ({date, unit: Math.round(unit * (1 + adjustmentLoss/100))}))

let [minDate, maxDate] = d3.extent(lossData, d => d.date)

$: adjustedData = adjust(lossData, $lossAdjustment)
$: alreadyDestroyed = d3.max(adjustedData, d => d.unit)
$: unitsMax = Math.max(alreadyDestroyed, total)

$: currentDestroyRate = alreadyDestroyed / (maxDate - minDate)
$: futureDestroyRate = (currentDestroyRate * $futureIntensity / 100.0)
$: unitsTodo = total - alreadyDestroyed
$: lastDestroyedDate = new Date(+maxDate + (unitsTodo / futureDestroyRate))

$: xScale = d3.scaleTime()
  .domain([minDate, lastDestroyedDate])
  .range([0, 700])

$: yScale = d3.scaleLinear()
  .domain([0, unitsMax])
  .nice()
  .range([500, 0])

$: pathData = d3.line()
  .x(d => xScale(d.date))
  .y(d => yScale(d.unit))
  (adjustedData)

$: trendPathData = d3.line()
  .x(d => xScale(d.date))
  .y(d => yScale(d.unit))
  ([adjustedData[0], adjustedData[adjustedData.length - 1], {unit: total, date: lastDestroyedDate}])

$: totalPathData = d3.line()
  .x(xScale)
  .y(yScale(unitsMax))
  ([minDate, lastDestroyedDate])

$: xAxis = d3.axisBottom()
  .scale(xScale)
  .tickFormat(d3.timeFormat("%e %b %Y"))

$: yAxisL = d3
  .axisLeft()
  .scale(yScale)

$: yScaleR = d3.scaleLinear()
  .domain([0, 100])
  .range([
    yScale(0),
    yScale(unitsMax)
  ])

$: yAxisR =  d3
  .axisRight()
  .scale(yScaleR)
  .tickFormat(n => `${n}%`)
</script>

<Graph {pathData} {trendPathData} {totalPathData} {xAxis} {yAxisL} {yAxisR} />
<div>Russia will lose its last {label} on {d3.timeFormat("%e %b %Y")(lastDestroyedDate)}</div>
```

Arguably some of the code could be migrated to the stores, but it won't hurt to keep it here.


### 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-40/).

### Coming next

In the next episode I'll add some more sophisticated way of predicting future changes.