DEV Community

Tomasz Wegrzanowski
Tomasz Wegrzanowski

Posted on

2 2

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.

SurveyJS custom survey software

Build Your Own Forms without Manual Coding

SurveyJS UI libraries let you build a JSON-based form management system that integrates with any backend, giving you full control over your data with no user limits. Includes support for custom question types, skip logic, an integrated CSS editor, PDF export, real-time analytics, and more.

Learn more

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more