DEV Community

Tomasz Wegrzanowski
Tomasz Wegrzanowski

Posted on

 

Open Source Adventures: Episode 34: Making Last Russian Tank Predictor mobile friendly

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:

adjustedData.at(-1)
Enter fullscreen mode Exit fullscreen mode

We can do this:

adjustedData[adjustedData.length - 1]
Enter fullscreen mode Exit fullscreen mode

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:

svg {
  width: 800px;
  max-width: 100vw;
  display: block;
}
Enter fullscreen mode Exit fullscreen mode

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:

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

src/Graph.svelte

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

<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>
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 episode, I'll try to do a few more things with the app.

Top comments (0)

The JavaScript Brief

1. Top 5 MERN STACK projects to improve your practical understanding

Boost your MERN Stack development skills by undertaking interesting beginner projects. These five engaging projects cover web applications and range from social media website applications to geo-social networking maps. Hone your understanding and apply modern techniques backed up by hands-on experience.

2. How To Optimize Your React App’s Performance

Learn the best optimizing techniques to make your React applications faster and more efficient. Focusing on the identification of performance bottlenecks and common pitfalls to avoid, these optimization strategies will keep your applications running smoothly even when faced with growing complexity.

3. A story of let, const, object mutation, and a bug in my code

In the pursuit of bug-free code, explore an incident involving a mix-up between const and let, making sure your custom code works effectively with third

party documentation. Discover best practices on program flow and learn about JavaScript's unpredictable aspects to ensure your core code is robust.