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
/// Re-export the shared `ColorMap` type from the histogram2d module.
pub use crate::plot::histogram2d::ColorMap;
/// Aggregation function applied to a third variable `z` over points that fall
/// into the same hexagonal bin.
///
/// Used by [`HexbinPlot::with_z`] to choose how multiple `z` values in one bin
/// are reduced to a single color-mapped scalar.
#[derive(Debug, Clone, Default)]
pub enum ZReduce {
/// Number of data points in the bin. **(default)**
#[default]
Count,
/// Arithmetic mean of `z` over all points in the bin.
Mean,
/// Sum of `z` over all points in the bin.
Sum,
/// Median of `z` over all points in the bin.
Median,
/// Minimum `z` value in the bin.
Min,
/// Maximum `z` value in the bin.
Max,
}
/// Builder for a hexbin (hexagonal-bin) density plot.
///
/// A hexbin plot divides a 2-D scatter into a regular hexagonal grid and
/// colors each cell by the number of points (or by an aggregated third
/// variable `z`) it contains. Hexagonal bins produce a more visually uniform
/// density estimate than rectangular bins because every hex is equidistant
/// from its six neighbors.
///
/// # Basic usage
///
/// ```rust,no_run
/// use kuva::plot::hexbin::HexbinPlot;
/// use kuva::render::plots::Plot;
/// use kuva::render::layout::Layout;
/// use kuva::render::render::render_multiple;
/// use kuva::backend::svg::SvgBackend;
///
/// let x: Vec<f64> = (0..200).map(|i| (i as f64 / 10.0).sin()).collect();
/// let y: Vec<f64> = (0..200).map(|i| (i as f64 / 10.0).cos()).collect();
///
/// let plot = HexbinPlot::new()
/// .with_data(x, y)
/// .with_n_bins(25);
///
/// let plots = vec![Plot::Hexbin(plot)];
/// let layout = Layout::auto_from_plots(&plots)
/// .with_title("Hexbin Plot")
/// .with_x_label("X")
/// .with_y_label("Y");
///
/// let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
/// std::fs::write("hexbin.svg", svg).unwrap();
/// ```
pub struct HexbinPlot {
/// X coordinates of the scatter points.
pub x: Vec<f64>,
/// Y coordinates of the scatter points.
pub y: Vec<f64>,
/// Optional third variable for aggregation-based coloring.
pub z: Option<Vec<f64>>,
/// Target number of hexagonal bins across the x-axis. Default: `20`.
pub n_bins: usize,
/// Explicit circumradius of each hex in pixels. Overrides `n_bins` when set.
pub bin_size: Option<f64>,
/// Colormap applied to bin values. Default: [`ColorMap::Viridis`].
pub color_map: ColorMap,
/// Aggregation function for the `z` variable. Default: [`ZReduce::Count`].
pub z_reduce: ZReduce,
/// Apply log₁₀ scaling to bin values before color mapping. Default: `false`.
pub log_color: bool,
/// Minimum number of points required to render a bin. Default: `1`.
pub min_count: usize,
/// Divide counts by the total number of points (fractional density). Default: `false`.
pub normalize: bool,
/// Show a colorbar legend on the right margin. Default: `true`.
pub show_colorbar: bool,
/// Custom label for the colorbar. Auto-derived from `z_reduce` when `None`.
pub colorbar_label: Option<String>,
/// Outline color for each hexagon. `None` = no outline (default).
pub stroke_color: Option<String>,
/// Outline stroke width when `stroke_color` is set. Default: `0.5`.
pub stroke_width: f64,
/// `true` = flat-top orientation; `false` = pointy-top (default).
pub flat_top: bool,
/// Explicit data-space x extent for binning and axis limits.
pub x_range: Option<(f64, f64)>,
/// Explicit data-space y extent for binning and axis limits.
pub y_range: Option<(f64, f64)>,
/// Clamp the color scale to `(lo, hi)` instead of using the data range.
pub color_range: Option<(f64, f64)>,
}
impl Default for HexbinPlot {
fn default() -> Self {
Self::new()
}
}
impl HexbinPlot {
/// Create a hexbin plot with default settings.
///
/// Defaults: 20 bins, Viridis colormap, Count aggregation, pointy-top hexagons,
/// colorbar shown, no stroke, no z variable.
pub fn new() -> Self {
Self {
x: vec![],
y: vec![],
z: None,
n_bins: 20,
bin_size: None,
color_map: ColorMap::Viridis,
z_reduce: ZReduce::Count,
log_color: false,
min_count: 1,
normalize: false,
show_colorbar: true,
colorbar_label: None,
stroke_color: None,
stroke_width: 0.5,
flat_top: false,
x_range: None,
y_range: None,
color_range: None,
}
}
/// Load x and y scatter data.
///
/// Accepts any iterable of values convertible to `f64`.
///
/// ```rust,no_run
/// use kuva::plot::hexbin::HexbinPlot;
/// let plot = HexbinPlot::new().with_data(
/// vec![1.0_f64, 2.0, 3.0],
/// vec![4.0_f64, 5.0, 6.0],
/// );
/// ```
pub fn with_data(
mut self,
x: impl IntoIterator<Item = impl Into<f64>>,
y: impl IntoIterator<Item = impl Into<f64>>,
) -> Self {
self.x = x.into_iter().map(Into::into).collect();
self.y = y.into_iter().map(Into::into).collect();
self
}
/// Attach a third variable `z` and choose the aggregation function.
///
/// When set, bins are colored by the aggregated `z` value rather than by
/// point count.
///
/// ```rust,no_run
/// use kuva::plot::hexbin::{HexbinPlot, ZReduce};
/// let plot = HexbinPlot::new()
/// .with_data(vec![1.0_f64, 2.0], vec![3.0_f64, 4.0])
/// .with_z(vec![10.0_f64, 20.0], ZReduce::Mean);
/// ```
pub fn with_z(mut self, z: impl IntoIterator<Item = impl Into<f64>>, reduce: ZReduce) -> Self {
self.z = Some(z.into_iter().map(Into::into).collect());
self.z_reduce = reduce;
self
}
/// Set the target number of hexagonal bins across the x-axis. Default: `20`.
pub fn with_n_bins(mut self, n: usize) -> Self {
self.n_bins = n;
self
}
/// Override the hex circumradius in pixels instead of using `n_bins`.
pub fn with_bin_size(mut self, s: f64) -> Self {
self.bin_size = Some(s);
self
}
/// Set the colormap. Default: [`ColorMap::Viridis`].
pub fn with_color_map(mut self, m: ColorMap) -> Self {
self.color_map = m;
self
}
/// Apply log₁₀ scaling to bin values before color mapping.
pub fn with_log_color(mut self, b: bool) -> Self {
self.log_color = b;
self
}
/// Set the minimum number of points required to render a bin. Default: `1`.
pub fn with_min_count(mut self, n: usize) -> Self {
self.min_count = n;
self
}
/// Divide counts by the total number of points (fractional density).
pub fn with_normalize(mut self, b: bool) -> Self {
self.normalize = b;
self
}
/// Show or hide the colorbar. Default: `true`.
pub fn with_colorbar(mut self, b: bool) -> Self {
self.show_colorbar = b;
self
}
/// Set a custom colorbar label. Auto-derived when not set.
pub fn with_colorbar_label(mut self, s: impl Into<String>) -> Self {
self.colorbar_label = Some(s.into());
self
}
/// Add a hex outline stroke with the given CSS color.
pub fn with_stroke(mut self, color: impl Into<String>) -> Self {
self.stroke_color = Some(color.into());
self
}
/// Set the hex outline stroke width. Default: `0.5`.
pub fn with_stroke_width(mut self, w: f64) -> Self {
self.stroke_width = w;
self
}
/// Use flat-top hex orientation instead of pointy-top. Default: `false`.
pub fn with_flat_top(mut self, b: bool) -> Self {
self.flat_top = b;
self
}
/// Clip data and fix the x-axis extent to `[lo, hi]`.
pub fn with_x_range(mut self, lo: f64, hi: f64) -> Self {
self.x_range = Some((lo, hi));
self
}
/// Clip data and fix the y-axis extent to `[lo, hi]`.
pub fn with_y_range(mut self, lo: f64, hi: f64) -> Self {
self.y_range = Some((lo, hi));
self
}
/// Clamp the colorbar scale to `(lo, hi)` instead of using the data range.
pub fn with_color_range(mut self, lo: f64, hi: f64) -> Self {
self.color_range = Some((lo, hi));
self
}
}