Skip to content

Instantly share code, notes, and snippets.

@postspectacular
Last active August 28, 2023 12:29
Show Gist options
  • Save postspectacular/6a379a2bb8cd46e242163b9c9563522f to your computer and use it in GitHub Desktop.
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
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