aboutsummaryrefslogtreecommitdiff
path: root/src/bin/tc-calmap.rs
blob: 566815a825f5f59e5748a4a316f336880a47f122 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
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())
}