aboutsummaryrefslogtreecommitdiff
path: root/src/bin
diff options
context:
space:
mode:
authorJesse Morgan <jesse@jesterpm.net>2026-01-25 17:28:28 -0800
committerJesse Morgan <jesse@jesterpm.net>2026-01-25 17:28:28 -0800
commit5cf9a5e311d163b30a6cf22f5cc980a09c34e2a0 (patch)
treea83101e9e52469d1eff321ec4378994f98fe2fea /src/bin
Initial commitHEADmaster
Diffstat (limited to 'src/bin')
-rw-r--r--src/bin/tc-calmap.rs110
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())
+}