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())
}
|