diff options
| author | Jesse Morgan <jesse@jesterpm.net> | 2026-01-25 17:28:28 -0800 |
|---|---|---|
| committer | Jesse Morgan <jesse@jesterpm.net> | 2026-01-25 17:28:28 -0800 |
| commit | 5cf9a5e311d163b30a6cf22f5cc980a09c34e2a0 (patch) | |
| tree | a83101e9e52469d1eff321ec4378994f98fe2fea /src/bin | |
Diffstat (limited to 'src/bin')
| -rw-r--r-- | src/bin/tc-calmap.rs | 110 |
1 files changed, 110 insertions, 0 deletions
diff --git a/src/bin/tc-calmap.rs b/src/bin/tc-calmap.rs new file mode 100644 index 0000000..566815a --- /dev/null +++ b/src/bin/tc-calmap.rs @@ -0,0 +1,110 @@ +use std::io::{Write, stderr, stdin, stdout}; +use std::process::exit; + +use chrono::{Datelike, NaiveDate}; +use clap::{Parser, ValueEnum}; + +use timechart::heatmap::{CalendarProjection, Heatmap, IsoProjection, Projection}; +use timechart::{Data, Result}; + +/// Render a calendar-like heatmap from a list of dates and values. +/// +/// Values are read from stdin. Output is written to stdout. +#[derive(Parser, Debug)] +struct Args { + /// The size of each square in the heat map (in px) + #[arg(long, default_value = "10")] + size: u32, + + /// The padding between the squares (in px) + #[arg(long, default_value = "2")] + padding: u32, + + /// The color scale + #[arg( + long, + default_values = ["#e7ebef","#deebf7","#c6dbef","#9ecae1","#6baed6","#2171b5"] + )] + colors: Vec<String>, + + /// The unit description to use in the captions + #[arg(long)] + unit: Option<String>, + + /// Use a log scale instead of a linear scale + #[arg(long, default_value = "true")] + log_scale: bool, + + #[arg( + long("mode"), + value_enum, + default_value_t = Mode::Calendar + )] + mode: Mode, +} + +#[derive(Debug, Clone, ValueEnum)] +enum Mode { + Calendar, + Iso, +} + +fn main() { + match run() { + Ok(_) => {} + Err(e) => { + let _ = writeln!(stderr(), "Error: {e}"); + exit(1); + } + } +} + +fn run() -> Result<()> { + let args = Args::parse(); + + // Read data from stdin. + let mut data = Data::new(); + for line in stdin().lines() { + if let Some((ts, value)) = line?.split_once(' ') { + let date: NaiveDate = ts.parse()?; + let value: f64 = value.parse()?; + data.entry(date) + .and_modify(|v| *v += value) + .or_insert(value); + } + } + + // Find the range of dates that we will render. + let (start, end) = data + .keys() + .copied() + .fold((NaiveDate::MAX, NaiveDate::MIN), |acc, v| { + let min = if acc.0 < v { + acc.0 + } else { + NaiveDate::from_ymd_opt(v.year(), 1, 1).unwrap() + }; + let max = if acc.1 > v { + acc.1 + } else { + NaiveDate::from_ymd_opt(v.year(), 12, 31).unwrap() + }; + (min, max) + }); + + // Choose a drawing style. + let projection: Box<dyn Projection> = match args.mode { + Mode::Calendar => Box::new(CalendarProjection::new(start, end, args.size, args.padding)), + Mode::Iso => Box::new(IsoProjection::new(start, end, args.size, args.padding)), + }; + + // Render the heatmap. + let heatmap = Heatmap { + projection, + colors: args.colors, + unit: args.unit, + log_scale: args.log_scale, + }; + + heatmap.render(&data, stdout()) +} |
