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
pub use crate::plot::colormap::ColorMap;
/// Builder for a 2D histogram (density map).
///
/// A 2D histogram bins scatter points `(x, y)` into a rectangular grid and
/// colors each cell by its count. The colorbar (labeled **"Count"**) is added
/// to the right margin automatically.
///
/// # Data loading
///
/// Pass scatter points, explicit axis ranges, and bin counts to
/// [`with_data`](Self::with_data). Points outside the specified ranges are
/// silently discarded. The range should start at `0.0` — see the note below.
///
/// # Range convention
///
/// The x and y axis extents reported to the layout are the physical
/// `x_range` and `y_range` values supplied to `with_data`. The renderer maps
/// each bin's physical coordinate through this same range, so the axis ticks
/// always reflect real data units regardless of bin count.
///
/// # Correlation annotation
///
/// [`with_correlation()`](Self::with_correlation) overlays the Pearson r
/// coefficient in the top-right corner, computed from the raw scatter points.
///
/// # Example
///
/// ```rust,no_run
/// use kuva::plot::Histogram2D;
/// use kuva::plot::histogram2d::ColorMap;
/// use kuva::backend::svg::SvgBackend;
/// use kuva::render::render::render_multiple;
/// use kuva::render::layout::Layout;
/// use kuva::render::plots::Plot;
///
/// // (x, y) scatter points
/// let data: Vec<(f64, f64)> = vec![(5.0, 6.0), (14.0, 15.0), (15.0, 14.0)];
///
/// let hist = Histogram2D::new()
/// .with_data(data, (0.0, 20.0), (0.0, 20.0), 20, 20)
/// .with_color_map(ColorMap::Viridis)
/// .with_correlation();
///
/// let plots = vec![Plot::Histogram2d(hist)];
/// let layout = Layout::auto_from_plots(&plots)
/// .with_title("2D Histogram")
/// .with_x_label("X")
/// .with_y_label("Y");
///
/// let svg = SvgBackend.render_scene(&render_multiple(plots, layout));
/// std::fs::write("hist2d.svg", svg).unwrap();
/// ```
#[derive(Clone)]
pub struct Histogram2D {
/// Raw scatter points used for correlation computation.
pub data: Vec<(f64, f64)>,
/// Pre-computed bin counts indexed as `bins[row][col]`.
pub bins: Vec<Vec<usize>>,
/// Physical x-axis range `(min, max)` used for binning.
pub x_range: (f64, f64),
/// Physical y-axis range `(min, max)` used for binning.
pub y_range: (f64, f64),
/// Number of bins along the x-axis. Default: `10`.
pub bins_x: usize,
/// Number of bins along the y-axis. Default: `10`.
pub bins_y: usize,
/// Colormap applied to normalized bin counts. Default: [`ColorMap::Viridis`].
pub color_map: ColorMap,
/// When `true`, the Pearson r coefficient is printed in the top-right corner.
pub show_correlation: bool,
/// When `true`, bin counts are log-scaled before color mapping (`log₁₀(count+1)`).
pub log_count: bool,
}
impl Default for Histogram2D {
fn default() -> Self {
Self::new()
}
}
impl Histogram2D {
/// Create a 2D histogram with default settings.
///
/// Defaults: 10×10 bins, Viridis colormap, no correlation annotation.
/// Call [`with_data`](Self::with_data) to load points.
pub fn new() -> Self {
Self {
data: vec![],
bins: vec![],
x_range: (0.0, 0.0),
y_range: (0.0, 0.0),
bins_x: 10,
bins_y: 10,
color_map: ColorMap::Viridis,
show_correlation: false,
log_count: false,
}
}
/// Load scatter points and bin them into a grid.
///
/// - `data` — `(x, y)` pairs; any type implementing `Into<f64>`.
/// - `x_range` / `y_range` — axis extents `(min, max)`. Points outside
/// these bounds are silently discarded. Start at `0.0` to keep bin-index
/// and layout coordinates aligned.
/// - `bins_x` / `bins_y` — number of columns / rows in the grid.
///
/// ```rust,no_run
/// use kuva::plot::Histogram2D;
///
/// let data: Vec<(f64, f64)> = vec![(5.0, 8.0), (12.0, 3.0), (7.0, 15.0)];
/// let hist = Histogram2D::new()
/// .with_data(data, (0.0, 20.0), (0.0, 20.0), 20, 20);
/// ```
pub fn with_data<T: Into<f64>>(
mut self,
data: Vec<(T, T)>,
x_range: (f64, f64),
y_range: (f64, f64),
bins_x: usize,
bins_y: usize,
) -> Self {
let mut bins = vec![vec![0usize; bins_x]; bins_y];
let x_bin_width = (x_range.1 - x_range.0) / bins_x as f64;
let y_bin_height = (y_range.1 - y_range.0) / bins_y as f64;
for (x_raw, y_raw) in data {
let x = x_raw.into();
let y = y_raw.into();
self.data.push((x, y));
if x < x_range.0 || x > x_range.1 || y < y_range.0 || y > y_range.1 {
continue; // ignore out-of-bounds
}
// Clamp to last bin so points at exactly x_range.1 / y_range.1
// fall into the final bin rather than being silently dropped.
let col = (((x - x_range.0) / x_bin_width).floor() as usize).min(bins_x - 1);
let row = (((y - y_range.0) / y_bin_height).floor() as usize).min(bins_y - 1);
bins[row][col] += 1;
}
// self.data = data;
self.bins = bins;
self.x_range = x_range;
self.y_range = y_range;
self.bins_x = bins_x;
self.bins_y = bins_y;
self
}
/// Set the colormap for bin counts. Default: [`ColorMap::Viridis`].
///
/// ```rust,no_run
/// use kuva::plot::Histogram2D;
/// use kuva::plot::histogram2d::ColorMap;
///
/// let hist = Histogram2D::new()
/// .with_data(vec![(5.0_f64, 5.0_f64)], (0.0, 10.0), (0.0, 10.0), 10, 10)
/// .with_color_map(ColorMap::Inferno);
/// ```
pub fn with_color_map(mut self, map: ColorMap) -> Self {
self.color_map = map;
self
}
/// Overlay the Pearson correlation coefficient in the top-right corner.
///
/// The coefficient is computed from all points passed to
/// [`with_data`](Self::with_data), including those clipped outside the
/// plot range. Displayed as `r = 0.85`.
///
/// ```rust,no_run
/// use kuva::plot::Histogram2D;
///
/// let hist = Histogram2D::new()
/// .with_data(vec![(5.0_f64, 6.0_f64)], (0.0, 10.0), (0.0, 10.0), 10, 10)
/// .with_correlation();
/// ```
pub fn with_correlation(mut self) -> Self {
self.show_correlation = true;
self
}
/// Apply logarithmic scaling to bin counts before color mapping.
///
/// Uses `log₁₀(count + 1)` so that zero counts map to 0.0 and the dynamic
/// range is compressed. Useful when a few high-density bins dominate the
/// color scale and obscure structure in low-density regions.
pub fn with_log_count(mut self) -> Self {
self.log_count = true;
self
}
}