Open Source Adventures: Episode 34: Making Last Russian Tank Predictor mobile friendly
Tags: javascript, d3, svelte
Published: 2022-04-07 18:56:41

There are two big issues with the app we have so far:

* it just plain doesn't work in Safari, due to its lack of `Array.prototype.at`
* on small mobile screens the graph overflows the screen slightly

The most common way to develop web apps is to make them work in Desktop Chrome first (Desktop Firefox would work too), as you're coding on a computer already, not a phone, and Chrome has the most complete set of functionality, and best developer tooling.

Chrome and Chrome-like browsers are so dominant these days, and Firefox is largely compatible with them, that this works quite well, and most web sites will just work everywhere.

And the last thing is splitting current `Graph` component into `TankLosses` parent which does the calculations, and `Graph` which only does display.

### Safari

Safari is always a few years behind times. There are complex polyfill solutions, but in this case it's just a single line of code we need to fix. Instead of this:

```javascript
adjustedData.at(-1)
```

We can do this:

```javascript
adjustedData[adjustedData.length - 1]
```

Definitely uglier, but it's just one tiny thing, not worth setting up whole polyfill system over.

### Small screen support

It's a lot easier to do graphs if you know size of the target, than support any size. That works on desktop, but not so well on mobile.

Fortunately SVG has a nice trick, and it supports having separate external size (which will be device dependent) and internal size (which is constant and we'll use to put everything in the right places).

We can declare internal size with `<svg viewBox="0 0 800 600">`, then external size with:

```css
svg {
  width: 800px;
  max-width: 100vw;
  display: block;
}
```

### `src/TankLosses.svelte`

Here's the parent `TankLosses` component, responsible for the whole app except for async loading of data, as that's usually best kept outside:

```html
<script>
import * as d3 from "d3"
import Form from "./Form.svelte"
import Graph from "./Graph.svelte"

export let data

let adjust = (data, adjustmentLoss) => data.map(({date, tank}) => ({date, tank: Math.round(tank * (1 + adjustmentLoss/100))}))

// put some dummy data to avoid issues with initialization order
let adjustmentLoss = 0, futureIntensity = 100, totalTanks = 0

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

$: adjustedData = adjust(data, adjustmentLoss)
$: alreadyDestroyedTanks = d3.max(adjustedData, d => d.tank)
$: tanksMax = Math.max(alreadyDestroyedTanks, totalTanks)

$: currentTankRate = alreadyDestroyedTanks / (maxDate - minDate)
$: futureTankRate = (currentTankRate * futureIntensity / 100.0)
$: tanksTodo = totalTanks - alreadyDestroyedTanks
$: lastTankDate = new Date(+maxDate + (tanksTodo / futureTankRate))

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

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

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

$: trendPathData = d3.line()
  .x(d => xScale(d.date))
  .y(d => yScale(d.tank))
  ([adjustedData[0], adjustedData[adjustedData.length - 1], {tank: totalTanks, date: lastTankDate}])

$: tankTotalPathData = d3.line()
  .x(xScale)
  .y(yScale(tanksMax))
  ([minDate, lastTankDate])

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

$: yAxis = d3
  .axisLeft()
  .scale(yScale)
</script>

<h1>Russian Tank Losses</h1>
<Graph {pathData} {trendPathData} {tankTotalPathData} {xAxis} {yAxis}/>
<Form bind:adjustmentLoss bind:futureIntensity bind:totalTanks />
<div>Russia will lose its last tank on {d3.timeFormat("%e %b %Y")(lastTankDate)}</div>
```

### `src/Graph.svelte`

And here's the `Graph` component, it's only responsible for displaying data that's already calculated:

```html
<script>
import Axis from "./Axis.svelte"
export let pathData, trendPathData, tankTotalPathData, xAxis, yAxis
</script>

<svg viewBox="0 0 800 600">
  <g class="graph">
    <path class="data" d={pathData}/>
    <path class="trendline" d={trendPathData}/>
    <path class="tanktotal" d={tankTotalPathData}/>
  </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.tanktotal {
  stroke: blue;
  stroke-width: 1.5;
}
.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-34/).

### Coming next

In the next episode, I'll try to do a few more things with the app.