Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
April 10, 2022 04:49 pm GMT

Open Source Adventures: Episode 36: Using D3 to figure out when Russia will lose its last armored vehicle

We can extend the tank losses app to armored vehicles and artillery.

This could be done with just copy and paste, but I wanted to refactor the app a bit, to reduce such repetitive elements.

src/App.svelte

Due to the way equipment is categorized, I'm merging regular artillery with MRL.

<script>import * as d3 from "d3"import TankLosses from "./TankLosses.svelte"import ArmoredLosses from "./ArmoredLosses.svelte"import ArtilleryLosses from "./ArtilleryLosses.svelte"let parseRow = (row) => ({  date: new Date(row.date),  tank: +row.tank,  apc: +row.APC,  art: +row["field artillery"] + +row["MRL"],})let loadData = async () => {  let url = "./russia_losses_equipment.csv"  let data = await d3.csv(url, parseRow)  data.unshift({date: new Date("2022-02-24"), tank: 0, apc: 0, art: 0})  return data}let dataPromise = loadData()</script>{#await dataPromise then data}  <TankLosses {data} />  <ArmoredLosses {data} />  <ArtilleryLosses {data} />{/await}<style>:global(body) {  margin: 0;  min-height: 100vh;  display: flex;  flex-direction: column;  justify-content: center;  align-items: center;}</style>

src/TankLosses.svelte

I'll only show the tank side, as the other two are too similar. except there's no dedicated artillery storage.

<script>import TankForm from "./TankForm.svelte"import LossesGraph from "./LossesGraph.svelte"export let datalet lossData = data.map(({date, tank}) => ({date, unit: tank}))// put some dummy data to avoid issues with initialization orderlet adjustmentLoss = 0, futureIntensity = 100, total = 0</script><h1>Russian Tank Losses</h1><LossesGraph {lossData} {adjustmentLoss} {futureIntensity} {total} label="tank" /><TankForm bind:adjustmentLoss bind:futureIntensity bind:total />

src/TankForm.svelte

I moved the slider logic to Slider component. They're formatted differently (10, 10%, +10%), so we're passing format function to the component.

<script>import * as d3 from "d3"import Slider from "./Slider.svelte"export let adjustmentLoss = 0export let futureIntensity = 100let active = 3417let storage = 10200let storageGood = 10export let total$: total = Math.round(active + storage * storageGood / 100.0)</script><form>  <Slider label="Adjustment for losses data" min={-30} max={50} bind:value={adjustmentLoss} format={(v) => d3.format("+d")(v) + "%"} />  <Slider label="Predicted future war intensity" min={-50} max={200} bind:value={futureIntensity} format={(v) => `${v}%`} />  <Slider label="Russian tanks at start of war" min={2500} max={3500} bind:value={active} format={(v) => v} />  <Slider label="Russian tanks in storage" min={8000} max={12000} bind:value={storage} 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>{total}</span>  </div></form><style>form {  display: grid;  grid-template-columns: auto auto auto;}form > div {  display: contents;}</style>

src/Slider.svelte

The label for problem doesn't have a good solution. For this I'm just using randomly generated IDs.

<script>export let label, min, max, value, formatlet id = Math.random().toString(36).slice(2)</script><label for={id}>{label}:</label><input type="range" {min} {max} bind:value id={id} /><span>{format(value)}</span>

src/LossesGraph.svelte

The graph is the same for all kinds of losses, so I put all the calculations and display logic here:

<script>import * as d3 from "d3"import Graph from "./Graph.svelte"export let lossData, total, adjustmentLoss, futureIntensity, labellet 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, adjustmentLoss)$: 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"))$: yAxis = d3  .axisLeft()  .scale(yScale)</script><Graph {pathData} {trendPathData} {totalPathData} {xAxis} {yAxis}/><div>Russia will lose its last {label} on {d3.timeFormat("%e %b %Y")(lastDestroyedDate)}</div>

src/Graph.svelte

This component just gets the calculated data and paths and plots them:

<script>import Axis from "./Axis.svelte"export let pathData, trendPathData, totalPathData, xAxis, yAxis</script><svg viewBox="0 0 800 600">  <g class="graph">    <path class="data" d={pathData}/>    <path class="trendline" d={trendPathData}/>    <path class="total" d={totalPathData}/>  </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;}path.data {  stroke: red;  stroke-width: 1.5;}path.trendline {  stroke: red;  stroke-width: 1.5;  stroke-dasharray: 3px;}path.total {  stroke: blue;  stroke-width: 1.5;}.x-axis {  transform: translate(50px, 520px);}.y-axis {  transform: translate(50px, 20px);}</style>

src/Axis.svelte

It's a small wrapper to hand over control over <g> from Svelte to D3.

<script>import * as d3 from "d3"export let axislet axisNode$: {  d3.select(axisNode).selectAll("*").remove()  d3.select(axisNode).call(axis)}</script><g bind:this={axisNode}></g>

Story so far

All the code is on GitHub.

I deployed this on GitHub Pages, you can see it here.

Coming next

That's enough for now. For the next episode we'll try something completely different.


Original Link: https://dev.to/taw/open-source-adventures-episode-36-using-d3-to-figure-out-when-russia-will-lose-its-last-armored-vehicle-31jn

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To