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)
We can do this:
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:
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:
<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:
<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
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)