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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
use crate::plot::colormap::ColorMap;
use crate::plot::phylo::PhyloTree;
/// Normalization applied to the data matrix before color mapping.
#[derive(Clone, Debug, Default)]
pub enum ClustermapNorm {
/// No normalization — raw values are color-mapped.
#[default]
None,
/// Each row is z-score normalized (mean 0, std 1).
RowZScore,
/// Each column is z-score normalized (mean 0, std 1).
ColZScore,
}
/// A colored strip of cells alongside the heatmap body, used to annotate
/// rows or columns with categorical metadata (e.g. sample group, treatment).
#[derive(Clone, Debug)]
pub struct AnnotationTrack {
/// One CSS color string per row/column, in the **original data order**
/// (before clustering). The renderer reorders them automatically.
pub colors: Vec<String>,
/// Optional label displayed above (column tracks) or to the left (row tracks).
pub label: Option<String>,
/// Width (row tracks) or height (col tracks) of the strip in pixels. Default 15.
pub width: f64,
}
impl AnnotationTrack {
/// Create a new annotation track from an iterable of CSS color strings.
pub fn new(colors: impl IntoIterator<Item = impl Into<String>>) -> Self {
Self {
colors: colors.into_iter().map(|s| s.into()).collect(),
label: None,
width: 15.0,
}
}
/// Set a label for this track.
pub fn with_label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
/// Set the width (row tracks) or height (col tracks) of the strip in pixels.
pub fn with_width(mut self, width: f64) -> Self {
self.width = width;
self
}
}
/// A clustermap: a heatmap with integrated hierarchical clustering dendrograms.
///
/// Unlike `Heatmap + PhyloTree` in a `Figure`, `Clustermap` guarantees
/// pixel-perfect alignment between dendrogram leaves and heatmap cell centres
/// because both share the same internal layout computation.
///
/// Rows and columns are clustered automatically via UPGMA (Euclidean distance)
/// unless clustering is disabled or a pre-built `PhyloTree` is supplied.
///
/// # Example
///
/// ```rust,no_run
/// use kuva::plot::clustermap::{Clustermap, ClustermapNorm, AnnotationTrack};
/// use kuva::plot::ColorMap;
/// use kuva::render::plots::Plot;
/// use kuva::render::layout::Layout;
/// use kuva::render::render::render_multiple;
/// use kuva::backend::svg::SvgBackend;
///
/// let data = vec![
/// vec![5.0, 1.0, 0.5],
/// vec![0.1, 4.0, 0.2],
/// vec![0.3, 0.2, 6.0],
/// ];
///
/// let cm = Clustermap::new()
/// .with_data(data)
/// .with_row_labels(["A", "B", "C"])
/// .with_col_labels(["X", "Y", "Z"])
/// .with_legend("Expression");
///
/// let plots = vec![Plot::Clustermap(cm)];
/// let layout = Layout::auto_from_plots(&plots).with_title("Clustermap");
/// let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
/// std::fs::write("clustermap.svg", svg).unwrap();
/// ```
#[derive(Clone)]
pub struct Clustermap {
/// Rows × columns grid of values. All rows must have the same length.
pub data: Vec<Vec<f64>>,
/// Optional row labels in original data order.
pub row_labels: Option<Vec<String>>,
/// Optional column labels in original data order.
pub col_labels: Option<Vec<String>>,
/// Whether to cluster rows (default `true`).
pub cluster_rows: bool,
/// Whether to cluster columns (default `true`).
pub cluster_cols: bool,
/// Pre-computed row tree. Overrides auto-clustering when provided.
pub row_tree: Option<PhyloTree>,
/// Pre-computed column tree. Overrides auto-clustering when provided.
pub col_tree: Option<PhyloTree>,
/// Color map applied to normalized cell values. Default: `Viridis`.
pub color_map: ColorMap,
/// When `true`, display the numeric value inside each cell.
pub show_values: bool,
/// Normalization applied before color mapping.
pub normalization: ClustermapNorm,
/// Branch color for both dendrograms. Default: `"black"`.
pub branch_color: String,
/// Width in pixels of the row dendrogram panel. Default: `100.0`.
pub row_dendrogram_width: f64,
/// Height in pixels of the column dendrogram panel. Default: `80.0`.
pub col_dendrogram_height: f64,
/// Annotation tracks displayed to the right of the row dendrogram.
pub row_annotations: Vec<AnnotationTrack>,
/// Annotation tracks displayed below the column dendrogram.
pub col_annotations: Vec<AnnotationTrack>,
/// Label for the colorbar in the right margin.
pub legend_label: Option<String>,
/// Enable SVG tooltip overlays on heatmap cells.
pub show_tooltips: bool,
}
impl Default for Clustermap {
fn default() -> Self {
Self::new()
}
}
impl Clustermap {
/// Create a clustermap with default settings.
pub fn new() -> Self {
Self {
data: vec![],
row_labels: None,
col_labels: None,
cluster_rows: true,
cluster_cols: true,
row_tree: None,
col_tree: None,
color_map: ColorMap::Viridis,
show_values: false,
normalization: ClustermapNorm::None,
branch_color: "black".to_string(),
row_dendrogram_width: 100.0,
col_dendrogram_height: 80.0,
row_annotations: vec![],
col_annotations: vec![],
legend_label: None,
show_tooltips: false,
}
}
/// Set the data matrix (rows × columns). Accepts any nested numeric iterables.
pub fn with_data<U, T, I>(mut self, data: I) -> Self
where
I: IntoIterator<Item = T>,
T: IntoIterator<Item = U>,
U: Into<f64>,
{
let mut row: Vec<f64> = vec![];
for r in data.into_iter() {
for v in r {
row.push(v.into());
}
self.data.push(row);
row = vec![];
}
self
}
/// Set row labels in original data order (top to bottom).
pub fn with_row_labels(mut self, labels: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.row_labels = Some(labels.into_iter().map(|s| s.into()).collect());
self
}
/// Set column labels in original data order (left to right).
pub fn with_col_labels(mut self, labels: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.col_labels = Some(labels.into_iter().map(|s| s.into()).collect());
self
}
/// Enable or disable row clustering (default `true`).
pub fn with_cluster_rows(mut self, v: bool) -> Self {
self.cluster_rows = v;
self
}
/// Enable or disable column clustering (default `true`).
pub fn with_cluster_cols(mut self, v: bool) -> Self {
self.cluster_cols = v;
self
}
/// Provide a pre-built row tree instead of auto-clustering.
///
/// The tree's leaf labels must match the row labels set via `with_row_labels`.
pub fn with_row_tree(mut self, tree: PhyloTree) -> Self {
self.row_tree = Some(tree);
self
}
/// Provide a pre-built column tree instead of auto-clustering.
///
/// The tree's leaf labels must match the column labels set via `with_col_labels`.
pub fn with_col_tree(mut self, tree: PhyloTree) -> Self {
self.col_tree = Some(tree);
self
}
/// Set the color map (default [`ColorMap::Viridis`]).
pub fn with_color_map(mut self, map: ColorMap) -> Self {
self.color_map = map;
self
}
/// Overlay raw numeric values inside each cell.
pub fn with_values(mut self) -> Self {
self.show_values = true;
self
}
/// Set the normalization applied before color mapping.
pub fn with_normalization(mut self, norm: ClustermapNorm) -> Self {
self.normalization = norm;
self
}
/// Set the branch color for both dendrograms (default `"black"`).
pub fn with_branch_color(mut self, color: impl Into<String>) -> Self {
self.branch_color = color.into();
self
}
/// Set the pixel width of the row dendrogram panel (default `100.0`).
pub fn with_row_dendrogram_width(mut self, w: f64) -> Self {
self.row_dendrogram_width = w;
self
}
/// Set the pixel height of the column dendrogram panel (default `80.0`).
pub fn with_col_dendrogram_height(mut self, h: f64) -> Self {
self.col_dendrogram_height = h;
self
}
/// Add a row annotation track (displayed between the row dendrogram and the heatmap body).
pub fn with_row_annotation(mut self, track: AnnotationTrack) -> Self {
self.row_annotations.push(track);
self
}
/// Add a column annotation track (displayed between the column dendrogram and the heatmap body).
pub fn with_col_annotation(mut self, track: AnnotationTrack) -> Self {
self.col_annotations.push(track);
self
}
/// Set the colorbar legend label.
pub fn with_legend(mut self, label: impl Into<String>) -> Self {
self.legend_label = Some(label.into());
self
}
/// Enable SVG tooltip overlays showing cell values on hover.
pub fn with_tooltips(mut self) -> Self {
self.show_tooltips = true;
self
}
}