Calendar Heatmap

A calendar heatmap (GitHub contribution graph style) displays daily data values in a grid of week columns × 7 day rows. Multiple years or arbitrary date ranges can be stacked vertically. Cell color encodes the aggregated value for that day.

Basic usage — contribution graph

#![allow(unused)]
fn main() {
use kuva::plot::calendar::{CalendarPlot, CalendarAgg, WeekStart};
use kuva::render::{plots::Plot, layout::Layout, render::render_calendar};
use kuva::backend::svg::SvgBackend;

let data: &[(&str, f64)] = &[
    ("2025-04-16",  1.0), ("2025-04-17",  1.0), ("2025-04-30",  1.0),
    ("2025-05-05",  6.0), ("2025-05-06",  2.0), ("2025-05-07",  2.0),
    ("2025-05-08",  4.0), ("2025-05-09",  3.0), ("2025-05-10",  2.0),
    ("2025-06-10",  1.0), ("2025-07-08",  1.0), ("2025-07-09",  7.0),
    ("2025-07-10",  7.0), ("2025-07-17",  2.0), ("2025-07-23",  1.0),
    ("2025-07-24",  1.0), ("2025-07-25",  2.0), ("2025-07-29",  1.0),
    ("2025-08-01",  1.0), ("2025-08-05",  2.0), ("2025-08-06",  3.0),
    ("2025-08-07",  1.0), ("2025-09-02",  1.0), ("2025-09-08",  2.0),
    ("2025-09-12",  5.0), ("2025-10-02",  1.0), ("2025-10-20",  4.0),
    ("2025-10-21",  1.0), ("2025-10-22",  1.0), ("2025-10-23", 10.0),
    ("2025-10-24",  2.0), ("2025-10-28",  2.0), ("2025-10-29",  2.0),
    ("2025-11-20",  1.0), ("2025-11-27",  4.0), ("2025-12-03",  4.0),
    ("2025-12-08", 30.0), ("2025-12-09",  5.0), ("2026-01-23", 13.0),
    ("2026-01-27",  6.0), ("2026-01-28", 10.0), ("2026-02-06", 21.0),
    ("2026-02-07", 23.0), ("2026-02-09",  7.0), ("2026-02-10", 18.0),
    ("2026-02-12",  4.0), ("2026-02-13", 18.0), ("2026-02-16",  3.0),
    ("2026-02-17",  3.0), ("2026-02-18",  4.0), ("2026-02-19",  1.0),
    ("2026-02-20", 22.0), ("2026-02-21",  9.0), ("2026-02-22", 18.0),
    ("2026-02-23", 13.0), ("2026-02-24",  7.0), ("2026-02-25",  7.0),
    ("2026-02-26", 13.0), ("2026-02-27", 10.0), ("2026-02-28", 24.0),
    ("2026-03-01", 13.0), ("2026-03-02", 14.0), ("2026-03-03", 22.0),
    ("2026-03-04", 13.0), ("2026-03-06",  1.0), ("2026-03-09",  6.0),
    ("2026-03-10", 21.0), ("2026-03-11", 15.0), ("2026-03-12", 15.0),
    ("2026-03-16", 23.0), ("2026-03-20",  9.0), ("2026-03-26", 13.0),
    ("2026-03-30",  5.0), ("2026-03-31", 13.0), ("2026-04-01", 22.0),
    ("2026-04-02",  3.0), ("2026-04-03",  1.0), ("2026-04-08",  1.0),
    ("2026-04-09",  6.0), ("2026-04-13",  3.0),
];

let plot = CalendarPlot::new()
    .with_data(data.iter().map(|&(d, v)| (d, v)))
    .with_aggregation(CalendarAgg::Sum)
    .with_period("Apr 2025 \u{2013} Apr 2026", "2025-04-13", "2026-04-13")
    .with_week_start(WeekStart::Sunday)
    .with_legend_label("contributions");

let layout = Layout::auto_from_plots(&[Plot::Calendar(plot.clone())]);
let svg = SvgBackend.render_scene(&render_calendar(plot, layout));
std::fs::write("calendar.svg", svg).unwrap();
}
GitHub-style contribution calendar for Apr 2025 – Apr 2026

Numeric data — full year with varied values

#![allow(unused)]
fn main() {
use kuva::plot::calendar::{CalendarPlot, CalendarAgg};
use kuva::render::{plots::Plot, layout::Layout, render::render_calendar};
use kuva::backend::svg::SvgBackend;

// Generate a full year of data, skipping ~20% of days
let mut data = Vec::new();
let days_per_month = [31u32, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
for (mi, &days) in days_per_month.iter().enumerate() {
    let m = mi as u32 + 1;
    for d in 1..=days {
        if (m + d) % 5 == 0 { continue; }
        let val = ((m * 7 + d * 3) % 15 + 1) as f64;
        data.push((format!("2024-{m:02}-{d:02}"), val));
    }
}

let plot = CalendarPlot::new()
    .with_data(data)
    .with_aggregation(CalendarAgg::Sum)
    .with_year(2024)
    .with_legend_label("activity");

let layout = Layout::auto_from_plots(&[Plot::Calendar(plot.clone())]);
let svg = SvgBackend.render_scene(&render_calendar(plot, layout));
std::fs::write("calendar.svg", svg).unwrap();
}
Full-year calendar heatmap with varied activity values

Multiple years

#![allow(unused)]
fn main() {
use kuva::plot::calendar::CalendarPlot;

let plot = CalendarPlot::new()
    .with_data(data)          // data spans 2023–2024
    .with_years([2023, 2024]) // one row per year; auto-detected if omitted
    .with_legend_label("downloads");
}

If neither with_year nor with_years is called, years are auto-detected from the data dates.

Custom date ranges (financial year, rolling window, …)

Single named period

#![allow(unused)]
fn main() {
use kuva::plot::calendar::{CalendarPlot, CalendarAgg};
use kuva::render::{plots::Plot, layout::Layout, render::render_calendar};
use kuva::backend::svg::SvgBackend;

let mut data = Vec::new();
// Q1 Jul–Sep 2023
for m in 7u32..=9  { for d in [5, 12, 19, 26] { data.push((format!("2023-{m:02}-{d:02}"), (m * d) as f64 % 8.0 + 1.0)); } }
// Q2 Oct–Dec 2023
for m in 10u32..=12 { for d in [3, 10, 17, 24] { data.push((format!("2023-{m:02}-{d:02}"), (m + d) as f64 % 6.0 + 2.0)); } }
// Q3 Jan–Mar 2024
for m in 1u32..=3  { for d in [8, 15, 22, 29] { data.push((format!("2024-{m:02}-{d:02}"), (m * d) as f64 % 9.0 + 1.0)); } }
// Q4 Apr–Jun 2024
for m in 4u32..=6  { for d in [1u32, 8, 15, 22, 29] { if d <= 30 { data.push((format!("2024-{m:02}-{d:02}"), (m + d) as f64 % 7.0 + 1.0)); } } }

let plot = CalendarPlot::new()
    .with_data(data)
    .with_aggregation(CalendarAgg::Sum)
    .with_period("FY2023/24", "2023-07-01", "2024-06-30")
    .with_legend_label("contributions");

let layout = Layout::auto_from_plots(&[Plot::Calendar(plot.clone())]);
let svg = SvgBackend.render_scene(&render_calendar(plot, layout));
std::fs::write("calendar.svg", svg).unwrap();
}
Australian financial year calendar Jul 2023 – Jun 2024

Multiple named periods

#![allow(unused)]
fn main() {
use kuva::plot::calendar::{CalendarPlot, CalendarAgg};
use kuva::render::{plots::Plot, layout::Layout, render::render_calendar};
use kuva::backend::svg::SvgBackend;

fn fy_data(cal_year: i32, next_cal_year: i32) -> Vec<(String, f64)> {
    let mut v = Vec::new();
    for m in 7u32..=12 {
        for d in (1u32..=28).step_by(4) {
            v.push((format!("{cal_year}-{m:02}-{d:02}"), (m + d) as f64 % 7.0 + 1.0));
        }
    }
    for m in 1u32..=6 {
        for d in (1u32..=28).step_by(4) {
            v.push((format!("{next_cal_year}-{m:02}-{d:02}"), (m * d) as f64 % 8.0 + 1.0));
        }
    }
    v
}

let mut data = fy_data(2022, 2023);
data.extend(fy_data(2023, 2024));

let plot = CalendarPlot::new()
    .with_data(data)
    .with_aggregation(CalendarAgg::Sum)
    .with_periods([
        ("FY2022/23", "2022-07-01", "2023-06-30"),
        ("FY2023/24", "2023-07-01", "2024-06-30"),
    ])
    .with_legend_label("commits");

let layout = Layout::auto_from_plots(&[Plot::Calendar(plot.clone())]);
let svg = SvgBackend.render_scene(&render_calendar(plot, layout));
std::fs::write("calendar.svg", svg).unwrap();
}
Two consecutive Australian financial years stacked

A period can also span more than a year — each period becomes one calendar row.

Aggregation modes

VariantBehaviour
Count (default)Number of data points on each day
SumSum of all values for each day
MeanAverage of all values for each day
MaxMaximum value for each day
#![allow(unused)]
fn main() {
use kuva::plot::calendar::{CalendarPlot, CalendarAgg};

let plot = CalendarPlot::new()
    .with_aggregation(CalendarAgg::Mean);
}

Week start

#![allow(unused)]
fn main() {
use kuva::plot::calendar::{CalendarPlot, WeekStart};

// GitHub-style: Sunday at the top
let plot = CalendarPlot::new()
    .with_week_start(WeekStart::Sunday);

// ISO default: Monday at the top
let plot = CalendarPlot::new()
    .with_week_start(WeekStart::Monday);
}

Color customization

Changing the color map

The default colormap is a light-green → dark-green gradient with sqrt-gamma that mimics GitHub's contribution graph. Any ColorMap variant can be used instead:

#![allow(unused)]
fn main() {
use kuva::plot::calendar::CalendarPlot;
use kuva::plot::ColorMap;

// Viridis
let plot = CalendarPlot::new()
    .with_color_map(ColorMap::Viridis);

// YellowOrangeRed (ColorBrewer)
let plot = CalendarPlot::new()
    .with_color_map(ColorMap::YellowOrangeRed);
}

Custom color function

#![allow(unused)]
fn main() {
use std::sync::Arc;
use kuva::plot::calendar::CalendarPlot;
use kuva::plot::ColorMap;

let plot = CalendarPlot::new()
    .with_color_map(ColorMap::Custom(Arc::new(|t: f64| {
        // Ice-blue to red heat map
        let r = (t * 220.0) as u8;
        let b = ((1.0 - t) * 220.0) as u8;
        format!("rgb({r},30,{b})")
    })));
}

Missing-day and zero-value colors

#![allow(unused)]
fn main() {
use kuva::plot::calendar::CalendarPlot;

let plot = CalendarPlot::new()
    .with_missing_color("#f0f0f0")   // days absent from the dataset
    .with_zero_color("#e8e8e8");     // days present with value == 0
                                     // (falls back to missing_color if unset)
}

Explicit color scale range

#![allow(unused)]
fn main() {
use kuva::plot::calendar::CalendarPlot;

let plot = CalendarPlot::new()
    .with_value_range(0.0, 100.0);  // clamp scale regardless of data max
}

Builder reference

MethodDefaultDescription
with_data(iter)Add (date, value) pairs; date format "YYYY-MM-DD"
with_events(iter)Add bare date strings; each occurrence counts as 1.0
with_aggregation(agg)CountCalendarAgg::Count/Sum/Mean/Max
with_year(y)autoDisplay a single full calendar year
with_years(iter)autoDisplay multiple full calendar years, one row each
with_period(label, start, end)Display a single named date range
with_periods(iter)Display multiple named date ranges
with_date_range(start, end)Unnamed single date range (label from start year)
with_week_start(ws)MondayWeekStart::Monday (ISO) or Sunday (GitHub)
with_color_map(cmap)GitHub green[ColorMap] variant for the value → color mapping
with_missing_color(color)"#ebedf0"CSS color for days absent from the dataset
with_zero_color(color)NoneCSS color for days with value exactly 0; falls back to missing_color
with_value_range(min, max)autoExplicit color scale endpoints
with_month_labels(bool)trueShow abbreviated month names above the grid
with_day_labels(bool)trueShow Mon/Wed/Fri labels on the left
with_cell_size(px)13.0Size of each day cell in pixels
with_cell_gap(px)2.0Gap between cells in pixels
with_legend(bool)trueShow the colorbar legend
with_legend_label(label)NoneLabel beneath the colorbar