DEV Community

Tomasz Wegrzanowski
Tomasz Wegrzanowski

Posted on

Open Source Adventures: Episode 42: Projecting Russian Personnel Losses

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.

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

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):

export let kiaRegular = writable(80)
export let wiaToKia = writable(250)
Enter fullscreen mode Exit fullscreen mode

SoldierLosses.svelte

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

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

SoldierForm.svelte

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

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

SoldierLossesGraph.svelte

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

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:

<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>
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 few episodes I'll take a break from the war, and check out some other technologies.

Top comments (0)