From 5cf9a5e311d163b30a6cf22f5cc980a09c34e2a0 Mon Sep 17 00:00:00 2001 From: Jesse Morgan Date: Sun, 25 Jan 2026 17:28:28 -0800 Subject: Initial commit --- .gitignore | 2 + Cargo.lock | 420 +++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 11 ++ LICENSE | 19 +++ README.md | 65 ++++++++ src/bin/tc-calmap.rs | 110 ++++++++++++++ src/heatmap.rs | 203 +++++++++++++++++++++++++ src/lib.rs | 14 ++ 8 files changed, 844 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 src/bin/tc-calmap.rs create mode 100644 src/heatmap.rs create mode 100644 src/lib.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..890bfb5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +*.sw[lmnop] diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..ba301c3 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,420 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[package]] +name = "cc" +version = "1.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.5.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "find-msvc-tools" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "timechart" +version = "0.1.0" +dependencies = [ + "chrono", + "clap", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ee08ffe --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "timechart" +version = "0.1.0" +description = "Tools to visualize time series data" +repository = "https://git.jesterpm.net/pub/jesterpm/timechart.git" +license = "MIT" +edition = "2021" + +[dependencies] +chrono = "0.4" +clap = { version = "4.4.14", features = ["derive"] } diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..78e4f84 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright © 2026 Jesse Morgan + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the “Software”), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e93a31d --- /dev/null +++ b/README.md @@ -0,0 +1,65 @@ +timechart +========= + +A collection of tools to visualize time series data. Each tool takes pairs +of dates and values in stdin and produces an SVG on stdout. + +Example input: + + 2026-01-05 4.7 + 2026-01-07 6.5 + 2026-01-09 7.5 + 2026-01-12 10.0 + 2026-01-14 6.8 + 2026-01-16 6.0 + 2026-01-20 7.7 + 2026-01-22 6.4 + +tc-calmap +--------- + +The calmap tool produces a calendar-like heatmap, where each square +represents a date and the color represents the value. + +There are two different rendering modes: calendar and iso. The calendar +mode produces a graphic that looks like a normal month-by-month calendar. +The iso mode produces a denser grid where each row is the day of the week +and each column is an ISO week. + + + Usage: tc-calmap [OPTIONS] + + Options: + --size + The size of each square in the heat map (in px) + + [default: 10] + + --padding + The padding between the squares (in px) + + [default: 2] + + --colors + The color scale + + [default: #e7ebef #deebf7 #c6dbef #9ecae1 #6baed6 #2171b5] + + --unit + The unit description to use in the captions + + --log-scale + Use a log scale instead of a linear scale + + --mode + [default: calendar] + [possible values: calendar, iso] + + -h, --help + Print help (see a summary with '-h') + + +Contributing +------------ + +Send questions, bug reports, and patches to jesse@jesterpm.net. 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, + + /// The unit description to use in the captions + #[arg(long)] + unit: Option, + + /// 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 = 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()) +} diff --git a/src/heatmap.rs b/src/heatmap.rs new file mode 100644 index 0000000..dd3ac3b --- /dev/null +++ b/src/heatmap.rs @@ -0,0 +1,203 @@ +use std::io::Write; + +use chrono::{Datelike, NaiveDate}; + +use crate::{Data, Result}; + +/// Renders an SVG heatmap. +pub struct Heatmap { + pub projection: Box, + pub colors: Vec, + pub unit: Option, + pub log_scale: bool, +} + +impl Heatmap { + pub fn render(&self, data: &Data, mut io: T) -> Result<()> { + // No data, no image. + if data.is_empty() { + writeln!( + io, + r#""# + )?; + return Ok(()); + } + + let (y0, y1) = data.values().copied().fold((f64::MAX, f64::MIN), |acc, v| { + let min = if acc.0 < v { acc.0 } else { v }; + let max = if acc.1 > v { acc.1 } else { v }; + (min, max) + }); + + // The color scale range. + let y_range = if self.log_scale { + y1.log10() - y0.log10() + } else { + y1 - y0 + }; + + let (width, height) = self.projection.dimensions(); + write!( + io, + r#""# + )?; + + let (start, end) = self.projection.range(); + for date in start.iter_days() { + if date > end { + break; + } + + let (x, y) = self.projection.map(date); + let value = data.get(&date).copied().unwrap_or(0.0); + + // Scale + let scaled_value = if self.log_scale { + (value.signum() * (1.0 + value.abs()).log10()) + / (y1.signum() * (1.0 + y1.abs()).log10()) + } else { + (value + y0) / y_range + }; + + let color = &self.colors[(scaled_value * (self.colors.len() - 1) as f64) as usize]; + + writeln!( + io, + r#""#, + self.projection.size() + )?; + if let Some(ref unit) = self.unit { + writeln!(io, "{value} {unit} on {date}")?; + } else { + writeln!(io, "{value} {date}")?; + } + writeln!(io, "")?; + } + writeln!(io, "")?; + + Ok(()) + } +} + +/// Projection instances determine where each date goes in the image. +pub trait Projection { + /// Transform a date to x,y coordinates. + fn map(&self, date: NaiveDate) -> (u32, u32); + + /// Return the width and height of the image. + fn dimensions(&self) -> (u32, u32); + + /// Return the size of each cell in the image. + fn size(&self) -> u32; + + /// Return the range of dates in the image. + fn range(&self) -> (NaiveDate, NaiveDate); +} + +/// CalendarProjection creates a heatmap that looks like a calendar, with all +/// of the months in a row. +pub struct CalendarProjection { + start: NaiveDate, + end: NaiveDate, + size: u32, + padding: u32, +} + +impl CalendarProjection { + pub fn new(start: NaiveDate, end: NaiveDate, size: u32, padding: u32) -> Self { + Self { + start, + end, + size, + padding, + } + } +} + +impl Projection for CalendarProjection { + fn map(&self, date: NaiveDate) -> (u32, u32) { + let fom = date.with_day(1).unwrap(); + let year_number = (date.year() - self.start.year()) as u32; + let week_number = (fom.weekday().num_days_from_monday() + date.day() - 1) / 7 + 1; + + let x = self.padding + + ((date.month() - self.start.month()) % 12) + * (self.padding + 7 * (self.size + self.padding)) + + date.weekday().num_days_from_monday() * (self.size + self.padding); + + let y = self.padding + + week_number * (self.size + self.padding) + + year_number * 6 * (self.size + self.padding); + + (x, y) + } + + fn dimensions(&self) -> (u32, u32) { + let years = (self.end.year() - self.start.year() + 1) as u32; + let width = self.padding + ((self.size + self.padding) * 8) * 12; + let height = self.padding + (self.size + self.padding) * 6 * years; + (width, height) + } + + fn size(&self) -> u32 { + self.size + } + + fn range(&self) -> (NaiveDate, NaiveDate) { + (self.start, self.end) + } +} + +/// IsoProjection creates a denser heatmap where each column is a week of the +/// ISO calendar year and each row is a day of the week (starting with Monday). +pub struct IsoProjection { + start: NaiveDate, + end: NaiveDate, + size: u32, + padding: u32, +} + +impl IsoProjection { + pub fn new(start: NaiveDate, end: NaiveDate, size: u32, padding: u32) -> Self { + Self { + start, + end, + size, + padding, + } + } +} + +impl Projection for IsoProjection { + fn map(&self, date: NaiveDate) -> (u32, u32) { + let start = self.start.iso_week(); + let iso_date = date.iso_week(); + + let year_number = (iso_date.year() - start.year()) as u32; + + let x = self.padding + iso_date.week0() * (self.size + self.padding); + + let y = self.padding + + date.weekday().num_days_from_monday() * (self.size + self.padding) + + year_number * 8 * (self.size + self.padding); + + (x, y) + } + + fn dimensions(&self) -> (u32, u32) { + let start = self.start.iso_week(); + let end = self.end.iso_week(); + let width = self.padding + (self.size + self.padding) * 53; + let height = + self.padding + (self.size + self.padding) * 8 * (end.year() - start.year() + 1) as u32; + (width, height) + } + + fn size(&self) -> u32 { + self.size + } + + fn range(&self) -> (NaiveDate, NaiveDate) { + (self.start, self.end) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..d1ae1a9 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,14 @@ +use std::collections::BTreeMap; + +use chrono::NaiveDate; + +pub mod heatmap; + +/// The type used for the collection of date/value pairs. +pub type Data = BTreeMap; + +/// The Error type used within the library. +pub type Error = Box; + +/// The Result type used within the library. +pub type Result = std::result::Result; -- cgit v1.2.3