1use super::{
4 Plot, PlotDataLayout, PlotError, PlotItemStyle, plot_spec_with_style, with_plot_str_or_empty,
5};
6use crate::{FloatFormat, FloatFormatError, HeatmapFlags, ItemFlags, sys};
7use dear_imgui_rs::with_scratch_txt_two;
8use std::borrow::Cow;
9
10fn validate_grid_counts(
11 caller: &str,
12 rows: usize,
13 cols: usize,
14 values_len: usize,
15) -> Result<(), PlotError> {
16 if rows == 0 || cols == 0 {
17 return Err(PlotError::InvalidData(
18 "Rows and columns must be positive".to_string(),
19 ));
20 }
21
22 let expected_size = rows
23 .checked_mul(cols)
24 .ok_or_else(|| PlotError::InvalidData(format!("{caller} rows * cols overflowed usize")))?;
25 let _ = heatmap_count_to_i32(caller, "rows", rows)?;
26 let _ = heatmap_count_to_i32(caller, "cols", cols)?;
27
28 if values_len != expected_size {
29 return Err(PlotError::DataLengthMismatch {
30 x_len: expected_size,
31 y_len: values_len,
32 });
33 }
34
35 Ok(())
36}
37
38fn heatmap_count_to_i32(caller: &str, name: &str, value: usize) -> Result<i32, PlotError> {
39 i32::try_from(value)
40 .map_err(|_| PlotError::InvalidData(format!("{caller} {name} exceeded ImPlot's i32 range")))
41}
42
43pub struct HeatmapPlot<'a, F = &'static str> {
45 label: &'a str,
46 values: &'a [f64],
47 style: PlotItemStyle,
48 rows: usize,
49 cols: usize,
50 scale_min: f64,
51 scale_max: f64,
52 label_fmt: Option<F>,
53 bounds_min: sys::ImPlotPoint,
54 bounds_max: sys::ImPlotPoint,
55 flags: HeatmapFlags,
56 item_flags: ItemFlags,
57}
58
59impl<F> super::PlotItemStyled for HeatmapPlot<'_, F> {
60 fn style_mut(&mut self) -> &mut PlotItemStyle {
61 &mut self.style
62 }
63}
64
65impl<'a> HeatmapPlot<'a> {
66 pub fn new(label: &'a str, values: &'a [f64], rows: usize, cols: usize) -> Self {
74 Self {
75 label,
76 values,
77 style: PlotItemStyle::default(),
78 rows,
79 cols,
80 scale_min: 0.0,
81 scale_max: 0.0, label_fmt: Some("%.1f"),
83 bounds_min: sys::ImPlotPoint { x: 0.0, y: 0.0 },
84 bounds_max: sys::ImPlotPoint { x: 1.0, y: 1.0 },
85 flags: HeatmapFlags::NONE,
86 item_flags: ItemFlags::NONE,
87 }
88 }
89}
90
91impl<'a, F: AsRef<str>> HeatmapPlot<'a, F> {
92 pub fn with_scale(mut self, min: f64, max: f64) -> Self {
95 self.scale_min = min;
96 self.scale_max = max;
97 self
98 }
99
100 pub fn with_label_format<'fmt>(
102 self,
103 format: FloatFormat<'fmt>,
104 ) -> HeatmapPlot<'a, FloatFormat<'fmt>> {
105 HeatmapPlot {
106 label: self.label,
107 values: self.values,
108 style: self.style,
109 rows: self.rows,
110 cols: self.cols,
111 scale_min: self.scale_min,
112 scale_max: self.scale_max,
113 label_fmt: Some(format),
114 bounds_min: self.bounds_min,
115 bounds_max: self.bounds_max,
116 flags: self.flags,
117 item_flags: self.item_flags,
118 }
119 }
120
121 pub fn try_label_format<'fmt>(
123 self,
124 format: impl Into<Cow<'fmt, str>>,
125 ) -> Result<HeatmapPlot<'a, FloatFormat<'fmt>>, FloatFormatError> {
126 Ok(self.with_label_format(FloatFormat::new(format)?))
127 }
128
129 pub fn without_value_labels(mut self) -> Self {
131 self.label_fmt = None;
132 self
133 }
134
135 pub fn with_bounds(mut self, min_x: f64, min_y: f64, max_x: f64, max_y: f64) -> Self {
137 self.bounds_min = sys::ImPlotPoint { x: min_x, y: min_y };
138 self.bounds_max = sys::ImPlotPoint { x: max_x, y: max_y };
139 self
140 }
141
142 pub fn with_bounds_points(mut self, min: sys::ImPlotPoint, max: sys::ImPlotPoint) -> Self {
144 self.bounds_min = min;
145 self.bounds_max = max;
146 self
147 }
148
149 pub fn with_flags(mut self, flags: HeatmapFlags) -> Self {
151 self.flags = flags;
152 self
153 }
154
155 pub fn with_item_flags(mut self, flags: ItemFlags) -> Self {
157 self.item_flags = flags;
158 self
159 }
160
161 pub fn column_major(mut self) -> Self {
163 self.flags |= HeatmapFlags::COL_MAJOR;
164 self
165 }
166
167 pub fn validate(&self) -> Result<(), PlotError> {
169 if self.values.is_empty() {
170 return Err(PlotError::EmptyData);
171 }
172
173 validate_grid_counts(
174 "HeatmapPlot::validate()",
175 self.rows,
176 self.cols,
177 self.values.len(),
178 )
179 }
180}
181
182impl<F: AsRef<str>> Plot for HeatmapPlot<'_, F> {
183 fn plot(&self, plot_ui: &crate::PlotUi<'_>) {
184 if self.validate().is_err() {
185 return; }
187 let Ok(rows) = heatmap_count_to_i32("HeatmapPlot::plot()", "rows", self.rows) else {
188 return;
189 };
190 let Ok(cols) = heatmap_count_to_i32("HeatmapPlot::plot()", "cols", self.cols) else {
191 return;
192 };
193 let label_fmt = self.label_fmt.as_ref().map(AsRef::as_ref);
194 plot_ui.with_bound_context(|| match label_fmt {
195 Some(label_fmt) => {
196 let label = if self.label.contains('\0') {
197 ""
198 } else {
199 self.label
200 };
201 with_scratch_txt_two(label, label_fmt, |label_ptr, label_fmt_ptr| unsafe {
202 let spec = plot_spec_with_style(
203 self.style,
204 self.flags.bits() | self.item_flags.bits(),
205 PlotDataLayout::DEFAULT,
206 );
207 sys::ImPlot_PlotHeatmap_doublePtr(
208 label_ptr,
209 self.values.as_ptr(),
210 rows,
211 cols,
212 self.scale_min,
213 self.scale_max,
214 label_fmt_ptr,
215 self.bounds_min,
216 self.bounds_max,
217 spec,
218 );
219 })
220 }
221 None => with_plot_str_or_empty(self.label, |label_ptr| unsafe {
222 let spec = plot_spec_with_style(
223 self.style,
224 self.flags.bits() | self.item_flags.bits(),
225 PlotDataLayout::DEFAULT,
226 );
227 sys::ImPlot_PlotHeatmap_doublePtr(
228 label_ptr,
229 self.values.as_ptr(),
230 rows,
231 cols,
232 self.scale_min,
233 self.scale_max,
234 std::ptr::null(),
235 self.bounds_min,
236 self.bounds_max,
237 spec,
238 );
239 }),
240 })
241 }
242
243 fn label(&self) -> &str {
244 self.label
245 }
246}
247
248pub struct HeatmapPlotF32<'a, F = &'static str> {
250 label: &'a str,
251 values: &'a [f32],
252 style: PlotItemStyle,
253 rows: usize,
254 cols: usize,
255 scale_min: f64,
256 scale_max: f64,
257 label_fmt: Option<F>,
258 bounds_min: sys::ImPlotPoint,
259 bounds_max: sys::ImPlotPoint,
260 flags: HeatmapFlags,
261 item_flags: ItemFlags,
262}
263
264impl<F> super::PlotItemStyled for HeatmapPlotF32<'_, F> {
265 fn style_mut(&mut self) -> &mut PlotItemStyle {
266 &mut self.style
267 }
268}
269
270impl<'a> HeatmapPlotF32<'a> {
271 pub fn new(label: &'a str, values: &'a [f32], rows: usize, cols: usize) -> Self {
273 Self {
274 label,
275 values,
276 style: PlotItemStyle::default(),
277 rows,
278 cols,
279 scale_min: 0.0,
280 scale_max: 0.0,
281 label_fmt: Some("%.1f"),
282 bounds_min: sys::ImPlotPoint { x: 0.0, y: 0.0 },
283 bounds_max: sys::ImPlotPoint { x: 1.0, y: 1.0 },
284 flags: HeatmapFlags::NONE,
285 item_flags: ItemFlags::NONE,
286 }
287 }
288}
289
290impl<'a, F: AsRef<str>> HeatmapPlotF32<'a, F> {
291 pub fn with_scale(mut self, min: f64, max: f64) -> Self {
293 self.scale_min = min;
294 self.scale_max = max;
295 self
296 }
297
298 pub fn with_label_format<'fmt>(
300 self,
301 format: FloatFormat<'fmt>,
302 ) -> HeatmapPlotF32<'a, FloatFormat<'fmt>> {
303 HeatmapPlotF32 {
304 label: self.label,
305 values: self.values,
306 style: self.style,
307 rows: self.rows,
308 cols: self.cols,
309 scale_min: self.scale_min,
310 scale_max: self.scale_max,
311 label_fmt: Some(format),
312 bounds_min: self.bounds_min,
313 bounds_max: self.bounds_max,
314 flags: self.flags,
315 item_flags: self.item_flags,
316 }
317 }
318
319 pub fn try_label_format<'fmt>(
321 self,
322 format: impl Into<Cow<'fmt, str>>,
323 ) -> Result<HeatmapPlotF32<'a, FloatFormat<'fmt>>, FloatFormatError> {
324 Ok(self.with_label_format(FloatFormat::new(format)?))
325 }
326
327 pub fn without_value_labels(mut self) -> Self {
329 self.label_fmt = None;
330 self
331 }
332
333 pub fn with_bounds(mut self, min_x: f64, min_y: f64, max_x: f64, max_y: f64) -> Self {
335 self.bounds_min = sys::ImPlotPoint { x: min_x, y: min_y };
336 self.bounds_max = sys::ImPlotPoint { x: max_x, y: max_y };
337 self
338 }
339
340 pub fn with_flags(mut self, flags: HeatmapFlags) -> Self {
342 self.flags = flags;
343 self
344 }
345
346 pub fn with_item_flags(mut self, flags: ItemFlags) -> Self {
348 self.item_flags = flags;
349 self
350 }
351
352 pub fn column_major(mut self) -> Self {
354 self.flags |= HeatmapFlags::COL_MAJOR;
355 self
356 }
357
358 pub fn validate(&self) -> Result<(), PlotError> {
360 if self.values.is_empty() {
361 return Err(PlotError::EmptyData);
362 }
363
364 validate_grid_counts(
365 "HeatmapPlotF32::validate()",
366 self.rows,
367 self.cols,
368 self.values.len(),
369 )
370 }
371}
372
373impl<F: AsRef<str>> Plot for HeatmapPlotF32<'_, F> {
374 fn plot(&self, plot_ui: &crate::PlotUi<'_>) {
375 if self.validate().is_err() {
376 return;
377 }
378 let Ok(rows) = heatmap_count_to_i32("HeatmapPlotF32::plot()", "rows", self.rows) else {
379 return;
380 };
381 let Ok(cols) = heatmap_count_to_i32("HeatmapPlotF32::plot()", "cols", self.cols) else {
382 return;
383 };
384 let label_fmt = self.label_fmt.as_ref().map(AsRef::as_ref);
385 plot_ui.with_bound_context(|| match label_fmt {
386 Some(label_fmt) => {
387 let label = if self.label.contains('\0') {
388 ""
389 } else {
390 self.label
391 };
392 with_scratch_txt_two(label, label_fmt, |label_ptr, label_fmt_ptr| unsafe {
393 let spec = plot_spec_with_style(
394 self.style,
395 self.flags.bits() | self.item_flags.bits(),
396 PlotDataLayout::DEFAULT,
397 );
398 sys::ImPlot_PlotHeatmap_FloatPtr(
399 label_ptr,
400 self.values.as_ptr(),
401 rows,
402 cols,
403 self.scale_min,
404 self.scale_max,
405 label_fmt_ptr,
406 self.bounds_min,
407 self.bounds_max,
408 spec,
409 );
410 })
411 }
412 None => with_plot_str_or_empty(self.label, |label_ptr| unsafe {
413 let spec = plot_spec_with_style(
414 self.style,
415 self.flags.bits() | self.item_flags.bits(),
416 PlotDataLayout::DEFAULT,
417 );
418 sys::ImPlot_PlotHeatmap_FloatPtr(
419 label_ptr,
420 self.values.as_ptr(),
421 rows,
422 cols,
423 self.scale_min,
424 self.scale_max,
425 std::ptr::null(),
426 self.bounds_min,
427 self.bounds_max,
428 spec,
429 );
430 }),
431 })
432 }
433
434 fn label(&self) -> &str {
435 self.label
436 }
437}
438
439impl<'ui> crate::PlotUi<'ui> {
441 pub fn heatmap_plot(
443 &self,
444 label: &str,
445 values: &[f64],
446 rows: usize,
447 cols: usize,
448 ) -> Result<(), PlotError> {
449 let plot = HeatmapPlot::new(label, values, rows, cols);
450 plot.validate()?;
451 plot.plot(self);
452 Ok(())
453 }
454
455 pub fn heatmap_plot_f32(
457 &self,
458 label: &str,
459 values: &[f32],
460 rows: usize,
461 cols: usize,
462 ) -> Result<(), PlotError> {
463 let plot = HeatmapPlotF32::new(label, values, rows, cols);
464 plot.validate()?;
465 plot.plot(self);
466 Ok(())
467 }
468
469 pub fn heatmap_plot_scaled(
471 &self,
472 label: &str,
473 values: &[f64],
474 rows: usize,
475 cols: usize,
476 scale_min: f64,
477 scale_max: f64,
478 bounds_min: sys::ImPlotPoint,
479 bounds_max: sys::ImPlotPoint,
480 ) -> Result<(), PlotError> {
481 let plot = HeatmapPlot::new(label, values, rows, cols)
482 .with_scale(scale_min, scale_max)
483 .with_bounds_points(bounds_min, bounds_max);
484 plot.validate()?;
485 plot.plot(self);
486 Ok(())
487 }
488}
489
490#[cfg(test)]
491mod tests {
492 use super::{HeatmapPlot, HeatmapPlotF32};
493 use crate::PlotError;
494
495 fn invalid_data_message(err: PlotError) -> String {
496 match err {
497 PlotError::InvalidData(message) => message,
498 other => panic!("expected invalid data error, got {other:?}"),
499 }
500 }
501
502 #[test]
503 fn heatmap_rejects_zero_counts_before_ffi() {
504 let values = [1.0];
505 let err = HeatmapPlot::new("heat", &values, 0, 1)
506 .validate()
507 .expect_err("zero row count must be rejected");
508 assert!(invalid_data_message(err).contains("Rows and columns must be positive"));
509 }
510
511 #[test]
512 fn heatmap_rejects_count_multiplication_overflow_before_ffi() {
513 let values = [1.0];
514 let err = HeatmapPlot::new("heat", &values, usize::MAX, 2)
515 .validate()
516 .expect_err("overflowing grid size must be rejected");
517 assert!(invalid_data_message(err).contains("rows * cols overflowed"));
518 }
519
520 #[test]
521 fn heatmap_rejects_i32_count_overflow_before_ffi() {
522 let values = [1.0];
523 let err = HeatmapPlot::new("heat", &values, i32::MAX as usize + 1, 1)
524 .validate()
525 .expect_err("oversized row count must be rejected");
526 assert!(invalid_data_message(err).contains("rows exceeded ImPlot's i32 range"));
527 }
528
529 #[test]
530 fn heatmap_f32_uses_checked_grid_counts() {
531 let values = [1.0f32];
532 let err = HeatmapPlotF32::new("heat", &values, 1, i32::MAX as usize + 1)
533 .validate()
534 .expect_err("oversized column count must be rejected");
535 assert!(invalid_data_message(err).contains("cols exceeded ImPlot's i32 range"));
536 }
537}