Skip to main content

tui_lipan/widgets/heatmap/
mod.rs

1//! Heatmap widget.
2
3mod layout;
4mod node;
5mod reconcile;
6
7pub use layout::measure_heatmap;
8pub use node::HeatmapNode;
9pub(crate) use reconcile::reconcile_heatmap_node;
10
11use std::sync::Arc;
12
13use crate::core::element::{Element, ElementKind};
14use crate::layout::hash::LayoutHash;
15use crate::style::{BorderStyle, Color, Length, Padding, Style};
16use crate::utils::gradient::ColorGradient;
17use unicode_width::UnicodeWidthStr;
18
19/// Rendering strategy for heatmap cells.
20#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
21pub enum HeatmapCellMode {
22    /// Render each cell as a colored background block.
23    #[default]
24    Background,
25    /// Render each cell with a centered glyph string over the colored background.
26    Glyph(Arc<str>),
27    /// Render only the glyph in the mapped color, leaving the cell background untouched.
28    GlyphForeground(Arc<str>),
29}
30
31/// Horizontal layout mode for the heatmap legend.
32#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
33pub enum HeatmapLegendWidth {
34    /// Start the legend at the same x-position as the heatmap grid.
35    #[default]
36    Grid,
37    /// Let the legend span the full inner width, ignoring the row-label gutter.
38    Full,
39}
40
41/// A heatmap widget that visualizes a 2D matrix of values as colored cells.
42#[derive(Clone)]
43pub struct Heatmap {
44    pub(crate) data: Vec<Vec<f64>>,
45    pub(crate) row_labels: Vec<Arc<str>>,
46    pub(crate) column_labels: Vec<Arc<str>>,
47    pub(crate) gradient: ColorGradient,
48    pub(crate) range_min: Option<f64>,
49    pub(crate) range_max: Option<f64>,
50    pub(crate) cell_mode: HeatmapCellMode,
51    pub(crate) cell_width: u16,
52    pub(crate) gap_x: u16,
53    pub(crate) gap_y: u16,
54    pub(crate) legend_gap: u16,
55    pub(crate) legend_spacing: u16,
56    pub(crate) legend_width: HeatmapLegendWidth,
57    pub(crate) show_values: bool,
58    pub(crate) show_legend: bool,
59    pub(crate) style: Style,
60    pub(crate) label_style: Style,
61    pub(crate) legend_style: Style,
62    pub(crate) padding: Padding,
63    pub(crate) border: bool,
64    pub(crate) border_style: BorderStyle,
65    pub(crate) width: Length,
66    pub(crate) height: Length,
67}
68
69impl Default for Heatmap {
70    fn default() -> Self {
71        Self {
72            data: Vec::new(),
73            row_labels: Vec::new(),
74            column_labels: Vec::new(),
75            gradient: ColorGradient::new(Color::Rgb(60, 179, 113), Color::Rgb(226, 82, 87)),
76            range_min: None,
77            range_max: None,
78            cell_mode: HeatmapCellMode::Background,
79            cell_width: 4,
80            gap_x: 0,
81            gap_y: 0,
82            legend_gap: 0,
83            legend_spacing: 0,
84            legend_width: HeatmapLegendWidth::Grid,
85            show_values: false,
86            show_legend: false,
87            style: Style::default(),
88            label_style: Style::default(),
89            legend_style: Style::default(),
90            padding: Padding::default(),
91            border: false,
92            border_style: BorderStyle::Plain,
93            width: Length::Auto,
94            height: Length::Auto,
95        }
96    }
97}
98
99impl Heatmap {
100    /// Create a new heatmap from a 2D matrix of values.
101    pub fn new(data: impl IntoIterator<Item = impl IntoIterator<Item = f64>>) -> Self {
102        Self {
103            data: data
104                .into_iter()
105                .map(|row| row.into_iter().collect())
106                .collect(),
107            ..Self::default()
108        }
109    }
110
111    /// Set row labels displayed on the left.
112    pub fn row_labels(mut self, labels: impl IntoIterator<Item = impl Into<Arc<str>>>) -> Self {
113        self.row_labels = labels.into_iter().map(Into::into).collect();
114        self
115    }
116
117    /// Set column labels displayed on top.
118    pub fn column_labels(mut self, labels: impl IntoIterator<Item = impl Into<Arc<str>>>) -> Self {
119        self.column_labels = labels.into_iter().map(Into::into).collect();
120        self
121    }
122
123    /// Set the color gradient for mapping values to colors.
124    pub fn gradient(mut self, gradient: ColorGradient) -> Self {
125        self.gradient = gradient;
126        self
127    }
128
129    /// Set the explicit value range for gradient mapping.
130    pub fn range(mut self, min: f64, max: f64) -> Self {
131        self.range_min = Some(min);
132        self.range_max = Some(max);
133        self
134    }
135
136    /// Set how cells are rendered.
137    pub fn cell_mode(mut self, cell_mode: HeatmapCellMode) -> Self {
138        self.cell_mode = cell_mode;
139        self
140    }
141
142    /// Set the width of each cell in characters.
143    pub fn cell_width(mut self, cell_width: u16) -> Self {
144        self.cell_width = cell_width.max(1);
145        self
146    }
147
148    /// Set horizontal spacing between cells in characters.
149    pub fn gap_x(mut self, gap_x: u16) -> Self {
150        self.gap_x = gap_x;
151        self
152    }
153
154    /// Set vertical spacing between rows in lines.
155    pub fn gap_y(mut self, gap_y: u16) -> Self {
156        self.gap_y = gap_y;
157        self
158    }
159
160    /// Set spacing between legend color cells in characters.
161    pub fn legend_gap(mut self, legend_gap: u16) -> Self {
162        self.legend_gap = legend_gap;
163        self
164    }
165
166    /// Set vertical spacing between the heatmap grid and legend in lines.
167    pub fn legend_spacing(mut self, legend_spacing: u16) -> Self {
168        self.legend_spacing = legend_spacing;
169        self
170    }
171
172    /// Set whether the legend aligns with the grid or spans the full inner width.
173    pub fn legend_width(mut self, legend_width: HeatmapLegendWidth) -> Self {
174        self.legend_width = legend_width;
175        self
176    }
177
178    /// Show numeric values inside cells.
179    pub fn show_values(mut self, show: bool) -> Self {
180        self.show_values = show;
181        self
182    }
183
184    /// Show a gradient legend below the heatmap.
185    pub fn show_legend(mut self, show: bool) -> Self {
186        self.show_legend = show;
187        self
188    }
189
190    /// Set the base style.
191    pub fn style(mut self, style: Style) -> Self {
192        self.style = style;
193        self
194    }
195
196    /// Set the label style for row/column labels.
197    pub fn label_style(mut self, style: Style) -> Self {
198        self.label_style = style;
199        self
200    }
201
202    /// Set the legend style.
203    pub fn legend_style(mut self, style: Style) -> Self {
204        self.legend_style = style;
205        self
206    }
207
208    /// Set inner padding.
209    pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
210        self.padding = padding.into();
211        self
212    }
213
214    /// Enable or disable border.
215    pub fn border(mut self, border: bool) -> Self {
216        self.border = border;
217        self
218    }
219
220    /// Set border style.
221    pub fn border_style(mut self, border_style: BorderStyle) -> Self {
222        self.border_style = border_style;
223        self
224    }
225
226    /// Set requested width.
227    pub fn width(mut self, width: Length) -> Self {
228        self.width = width;
229        self
230    }
231
232    /// Set requested height.
233    pub fn height(mut self, height: Length) -> Self {
234        self.height = height;
235        self
236    }
237
238    pub(crate) fn effective_cell_width(&self) -> u16 {
239        let min_width = match &self.cell_mode {
240            HeatmapCellMode::Background => 1,
241            HeatmapCellMode::Glyph(glyph) | HeatmapCellMode::GlyphForeground(glyph) => {
242                UnicodeWidthStr::width(glyph.as_ref())
243                    .max(1)
244                    .min(u16::MAX as usize) as u16
245            }
246        };
247
248        self.cell_width.max(min_width)
249    }
250}
251
252impl From<Heatmap> for Element {
253    fn from(value: Heatmap) -> Self {
254        Element::new(ElementKind::Heatmap(value))
255    }
256}
257
258impl LayoutHash for Heatmap {
259    fn layout_hash(
260        &self,
261        hasher: &mut impl std::hash::Hasher,
262        _recurse: &dyn Fn(&Element) -> Option<u64>,
263    ) -> Option<()> {
264        use std::hash::Hash;
265
266        self.width.hash(hasher);
267        self.height.hash(hasher);
268        self.row_labels.hash(hasher);
269        self.column_labels.hash(hasher);
270        self.cell_mode.hash(hasher);
271        self.cell_width.hash(hasher);
272        self.gap_x.hash(hasher);
273        self.gap_y.hash(hasher);
274        self.legend_gap.hash(hasher);
275        self.legend_spacing.hash(hasher);
276        self.legend_width.hash(hasher);
277        self.show_legend.hash(hasher);
278        self.padding.hash(hasher);
279        self.border.hash(hasher);
280        self.data.len().hash(hasher);
281        for row in &self.data {
282            row.len().hash(hasher);
283        }
284
285        Some(())
286    }
287}