Last active
August 28, 2023 12:29
-
-
Save postspectacular/6a379a2bb8cd46e242163b9c9563522f to your computer and use it in GitHub Desktop.
#HowToThing #008 — Multi-plot COVID data visualization via https://thi.ng/csv and https://thi.ng/viz
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { epoch, int, parseCSV, type CSVRecord, type ColumnSpecs } from "@thi.ng/csv"; | |
import { defFormat, months } from "@thi.ng/date"; | |
import { readText, writeText } from "@thi.ng/file-io"; | |
import { serialize } from "@thi.ng/hiccup"; | |
import { svg } from "@thi.ng/hiccup-svg"; | |
import { closedOpen } from "@thi.ng/intervals"; | |
import { split } from "@thi.ng/strings"; | |
import { comp, filter, push, transduce } from "@thi.ng/transducers"; | |
import { | |
barPlot, dataBounds, dataMaxLog, | |
linePlot,linearAxis, logAxis, | |
logTicksMajor, logTicksMinor, plotCartesian, | |
} from "@thi.ng/viz"; | |
// https://github.com/owid/covid-19-data/tree/master/public/data/cases_deaths | |
const CSV_PATH = "tools/20230828-new_cases.csv"; | |
// define semi-open date interval [min..max) | |
// see: https://thi.ng/intervals readme/docs | |
const DATE_RANGE = closedOpen(Date.parse("2020-03-01"), Date.parse("2022-01-01")); | |
// regions for interleaved bar plots | |
const REGIONS = [ | |
{ name: "United Kingdom", alias: "uk", stroke: "#f00" }, | |
{ name: "United States", alias: "us", stroke: "#00f" }, | |
]; | |
const ALL_REGIONS = [...REGIONS, { name: "World", alias: "world", stroke: "#0cf" }]; | |
// parse & filter CSV dataset | |
const data = transduce( | |
// compose transforms | |
comp( | |
// first parse as CSV row | |
// see: https://thi.ng/csv readme/docs | |
parseCSV({ | |
// only keep specified columns | |
all: false, | |
// build CSV specs with column aliases and value transforms | |
cols: ALL_REGIONS.reduce( | |
(specs, { alias, name }) => (specs[name] = { alias, tx: int() }, specs), | |
<ColumnSpecs>{ date: { tx: epoch() } } | |
), | |
}), | |
// then filter by date | |
filter((x) => DATE_RANGE.contains(x.date)) | |
), | |
// collect as array | |
push<CSVRecord>(), | |
// CSV data as line iterator (faster than String.split) | |
// https://docs.thi.ng/umbrella/strings/functions/split.html | |
split(readText(CSV_PATH)) | |
); | |
const width = data.length * REGIONS.length; | |
writeText( | |
"covid-new-cases.svg", | |
// serialize visualization (originally in thi.ng/hiccup format) | |
// to proper SVG syntax | |
serialize( | |
svg( | |
{ | |
width: width + 110 + 30, height: 560, | |
"font-size": "10px", | |
__convert: true, | |
}, | |
// create plots in a cartesian coordinate system | |
plotCartesian({ | |
// linear X-axis config | |
xaxis: linearAxis({ | |
// source value range | |
domain: dataBounds((x) => x.date, data), | |
// target value range (coordinates) | |
range: [110, 110 + width], | |
// axis position (here the *Y* coordinate) | |
pos: 500, | |
// define custom date formatter (e.g. "Dec 2021") | |
format: defFormat(["MMM", " ", "yyyy"]), | |
// label config | |
labelOffset: [0, 20], | |
labelAttribs: { "text-anchor": "middle" }, | |
// only require major ticks | |
// https://docs.thi.ng/umbrella/date/functions/months.html | |
major: { ticks: months }, | |
}), | |
// logarithmic Y-axis (same kind of settings as for X) | |
yaxis: logAxis({ | |
// round log target range to powers of 10 (base configurable) | |
domain: [1, dataMaxLog((x) => x.world, data)], | |
range: [500, 20], | |
pos: 100, | |
format: (x) => x.toLocaleString(), | |
labelOffset: [-15, 3], | |
labelAttribs: { "text-anchor": "end" }, | |
major: { ticks: logTicksMajor() }, | |
minor: { ticks: logTicksMinor() }, | |
}), | |
// dynamically generate plot specs to include | |
plots: [ | |
// world totals as line plot | |
linePlot(data.map((x) => [x.date, x.world]), { attribs: { stroke: "#0cf" } }), | |
// regions as interleaved bar plots | |
...REGIONS.map(({ alias, stroke }, i) => | |
barPlot( | |
data.map((x) => [x.date, x[alias]]), | |
{ | |
attribs: { stroke }, | |
interleave: REGIONS.length, | |
offset: i, | |
width: 1, | |
} | |
) | |
), | |
], | |
// disable background grid lines for minor X-ticks | |
grid: { xminor: false }, | |
}) | |
) | |
) | |
); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment