Open Source Adventures: Episode 42: Projecting Russian Personnel Losses
Tags: javascript, svelte, d3
Published: 2022-04-18 19:57:11

The Russian Losses App needs one more thing - projecting personnel losses.

This will work differently from the other kinds, as they cannot realistically "run out" of personnel, they can train more even if there's millions dead.

### `App.svelte`

We need to modify `App.svelte` to load data from two CSVs and merge them. JavaScript doesn't have `Array.prototype.zip` yet, so we do an old style loop.

```html
<script>
import * as d3 from "d3"
import TankLosses from "./TankLosses.svelte"
import ArmoredLosses from "./ArmoredLosses.svelte"
import ArtilleryLosses from "./ArtilleryLosses.svelte"
import SoldierLosses from "./SoldierLosses.svelte"
import { dataDays } from "./stores"

let parseRow = (row1, row2) => ({
  date: new Date(row1.date),
  tank: +row1.tank,
  apc: +row1.APC,
  art: +row1["field artillery"] + +row1["MRL"],
  kia: +row2.personnel,
})

let loadData = async () => {
  let data1 = await d3.csv("./russia_losses_equipment.csv")
  let data2 = await d3.csv("./russia_losses_personnel.csv")
  let data = [{date: new Date("2022-02-24"), tank: 0, apc: 0, art: 0, kia: 0}]
  for(let i = 0; i < data1.length; i++) {
    data.push(parseRow(data1[i], data2[i]))
  }
  $dataDays = data.length - 1
  return data
}

let dataPromise = loadData()
</script>

{#await dataPromise then data}
  <TankLosses {data} />
  <ArmoredLosses {data} />
  <ArtilleryLosses {data} />
  <SoldierLosses {data} />
{/await}

<style>
:global(body) {
  margin: 0;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}
</style>
```

### `stores.js`

We only need to add these two, representing % of KIA that are regular soldiers (so not "separatist" militias, PMCs, Syrians, Rosgvardia), and KIA to WIA ratio (3x historically, but there are reasons to believe it's lower for Russians here):

```javascript
export let kiaRegular = writable(80)
export let wiaToKia = writable(250)
```

### `SoldierLosses.svelte`

It doesn't do much, just slices the right data and includes two child components.

```html
<script>
import SoldierForm from "./SoldierForm.svelte"
import SoldierLossesGraph from "./SoldierLossesGraph.svelte"

export let data

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

<h1>Russian Soldiers Losses</h1>
<SoldierLossesGraph {lossData} />
<SoldierForm />
```

### `SoldierForm.svelte`

There are just two additional controls, WIA to KIA ratio with a new format function, so `250` is displayed like `2.5x`:

```html
<script>
import Slider from "./Slider.svelte"
import CommonSliders from "./CommonSliders.svelte"
import { kiaRegular, wiaToKia } from "./stores"
</script>

<form>
  <CommonSliders />
  <Slider label="Percentage of KIA that are regular troops" min={50} max={100} value={$kiaRegular} format={(v) => `${v}%`} />
  <Slider label="Wounded to Killed Ratio" min={100} max={1000} value={$wiaToKia} format={(v) => `${v/100.0}x`} />
</form>

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

### `SoldierLossesGraph.svelte`

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

export let lossData

let adjustRow = ({date, unit}, totalLossAdjustment, wiaToKia) => {
  let kia = Math.round(unit * totalLossAdjustment)
  let wia = Math.round(kia * wiaToKia / 100)
  let total = kia + wia
  return {date, kia, wia, total}
}
let adjust = (data, totalLossAdjustment, wiaToKia) => data.map(row => adjustRow(row, totalLossAdjustment, wiaToKia))

let at = (array, idx) => ((idx < 0) ? array[array.length + idx] : array[idx])

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

$: adjustedData = adjust(lossData, ($kiaRegular/100) * (1 + $lossAdjustment / 100.0), $wiaToKia)
$: totalSoFar = d3.max(adjustedData, d => d.total)

$: timeInProjection = at(adjustedData, -$projectionBasis-1).date - at(adjustedData, -1).date
$: kiaInProjection = at(adjustedData, -$projectionBasis-1).kia - at(adjustedData, -1).kia
$: wiaInProjection = at(adjustedData, -$projectionBasis-1).wia - at(adjustedData, -1).wia
$: currentKiaRate = kiaInProjection / timeInProjection
$: currentWiaRate = wiaInProjection / timeInProjection

$: futureKiaRate = (currentKiaRate * $futureIntensity / 100.0)
$: futureWiaRate = (currentWiaRate * $futureIntensity / 100.0)
$: futureTotalRate = futureKiaRate + futureWiaRate

// Just hardcode as there's no obvious "finish date"
$: lastDate = new Date("2023-01-01")

// How many KIA+WIA by lastDate
$: unitsMax = Math.round((lastDate - maxDate) * futureTotalRate) + totalSoFar

$: trendData = [
  adjustedData[0],
  at(adjustedData, -1),
  {
    date: lastDate,
    kia: Math.round((lastDate - maxDate) * futureKiaRate) + d3.max(adjustedData, d => d.kia),
    wia: Math.round((lastDate - maxDate) * futureWiaRate) + d3.max(adjustedData, d => d.wia),
    total: Math.round((lastDate - maxDate) * futureTotalRate) + d3.max(adjustedData, d => d.total),
  },
]

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

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

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

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

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

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

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

$: kiaTrendData = d3.line()
  .x(d => xScale(d.date))
  .y(d => yScale(d.kia))
  (trendData)

$: wiaTrendData = d3.line()
  .x(d => xScale(d.date))
  .y(d => yScale(d.wia))
  (trendData)

$: totalTrendData = d3.line()
  .x(d => xScale(d.date))
  .y(d => yScale(d.total))
  (trendData)
</script>

<SoldierGraph {xAxis} {yAxis} {kiaData} {wiaData} {totalData} {kiaTrendData} {wiaTrendData} {totalTrendData} />
```

This component does a lot of repeated calculations, and perhaps I should refactor them somehow.

### `SoldierGraph.svelte`

And the final new component, just to display what we have:

```html
<script>
import Axis from "./Axis.svelte"
export let xAxis, yAxis, kiaData, wiaData, totalData, kiaTrendData, wiaTrendData, totalTrendData
</script>

<svg viewBox="0 0 800 600">
  <g class="graph">
    <path class="kia" d={kiaData} />
    <path class="wia" d={wiaData} />
    <path class="total" d={totalData} />
    <path class="kia trendline" d={kiaTrendData} />
    <path class="wia trendline" d={wiaTrendData} />
    <path class="total trendline" d={totalTrendData} />
  </g>
  <g class="x-axis"><Axis axis={xAxis}/></g>
  <g class="y-axis"><Axis axis={yAxis}/></g>
</svg>

<style>
svg {
  width: 800px;
  max-width: 100vw;
  display: block;
}
.graph {
  transform: translate(50px, 20px);
}
path {
  fill: none;
  stroke-width: 1.5;
}
path.kia {
  stroke: red;
}
path.wia {
  stroke: green;
}
path.total {
  stroke: blue;
}
path.trendline {
  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-42/).

### Coming next

In the next few episodes I'll take a break from the war, and check out some other technologies.