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.
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.
<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
<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
<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
I deployed this on GitHub Pages, you can see it here.
Coming next
In the next episode I'll add some more sophisticated way of predicting future changes.
Top comments (0)