diff --git a/demo/src/plot_demo.rs b/demo/src/plot_demo.rs index 01d8e21..9407db1 100644 --- a/demo/src/plot_demo.rs +++ b/demo/src/plot_demo.rs @@ -24,6 +24,7 @@ enum Panel { Interaction, CustomAxes, LinkedAxes, + Heatmap, } impl Default for Panel { @@ -44,6 +45,7 @@ pub struct PlotDemo { interaction_demo: InteractionDemo, custom_axes_demo: CustomAxesDemo, linked_axes_demo: LinkedAxesDemo, + heatmap_demo: HeatmapDemo, open_panel: Panel, } @@ -124,6 +126,7 @@ impl PlotDemo { ui.selectable_value(&mut self.open_panel, Panel::Interaction, "Interaction"); ui.selectable_value(&mut self.open_panel, Panel::CustomAxes, "Custom Axes"); ui.selectable_value(&mut self.open_panel, Panel::LinkedAxes, "Linked Axes"); + ui.selectable_value(&mut self.open_panel, Panel::Heatmap, "Heatmap"); }); ui.separator(); @@ -152,6 +155,9 @@ impl PlotDemo { Panel::LinkedAxes => { self.linked_axes_demo.ui(ui); } + Panel::Heatmap => { + self.heatmap_demo.ui(ui); + } } } } @@ -1204,3 +1210,92 @@ fn is_approx_zero(val: f64) -> bool { fn is_approx_integer(val: f64) -> bool { val.fract().abs() < 1e-6 } + +#[derive(PartialEq, serde::Serialize, serde::Deserialize)] +struct HeatmapDemo { + tick: f64, + animate: bool, + show_labels: bool, + palette: Vec, + rows: usize, + cols: usize, +} + +pub const TURBO_COLORMAP: [Color32; 10] = [ + Color32::from_rgb(48, 18, 59), + Color32::from_rgb(35, 106, 141), + Color32::from_rgb(30, 160, 140), + Color32::from_rgb(88, 200, 98), + Color32::from_rgb(164, 223, 39), + Color32::from_rgb(228, 223, 14), + Color32::from_rgb(250, 187, 13), + Color32::from_rgb(246, 135, 8), + Color32::from_rgb(213, 68, 2), + Color32::from_rgb(122, 4, 2), +]; + +impl Default for HeatmapDemo { + fn default() -> Self { + Self { + tick: 0.0, + animate: false, + show_labels: true, + palette: TURBO_COLORMAP.to_vec(), + rows: 10, + cols: 15, + } + } +} + +impl HeatmapDemo { + fn ui(&mut self, ui: &mut egui::Ui) -> Response { + ui.checkbox(&mut self.animate, "Animate"); + if self.animate { + ui.ctx().request_repaint(); + self.tick += 1.0; + } + ui.checkbox(&mut self.show_labels, "Show labels"); + ui.add(egui::Slider::new(&mut self.rows, 1..=100).text("Rows")); + ui.add(egui::Slider::new(&mut self.cols, 1..=100).text("Columns")); + let mut values = Vec::new(); + for y in 0..self.rows { + for x in 0..self.cols { + let y = y as f64; + let x = x as f64; + let cols = self.cols as f64; + let rows = self.rows as f64; + values.push(((x + self.tick) / rows).sin() + ((y + self.tick) / cols).cos()); + } + } + ui.add_enabled_ui(self.palette.len() > 1, |ui| { + if ui.button("Pop color").clicked() { + self.palette.pop(); + } + }); + if ui.button("Push color").clicked() { + self.palette + .push(*self.palette.last().expect("Palette is empty")); + } + ui.horizontal(|ui| { + for color in &mut self.palette { + ui.color_edit_button_srgba(color); + } + }); + + let heatmap = egui_plot::Heatmap::<128>::new(values, self.cols) + .expect("Failed to create heatmap") + .palette(&self.palette) + .show_labels(self.show_labels); + Plot::new("Heatmap Demo") + .legend(Legend::default()) + .allow_zoom(false) + .allow_scroll(false) + .allow_drag(false) + .allow_boxed_zoom(false) + .set_margin_fraction(vec2(0.0, 0.0)) + .show(ui, |plot_ui| { + plot_ui.heatmap(heatmap); + }) + .response + } +} diff --git a/demo/tests/snapshots/demos/Charts.png b/demo/tests/snapshots/demos/Charts.png index 55b7adf..d8c1202 100644 --- a/demo/tests/snapshots/demos/Charts.png +++ b/demo/tests/snapshots/demos/Charts.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a8a4457888659af1357055da72f8b0cbc6a8c87fb1d48f53e6e3686edd862d21 -size 85593 +oid sha256:14e1155781f5923022c731cdfa37fcc0b250427d99b520d0dcce0c8b6a240898 +size 86572 diff --git a/demo/tests/snapshots/demos/Custom Axes.png b/demo/tests/snapshots/demos/Custom Axes.png index 16ff1d8..906767b 100644 --- a/demo/tests/snapshots/demos/Custom Axes.png +++ b/demo/tests/snapshots/demos/Custom Axes.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1f761e349271bc852c61854b52e206ee9fa2130526207f0d4e0f199df3fc7fe7 -size 70547 +oid sha256:21d334207879c3e923faaa135049ce97ac5908d3caefbb0e221d0bd0560a9002 +size 71525 diff --git a/demo/tests/snapshots/demos/Heatmap.png b/demo/tests/snapshots/demos/Heatmap.png new file mode 100644 index 0000000..e8540d7 --- /dev/null +++ b/demo/tests/snapshots/demos/Heatmap.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:36d8f726d389cd63036c150bff8bea11b028018bf37477109d185ee770333dd7 +size 97962 diff --git a/demo/tests/snapshots/demos/Interaction.png b/demo/tests/snapshots/demos/Interaction.png index 6b18aa8..a6f0d57 100644 --- a/demo/tests/snapshots/demos/Interaction.png +++ b/demo/tests/snapshots/demos/Interaction.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cd029ed29d21fcaa9ea707dca29b4aa165915399bcb97c2f33b0c53f8749092a -size 77248 +oid sha256:0c5af8b1f1a487670a7e8c7896c784e518611c2bcaa9e825365bf8f2ade7b533 +size 78227 diff --git a/demo/tests/snapshots/demos/Items.png b/demo/tests/snapshots/demos/Items.png index 75defb6..18c83e8 100644 --- a/demo/tests/snapshots/demos/Items.png +++ b/demo/tests/snapshots/demos/Items.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eb41447732a0cadb2ceedc0412bfcba684d6cdc065b3cbbafb28af9bee04fec1 -size 109204 +oid sha256:ea040c08efcdc6811d24217d13d79ee0783e5a55d5e018ffec71f5efc994683a +size 110204 diff --git a/demo/tests/snapshots/demos/Legend.png b/demo/tests/snapshots/demos/Legend.png index e1dbe30..023c46b 100644 --- a/demo/tests/snapshots/demos/Legend.png +++ b/demo/tests/snapshots/demos/Legend.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f390bfb068ad93738a8fed063e8f9e79a7f7c91d3af959262a3e6b0f0c6dd736 -size 131120 +oid sha256:573778d46ed83649d51d7dbb194de85919cac91d25b1030b72ed606578eb59e5 +size 132098 diff --git a/demo/tests/snapshots/demos/Lines.png b/demo/tests/snapshots/demos/Lines.png index 7d03fba..903bc4d 100644 --- a/demo/tests/snapshots/demos/Lines.png +++ b/demo/tests/snapshots/demos/Lines.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eb6bc7412c003876604b206052eb91de42009e992a4577b3216195edc66d7ac1 -size 122333 +oid sha256:0877c7aab344b2eb9dbaf09e67d3a69f45bb834a44d0938d894f7fa9ee785871 +size 123282 diff --git a/demo/tests/snapshots/demos/Linked Axes.png b/demo/tests/snapshots/demos/Linked Axes.png index b71adeb..9c061ec 100644 --- a/demo/tests/snapshots/demos/Linked Axes.png +++ b/demo/tests/snapshots/demos/Linked Axes.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:28a6deff5454549b42e080eae6ad215cd668b6a343569e92b4e9396b5f49cc97 -size 83985 +oid sha256:4798f1c3159f54087802dae01d098c96aa83e0f25030318bd73df63749f85fbe +size 84961 diff --git a/demo/tests/snapshots/demos/Markers.png b/demo/tests/snapshots/demos/Markers.png index 229aceb..9f21412 100644 --- a/demo/tests/snapshots/demos/Markers.png +++ b/demo/tests/snapshots/demos/Markers.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ee6b61d601076c9345362382d8abad26019452cffbc85fa9ad26b47bfb760452 -size 105842 +oid sha256:55ed31b18deb6f76bf7b812dce702c1c7895a7f39b55113a8c39125624af3ecf +size 106817 diff --git a/demo/tests/snapshots/light_mode.png b/demo/tests/snapshots/light_mode.png index 1d0f926..5bc1924 100644 --- a/demo/tests/snapshots/light_mode.png +++ b/demo/tests/snapshots/light_mode.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a95e6beb9c992efe9e98bd9b461831c7feaff9bffd0f9a724282571e983d34b2 -size 118734 +oid sha256:1e75c2fdeeca48f557fcf3c9ebf73c6e2e42e1d5c53720751d780510b9fdb2eb +size 119651 diff --git a/demo/tests/snapshots/scale_0.50.png b/demo/tests/snapshots/scale_0.50.png index bee8f59..ea12b8c 100644 --- a/demo/tests/snapshots/scale_0.50.png +++ b/demo/tests/snapshots/scale_0.50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:070365cd0e9b3ef6891bf830dea35bfd4849520f8e511eb5086780ea7beffb70 -size 61532 +oid sha256:8af17622490b1ad97399c6bd35654416371629594012da60c0d39be035495a52 +size 61990 diff --git a/demo/tests/snapshots/scale_1.00.png b/demo/tests/snapshots/scale_1.00.png index e3d0999..a0d6412 100644 --- a/demo/tests/snapshots/scale_1.00.png +++ b/demo/tests/snapshots/scale_1.00.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:48c84fd502e599ab4fcd3e2950eea41e23585e3f35d613f39f8987782af92c3e -size 122190 +oid sha256:aa1169abd88b54540d2cd9f126d082ba10a8a64de12588f4987e4bbc730ec0f7 +size 123139 diff --git a/demo/tests/snapshots/scale_1.39.png b/demo/tests/snapshots/scale_1.39.png index bd6c4b0..09ff327 100644 --- a/demo/tests/snapshots/scale_1.39.png +++ b/demo/tests/snapshots/scale_1.39.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bd3685434100daab51c8fc057f5d3e9b818c4873f96a62e071dfb267f8b6d763 -size 227418 +oid sha256:9a4dde9c9b69381a7784ffb2b77efbac98109e7ee7450fe89b63187bd1f123b9 +size 231912 diff --git a/demo/tests/snapshots/scale_2.00.png b/demo/tests/snapshots/scale_2.00.png index f95c551..d11c401 100644 --- a/demo/tests/snapshots/scale_2.00.png +++ b/demo/tests/snapshots/scale_2.00.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:93b014741cbfb4d18ed805871e967796bcd1d6749487dc23828b644b599df745 -size 259898 +oid sha256:60f099ecaa38b2b2e0c9c5ff2acddd87ccccac81c40cc1f3c2f300502378b02c +size 262047 diff --git a/egui_plot/src/items/heatmap.rs b/egui_plot/src/items/heatmap.rs new file mode 100644 index 0000000..f33d752 --- /dev/null +++ b/egui_plot/src/items/heatmap.rs @@ -0,0 +1,469 @@ +use std::ops::RangeInclusive; + +use egui::{Color32, Mesh, NumExt as _, Pos2, Rect, Rgba, Shape, TextStyle, Ui, Vec2, WidgetText}; + +use crate::{ClosestElem, PlotConfig, PlotGeometry, PlotItem, PlotItemBase, PlotPoint}; + +use super::{Cursor, LabelFormatter, PlotBounds, PlotTransform}; +use emath::Float as _; + +/// Default base colors for heatmap palette +pub const BASE_COLORS: [Color32; 10] = [ + Color32::from_rgb(48, 18, 59), + Color32::from_rgb(35, 106, 141), + Color32::from_rgb(30, 160, 140), + Color32::from_rgb(88, 200, 98), + Color32::from_rgb(164, 223, 39), + Color32::from_rgb(228, 223, 14), + Color32::from_rgb(250, 187, 13), + Color32::from_rgb(246, 135, 8), + Color32::from_rgb(213, 68, 2), + Color32::from_rgb(122, 4, 2), +]; + +#[derive(Debug)] +pub enum HeatmapErr { + ZeroColumns, + ZeroRows, + BadLength, +} + +impl std::fmt::Display for HeatmapErr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::ZeroColumns => write!(f, "number of columns must not be zero"), + Self::ZeroRows => write!(f, "number of rows must not be zero"), + Self::BadLength => write!( + f, + "length of value vector is not divisible by number of columns" + ), + } + } +} + +impl std::error::Error for HeatmapErr {} + +/// A heatmap. +pub struct Heatmap { + base: PlotItemBase, + + /// occupied space in absolute plot coordinates + pos: PlotPoint, + + /// values to plot + pub(crate) values: Vec, + + /// number of columns in heatmap + cols: usize, + + /// number of rows in heatmap + rows: usize, + + /// minimum value in colormap. + /// Everything smaller will be mapped to the first color in `palette` + min: f64, + + /// maximum value in heatmap + /// Everything greater will be mapped to `palette.last()` + max: f64, + + /// formatter for labels on tiles + formatter: Box String>, + + /// custom mapping of values to color + custom_mapping: Option Color32>>, + + /// show labels on tiles + show_labels: bool, + + /// possible colors, sorted by index + palette: [Color32; RESOLUTION], + + /// is widget is highlighted + highlight: bool, + + /// plot name + name: String, + + /// Size of one tile in plot coordinates + tile_size: Vec2, +} + +impl PartialEq for Heatmap { + /// manual implementation of `PartialEq` because formatter and color mapping do not impl `PartialEq`. + /// + /// > NOTE: custom_mapping and formatter are ignored + fn eq(&self, other: &Self) -> bool { + self.pos == other.pos + && self.values == other.values + && self.cols == other.cols + && self.rows == other.rows + && self.min == other.min + && self.max == other.max + && self.show_labels == other.show_labels + && self.palette == other.palette + && self.highlight == other.highlight + && self.name == other.name + && self.tile_size == other.tile_size + } +} + +impl Heatmap { + /// Create a 2D heatmap. Will automatically infer number of rows. + /// + /// - `values` contains magnitude of each tile. The alignment is row by row. + /// - `cols` is the number of columns (i.e. the length of each row) and must not be zero. + /// - `values.len()` must be a multiple of `cols`. + /// + /// Example: To display this + /// + /// | -- | -- | + /// | 0.0 | 0.1 | + /// | 0.3 | 0.4 | + /// + /// pass `values = vec![0.0, 0.1, 0.3, 0.4]` and `cols = 2`. + /// + /// Returns error type if provided parameters are inconsistent + /// + /// # Errors + /// - `HeatmapErr::ZeroColumns` if `cols` is zero + /// - `HeatmapErr::ZeroRows` if `values` is empty + /// - `HeatmapErr::BadLength` if `values.len()` is not divisible by `cols` + pub fn new(values: Vec, cols: usize) -> Result { + if cols == 0 { + return Err(HeatmapErr::ZeroColumns); + } + if (values.len() % cols) != 0 { + return Err(HeatmapErr::BadLength); + } + let rows = values.len() / cols; + if rows == 0 { + return Err(HeatmapErr::ZeroRows); + } + + // determine range + let mut min = f64::MAX; + let mut max = f64::MIN; + for v in &values { + min = min.min(*v); + max = max.max(*v); + } + + Ok(Self { + base: PlotItemBase::new(String::new()), + pos: PlotPoint { x: 0.0, y: 0.0 }, + values, + cols, + rows, + min, + max, + formatter: Box::new(|v| format!("{v:.1}")), + custom_mapping: None, + show_labels: true, + palette: Self::linear_gradient_from_base_colors(&BASE_COLORS), + highlight: false, + name: String::new(), + tile_size: Vec2 { x: 1.0, y: 1.0 }, + }) + } + + /// Set color palette by specifying base colors from low to high + #[inline] + pub fn palette(mut self, base_colors: &[Color32]) -> Self { + self.palette = Self::linear_gradient_from_base_colors(base_colors); + self + } + + /// Interpolate linear gradient with `RESOLUTION` steps from an arbitrary number of base colors. + fn linear_gradient_from_base_colors(base_colors: &[Color32]) -> [Color32; RESOLUTION] { + let mut interpolated = [Color32::TRANSPARENT; RESOLUTION]; + if base_colors.is_empty() { + return interpolated; + } + if base_colors.len() == 1 { + // single color, no gradient + interpolated = [base_colors[0]; RESOLUTION]; + return interpolated; + } + for (i, color) in interpolated.iter_mut().enumerate() { + let i_rel: f64 = i as f64 / (RESOLUTION - 1) as f64; + if i_rel == 1.0 { + // last element + *color = *base_colors.last().expect("Base colors should not be empty"); + } else { + let base_index_float: f64 = i_rel * (base_colors.len() - 1) as f64; + let base_index: usize = base_index_float as usize; + let start_color = base_colors[base_index]; + let end_color = base_colors[base_index + 1]; + let gradient_level = base_index_float - base_index as f64; + + let delta_r = (end_color.r() as f64 - start_color.r() as f64) * gradient_level; + let delta_g = (end_color.g() as f64 - start_color.g() as f64) * gradient_level; + let delta_b = (end_color.b() as f64 - start_color.b() as f64) * gradient_level; + + // interpolate + let r = (start_color.r() as f64 + delta_r).round() as u8; + let g = (start_color.g() as f64 + delta_g).round() as u8; + let b = (start_color.b() as f64 + delta_b).round() as u8; + *color = Color32::from_rgb(r, g, b); + } + } + interpolated + } + + /// Specify custom range of values to map onto color palette. + /// + /// - `min` and everything smaller will be the first color on the color palette. + /// - `max` and everything greater will be the last color on the color palette. + #[inline] + pub fn range(mut self, min: f64, max: f64) -> Self { + assert!(min < max, "min must be smaller than max"); + self.min = min; + self.max = max; + self + } + + /// Add a custom way to format an element. + /// Can be used to display a set number of decimals or custom labels. + #[inline] + pub fn formatter(mut self, formatter: Box String>) -> Self { + self.formatter = formatter; + self + } + + /// Add a custom way to map values to a color. + #[inline] + pub fn custom_mapping(mut self, custom_mapping: Box Color32>) -> Self { + self.custom_mapping = Some(custom_mapping); + self + } + + /// Show labels for each tile in heatmap. Defaults to 'true' + #[inline] + pub fn show_labels(mut self, en: bool) -> Self { + self.show_labels = en; + self + } + + /// Place lower left corner of heatmap at `pos`. Default is (0.0, 0.0) + #[inline] + pub fn at(mut self, pos: PlotPoint) -> Self { + self.pos = pos; + self + } + + /// Name of this heatmap. + /// + /// This name will show up in the plot legend, if legends are turned on. Multiple heatmaps may + /// share the same name, in which case they will also share an entry in the legend. + #[expect(clippy::needless_pass_by_value)] + #[inline] + pub fn name(mut self, name: impl ToString) -> Self { + self.name = name.to_string(); + self + } + + /// Manually set width and height of tiles in plot coordinate space. + #[inline] + pub fn tile_size(mut self, x: f32, y: f32) -> Self { + self.tile_size = Vec2 { x, y }; + self + } + + /// Set size of heatmap in plot coordinate space. + /// Will adjust the heatmap tile size in plot coordinate space. + #[inline] + pub fn size(mut self, x: f32, y: f32) -> Self { + self.tile_size = Vec2 { + x: x / self.cols as f32, + y: y / self.rows as f32, + }; + self + } + + /// Highlight all plot elements. + #[inline] + pub fn highlight(mut self, highlight: bool) -> Self { + // FIXME: for some reason highlighting is not detected + self.highlight = highlight; + self + } + + fn push_shapes(&self, ui: &Ui, transform: &PlotTransform, shapes: &mut Vec) { + let mut mesh = Mesh::default(); + let mut labels: Vec = Vec::new(); + for i in 0..self.values.len() { + let (rect, color, text) = self.tile_view_info(ui, transform, i); + mesh.add_colored_rect(rect, color); + if self.show_labels { + labels.push(text); + } + } + shapes.push(Shape::mesh(mesh)); + if self.show_labels { + shapes.extend(labels); + } + } + + fn tile_view_info( + &self, + ui: &Ui, + transform: &PlotTransform, + index: usize, + ) -> (Rect, Color32, Shape) { + let v = self.values[index]; + + // calculate color value + let mut fill_color: Color32; + if let Some(mapping) = &self.custom_mapping { + fill_color = mapping(v); + } else { + // convert to value in [0.0, 1.0] + let v_rel = (v - self.min) / (self.max - self.min); + + // convert to color palette index + let palette_index = (v_rel * (self.palette.len() - 1) as f64).round() as usize; + + fill_color = self.palette[palette_index]; + } + + if self.highlight { + let fill = Rgba::from(fill_color); + let fill_alpha = (2.0 * fill.a()).at_most(1.0); + let fill = fill.to_opaque().multiply(fill_alpha); + fill_color = fill.into(); + } + + let x = index % self.cols; + let y = index / self.cols; + let tile_rect = transform.rect_from_values( + &PlotPoint { + x: self.pos.x + self.tile_size.x as f64 * x as f64, + y: self.pos.y + self.tile_size.y as f64 * y as f64, + }, + &PlotPoint { + x: self.pos.x + self.tile_size.x as f64 * (x + 1) as f64, + y: self.pos.y + self.tile_size.y as f64 * (y + 1) as f64, + }, + ); + // Text + + let text: WidgetText = (self.formatter)(v).into(); + + // calculate color that is readable on coloured tiles + let luminance = 0.2126 * fill_color.r() as f32 + + 0.7151 * fill_color.g() as f32 + + 0.0721 * fill_color.b() as f32; + + let inverted_color = if luminance < 140.0 { + Color32::WHITE + } else { + Color32::BLACK + }; + + let text = text.color(inverted_color); + let galley = text.into_galley( + ui, + Some(egui::TextWrapMode::Truncate), + f32::INFINITY, + TextStyle::Monospace, + ); + let text_pos = tile_rect.center() - galley.size() / 2.0; + + let text = Shape::galley(text_pos, galley.clone(), Color32::WHITE); + (tile_rect, fill_color, text) + } +} + +impl PlotItem for Heatmap { + fn shapes(&self, ui: &Ui, transform: &PlotTransform, shapes: &mut Vec) { + self.push_shapes(ui, transform, shapes); + } + + fn initialize(&mut self, _x_range: RangeInclusive) { + // nothing to do + } + + fn name(&self) -> &str { + self.name.as_str() + } + + // FIXME: heatmap has multiple colors + fn color(&self) -> Color32 { + self.palette[0] + } + + fn highlight(&mut self) { + self.highlight = true; + } + + fn highlighted(&self) -> bool { + self.highlight + } + + fn geometry(&self) -> PlotGeometry<'_> { + PlotGeometry::Rects + } + + fn bounds(&self) -> PlotBounds { + PlotBounds { + min: [self.pos.x, self.pos.y], + max: [ + self.pos.x + self.tile_size.x as f64 * self.cols as f64, + self.pos.y + self.tile_size.y as f64 * self.rows as f64, + ], + } + } + + fn find_closest(&self, point: Pos2, transform: &PlotTransform) -> Option { + self.values + .clone() // FIXME: is there a better solution that cloning? + .into_iter() + .enumerate() + .map(|(index, _)| { + let x = index % self.cols; + let y = index / self.cols; + let tile_rect = transform.rect_from_values( + &PlotPoint { + x: self.pos.x + self.tile_size.x as f64 * x as f64, + y: self.pos.y + self.tile_size.y as f64 * y as f64, + }, + &PlotPoint { + x: self.pos.x + self.tile_size.x as f64 * (x + 1) as f64, + y: self.pos.y + self.tile_size.y as f64 * (y + 1) as f64, + }, + ); + let dist_sq = tile_rect.distance_sq_to_pos(point); + + ClosestElem { index, dist_sq } + }) + .min_by_key(|e| e.dist_sq.ord()) + } + + fn on_hover( + &self, + _plot_area_response: &egui::Response, + elem: ClosestElem, + shapes: &mut Vec, + _cursors: &mut Vec, + plot: &PlotConfig<'_>, + _: &LabelFormatter<'_>, + ) { + let (rect, color, text) = self.tile_view_info(plot.ui, plot.transform, elem.index); + let mut mesh = Mesh::default(); + mesh.add_colored_rect(rect, color); + shapes.push(Shape::mesh(mesh)); + if self.show_labels { + shapes.push(text); + } + // v.add_rulers_and_text(self, plot, shapes, cursors); TODO(?) + } + + fn base(&self) -> &super::PlotItemBase { + &self.base + } + + fn base_mut(&mut self) -> &mut super::PlotItemBase { + &mut self.base + } +} diff --git a/egui_plot/src/items/mod.rs b/egui_plot/src/items/mod.rs index afdfc8a..2c63dac 100644 --- a/egui_plot/src/items/mod.rs +++ b/egui_plot/src/items/mod.rs @@ -24,6 +24,7 @@ pub use values::{ mod bar; mod box_elem; +pub mod heatmap; mod rect_elem; mod values; diff --git a/egui_plot/src/lib.rs b/egui_plot/src/lib.rs index afc721b..e1f6a81 100644 --- a/egui_plot/src/lib.rs +++ b/egui_plot/src/lib.rs @@ -30,6 +30,7 @@ pub use crate::{ Arrows, Bar, BarChart, BoxElem, BoxPlot, BoxSpread, ClosestElem, HLine, Line, LineStyle, MarkerShape, Orientation, PlotConfig, PlotGeometry, PlotImage, PlotItem, PlotItemBase, PlotPoint, PlotPoints, Points, Polygon, Text, VLine, + heatmap::{Heatmap, HeatmapErr}, }, legend::{ColorConflictHandling, Corner, Legend}, memory::PlotMemory, diff --git a/egui_plot/src/plot_ui.rs b/egui_plot/src/plot_ui.rs index 61eb159..9f2f77b 100644 --- a/egui_plot/src/plot_ui.rs +++ b/egui_plot/src/plot_ui.rs @@ -256,4 +256,12 @@ impl<'a> PlotUi<'a> { } self.items.push(Box::new(chart)); } + + /// Add a heatmap. + pub fn heatmap(&mut self, heatmap: crate::Heatmap) { + if heatmap.values.is_empty() { + return; + } + self.items.push(Box::new(heatmap)); + } }