diff --git a/crates/egui_demo_lib/src/demo/context_menu.rs b/crates/egui_demo_lib/src/demo/context_menu.rs index ffcc73379d8..34b4b030f50 100644 --- a/crates/egui_demo_lib/src/demo/context_menu.rs +++ b/crates/egui_demo_lib/src/demo/context_menu.rs @@ -149,7 +149,9 @@ impl ContextMenus { .width(self.width) .height(self.height) .data_aspect(1.0) - .show(ui, |plot_ui| plot_ui.line(line)) + .show(ui, |plot_ui| { + plot_ui.line(line); + }) .response } diff --git a/crates/egui_demo_lib/src/demo/plot_demo.rs b/crates/egui_demo_lib/src/demo/plot_demo.rs index fef3b4fc6dd..0f69956c271 100644 --- a/crates/egui_demo_lib/src/demo/plot_demo.rs +++ b/crates/egui_demo_lib/src/demo/plot_demo.rs @@ -21,6 +21,7 @@ enum Panel { Interaction, CustomAxes, LinkedAxes, + ScatterPlot, } impl Default for Panel { @@ -41,6 +42,7 @@ pub struct PlotDemo { interaction_demo: InteractionDemo, custom_axes_demo: CustomAxesDemo, linked_axes_demo: LinkedAxesDemo, + scatter_plot: ScatterPlot, open_panel: Panel, } @@ -87,6 +89,7 @@ impl super::View for 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::ScatterPlot, "Scatter Plot"); }); ui.separator(); @@ -115,6 +118,9 @@ impl super::View for PlotDemo { Panel::LinkedAxes => { self.linked_axes_demo.ui(ui); } + Panel::ScatterPlot => { + self.scatter_plot.ui(ui); + } } } } @@ -290,9 +296,10 @@ impl LineDemo { plot = plot.coordinates_formatter(Corner::LeftBottom, CoordinatesFormatter::default()); } plot.show(ui, |plot_ui| { - plot_ui.line(self.circle()); - plot_ui.line(self.sin()); - plot_ui.line(self.thingy()); + plot_ui + .line(self.circle()) + .line(self.sin()) + .line(self.thingy()); }) .response } @@ -320,7 +327,7 @@ impl Default for MarkerDemo { } impl MarkerDemo { - fn markers(&self) -> Vec { + fn markers(&self) -> Vec> { MarkerShape::all() .enumerate() .map(|(i, marker)| { @@ -368,7 +375,7 @@ impl MarkerDemo { markers_plot .show(ui, |plot_ui| { for marker in self.markers() { - plot_ui.points(marker); + plot_ui.owned_points(marker); } }) .response @@ -442,11 +449,12 @@ impl LegendDemo { .data_aspect(1.0); legend_plot .show(ui, |plot_ui| { - plot_ui.line(Self::line_with_slope(0.5).name("lines")); - plot_ui.line(Self::line_with_slope(1.0).name("lines")); - plot_ui.line(Self::line_with_slope(2.0).name("lines")); - plot_ui.line(Self::sin().name("sin(x)")); - plot_ui.line(Self::cos().name("cos(x)")); + plot_ui + .line(Self::line_with_slope(0.5).name("lines")) + .line(Self::line_with_slope(1.0).name("lines")) + .line(Self::line_with_slope(2.0).name("lines")) + .line(Self::sin().name("sin(x)")) + .line(Self::cos().name("cos(x)")); }) .response } @@ -636,12 +644,13 @@ impl LinkedAxesDemo { )) } - fn configure_plot(plot_ui: &mut egui_plot::PlotUi) { - plot_ui.line(Self::line_with_slope(0.5)); - plot_ui.line(Self::line_with_slope(1.0)); - plot_ui.line(Self::line_with_slope(2.0)); - plot_ui.line(Self::sin()); - plot_ui.line(Self::cos()); + fn configure_plot(plot_ui: &mut egui_plot::PlotUi<'_>) { + plot_ui + .line(Self::line_with_slope(0.5)) + .line(Self::line_with_slope(1.0)) + .line(Self::line_with_slope(2.0)) + .line(Self::sin()) + .line(Self::cos()); } fn ui(&mut self, ui: &mut Ui) -> Response { @@ -690,6 +699,97 @@ impl LinkedAxesDemo { // ---------------------------------------------------------------------------- +struct ScatterPlot { + points: Vec, + step: usize, + point_radius: f32, + fill_points: bool, +} + +impl PartialEq for ScatterPlot { + fn eq(&self, other: &Self) -> bool { + self.point_radius == other.point_radius && self.fill_points == other.fill_points + } +} + +impl Default for ScatterPlot { + fn default() -> Self { + Self::new() + } +} + +impl ScatterPlot { + fn new() -> Self { + Self { + points: Self::calculate_points(), + step: 1, + point_radius: 1.5, + fill_points: true, + } + } + + fn calculate_points() -> Vec { + (0..50_000) + .zip((-10..10).cycle()) + .filter_map(|(x, offset)| { + if offset != 0 { + let x = x as f64 / 1000.0; + let y = x * 1.5 + offset as f64; + Some(PlotPoint::new(x, y)) + } else { + None + } + }) + .collect() + } + + fn ui(&mut self, ui: &mut Ui) -> Response { + ui.label("Plot iterators!"); + ui.add( + egui::DragValue::new(&mut self.step) + .speed(1) + .clamp_range(1..=100) + .prefix("Filter point step: "), + ); + ui.horizontal(|ui| { + ui.checkbox(&mut self.fill_points, "Fill"); + ui.add( + egui::DragValue::new(&mut self.point_radius) + .speed(0.1) + .clamp_range(0.0..=f64::INFINITY) + .prefix("Radius: "), + ); + }); + + Plot::new("scatter_plot") + .legend(Legend::default().position(Corner::LeftTop)) + .y_axis_width(4) + .show_axes(true) + .show_grid(true) + // .view_aspect(1.0) + // .data_aspect(1.0) + .show(ui, |plot_ui| { + let points = Points::new_generic(self.points.iter().step_by(self.step)) + .name("Points") + .filled(self.fill_points) + .radius(self.point_radius); + + plot_ui.borrowed_points(points).line( + Line::new(PlotPoints::from_explicit_callback( + |x| x * 1.5, + 0.0..=50.0, + 100, + )) + .name("Line") + .width(self.point_radius * 1.5), + ); + }) + .response + } +} + +// ---------------------------------------------------------------------------- + #[derive(PartialEq, Default)] struct ItemsDemo { texture: Option, @@ -743,19 +843,20 @@ impl ItemsDemo { .show_y(false) .data_aspect(1.0); plot.show(ui, |plot_ui| { - plot_ui.hline(HLine::new(9.0).name("Lines horizontal")); - plot_ui.hline(HLine::new(-9.0).name("Lines horizontal")); - plot_ui.vline(VLine::new(9.0).name("Lines vertical")); - plot_ui.vline(VLine::new(-9.0).name("Lines vertical")); - plot_ui.line(line.name("Line with fill")); - plot_ui.polygon(polygon.name("Convex polygon")); - plot_ui.points(points.name("Points with stems")); - plot_ui.text(Text::new(PlotPoint::new(-3.0, -3.0), "wow").name("Text")); - plot_ui.text(Text::new(PlotPoint::new(-2.0, 2.5), "so graph").name("Text")); - plot_ui.text(Text::new(PlotPoint::new(3.0, 3.0), "much color").name("Text")); - plot_ui.text(Text::new(PlotPoint::new(2.5, -2.0), "such plot").name("Text")); - plot_ui.image(image.name("Image")); - plot_ui.arrows(arrows.name("Arrows")); + plot_ui + .hline(HLine::new(9.0).name("Lines horizontal")) + .hline(HLine::new(-9.0).name("Lines horizontal")) + .vline(VLine::new(9.0).name("Lines vertical")) + .vline(VLine::new(-9.0).name("Lines vertical")) + .line(line.name("Line with fill")) + .polygon(polygon.name("Convex polygon")) + .owned_points(points.name("Points with stems")) + .text(Text::new(PlotPoint::new(-3.0, -3.0), "wow").name("Text")) + .text(Text::new(PlotPoint::new(-2.0, 2.5), "so graph").name("Text")) + .text(Text::new(PlotPoint::new(3.0, 3.0), "much color").name("Text")) + .text(Text::new(PlotPoint::new(2.5, -2.0), "such plot").name("Text")) + .image(image.name("Image")) + .arrows(arrows.name("Arrows")); }) .response } @@ -789,13 +890,15 @@ impl InteractionDemo { inner: (screen_pos, pointer_coordinate, pointer_coordinate_drag_delta, bounds, hovered), .. } = plot.show(ui, |plot_ui| { - ( + let return_value = ( plot_ui.screen_from_plot(PlotPoint::new(0.0, 0.0)), plot_ui.pointer_coordinate(), plot_ui.pointer_coordinate_drag_delta(), plot_ui.plot_bounds(), plot_ui.response().hovered(), - ) + ); + + return_value }); ui.label(format!( @@ -925,7 +1028,9 @@ impl ChartsDemo { .y_axis_width(3) .allow_zoom(self.allow_zoom) .allow_drag(self.allow_drag) - .show(ui, |plot_ui| plot_ui.bar_chart(chart)) + .show(ui, |plot_ui| { + plot_ui.bar_chart(chart); + }) .response } @@ -985,10 +1090,11 @@ impl ChartsDemo { .data_aspect(1.0) .allow_drag(self.allow_drag) .show(ui, |plot_ui| { - plot_ui.bar_chart(chart1); - plot_ui.bar_chart(chart2); - plot_ui.bar_chart(chart3); - plot_ui.bar_chart(chart4); + plot_ui + .bar_chart(chart1) + .bar_chart(chart2) + .bar_chart(chart3) + .bar_chart(chart4); }) .response } @@ -1030,9 +1136,7 @@ impl ChartsDemo { .allow_zoom(self.allow_zoom) .allow_drag(self.allow_drag) .show(ui, |plot_ui| { - plot_ui.box_plot(box1); - plot_ui.box_plot(box2); - plot_ui.box_plot(box3); + plot_ui.box_plot(box1).box_plot(box2).box_plot(box3); }) .response } diff --git a/crates/egui_demo_lib/src/demo/widget_gallery.rs b/crates/egui_demo_lib/src/demo/widget_gallery.rs index 4189e61c9ff..b3c837fe922 100644 --- a/crates/egui_demo_lib/src/demo/widget_gallery.rs +++ b/crates/egui_demo_lib/src/demo/widget_gallery.rs @@ -269,7 +269,9 @@ fn example_plot(ui: &mut egui::Ui) -> egui::Response { .height(32.0) .show_axes(false) .data_aspect(1.0) - .show(ui, |plot_ui| plot_ui.line(line)) + .show(ui, |plot_ui| { + plot_ui.line(line); + }) .response } diff --git a/crates/egui_plot/src/items/mod.rs b/crates/egui_plot/src/items/mod.rs index 24bcb9d6ef9..c8325590c1c 100644 --- a/crates/egui_plot/src/items/mod.rs +++ b/crates/egui_plot/src/items/mod.rs @@ -66,6 +66,11 @@ pub(super) trait PlotItem { PlotGeometry::Rects => { panic!("If the PlotItem is made of rects, it should implement find_closest()") } + PlotGeometry::GenericPoints => { + panic!( + "If the PlotItem is made of Generic points, it should implement find_closest()" + ) + } } } @@ -85,6 +90,9 @@ pub(super) trait PlotItem { PlotGeometry::Rects => { panic!("If the PlotItem is made of rects, it should implement on_hover()") } + PlotGeometry::GenericPoints => { + panic!("If the PlotItem is made of generic points, it should implement on_hover()") + } }; let line_color = if plot.ui.visuals().dark_mode { @@ -112,6 +120,42 @@ pub(super) trait PlotItem { // ---------------------------------------------------------------------------- +/// Trait shared by structs that are plot points. +/// This abstracts behaviour away from owned, generated and borrowed plot points + +pub trait GenericPlotPoints { + type Item; + type Iter: Iterator + Clone; + + fn points(&self) -> Self::Iter; + + fn bounds(&self) -> PlotBounds; + + fn initialize(&mut self, _: RangeInclusive) {} +} + +impl<'a, T> GenericPlotPoints for T +where + T: Iterator + Clone, +{ + type Item = &'a PlotPoint; + type Iter = Self; + + fn points(&self) -> Self::Iter { + self.clone() + } + + fn bounds(&self) -> PlotBounds { + let mut bounds = PlotBounds::NOTHING; + for point in self.clone() { + bounds.extend_with(point); + } + bounds + } +} + +// ---------------------------------------------------------------------------- + /// A horizontal line in a plot, filling the full width #[derive(Clone, Debug, PartialEq)] pub struct HLine { @@ -771,8 +815,8 @@ impl PlotItem for Text { } /// A set of points. -pub struct Points { - pub(super) series: PlotPoints, +pub struct Points { + pub(super) series: T, pub(super) shape: MarkerShape, @@ -792,7 +836,25 @@ pub struct Points { pub(super) stems: Option, } -impl Points { +impl Points +where + T: GenericPlotPoints, +{ + pub fn new_generic(t: T) -> Self { + Self { + series: t, + shape: MarkerShape::Circle, + color: Color32::TRANSPARENT, + filled: true, + radius: 1.0, + name: Default::default(), + highlight: false, + stems: None, + } + } +} + +impl Points { pub fn new(series: impl Into) -> Self { Self { series: series.into(), @@ -805,7 +867,9 @@ impl Points { stems: None, } } +} +impl Points { /// Set the shape of the markers. #[inline] pub fn shape(mut self, shape: MarkerShape) -> Self { @@ -813,6 +877,12 @@ impl Points { self } + /// Set the shape of the markers. + #[inline] + pub fn shape_mut(&mut self, shape: MarkerShape) { + self.shape = shape; + } + /// Highlight these points in the plot by scaling up their markers. #[inline] pub fn highlight(mut self, highlight: bool) -> Self { @@ -820,6 +890,12 @@ impl Points { self } + /// Highlight these points in the plot by scaling up their markers. + #[inline] + pub fn highlight_mut(&mut self, highlight: bool) { + self.highlight = highlight; + } + /// Set the marker's color. #[inline] pub fn color(mut self, color: impl Into) -> Self { @@ -827,6 +903,12 @@ impl Points { self } + /// Set the marker's color. + #[inline] + pub fn color_mut(&mut self, color: impl Into) { + self.color = color.into(); + } + /// Whether to fill the marker. #[inline] pub fn filled(mut self, filled: bool) -> Self { @@ -834,6 +916,12 @@ impl Points { self } + /// Whether to fill the marker. + #[inline] + pub fn filled_mut(&mut self, filled: bool) { + self.filled = filled; + } + /// Whether to add stems between the markers and a horizontal reference line. #[inline] pub fn stems(mut self, y_reference: impl Into) -> Self { @@ -841,6 +929,12 @@ impl Points { self } + /// Whether to add stems between the markers and a horizontal reference line. + #[inline] + pub fn stems_mut(&mut self, y_reference: impl Into) { + self.stems = Some(y_reference.into()); + } + /// Set the maximum extent of the marker around its position. #[inline] pub fn radius(mut self, radius: impl Into) -> Self { @@ -848,6 +942,12 @@ impl Points { self } + /// Set the maximum extent of the marker around its position. + #[inline] + pub fn radius_mut(&mut self, radius: impl Into) { + self.radius = radius.into(); + } + /// Name of this set of points. /// /// This name will show up in the plot legend, if legends are turned on. @@ -862,7 +962,7 @@ impl Points { } } -impl PlotItem for Points { +impl PlotItem for Points { fn shapes(&self, _ui: &mut Ui, transform: &PlotTransform, shapes: &mut Vec) { let sqrt_3 = 3_f32.sqrt(); let frac_sqrt_3_2 = 3_f32.sqrt() / 2.0; @@ -1020,6 +1120,209 @@ impl PlotItem for Points { } } +impl<'a, T> PlotItem for Points +where + T: GenericPlotPoints, +{ + fn shapes(&self, _ui: &mut Ui, transform: &PlotTransform, shapes: &mut Vec) { + let sqrt_3 = 3_f32.sqrt(); + let frac_sqrt_3_2 = 3_f32.sqrt() / 2.0; + let frac_1_sqrt_2 = 1.0 / 2_f32.sqrt(); + + let Self { + series, + shape, + color, + filled, + mut radius, + highlight, + stems, + .. + } = self; + + let stroke_size = radius / 5.0; + + let default_stroke = Stroke::new(stroke_size, *color); + let mut stem_stroke = default_stroke; + let (fill, stroke) = if *filled { + (*color, Stroke::NONE) + } else { + (Color32::TRANSPARENT, default_stroke) + }; + + if *highlight { + radius *= 2f32.sqrt(); + stem_stroke.width *= 2.0; + } + + let y_reference = stems.map(|y| transform.position_from_point(&PlotPoint::new(0.0, y)).y); + + series + .points() + .map(|value| transform.position_from_point(value)) + .for_each(|center| { + let tf = |dx: f32, dy: f32| -> Pos2 { center + radius * vec2(dx, dy) }; + + if let Some(y) = y_reference { + let stem = Shape::line_segment([center, pos2(center.x, y)], stem_stroke); + shapes.push(stem); + } + + match shape { + MarkerShape::Circle => { + shapes.push(Shape::Circle(epaint::CircleShape { + center, + radius, + fill, + stroke, + })); + } + MarkerShape::Diamond => { + let points = vec![ + tf(0.0, 1.0), // bottom + tf(-1.0, 0.0), // left + tf(0.0, -1.0), // top + tf(1.0, 0.0), // right + ]; + shapes.push(Shape::convex_polygon(points, fill, stroke)); + } + MarkerShape::Square => { + let points = vec![ + tf(-frac_1_sqrt_2, frac_1_sqrt_2), + tf(-frac_1_sqrt_2, -frac_1_sqrt_2), + tf(frac_1_sqrt_2, -frac_1_sqrt_2), + tf(frac_1_sqrt_2, frac_1_sqrt_2), + ]; + shapes.push(Shape::convex_polygon(points, fill, stroke)); + } + MarkerShape::Cross => { + let diagonal1 = [ + tf(-frac_1_sqrt_2, -frac_1_sqrt_2), + tf(frac_1_sqrt_2, frac_1_sqrt_2), + ]; + let diagonal2 = [ + tf(frac_1_sqrt_2, -frac_1_sqrt_2), + tf(-frac_1_sqrt_2, frac_1_sqrt_2), + ]; + shapes.push(Shape::line_segment(diagonal1, default_stroke)); + shapes.push(Shape::line_segment(diagonal2, default_stroke)); + } + MarkerShape::Plus => { + let horizontal = [tf(-1.0, 0.0), tf(1.0, 0.0)]; + let vertical = [tf(0.0, -1.0), tf(0.0, 1.0)]; + shapes.push(Shape::line_segment(horizontal, default_stroke)); + shapes.push(Shape::line_segment(vertical, default_stroke)); + } + MarkerShape::Up => { + let points = + vec![tf(0.0, -1.0), tf(0.5 * sqrt_3, 0.5), tf(-0.5 * sqrt_3, 0.5)]; + shapes.push(Shape::convex_polygon(points, fill, stroke)); + } + MarkerShape::Down => { + let points = vec![ + tf(0.0, 1.0), + tf(-0.5 * sqrt_3, -0.5), + tf(0.5 * sqrt_3, -0.5), + ]; + shapes.push(Shape::convex_polygon(points, fill, stroke)); + } + MarkerShape::Left => { + let points = + vec![tf(-1.0, 0.0), tf(0.5, -0.5 * sqrt_3), tf(0.5, 0.5 * sqrt_3)]; + shapes.push(Shape::convex_polygon(points, fill, stroke)); + } + MarkerShape::Right => { + let points = vec![ + tf(1.0, 0.0), + tf(-0.5, 0.5 * sqrt_3), + tf(-0.5, -0.5 * sqrt_3), + ]; + shapes.push(Shape::convex_polygon(points, fill, stroke)); + } + MarkerShape::Asterisk => { + let vertical = [tf(0.0, -1.0), tf(0.0, 1.0)]; + let diagonal1 = [tf(-frac_sqrt_3_2, 0.5), tf(frac_sqrt_3_2, -0.5)]; + let diagonal2 = [tf(-frac_sqrt_3_2, -0.5), tf(frac_sqrt_3_2, 0.5)]; + shapes.push(Shape::line_segment(vertical, default_stroke)); + shapes.push(Shape::line_segment(diagonal1, default_stroke)); + shapes.push(Shape::line_segment(diagonal2, default_stroke)); + } + } + }); + } + + fn initialize(&mut self, x_range: RangeInclusive) { + self.series.initialize(x_range); + } + + fn name(&self) -> &str { + self.name.as_str() + } + + fn color(&self) -> Color32 { + self.color + } + + fn highlight(&mut self) { + self.highlight = true; + } + + fn highlighted(&self) -> bool { + self.highlight + } + + fn geometry(&self) -> PlotGeometry<'_> { + PlotGeometry::GenericPoints + } + + fn find_closest(&self, point: Pos2, transform: &PlotTransform) -> Option { + self.series + .points() + .enumerate() + .map(|(index, value)| { + let pos = transform.position_from_point(value); + let dist_sq = point.distance_sq(pos); + ClosestElem { index, dist_sq } + }) + .min_by_key(|e| e.dist_sq.ord()) + } + + fn on_hover( + &self, + elem: ClosestElem, + shapes: &mut Vec, + cursors: &mut Vec, + plot: &PlotConfig<'_>, + label_formatter: &LabelFormatter, + ) { + let line_color = if plot.ui.visuals().dark_mode { + Color32::from_gray(100).additive() + } else { + Color32::from_black_alpha(180) + }; + + // this method is only called, if the value is in the result set of find_closest() + if let Some(value) = self.series.points().nth(elem.index) { + let pointer = plot.transform.position_from_point(value); + shapes.push(Shape::circle_filled(pointer, 3.0, line_color)); + + rulers_at_value( + pointer, + *value, + self.name(), + plot, + shapes, + cursors, + label_formatter, + ); + } + } + + fn bounds(&self) -> PlotBounds { + self.series.bounds() + } +} + /// A set of arrows. pub struct Arrows { pub(super) origins: PlotPoints, diff --git a/crates/egui_plot/src/items/values.rs b/crates/egui_plot/src/items/values.rs index d05fdab36e7..c170b4137a5 100644 --- a/crates/egui_plot/src/items/values.rs +++ b/crates/egui_plot/src/items/values.rs @@ -156,7 +156,6 @@ impl Default for Orientation { pub enum PlotPoints { Owned(Vec), Generator(ExplicitGenerator), - // Borrowed(&[PlotPoint]), // TODO: Lifetimes are tricky in this case. } impl Default for PlotPoints { @@ -368,6 +367,11 @@ pub(crate) enum PlotGeometry<'a> { // Has currently no data, as it would require copying rects or iterating a list of pointers. // Instead, geometry-based functions are directly implemented in the respective PlotItem impl. Rects, + + /// Generic points + // Has no data, as without additional info it is not possible to efficiently index into the generic data. + // Instead, geometry-based functions are directly implemented in the respective PlotItem impl. + GenericPoints, } // ---------------------------------------------------------------------------- diff --git a/crates/egui_plot/src/legend.rs b/crates/egui_plot/src/legend.rs index a3b353e994c..0d867b99983 100644 --- a/crates/egui_plot/src/legend.rs +++ b/crates/egui_plot/src/legend.rs @@ -179,12 +179,16 @@ pub(super) struct LegendWidget { impl LegendWidget { /// Create a new legend from items, the names of items that are hidden and the style of the /// text. Returns `None` if the legend has no entries. - pub(super) fn try_new( + pub(super) fn try_new<'a, 'b, I>( rect: Rect, config: Legend, - items: &[Box], + items: I, hidden_items: &ahash::HashSet, // Existing hiddent items in the plot memory. - ) -> Option { + ) -> Option + where + 'b: 'a, + I: Iterator>, + { // If `config.hidden_items` is not `None`, it is used. let hidden_items = config.hidden_items.as_ref().unwrap_or(hidden_items); @@ -192,7 +196,7 @@ impl LegendWidget { // checkbox. If their colors don't match, we pick a neutral color for the checkbox. let mut entries: BTreeMap = BTreeMap::new(); items - .iter() + .into_iter() .filter(|item| !item.name().is_empty()) .for_each(|item| { entries diff --git a/crates/egui_plot/src/lib.rs b/crates/egui_plot/src/lib.rs index 55ec7007a15..766459ac273 100644 --- a/crates/egui_plot/src/lib.rs +++ b/crates/egui_plot/src/lib.rs @@ -12,11 +12,10 @@ mod legend; mod memory; mod transform; -use std::{ops::RangeInclusive, sync::Arc}; +use std::{marker::PhantomData, ops::RangeInclusive, sync::Arc}; use egui::ahash::HashMap; use epaint::util::FloatOrd; -use epaint::Hsva; use axis::AxisWidget; use items::PlotItem; @@ -29,6 +28,7 @@ pub use items::{ Orientation, PlotImage, PlotPoint, PlotPoints, Points, Polygon, Text, VLine, }; pub use legend::{Corner, Legend}; +pub use plot_ui::{PlotUi, PlotUiBuilder}; pub use transform::{PlotBounds, PlotTransform}; use items::{horizontal_line, rulers_color, vertical_line}; @@ -36,6 +36,8 @@ use items::{horizontal_line, rulers_color, vertical_line}; pub use axis::{Axis, AxisHints, HPlacement, Placement, VPlacement}; pub use memory::PlotMemory; +mod plot_ui; + type LabelFormatterFn = dyn Fn(&str, &PlotPoint) -> String; type LabelFormatter = Option>; @@ -138,10 +140,12 @@ pub struct PlotResponse { /// [x, x.sin()] /// }).collect(); /// let line = Line::new(sin); -/// Plot::new("my_plot").view_aspect(2.0).show(ui, |plot_ui| plot_ui.line(line)); +/// Plot::new("my_plot").view_aspect(2.0).show(ui, |plot_ui| { +/// plot_ui.line(line); +/// }); /// # }); /// ``` -pub struct Plot { +pub struct Plot<'a> { id_source: Id, id: Option, @@ -179,9 +183,11 @@ pub struct Plot { grid_spacers: [GridSpacer; 2], sharp_grid_lines: bool, clamp_grid: bool, + + phantom_plot_ui: std::marker::PhantomData>, } -impl Plot { +impl<'a> Plot<'a> { /// Give a unique id for each plot within the same [`Ui`]. pub fn new(id_source: impl std::hash::Hash) -> Self { Self { @@ -222,6 +228,8 @@ impl Plot { grid_spacers: [log_grid_spacer(10), log_grid_spacer(10)], sharp_grid_lines: true, clamp_grid: false, + + phantom_plot_ui: PhantomData, } } @@ -387,7 +395,9 @@ impl Plot { /// "".to_owned() /// } /// }) - /// .show(ui, |plot_ui| plot_ui.line(line)); + /// .show(ui, |plot_ui| { + /// plot_ui.line(line); + /// }); /// # }); /// ``` pub fn label_formatter( @@ -685,15 +695,11 @@ impl Plot { } /// Interact with and add items to the plot and finally draw it. - pub fn show(self, ui: &mut Ui, build_fn: impl FnOnce(&mut PlotUi) -> R) -> PlotResponse { - self.show_dyn(ui, Box::new(build_fn)) - } - fn show_dyn<'a, R>( - self, - ui: &mut Ui, - build_fn: Box R + 'a>, - ) -> PlotResponse { + pub fn show(self, ui: &mut Ui, build_fn: F) -> PlotResponse + where + F: FnOnce(&mut PlotUi<'a>) -> R, + { let Self { id_source, id, @@ -729,6 +735,8 @@ impl Plot { clamp_grid, grid_spacers, sharp_grid_lines, + + phantom_plot_ui: _, } = self; // Determine position of widget. @@ -870,7 +878,9 @@ impl Plot { bounds_modifications: Vec::new(), ctx: ui.ctx().clone(), }; + let inner = build_fn(&mut plot_ui); + let PlotUi { mut items, mut response, @@ -893,7 +903,8 @@ impl Plot { // --- Legend --- let legend = legend_config - .and_then(|config| LegendWidget::try_new(rect, config, &items, &hidden_items)); + .and_then(|config| LegendWidget::try_new(rect, config, items.iter(), &hidden_items)); + // Don't show hover cursor when hovering over legend. if hovered_item.is_some() { show_x = false; @@ -1321,235 +1332,6 @@ enum BoundsModification { Zoom(Vec2, PlotPoint), } -/// Provides methods to interact with a plot while building it. It is the single argument of the closure -/// provided to [`Plot::show`]. See [`Plot`] for an example of how to use it. -pub struct PlotUi { - items: Vec>, - next_auto_color_idx: usize, - last_plot_transform: PlotTransform, - last_auto_bounds: Vec2b, - response: Response, - bounds_modifications: Vec, - ctx: Context, -} - -impl PlotUi { - fn auto_color(&mut self) -> Color32 { - let i = self.next_auto_color_idx; - self.next_auto_color_idx += 1; - let golden_ratio = (5.0_f32.sqrt() - 1.0) / 2.0; // 0.61803398875 - let h = i as f32 * golden_ratio; - Hsva::new(h, 0.85, 0.5, 1.0).into() // TODO(emilk): OkLab or some other perspective color space - } - - pub fn ctx(&self) -> &Context { - &self.ctx - } - - /// The plot bounds as they were in the last frame. If called on the first frame and the bounds were not - /// further specified in the plot builder, this will return bounds centered on the origin. The bounds do - /// not change until the plot is drawn. - pub fn plot_bounds(&self) -> PlotBounds { - *self.last_plot_transform.bounds() - } - - /// Set the plot bounds. Can be useful for implementing alternative plot navigation methods. - pub fn set_plot_bounds(&mut self, plot_bounds: PlotBounds) { - self.bounds_modifications - .push(BoundsModification::Set(plot_bounds)); - } - - /// Move the plot bounds. Can be useful for implementing alternative plot navigation methods. - pub fn translate_bounds(&mut self, delta_pos: Vec2) { - self.bounds_modifications - .push(BoundsModification::Translate(delta_pos)); - } - - /// Whether the plot axes were in auto-bounds mode in the last frame. If called on the first - /// frame, this is the [`Plot`]'s default auto-bounds mode. - pub fn auto_bounds(&self) -> Vec2b { - self.last_auto_bounds - } - - /// Set the auto-bounds mode for the plot axes. - pub fn set_auto_bounds(&mut self, auto_bounds: Vec2b) { - self.bounds_modifications - .push(BoundsModification::AutoBounds(auto_bounds)); - } - - /// Can be used to check if the plot was hovered or clicked. - pub fn response(&self) -> &Response { - &self.response - } - - /// Scale the plot bounds around a position in screen coordinates. - /// - /// Can be useful for implementing alternative plot navigation methods. - /// - /// The plot bounds are divided by `zoom_factor`, therefore: - /// - `zoom_factor < 1.0` zooms out, i.e., increases the visible range to show more data. - /// - `zoom_factor > 1.0` zooms in, i.e., reduces the visible range to show more detail. - pub fn zoom_bounds(&mut self, zoom_factor: Vec2, center: PlotPoint) { - self.bounds_modifications - .push(BoundsModification::Zoom(zoom_factor, center)); - } - - /// Scale the plot bounds around the hovered position, if any. - /// - /// Can be useful for implementing alternative plot navigation methods. - /// - /// The plot bounds are divided by `zoom_factor`, therefore: - /// - `zoom_factor < 1.0` zooms out, i.e., increases the visible range to show more data. - /// - `zoom_factor > 1.0` zooms in, i.e., reduces the visible range to show more detail. - pub fn zoom_bounds_around_hovered(&mut self, zoom_factor: Vec2) { - if let Some(hover_pos) = self.pointer_coordinate() { - self.zoom_bounds(zoom_factor, hover_pos); - } - } - - /// The pointer position in plot coordinates. Independent of whether the pointer is in the plot area. - pub fn pointer_coordinate(&self) -> Option { - // We need to subtract the drag delta to keep in sync with the frame-delayed screen transform: - let last_pos = self.ctx().input(|i| i.pointer.latest_pos())? - self.response.drag_delta(); - let value = self.plot_from_screen(last_pos); - Some(value) - } - - /// The pointer drag delta in plot coordinates. - pub fn pointer_coordinate_drag_delta(&self) -> Vec2 { - let delta = self.response.drag_delta(); - let dp_dv = self.last_plot_transform.dpos_dvalue(); - Vec2::new(delta.x / dp_dv[0] as f32, delta.y / dp_dv[1] as f32) - } - - /// Read the transform between plot coordinates and screen coordinates. - pub fn transform(&self) -> &PlotTransform { - &self.last_plot_transform - } - - /// Transform the plot coordinates to screen coordinates. - pub fn screen_from_plot(&self, position: PlotPoint) -> Pos2 { - self.last_plot_transform.position_from_point(&position) - } - - /// Transform the screen coordinates to plot coordinates. - pub fn plot_from_screen(&self, position: Pos2) -> PlotPoint { - self.last_plot_transform.value_from_position(position) - } - - /// Add a data line. - pub fn line(&mut self, mut line: Line) { - if line.series.is_empty() { - return; - }; - - // Give the stroke an automatic color if no color has been assigned. - if line.stroke.color == Color32::TRANSPARENT { - line.stroke.color = self.auto_color(); - } - self.items.push(Box::new(line)); - } - - /// Add a polygon. The polygon has to be convex. - pub fn polygon(&mut self, mut polygon: Polygon) { - if polygon.series.is_empty() { - return; - }; - - // Give the stroke an automatic color if no color has been assigned. - if polygon.stroke.color == Color32::TRANSPARENT { - polygon.stroke.color = self.auto_color(); - } - self.items.push(Box::new(polygon)); - } - - /// Add a text. - pub fn text(&mut self, text: Text) { - if text.text.is_empty() { - return; - }; - - self.items.push(Box::new(text)); - } - - /// Add data points. - pub fn points(&mut self, mut points: Points) { - if points.series.is_empty() { - return; - }; - - // Give the points an automatic color if no color has been assigned. - if points.color == Color32::TRANSPARENT { - points.color = self.auto_color(); - } - self.items.push(Box::new(points)); - } - - /// Add arrows. - pub fn arrows(&mut self, mut arrows: Arrows) { - if arrows.origins.is_empty() || arrows.tips.is_empty() { - return; - }; - - // Give the arrows an automatic color if no color has been assigned. - if arrows.color == Color32::TRANSPARENT { - arrows.color = self.auto_color(); - } - self.items.push(Box::new(arrows)); - } - - /// Add an image. - pub fn image(&mut self, image: PlotImage) { - self.items.push(Box::new(image)); - } - - /// Add a horizontal line. - /// Can be useful e.g. to show min/max bounds or similar. - /// Always fills the full width of the plot. - pub fn hline(&mut self, mut hline: HLine) { - if hline.stroke.color == Color32::TRANSPARENT { - hline.stroke.color = self.auto_color(); - } - self.items.push(Box::new(hline)); - } - - /// Add a vertical line. - /// Can be useful e.g. to show min/max bounds or similar. - /// Always fills the full height of the plot. - pub fn vline(&mut self, mut vline: VLine) { - if vline.stroke.color == Color32::TRANSPARENT { - vline.stroke.color = self.auto_color(); - } - self.items.push(Box::new(vline)); - } - - /// Add a box plot diagram. - pub fn box_plot(&mut self, mut box_plot: BoxPlot) { - if box_plot.boxes.is_empty() { - return; - } - - // Give the elements an automatic color if no color has been assigned. - if box_plot.default_color == Color32::TRANSPARENT { - box_plot = box_plot.color(self.auto_color()); - } - self.items.push(Box::new(box_plot)); - } - - /// Add a bar chart. - pub fn bar_chart(&mut self, mut chart: BarChart) { - if chart.bars.is_empty() { - return; - } - - // Give the elements an automatic color if no color has been assigned. - if chart.default_color == Color32::TRANSPARENT { - chart = chart.color(self.auto_color()); - } - self.items.push(Box::new(chart)); - } -} - // ---------------------------------------------------------------------------- // Grid @@ -1625,8 +1407,8 @@ pub fn uniform_grid_spacer(spacer: impl Fn(GridInput) -> [f64; 3] + 'static) -> // ---------------------------------------------------------------------------- -struct PreparedPlot { - items: Vec>, +struct PreparedPlot<'a> { + items: Vec>, show_x: bool, show_y: bool, label_formatter: LabelFormatter, @@ -1643,8 +1425,10 @@ struct PreparedPlot { clamp_grid: bool, } -impl PreparedPlot { - fn ui(self, ui: &mut Ui, response: &Response) -> Vec { +impl<'a> PreparedPlot<'a> { + fn ui(&self, ui: &mut Ui, response: &Response) -> Vec { + let items = &self.items; + let mut axes_shapes = Vec::new(); if self.show_grid.x { @@ -1657,13 +1441,14 @@ impl PreparedPlot { // Sort the axes by strength so that those with higher strength are drawn in front. axes_shapes.sort_by(|(_, strength1), (_, strength2)| strength1.total_cmp(strength2)); - let mut shapes = axes_shapes.into_iter().map(|(shape, _)| shape).collect(); + let mut shapes: Vec<_> = axes_shapes.into_iter().map(|(shape, _)| shape).collect(); let transform = &self.transform; let mut plot_ui = ui.child_ui(*transform.frame(), Layout::default()); plot_ui.set_clip_rect(*transform.frame()); - for item in &self.items { + + for item in items { item.shapes(&mut plot_ui, transform, &mut shapes); } diff --git a/crates/egui_plot/src/plot_ui.rs b/crates/egui_plot/src/plot_ui.rs new file mode 100644 index 00000000000..64b9f1a58a9 --- /dev/null +++ b/crates/egui_plot/src/plot_ui.rs @@ -0,0 +1,328 @@ +use egui::{epaint::Hsva, Color32, Context, Pos2, Response, Vec2, Vec2b}; + +use crate::{ + items::{GenericPlotPoints, PlotItem}, + Arrows, BarChart, BoundsModification, BoxPlot, HLine, Line, PlotBounds, PlotImage, PlotPoint, + PlotPoints, PlotTransform, Points, Polygon, Text, VLine, +}; + +pub struct PlotUiBuilder { + pub(crate) next_auto_color_idx: usize, + pub(crate) last_plot_transform: PlotTransform, + pub(crate) last_auto_bounds: Vec2b, + pub(crate) response: Response, + pub(crate) bounds_modifications: Vec, + pub(crate) ctx: Context, +} + +impl PlotUiBuilder { + pub fn into_plot_ui<'a>(self) -> PlotUi<'a> { + let Self { + next_auto_color_idx, + last_plot_transform, + last_auto_bounds, + response, + bounds_modifications, + ctx, + } = self; + + PlotUi { + items: Vec::new(), + next_auto_color_idx, + last_plot_transform, + last_auto_bounds, + response, + bounds_modifications, + ctx, + } + } +} + +impl From for PlotUi<'_> { + fn from(value: PlotUiBuilder) -> Self { + let PlotUiBuilder { + next_auto_color_idx, + last_plot_transform, + last_auto_bounds, + response, + bounds_modifications, + ctx, + } = value; + + PlotUi { + items: Vec::new(), + next_auto_color_idx, + last_plot_transform, + last_auto_bounds, + response, + bounds_modifications, + ctx, + } + } +} + +/// Provides methods to interact with a plot while building it. It is the single argument of the closure +/// provided to [`crate::Plot::show`]. See [`crate::Plot`] for an example of how to use it. +pub struct PlotUi<'a> { + pub(crate) items: Vec>, + pub(crate) next_auto_color_idx: usize, + pub(crate) last_plot_transform: PlotTransform, + pub(crate) last_auto_bounds: Vec2b, + pub(crate) response: Response, + pub(crate) bounds_modifications: Vec, + pub(crate) ctx: Context, +} + +impl<'a> PlotUi<'a> { + fn auto_color(&mut self) -> Color32 { + let i = self.next_auto_color_idx; + self.next_auto_color_idx += 1; + let golden_ratio = (5.0_f32.sqrt() - 1.0) / 2.0; // 0.61803398875 + let h = i as f32 * golden_ratio; + Hsva::new(h, 0.85, 0.5, 1.0).into() // TODO(emilk): OkLab or some other perspective color space + } + + pub fn ctx(&self) -> &Context { + &self.ctx + } + + /// The plot bounds as they were in the last frame. If called on the first frame and the bounds were not + /// further specified in the plot builder, this will return bounds centered on the origin. The bounds do + /// not change until the plot is drawn. + pub fn plot_bounds(&self) -> PlotBounds { + *self.last_plot_transform.bounds() + } + + /// Set the plot bounds. Can be useful for implementing alternative plot navigation methods. + pub fn set_plot_bounds(&mut self, plot_bounds: PlotBounds) -> &mut Self { + self.bounds_modifications + .push(BoundsModification::Set(plot_bounds)); + + self + } + + /// Move the plot bounds. Can be useful for implementing alternative plot navigation methods. + pub fn translate_bounds(&mut self, delta_pos: Vec2) -> &mut Self { + self.bounds_modifications + .push(BoundsModification::Translate(delta_pos)); + + self + } + + /// Whether the plot axes were in auto-bounds mode in the last frame. If called on the first + /// frame, this is the [`crate::Plot`]'s default auto-bounds mode. + pub fn auto_bounds(&self) -> Vec2b { + self.last_auto_bounds + } + + /// Set the auto-bounds mode for the plot axes. + pub fn set_auto_bounds(&mut self, auto_bounds: Vec2b) -> &mut Self { + self.bounds_modifications + .push(BoundsModification::AutoBounds(auto_bounds)); + + self + } + + /// Can be used to check if the plot was hovered or clicked. + pub fn response(&self) -> &Response { + &self.response + } + + /// Scale the plot bounds around a position in screen coordinates. + /// + /// Can be useful for implementing alternative plot navigation methods. + /// + /// The plot bounds are divided by `zoom_factor`, therefore: + /// - `zoom_factor < 1.0` zooms out, i.e., increases the visible range to show more data. + /// - `zoom_factor > 1.0` zooms in, i.e., reduces the visible range to show more detail. + pub fn zoom_bounds(&mut self, zoom_factor: Vec2, center: PlotPoint) -> &mut Self { + self.bounds_modifications + .push(BoundsModification::Zoom(zoom_factor, center)); + + self + } + + /// Scale the plot bounds around the hovered position, if any. + /// + /// Can be useful for implementing alternative plot navigation methods. + /// + /// The plot bounds are divided by `zoom_factor`, therefore: + /// - `zoom_factor < 1.0` zooms out, i.e., increases the visible range to show more data. + /// - `zoom_factor > 1.0` zooms in, i.e., reduces the visible range to show more detail. + pub fn zoom_bounds_around_hovered(&mut self, zoom_factor: Vec2) -> &mut Self { + if let Some(hover_pos) = self.pointer_coordinate() { + self.zoom_bounds(zoom_factor, hover_pos) + } else { + self + } + } + + /// The pointer position in plot coordinates. Independent of whether the pointer is in the plot area. + pub fn pointer_coordinate(&self) -> Option { + // We need to subtract the drag delta to keep in sync with the frame-delayed screen transform: + let last_pos = self.ctx().input(|i| i.pointer.latest_pos())? - self.response.drag_delta(); + let value = self.plot_from_screen(last_pos); + Some(value) + } + + /// The pointer drag delta in plot coordinates. + pub fn pointer_coordinate_drag_delta(&self) -> Vec2 { + let delta = self.response.drag_delta(); + let dp_dv = self.last_plot_transform.dpos_dvalue(); + Vec2::new(delta.x / dp_dv[0] as f32, delta.y / dp_dv[1] as f32) + } + + /// Read the transform between plot coordinates and screen coordinates. + pub fn transform(&self) -> &PlotTransform { + &self.last_plot_transform + } + + /// Transform the plot coordinates to screen coordinates. + pub fn screen_from_plot(&self, position: PlotPoint) -> Pos2 { + self.last_plot_transform.position_from_point(&position) + } + + /// Transform the screen coordinates to plot coordinates. + pub fn plot_from_screen(&self, position: Pos2) -> PlotPoint { + self.last_plot_transform.value_from_position(position) + } + + /// Add a data line. + pub fn line(&mut self, mut line: Line) -> &mut Self { + if line.series.is_empty() { + return self; + }; + + // Give the stroke an automatic color if no color has been assigned. + if line.stroke.color == Color32::TRANSPARENT { + line.stroke.color = self.auto_color(); + } + self.items.push(Box::new(line)); + self + } + + /// Add a polygon. The polygon has to be convex. + pub fn polygon(&mut self, mut polygon: Polygon) -> &mut Self { + if polygon.series.is_empty() { + return self; + }; + + // Give the stroke an automatic color if no color has been assigned. + if polygon.stroke.color == Color32::TRANSPARENT { + polygon.stroke.color = self.auto_color(); + } + self.items.push(Box::new(polygon)); + self + } + + /// Add a text. + pub fn text(&mut self, text: Text) -> &mut Self { + if text.text.is_empty() { + return self; + }; + + self.items.push(Box::new(text)); + self + } + + /// Add data points. + pub fn owned_points(&mut self, mut points: Points) -> &mut Self { + // Give the points an automatic color if no color has been assigned. + if points.color == Color32::TRANSPARENT { + points.color = self.auto_color(); + } + self.items.push(Box::new(points)); + + self + } + + /// Add data points. + pub fn borrowed_points<'b, T: 'a>(&mut self, mut points: Points) -> &mut Self + where + T: GenericPlotPoints, + { + // Give the points an automatic color if no color has been assigned. + if points.color == Color32::TRANSPARENT { + points.color = self.auto_color(); + } + self.items.push(Box::new(points)); + + self + } + + /// Add arrows. + pub fn arrows(&mut self, mut arrows: Arrows) -> &mut Self { + if arrows.origins.is_empty() || arrows.tips.is_empty() { + return self; + }; + + // Give the arrows an automatic color if no color has been assigned. + if arrows.color == Color32::TRANSPARENT { + arrows.color = self.auto_color(); + } + self.items.push(Box::new(arrows)); + + self + } + + /// Add an image. + pub fn image(&mut self, image: PlotImage) -> &mut Self { + self.items.push(Box::new(image)); + + self + } + + /// Add a horizontal line. + /// Can be useful e.g. to show min/max bounds or similar. + /// Always fills the full width of the plot. + pub fn hline(&mut self, mut hline: HLine) -> &mut Self { + if hline.stroke.color == Color32::TRANSPARENT { + hline.stroke.color = self.auto_color(); + } + self.items.push(Box::new(hline)); + + self + } + + /// Add a vertical line. + /// Can be useful e.g. to show min/max bounds or similar. + /// Always fills the full height of the plot. + pub fn vline(&mut self, mut vline: VLine) -> &mut Self { + if vline.stroke.color == Color32::TRANSPARENT { + vline.stroke.color = self.auto_color(); + } + self.items.push(Box::new(vline)); + + self + } + + /// Add a box plot diagram. + pub fn box_plot(&mut self, mut box_plot: BoxPlot) -> &mut Self { + if box_plot.boxes.is_empty() { + return self; + } + + // Give the elements an automatic color if no color has been assigned. + if box_plot.default_color == Color32::TRANSPARENT { + box_plot = box_plot.color(self.auto_color()); + } + self.items.push(Box::new(box_plot)); + + self + } + + /// Add a bar chart. + pub fn bar_chart(&mut self, mut chart: BarChart) -> &mut Self { + if chart.bars.is_empty() { + return self; + } + + // Give the elements an automatic color if no color has been assigned. + if chart.default_color == Color32::TRANSPARENT { + chart = chart.color(self.auto_color()); + } + self.items.push(Box::new(chart)); + + self + } +}