mod types;
mod view;
pub use types::{CellDisplay, ColorScale};
use crate::style::Color;
use crate::widget::traits::WidgetProps;
use crate::{impl_props_builders, impl_styled_view};
#[derive(Clone, Debug)]
pub struct HeatMap {
pub(crate) data: Vec<Vec<f64>>,
pub(crate) _rows: usize,
pub(crate) cols: usize,
pub(crate) min_val: f64,
pub(crate) max_val: f64,
color_scale: ColorScale,
custom_colors: Option<(Color, Color)>,
cell_display: CellDisplay,
cell_width: usize,
cell_height: usize,
show_values: bool,
value_decimals: usize,
row_labels: Option<Vec<String>>,
col_labels: Option<Vec<String>>,
title: Option<String>,
show_legend: bool,
highlighted: Option<(usize, usize)>,
props: WidgetProps,
}
impl HeatMap {
pub fn new(data: Vec<Vec<f64>>) -> Self {
let rows = data.len();
let cols = data.first().map(|r| r.len()).unwrap_or(0);
let mut min_val = f64::INFINITY;
let mut max_val = f64::NEG_INFINITY;
for row in &data {
for &val in row {
if val < min_val {
min_val = val;
}
if val > max_val {
max_val = val;
}
}
}
if min_val == f64::INFINITY {
min_val = 0.0;
}
if max_val == f64::NEG_INFINITY {
max_val = 1.0;
}
Self {
data,
_rows: rows,
cols,
min_val,
max_val,
color_scale: ColorScale::default(),
custom_colors: None,
cell_display: CellDisplay::default(),
cell_width: 2,
cell_height: 1,
show_values: false,
value_decimals: 1,
row_labels: None,
col_labels: None,
title: None,
show_legend: false,
highlighted: None,
props: WidgetProps::new(),
}
}
pub fn from_flat(data: &[f64], rows: usize, cols: usize) -> Self {
let mut grid = Vec::with_capacity(rows);
for r in 0..rows {
let start = r * cols;
let end = (start + cols).min(data.len());
grid.push(data[start..end].to_vec());
}
Self::new(grid)
}
pub fn contribution_map(contributions: &[u32]) -> Self {
let weeks = 52;
let days = 7;
let mut data = vec![vec![0.0; weeks]; days];
let max_contrib = *contributions.iter().max().unwrap_or(&1) as f64;
for (i, &count) in contributions.iter().take(weeks * days).enumerate() {
let week = i / days;
let day = i % days;
data[day][week] = count as f64 / max_contrib;
}
Self::new(data)
.color_scale(ColorScale::Green)
.cell_width(2)
.cell_height(1)
}
pub fn correlation_matrix(correlations: &[Vec<f64>], labels: Vec<String>) -> Self {
Self::new(correlations.to_vec())
.color_scale(ColorScale::BlueRed)
.bounds(-1.0, 1.0)
.row_labels(labels.clone())
.col_labels(labels)
.show_values(true)
}
pub fn color_scale(mut self, scale: ColorScale) -> Self {
self.color_scale = scale;
self
}
pub fn custom_colors(mut self, low: Color, high: Color) -> Self {
self.color_scale = ColorScale::Custom;
self.custom_colors = Some((low, high));
self
}
pub fn bounds(mut self, min: f64, max: f64) -> Self {
self.min_val = min;
self.max_val = max;
self
}
pub fn cell_display(mut self, display: CellDisplay) -> Self {
self.cell_display = display;
self
}
pub fn cell_size(mut self, width: usize, height: usize) -> Self {
self.cell_width = width;
self.cell_height = height;
self
}
pub fn cell_width(mut self, width: usize) -> Self {
self.cell_width = width;
self
}
pub fn cell_height(mut self, height: usize) -> Self {
self.cell_height = height;
self
}
pub fn show_values(mut self, show: bool) -> Self {
self.show_values = show;
if show && self.cell_width < 4 {
self.cell_width = 4;
}
self
}
pub fn value_decimals(mut self, decimals: usize) -> Self {
self.value_decimals = decimals;
self
}
pub fn row_labels(mut self, labels: Vec<String>) -> Self {
self.row_labels = Some(labels);
self
}
pub fn col_labels(mut self, labels: Vec<String>) -> Self {
self.col_labels = Some(labels);
self
}
pub fn title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
pub fn show_legend(mut self, show: bool) -> Self {
self.show_legend = show;
self
}
pub fn highlight(mut self, row: usize, col: usize) -> Self {
self.highlighted = Some((row, col));
self
}
pub fn normalize(&self, value: f64) -> f64 {
if self.max_val == self.min_val {
0.5
} else {
(value - self.min_val) / (self.max_val - self.min_val)
}
}
pub fn color_for(&self, value: f64) -> Color {
let normalized = self.normalize(value);
if let (ColorScale::Custom, Some((low, high))) = (self.color_scale, &self.custom_colors) {
Color::rgb(
(low.r as f64 + normalized * (high.r as f64 - low.r as f64)) as u8,
(low.g as f64 + normalized * (high.g as f64 - low.g as f64)) as u8,
(low.b as f64 + normalized * (high.b as f64 - low.b as f64)) as u8,
)
} else {
self.color_scale.color_at(normalized)
}
}
pub fn render_cell(&self, value: f64) -> String {
if self.show_values {
format!(
"{:>width$.prec$}",
value,
width = self.cell_width,
prec = self.value_decimals
)
} else {
match self.cell_display {
CellDisplay::Block => "█".repeat(self.cell_width),
CellDisplay::HalfBlock => "▀".repeat(self.cell_width),
CellDisplay::Value => {
format!(
"{:>width$.prec$}",
value,
width = self.cell_width,
prec = self.value_decimals
)
}
CellDisplay::Custom => "■".repeat(self.cell_width),
}
}
}
}
impl_styled_view!(HeatMap);
impl_props_builders!(HeatMap);
pub fn heatmap(data: Vec<Vec<f64>>) -> HeatMap {
HeatMap::new(data)
}
pub fn contribution_map(contributions: &[u32]) -> HeatMap {
HeatMap::contribution_map(contributions)
}