diff --git a/egui_plot/src/lib.rs b/egui_plot/src/lib.rs index 068198fa..175e8ff7 100644 --- a/egui_plot/src/lib.rs +++ b/egui_plot/src/lib.rs @@ -12,41 +12,23 @@ mod axis; mod items; mod legend; mod memory; +mod plot; mod plot_ui; mod transform; use std::cmp::Ordering; use std::ops::RangeInclusive; -use std::sync::Arc; use ahash::HashMap; use axis::AxisWidget; -use egui::Align2; use egui::Color32; -use egui::CursorIcon; use egui::Id; -use egui::Layout; use egui::NumExt as _; -use egui::PointerButton; -use egui::Pos2; -use egui::Rangef; use egui::Rect; use egui::Response; -use egui::Sense; -use egui::Shape; -use egui::Stroke; -use egui::TextStyle; use egui::Ui; use egui::Vec2; use egui::Vec2b; -use egui::WidgetText; -use egui::epaint; -use egui::remap_clamp; -use egui::vec2; -use items::horizontal_line; -use items::rulers_color; -use items::vertical_line; -use legend::LegendWidget; pub use crate::axis::Axis; pub use crate::axis::AxisHints; @@ -80,6 +62,7 @@ pub use crate::legend::ColorConflictHandling; pub use crate::legend::Corner; pub use crate::legend::Legend; pub use crate::memory::PlotMemory; +pub use crate::plot::Plot; pub use crate::plot_ui::PlotUi; pub use crate::transform::PlotBounds; pub use crate::transform::PlotTransform; @@ -187,1303 +170,6 @@ pub struct PlotResponse { // ---------------------------------------------------------------------------- -/// A 2D plot, e.g. a graph of a function. -/// -/// [`Plot`] supports multiple lines and points. -/// -/// ``` -/// # egui::__run_test_ui(|ui| { -/// use egui_plot::Line; -/// use egui_plot::Plot; -/// use egui_plot::PlotPoints; -/// -/// let sin: PlotPoints = (0..1000) -/// .map(|i| { -/// let x = i as f64 * 0.01; -/// [x, x.sin()] -/// }) -/// .collect(); -/// let line = Line::new("sin", sin); -/// Plot::new("my_plot") -/// .view_aspect(2.0) -/// .show(ui, |plot_ui| plot_ui.line(line)); -/// # }); -/// ``` -pub struct Plot<'a> { - id_source: Id, - id: Option, - - center_axis: Vec2b, - allow_zoom: Vec2b, - allow_drag: Vec2b, - allow_axis_zoom_drag: Vec2b, - allow_scroll: Vec2b, - allow_double_click_reset: bool, - allow_boxed_zoom: bool, - default_auto_bounds: Vec2b, - min_auto_bounds: PlotBounds, - margin_fraction: Vec2, - pan_pointer_button: PointerButton, - boxed_zoom_pointer_button: PointerButton, - linked_axes: Option<(Id, Vec2b)>, - linked_cursors: Option<(Id, Vec2b)>, - - min_size: Vec2, - width: Option, - height: Option, - data_aspect: Option, - view_aspect: Option, - invert_x: bool, - invert_y: bool, - - reset: bool, - - show_x: bool, - show_y: bool, - label_formatter: LabelFormatter<'a>, - coordinates_formatter: Option<(Corner, CoordinatesFormatter<'a>)>, - x_axes: Vec>, // default x axes - y_axes: Vec>, // default y axes - legend_config: Option, - cursor_color: Option, - show_background: bool, - show_axes: Vec2b, - - show_grid: Vec2b, - grid_spacing: Rangef, - grid_spacers: [GridSpacer<'a>; 2], - clamp_grid: bool, - - sense: Sense, -} - -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 { - id_source: Id::new(id_source), - id: None, - - center_axis: false.into(), - allow_zoom: true.into(), - allow_drag: true.into(), - allow_axis_zoom_drag: true.into(), - allow_scroll: true.into(), - allow_double_click_reset: true, - allow_boxed_zoom: true, - default_auto_bounds: true.into(), - min_auto_bounds: PlotBounds::NOTHING, - margin_fraction: Vec2::splat(0.05), - pan_pointer_button: PointerButton::Primary, - boxed_zoom_pointer_button: PointerButton::Secondary, - linked_axes: None, - linked_cursors: None, - - min_size: Vec2::splat(64.0), - width: None, - height: None, - data_aspect: None, - view_aspect: None, - invert_x: false, - invert_y: false, - - reset: false, - - show_x: true, - show_y: true, - label_formatter: None, - coordinates_formatter: None, - x_axes: vec![AxisHints::new(Axis::X)], - y_axes: vec![AxisHints::new(Axis::Y)], - legend_config: None, - cursor_color: None, - show_background: true, - show_axes: true.into(), - - show_grid: true.into(), - grid_spacing: Rangef::new(8.0, 300.0), - grid_spacers: [log_grid_spacer(10), log_grid_spacer(10)], - clamp_grid: false, - - sense: egui::Sense::click_and_drag(), - } - } - - /// Set an explicit (global) id for the plot. - /// - /// This will override the id set by [`Self::new`]. - /// - /// This is the same `Id` that can be used for [`PlotMemory::load`]. - #[inline] - pub fn id(mut self, id: Id) -> Self { - self.id = Some(id); - self - } - - /// width / height ratio of the data. - /// For instance, it can be useful to set this to `1.0` for when the two - /// axes show the same unit. - /// By default the plot window's aspect ratio is used. - #[inline] - pub fn data_aspect(mut self, data_aspect: f32) -> Self { - self.data_aspect = Some(data_aspect); - self - } - - /// width / height ratio of the plot region. - /// By default no fixed aspect ratio is set (and width/height will fill the - /// ui it is in). - #[inline] - pub fn view_aspect(mut self, view_aspect: f32) -> Self { - self.view_aspect = Some(view_aspect); - self - } - - /// Set whether to invert the x-axis (i.e. positive values go to the left). - /// By default the x-axis is not inverted (i.e. positive values go to the - /// right). - #[inline] - pub fn invert_x(mut self, invert: bool) -> Self { - self.invert_x = invert; - self - } - - /// Set whether to invert the y-axis (i.e. positive values go down). - /// By default the y-axis is not inverted (i.e. positive values go up). - #[inline] - pub fn invert_y(mut self, invert: bool) -> Self { - self.invert_y = invert; - self - } - - /// Width of plot. By default a plot will fill the ui it is in. - /// If you set [`Self::view_aspect`], the width can be calculated from the - /// height. - #[inline] - pub fn width(mut self, width: f32) -> Self { - self.min_size.x = width; - self.width = Some(width); - self - } - - /// Height of plot. By default a plot will fill the ui it is in. - /// If you set [`Self::view_aspect`], the height can be calculated from the - /// width. - #[inline] - pub fn height(mut self, height: f32) -> Self { - self.min_size.y = height; - self.height = Some(height); - self - } - - /// Minimum size of the plot view. - #[inline] - pub fn min_size(mut self, min_size: Vec2) -> Self { - self.min_size = min_size; - self - } - - /// Show the x-value (e.g. when hovering). Default: `true`. - #[inline] - pub fn show_x(mut self, show_x: bool) -> Self { - self.show_x = show_x; - self - } - - /// Show the y-value (e.g. when hovering). Default: `true`. - #[inline] - pub fn show_y(mut self, show_y: bool) -> Self { - self.show_y = show_y; - self - } - - /// Always keep the X-axis centered. Default: `false`. - #[inline] - pub fn center_x_axis(mut self, on: bool) -> Self { - self.center_axis.x = on; - self - } - - /// Always keep the Y-axis centered. Default: `false`. - #[inline] - pub fn center_y_axis(mut self, on: bool) -> Self { - self.center_axis.y = on; - self - } - - /// Whether to allow zooming in the plot. Default: `true`. - /// - /// Note: Allowing zoom in one axis but not the other may lead to unexpected - /// results if used in combination with `data_aspect`. - #[inline] - pub fn allow_zoom(mut self, on: T) -> Self - where - T: Into, - { - self.allow_zoom = on.into(); - self - } - - /// Whether to allow scrolling in the plot. Default: `true`. - #[inline] - pub fn allow_scroll(mut self, on: T) -> Self - where - T: Into, - { - self.allow_scroll = on.into(); - self - } - - /// Whether to allow double clicking to reset the view. - /// Default: `true`. - #[inline] - pub fn allow_double_click_reset(mut self, on: bool) -> Self { - self.allow_double_click_reset = on; - self - } - - /// Set the side margin as a fraction of the plot size. Only used for auto - /// bounds. - /// - /// For instance, a value of `0.1` will add 10% space on both sides. - #[inline] - pub fn set_margin_fraction(mut self, margin_fraction: Vec2) -> Self { - self.margin_fraction = margin_fraction; - self - } - - /// Whether to allow zooming in the plot by dragging out a box with the - /// secondary mouse button. - /// - /// Default: `true`. - /// - /// The button to use is specified by [`Self::boxed_zoom_pointer_button`]. - #[inline] - pub fn allow_boxed_zoom(mut self, on: bool) -> Self { - self.allow_boxed_zoom = on; - self - } - - /// Config the button pointer to use for drag-to-pan. Default: - /// [`Secondary`](PointerButton::Primary) - #[inline] - pub fn pan_pointer_button(mut self, pan_pointer_button: PointerButton) -> Self { - self.pan_pointer_button = pan_pointer_button; - self - } - - /// Config the button pointer to use for boxed zooming. Default: - /// [`Secondary`](PointerButton::Secondary) - #[inline] - pub fn boxed_zoom_pointer_button(mut self, boxed_zoom_pointer_button: PointerButton) -> Self { - self.boxed_zoom_pointer_button = boxed_zoom_pointer_button; - self - } - - /// Whether to allow dragging in the plot to move the bounds. Default: - /// `true`. - /// - /// The button to use is specified by [`Self::pan_pointer_button`]. - #[inline] - pub fn allow_drag(mut self, on: T) -> Self - where - T: Into, - { - self.allow_drag = on.into(); - self - } - - /// Whether to allow dragging in the axis areas to zoom the plot. Default: - /// `true`. - #[inline] - pub fn allow_axis_zoom_drag(mut self, on: T) -> Self - where - T: Into, - { - self.allow_axis_zoom_drag = on.into(); - self - } - - /// Provide a function to customize the on-hover label for the x and y axis - /// - /// ``` - /// # egui::__run_test_ui(|ui| { - /// use egui_plot::Line; - /// use egui_plot::Plot; - /// use egui_plot::PlotPoints; - /// let sin: PlotPoints = (0..1000) - /// .map(|i| { - /// let x = i as f64 * 0.01; - /// [x, x.sin()] - /// }) - /// .collect(); - /// let line = Line::new("sin", sin); - /// Plot::new("my_plot") - /// .view_aspect(2.0) - /// .label_formatter(|name, value| { - /// if !name.is_empty() { - /// format!("{}: {:.*}%", name, 1, value.y) - /// } else { - /// "".to_owned() - /// } - /// }) - /// .show(ui, |plot_ui| plot_ui.line(line)); - /// # }); - /// ``` - #[inline] - pub fn label_formatter(mut self, label_formatter: impl Fn(&str, &PlotPoint) -> String + 'a) -> Self { - self.label_formatter = Some(Box::new(label_formatter)); - self - } - - /// Show the pointer coordinates in the plot. - #[inline] - pub fn coordinates_formatter(mut self, position: Corner, formatter: CoordinatesFormatter<'a>) -> Self { - self.coordinates_formatter = Some((position, formatter)); - self - } - - /// Configure how the grid in the background is spaced apart along the X - /// axis. - /// - /// Default is a log-10 grid, i.e. every plot unit is divided into 10 other - /// units. - /// - /// The function has this signature: - /// ```ignore - /// fn step_sizes(input: GridInput) -> Vec; - /// ``` - /// - /// This function should return all marks along the visible range of the X - /// axis. `step_size` also determines how thick/faint each line is - /// drawn. For example, if x = 80..=230 is visible and you want big - /// marks at steps of 100 and small ones at 25, you can return: - /// ```no_run - /// # use egui_plot::GridMark; - /// vec![ - /// // 100s - /// GridMark { - /// value: 100.0, - /// step_size: 100.0, - /// }, - /// GridMark { - /// value: 200.0, - /// step_size: 100.0, - /// }, - /// // 25s - /// GridMark { - /// value: 125.0, - /// step_size: 25.0, - /// }, - /// GridMark { - /// value: 150.0, - /// step_size: 25.0, - /// }, - /// GridMark { - /// value: 175.0, - /// step_size: 25.0, - /// }, - /// GridMark { - /// value: 225.0, - /// step_size: 25.0, - /// }, - /// ]; - /// # () - /// ``` - /// - /// There are helpers for common cases, see [`log_grid_spacer`] and - /// [`uniform_grid_spacer`]. - #[inline] - pub fn x_grid_spacer(mut self, spacer: impl Fn(GridInput) -> Vec + 'a) -> Self { - self.grid_spacers[0] = Box::new(spacer); - self - } - - /// Default is a log-10 grid, i.e. every plot unit is divided into 10 other - /// units. - /// - /// See [`Self::x_grid_spacer`] for explanation. - #[inline] - pub fn y_grid_spacer(mut self, spacer: impl Fn(GridInput) -> Vec + 'a) -> Self { - self.grid_spacers[1] = Box::new(spacer); - self - } - - /// Set when the grid starts showing. - /// - /// When grid lines are closer than the given minimum, they will be hidden. - /// When they get further apart they will fade in, until the reaches the - /// given maximum, at which point they are fully opaque. - #[inline] - pub fn grid_spacing(mut self, grid_spacing: impl Into) -> Self { - self.grid_spacing = grid_spacing.into(); - self - } - - /// Clamp the grid to only be visible at the range of data where we have - /// values. - /// - /// Default: `false`. - #[inline] - pub fn clamp_grid(mut self, clamp_grid: bool) -> Self { - self.clamp_grid = clamp_grid; - self - } - - /// Set the sense for the plot rect. - /// - /// Default: `Sense::click_and_drag()`. - #[inline] - pub fn sense(mut self, sense: Sense) -> Self { - self.sense = sense; - self - } - - /// Overwrite the starting and reset bounds used for the x axis. - /// Set the `default_auto_bounds` of the x axis to `false`. - /// - /// Panics in debug builds if `min >= max`. - #[inline] - pub fn default_x_bounds(mut self, min: f64, max: f64) -> Self { - debug_assert!(min < max, "`min` must be less than `max` in `default_x_bounds`"); - self.default_auto_bounds.x = false; - self.min_auto_bounds.min[0] = min; - self.min_auto_bounds.max[0] = max; - self - } - - /// Overwrite the starting and reset bounds used for the y axis. - /// Set the `default_auto_bounds` of the y axis to `false`. - /// - /// Panics in debug builds if `min >= max`. - #[inline] - pub fn default_y_bounds(mut self, min: f64, max: f64) -> Self { - debug_assert!(min < max, "`min` must be less than `max` in `default_y_bounds`"); - self.default_auto_bounds.y = false; - self.min_auto_bounds.min[1] = min; - self.min_auto_bounds.max[1] = max; - self - } - - /// Expand bounds to include the given x value. - /// For instance, to always show the y axis, call `plot.include_x(0.0)`. - #[inline] - pub fn include_x(mut self, x: impl Into) -> Self { - self.min_auto_bounds.extend_with_x(x.into()); - self - } - - /// Expand bounds to include the given y value. - /// For instance, to always show the x axis, call `plot.include_y(0.0)`. - #[inline] - pub fn include_y(mut self, y: impl Into) -> Self { - self.min_auto_bounds.extend_with_y(y.into()); - self - } - - /// Set whether the bounds should be automatically set based on data by - /// default. - /// - /// This is enabled by default. - #[inline] - pub fn auto_bounds(mut self, auto_bounds: impl Into) -> Self { - self.default_auto_bounds = auto_bounds.into(); - self - } - - /// Expand bounds to fit all items across the x axis, including values given - /// by `include_x`. - #[deprecated = "Use `auto_bounds` instead"] - #[inline] - pub fn auto_bounds_x(mut self) -> Self { - self.default_auto_bounds.x = true; - self - } - - /// Expand bounds to fit all items across the y axis, including values given - /// by `include_y`. - #[deprecated = "Use `auto_bounds` instead"] - #[inline] - pub fn auto_bounds_y(mut self) -> Self { - self.default_auto_bounds.y = true; - self - } - - /// Show a legend including all named items. - #[inline] - pub fn legend(mut self, legend: Legend) -> Self { - self.legend_config = Some(legend); - self - } - - /// Whether or not to show the background [`Rect`]. - /// - /// Can be useful to disable if the plot is overlaid over existing content. - /// Default: `true`. - #[inline] - pub fn show_background(mut self, show: bool) -> Self { - self.show_background = show; - self - } - - /// Show axis labels and grid tick values on the side of the plot. - /// - /// Default: `true`. - #[inline] - pub fn show_axes(mut self, show: impl Into) -> Self { - self.show_axes = show.into(); - self - } - - /// Show a grid overlay on the plot. - /// - /// Default: `true`. - #[inline] - pub fn show_grid(mut self, show: impl Into) -> Self { - self.show_grid = show.into(); - self - } - - /// Add this plot to an axis link group so that this plot will share the - /// bounds with other plots in the same group. A plot cannot belong to - /// more than one axis group. - #[inline] - pub fn link_axis(mut self, group_id: impl Into, link: impl Into) -> Self { - self.linked_axes = Some((group_id.into(), link.into())); - self - } - - /// Add this plot to a cursor link group so that this plot will share the - /// cursor position with other plots in the same group. A plot cannot - /// belong to more than one cursor group. - #[inline] - pub fn link_cursor(mut self, group_id: impl Into, link: impl Into) -> Self { - self.linked_cursors = Some((group_id.into(), link.into())); - self - } - - /// Round grid positions to full pixels to avoid aliasing. Improves plot - /// appearance but might have an undesired effect when shifting the plot - /// bounds. Enabled by default. - #[inline] - #[deprecated = "This no longer has any effect and is always enabled."] - pub fn sharp_grid_lines(self, _enabled: bool) -> Self { - self - } - - /// Resets the plot. - #[inline] - pub fn reset(mut self) -> Self { - self.reset = true; - self - } - - /// Set the x axis label of the main X-axis. - /// - /// Default: no label. - #[inline] - pub fn x_axis_label(mut self, label: impl Into) -> Self { - if let Some(main) = self.x_axes.first_mut() { - main.label = label.into(); - } - self - } - - /// Set the y axis label of the main Y-axis. - /// - /// Default: no label. - #[inline] - pub fn y_axis_label(mut self, label: impl Into) -> Self { - if let Some(main) = self.y_axes.first_mut() { - main.label = label.into(); - } - self - } - - /// Set the position of the main X-axis. - #[inline] - pub fn x_axis_position(mut self, placement: axis::VPlacement) -> Self { - if let Some(main) = self.x_axes.first_mut() { - main.placement = placement.into(); - } - self - } - - /// Set the position of the main Y-axis. - #[inline] - pub fn y_axis_position(mut self, placement: axis::HPlacement) -> Self { - if let Some(main) = self.y_axes.first_mut() { - main.placement = placement.into(); - } - self - } - - /// Specify custom formatter for ticks on the main X-axis. - /// - /// Arguments of `fmt`: - /// * the grid mark to format - /// * currently shown range on this axis. - #[inline] - pub fn x_axis_formatter(mut self, fmt: impl Fn(GridMark, &RangeInclusive) -> String + 'a) -> Self { - if let Some(main) = self.x_axes.first_mut() { - main.formatter = Arc::new(fmt); - } - self - } - - /// Specify custom formatter for ticks on the main Y-axis. - /// - /// Arguments of `fmt`: - /// * the grid mark to format - /// * currently shown range on this axis. - #[inline] - pub fn y_axis_formatter(mut self, fmt: impl Fn(GridMark, &RangeInclusive) -> String + 'a) -> Self { - if let Some(main) = self.y_axes.first_mut() { - main.formatter = Arc::new(fmt); - } - self - } - - /// Set the minimum width of the main y-axis, in ui points. - /// - /// The width will automatically expand if any tickmark text is wider than - /// this. - #[inline] - pub fn y_axis_min_width(mut self, min_width: f32) -> Self { - if let Some(main) = self.y_axes.first_mut() { - main.min_thickness = min_width; - } - self - } - - /// Set the main Y-axis-width by number of digits - #[inline] - #[deprecated = "Use `y_axis_min_width` instead"] - pub fn y_axis_width(self, digits: usize) -> Self { - self.y_axis_min_width(12.0 * digits as f32) - } - - /// Set custom configuration for X-axis - /// - /// More than one axis may be specified. The first specified axis is - /// considered the main axis. - #[inline] - pub fn custom_x_axes(mut self, hints: Vec>) -> Self { - self.x_axes = hints; - self - } - - /// Set custom configuration for left Y-axis - /// - /// More than one axis may be specified. The first specified axis is - /// considered the main axis. - #[inline] - pub fn custom_y_axes(mut self, hints: Vec>) -> Self { - self.y_axes = hints; - self - } - - /// Set custom cursor color. - /// - /// You may set the color to [`Color32::TRANSPARENT`] to hide the cursors. - #[inline] - pub fn cursor_color(mut self, color: Color32) -> Self { - self.cursor_color = Some(color); - self - } - - /// Interact with and add items to the plot and finally draw it. - pub fn show<'b, R>(self, ui: &mut Ui, build_fn: impl FnOnce(&mut PlotUi<'b>) -> R + 'a) -> PlotResponse { - self.show_dyn(ui, Box::new(build_fn)) - } - - #[expect(clippy::too_many_lines)] // TODO #166: shorten this function - fn show_dyn<'b, R>(self, ui: &mut Ui, build_fn: Box) -> R + 'a>) -> PlotResponse { - let Self { - id_source, - id, - center_axis, - allow_zoom, - allow_drag, - allow_axis_zoom_drag, - allow_scroll, - allow_double_click_reset, - allow_boxed_zoom, - pan_pointer_button, - boxed_zoom_pointer_button, - default_auto_bounds, - min_auto_bounds, - margin_fraction, - width, - height, - mut min_size, - data_aspect, - view_aspect, - invert_x, - invert_y, - mut show_x, - mut show_y, - label_formatter, - coordinates_formatter, - x_axes, - y_axes, - legend_config, - cursor_color, - reset, - show_background, - show_axes, - show_grid, - grid_spacing, - linked_axes, - linked_cursors, - - clamp_grid, - grid_spacers, - sense, - } = self; - - // Disable interaction if ui is disabled. - let allow_zoom = allow_zoom.and(ui.is_enabled()); - let allow_drag = allow_drag.and(ui.is_enabled()); - let allow_scroll = allow_scroll.and(ui.is_enabled()); - - // Determine position of widget. - let pos = ui.available_rect_before_wrap().min; - // Minimum values for screen protection - min_size.x = min_size.x.at_least(1.0); - min_size.y = min_size.y.at_least(1.0); - - // Determine size of widget. - let size = { - let width = width - .unwrap_or_else(|| { - if let (Some(height), Some(aspect)) = (height, view_aspect) { - height * aspect - } else { - ui.available_size_before_wrap().x - } - }) - .at_least(min_size.x); - - let height = height - .unwrap_or_else(|| { - if let Some(aspect) = view_aspect { - width / aspect - } else { - ui.available_size_before_wrap().y - } - }) - .at_least(min_size.y); - vec2(width, height) - }; - - // Determine complete rect of widget. - let complete_rect = Rect { - min: pos, - max: pos + size, - }; - - let plot_id = id.unwrap_or_else(|| ui.make_persistent_id(id_source)); - - let ([x_axis_widgets, y_axis_widgets], plot_rect) = axis_widgets( - PlotMemory::load(ui.ctx(), plot_id).as_ref(), // TODO #164: avoid loading plot memory twice - show_axes, - complete_rect, - [&x_axes, &y_axes], - ); - - // Allocate the plot window. - let response = ui.allocate_rect(plot_rect, sense); - - let x_axis_responses = x_axis_widgets - .iter() - .map(|widget| { - let axis_response = ui.allocate_rect(widget.rect, Sense::drag()); - if allow_axis_zoom_drag.x { - axis_response.on_hover_cursor(CursorIcon::ResizeHorizontal) - } else { - axis_response - } - }) - .collect::>(); - - let y_axis_responses = y_axis_widgets - .iter() - .map(|widget| { - let axis_response = ui.allocate_rect(widget.rect, Sense::drag()); - - if allow_axis_zoom_drag.y { - axis_response.on_hover_cursor(CursorIcon::ResizeVertical) - } else { - axis_response - } - }) - .collect::>(); - - // Load or initialize the memory. - ui.ctx().check_for_id_clash(plot_id, plot_rect, "Plot"); - - let mut mem = if reset { - if let Some((name, _)) = linked_axes.as_ref() { - ui.data_mut(|data| { - let link_groups: &mut BoundsLinkGroups = data.get_temp_mut_or_default(Id::NULL); - link_groups.0.remove(name); - }); - } - None - } else { - PlotMemory::load(ui.ctx(), plot_id) - } - .unwrap_or_else(|| PlotMemory { - auto_bounds: default_auto_bounds, - hovered_legend_item: None, - hidden_items: Default::default(), - transform: PlotTransform::new_with_invert_axis( - plot_rect, - min_auto_bounds, - center_axis, - Vec2b::new(invert_x, invert_y), - ), - last_click_pos_for_zoom: None, - x_axis_thickness: Default::default(), - y_axis_thickness: Default::default(), - }); - - let last_plot_transform = mem.transform; - - // Call the plot build function. - let mut plot_ui = PlotUi { - ctx: ui.ctx().clone(), - items: Vec::new(), - next_auto_color_idx: 0, - last_plot_transform, - last_auto_bounds: mem.auto_bounds, - response, - bounds_modifications: Vec::new(), - }; - let inner = build_fn(&mut plot_ui); - let PlotUi { - mut items, - mut response, - last_plot_transform, - bounds_modifications, - .. - } = plot_ui; - - // Background - if show_background { - ui.painter().with_clip_rect(plot_rect).add(epaint::RectShape::new( - plot_rect, - 2, - ui.visuals().extreme_bg_color, - ui.visuals().widgets.noninteractive.bg_stroke, - egui::StrokeKind::Inside, - )); - } - - // --- Legend --- - let legend = - legend_config.and_then(|config| LegendWidget::try_new(plot_rect, config, &items, &mem.hidden_items)); - // Don't show hover cursor when hovering over legend. - if mem.hovered_legend_item.is_some() { - show_x = false; - show_y = false; - } - // Remove the deselected items. - items.retain(|item| !mem.hidden_items.contains(&item.id())); - // Highlight the hovered items. - if let Some(item_id) = &mem.hovered_legend_item { - items - .iter_mut() - .filter(|entry| &entry.id() == item_id) - .for_each(|entry| entry.highlight()); - } - // Move highlighted items to front. - items.sort_by_key(|item| item.highlighted()); - - // --- Bound computation --- - let mut bounds = *last_plot_transform.bounds(); - - // Find the cursors from other plots we need to draw - let draw_cursors: Vec = if let Some((id, _)) = linked_cursors.as_ref() { - ui.data_mut(|data| { - let frames: &mut CursorLinkGroups = data.get_temp_mut_or_default(Id::NULL); - let cursors = frames.0.entry(*id).or_default(); - - // Look for our previous frame - let index = cursors - .iter() - .enumerate() - .find(|(_, frame)| frame.id == plot_id) - .map(|(i, _)| i); - - // Remove our previous frame and all older frames as these are no longer - // displayed. This avoids unbounded growth, as we add an entry - // each time we draw a plot. - index.map(|index| cursors.drain(0..=index)); - - // Gather all cursors of the remaining frames. This will be all the cursors of - // the other plots in the group. We want to draw these in the - // current plot too. - cursors.iter().flat_map(|frame| frame.cursors.iter().copied()).collect() - }) - } else { - Vec::new() - }; - - // Transfer the bounds from a link group. - if let Some((id, axes)) = linked_axes.as_ref() { - ui.data_mut(|data| { - let link_groups: &mut BoundsLinkGroups = data.get_temp_mut_or_default(Id::NULL); - if let Some(linked_bounds) = link_groups.0.get(id) { - if axes.x { - bounds.set_x(&linked_bounds.bounds); - mem.auto_bounds.x = linked_bounds.auto_bounds.x; - } - if axes.y { - bounds.set_y(&linked_bounds.bounds); - mem.auto_bounds.y = linked_bounds.auto_bounds.y; - } - } - }); - } - - // Allow double-clicking to reset to the initial bounds. - if allow_double_click_reset && response.double_clicked() { - mem.auto_bounds = true.into(); - } - - let any_dynamic_modifications = !bounds_modifications.is_empty(); - // Apply bounds modifications. - for modification in bounds_modifications { - match modification { - BoundsModification::SetX(range) => { - bounds.min[0] = *range.start(); - bounds.max[0] = *range.end(); - mem.auto_bounds.x = false; - } - BoundsModification::SetY(range) => { - bounds.min[1] = *range.start(); - bounds.max[1] = *range.end(); - mem.auto_bounds.y = false; - } - BoundsModification::Translate(delta) => { - let delta = (delta.x as f64, delta.y as f64); - bounds.translate(delta); - mem.auto_bounds = false.into(); - } - BoundsModification::AutoBounds(new_auto_bounds) => { - mem.auto_bounds = new_auto_bounds; - } - BoundsModification::Zoom(zoom_factor, center) => { - bounds.zoom(zoom_factor, center); - mem.auto_bounds = false.into(); - } - } - } - - // Reset bounds to initial bounds if they haven't been modified. - if (!default_auto_bounds.x && !any_dynamic_modifications) || mem.auto_bounds.x { - bounds.set_x(&min_auto_bounds); - } - if (!default_auto_bounds.y && !any_dynamic_modifications) || mem.auto_bounds.y { - bounds.set_y(&min_auto_bounds); - } - - let auto_x = mem.auto_bounds.x && (!min_auto_bounds.is_valid_x() || default_auto_bounds.x); - let auto_y = mem.auto_bounds.y && (!min_auto_bounds.is_valid_y() || default_auto_bounds.y); - - // Set bounds automatically based on content. - if auto_x || auto_y { - for item in &items { - let item_bounds = item.bounds(); - if auto_x { - bounds.merge_x(&item_bounds); - } - if auto_y { - bounds.merge_y(&item_bounds); - } - } - - if auto_x { - bounds.add_relative_margin_x(margin_fraction); - } - - if auto_y { - bounds.add_relative_margin_y(margin_fraction); - } - } - - mem.transform = - PlotTransform::new_with_invert_axis(plot_rect, bounds, center_axis, Vec2b::new(invert_x, invert_y)); - - // Enforce aspect ratio - if let Some(data_aspect) = data_aspect { - if let Some((_, linked_axes)) = &linked_axes { - let change_x = linked_axes.y && !linked_axes.x; - mem.transform - .set_aspect_by_changing_axis(data_aspect as f64, if change_x { Axis::X } else { Axis::Y }); - } else if default_auto_bounds.any() { - mem.transform.set_aspect_by_expanding(data_aspect as f64); - } else { - mem.transform.set_aspect_by_changing_axis(data_aspect as f64, Axis::Y); - } - } - - // Dragging - if allow_drag.any() && response.dragged_by(pan_pointer_button) { - response = response.on_hover_cursor(CursorIcon::Grabbing); - let mut delta = -response.drag_delta(); - if !allow_drag.x { - delta.x = 0.0; - } - if !allow_drag.y { - delta.y = 0.0; - } - mem.transform.translate_bounds((delta.x as f64, delta.y as f64)); - mem.auto_bounds = mem.auto_bounds.and(!allow_drag); - } - - // Drag axes to zoom: - for d in 0..2 { - if allow_axis_zoom_drag[d] { - if let Some(axis_response) = (if d == 0 { &x_axis_responses } else { &y_axis_responses }) - .iter() - .find(|r| r.dragged_by(PointerButton::Primary)) - { - if let Some(drag_start_pos) = ui.input(|i| i.pointer.press_origin()) { - let delta = axis_response.drag_delta(); - - let axis_zoom = 1.0 + (0.02 * delta[d]).clamp(-1.0, 1.0); - - let zoom = if data_aspect.is_some() { - // Zoom both axes equally to maintain aspect ratio: - Vec2::splat(axis_zoom) - } else { - let mut zoom = Vec2::splat(1.0); - zoom[d] = axis_zoom; - zoom - }; - - if zoom != Vec2::splat(1.0) { - let mut zoom_center = plot_rect.center(); - zoom_center[d] = drag_start_pos[d]; - mem.transform.zoom(zoom, zoom_center); - mem.auto_bounds = false.into(); - } - } - } - } - } - - // Zooming - let mut boxed_zoom_rect = None; - if allow_boxed_zoom { - // Save last click to allow boxed zooming - if response.drag_started() && response.dragged_by(boxed_zoom_pointer_button) { - // it would be best for egui that input has a memory of the last click pos - // because it's a common pattern - mem.last_click_pos_for_zoom = response.hover_pos(); - } - let box_start_pos = mem.last_click_pos_for_zoom; - let box_end_pos = response.hover_pos(); - if let (Some(box_start_pos), Some(box_end_pos)) = (box_start_pos, box_end_pos) { - // while dragging prepare a Shape and draw it later on top of the plot - if response.dragged_by(boxed_zoom_pointer_button) { - response = response.on_hover_cursor(CursorIcon::ZoomIn); - let rect = epaint::Rect::from_two_pos(box_start_pos, box_end_pos); - boxed_zoom_rect = Some(( - epaint::RectShape::stroke( - rect, - 0.0, - epaint::Stroke::new(4., Color32::DARK_BLUE), - egui::StrokeKind::Middle, - ), // Outer stroke - epaint::RectShape::stroke( - rect, - 0.0, - epaint::Stroke::new(2., Color32::WHITE), - egui::StrokeKind::Middle, - ), // Inner stroke - )); - } - // when the click is release perform the zoom - if response.drag_stopped() { - let box_start_pos = mem.transform.value_from_position(box_start_pos); - let box_end_pos = mem.transform.value_from_position(box_end_pos); - let new_bounds = PlotBounds { - min: [box_start_pos.x.min(box_end_pos.x), box_start_pos.y.min(box_end_pos.y)], - max: [box_start_pos.x.max(box_end_pos.x), box_start_pos.y.max(box_end_pos.y)], - }; - if new_bounds.is_valid() { - mem.transform.set_bounds(new_bounds); - mem.auto_bounds = false.into(); - } - // reset the boxed zoom state - mem.last_click_pos_for_zoom = None; - } - } - } - - // Note: we catch zoom/pan if the response contains the pointer, even if it - // isn't hovered. For instance: The user is painting another interactive - // widget on top of the plot but they still want to be able to pan/zoom - // the plot. - if let (true, Some(hover_pos)) = (response.contains_pointer(), ui.input(|i| i.pointer.hover_pos())) { - if allow_zoom.any() { - let mut zoom_factor = if data_aspect.is_some() { - Vec2::splat(ui.input(|i| i.zoom_delta())) - } else { - ui.input(|i| i.zoom_delta_2d()) - }; - if !allow_zoom.x { - zoom_factor.x = 1.0; - } - if !allow_zoom.y { - zoom_factor.y = 1.0; - } - if zoom_factor != Vec2::splat(1.0) { - mem.transform.zoom(zoom_factor, hover_pos); - mem.auto_bounds = mem.auto_bounds.and(!allow_zoom); - } - } - if allow_scroll.any() { - let mut scroll_delta = ui.input(|i| i.smooth_scroll_delta); - if !allow_scroll.x { - scroll_delta.x = 0.0; - } - if !allow_scroll.y { - scroll_delta.y = 0.0; - } - if scroll_delta != Vec2::ZERO { - mem.transform - .translate_bounds((-scroll_delta.x as f64, -scroll_delta.y as f64)); - mem.auto_bounds = false.into(); - } - } - } - - // --- transform initialized - - // Add legend widgets to plot - let bounds = mem.transform.bounds(); - let x_axis_range = bounds.range_x(); - let x_steps = Arc::new({ - let input = GridInput { - bounds: (bounds.min[0], bounds.max[0]), - base_step_size: mem.transform.dvalue_dpos()[0].abs() * grid_spacing.min as f64, - }; - (grid_spacers[0])(input) - }); - let y_axis_range = bounds.range_y(); - let y_steps = Arc::new({ - let input = GridInput { - bounds: (bounds.min[1], bounds.max[1]), - base_step_size: mem.transform.dvalue_dpos()[1].abs() * grid_spacing.min as f64, - }; - (grid_spacers[1])(input) - }); - for (i, mut widget) in x_axis_widgets.into_iter().enumerate() { - widget.range = x_axis_range.clone(); - widget.transform = Some(mem.transform); - widget.steps = x_steps.clone(); - let (_response, thickness) = widget.ui(ui, Axis::X); - mem.x_axis_thickness.insert(i, thickness); - } - for (i, mut widget) in y_axis_widgets.into_iter().enumerate() { - widget.range = y_axis_range.clone(); - widget.transform = Some(mem.transform); - widget.steps = y_steps.clone(); - let (_response, thickness) = widget.ui(ui, Axis::Y); - mem.y_axis_thickness.insert(i, thickness); - } - - // Initialize values from functions. - for item in &mut items { - item.initialize(mem.transform.bounds().range_x()); - } - - let prepared = PreparedPlot { - plot_area_response: &response, - items, - show_x, - show_y, - label_formatter, - coordinates_formatter, - show_grid, - grid_spacing, - transform: mem.transform, - draw_cursor_x: linked_cursors.as_ref().is_some_and(|group| group.1.x), - draw_cursor_y: linked_cursors.as_ref().is_some_and(|group| group.1.y), - draw_cursors, - cursor_color, - grid_spacers, - clamp_grid, - }; - - let (plot_cursors, mut hovered_plot_item) = prepared.ui(ui, &response); - - if let Some(boxed_zoom_rect) = boxed_zoom_rect { - ui.painter().with_clip_rect(plot_rect).add(boxed_zoom_rect.0); - ui.painter().with_clip_rect(plot_rect).add(boxed_zoom_rect.1); - } - - if let Some(mut legend) = legend { - ui.add(&mut legend); - mem.hidden_items = legend.hidden_items(); - mem.hovered_legend_item = legend.hovered_item(); - - if let Some(item_id) = &mem.hovered_legend_item { - hovered_plot_item.get_or_insert(*item_id); - } - } - - if let Some((id, _)) = linked_cursors.as_ref() { - // Push the frame we just drew to the list of frames - ui.data_mut(|data| { - let frames: &mut CursorLinkGroups = data.get_temp_mut_or_default(Id::NULL); - let cursors = frames.0.entry(*id).or_default(); - cursors.push(PlotFrameCursors { - id: plot_id, - cursors: plot_cursors, - }); - }); - } - - if let Some((id, _)) = linked_axes.as_ref() { - // Save the linked bounds. - ui.data_mut(|data| { - let link_groups: &mut BoundsLinkGroups = data.get_temp_mut_or_default(Id::NULL); - link_groups.0.insert( - *id, - LinkedBounds { - bounds: *mem.transform.bounds(), - auto_bounds: mem.auto_bounds, - }, - ); - }); - } - - let transform = mem.transform; - mem.store(ui.ctx(), plot_id); - - let response = if show_x || show_y { - response.on_hover_cursor(CursorIcon::Crosshair) - } else { - response - }; - - ui.advance_cursor_after_rect(complete_rect); - - PlotResponse { - inner, - response, - transform, - hovered_plot_item, - } - } -} - /// Returns the rect left after adding axes. fn axis_widgets<'a>( mem: Option<&PlotMemory>, @@ -1703,270 +389,6 @@ pub fn uniform_grid_spacer<'a>(spacer: impl Fn(GridInput) -> [f64; 3] + 'a) -> G // ---------------------------------------------------------------------------- -struct PreparedPlot<'a> { - /// The response of the whole plot area - plot_area_response: &'a Response, - items: Vec>, - show_x: bool, - show_y: bool, - label_formatter: LabelFormatter<'a>, - coordinates_formatter: Option<(Corner, CoordinatesFormatter<'a>)>, - // axis_formatters: [AxisFormatter; 2], - transform: PlotTransform, - show_grid: Vec2b, - grid_spacing: Rangef, - grid_spacers: [GridSpacer<'a>; 2], - draw_cursor_x: bool, - draw_cursor_y: bool, - draw_cursors: Vec, - cursor_color: Option, - - clamp_grid: bool, -} - -impl PreparedPlot<'_> { - fn ui(self, ui: &mut Ui, response: &Response) -> (Vec, Option) { - let mut axes_shapes = Vec::new(); - - if self.show_grid.x { - self.paint_grid(ui, &mut axes_shapes, Axis::X, self.grid_spacing); - } - if self.show_grid.y { - self.paint_grid(ui, &mut axes_shapes, Axis::Y, self.grid_spacing); - } - - // 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 transform = &self.transform; - - let mut plot_ui = ui.new_child( - egui::UiBuilder::new() - .max_rect(*transform.frame()) - .layout(Layout::default()), - ); - plot_ui.set_clip_rect(transform.frame().intersect(ui.clip_rect())); - for item in &self.items { - item.shapes(&plot_ui, transform, &mut shapes); - } - - let hover_pos = response.hover_pos(); - let (cursors, hovered_item_id) = if let Some(pointer) = hover_pos { - self.hover(ui, pointer, &mut shapes) - } else { - (Vec::new(), None) - }; - - // Draw cursors - let line_color = self.cursor_color.unwrap_or_else(|| rulers_color(ui)); - - let mut draw_cursor = |cursors: &Vec, always| { - for &cursor in cursors { - match cursor { - Cursor::Horizontal { y } => { - if self.draw_cursor_y || always { - shapes.push(horizontal_line( - transform.position_from_point(&PlotPoint::new(0.0, y)), - &self.transform, - line_color, - )); - } - } - Cursor::Vertical { x } => { - if self.draw_cursor_x || always { - shapes.push(vertical_line( - transform.position_from_point(&PlotPoint::new(x, 0.0)), - &self.transform, - line_color, - )); - } - } - } - } - }; - - draw_cursor(&self.draw_cursors, false); - draw_cursor(&cursors, true); - - let painter = ui.painter().with_clip_rect(*transform.frame()); - painter.extend(shapes); - - // Show coordinates in a corner of the plot: - if let Some((corner, formatter)) = self.coordinates_formatter.as_ref() { - let hover_pos = response.hover_pos(); - if let Some(pointer) = hover_pos { - let font_id = TextStyle::Monospace.resolve(ui.style()); - let coordinate = transform.value_from_position(pointer); - let text = formatter.format(&coordinate, transform.bounds()); - let padded_frame = transform.frame().shrink(4.0); - let (anchor, position) = match corner { - Corner::LeftTop => (Align2::LEFT_TOP, padded_frame.left_top()), - Corner::RightTop => (Align2::RIGHT_TOP, padded_frame.right_top()), - Corner::LeftBottom => (Align2::LEFT_BOTTOM, padded_frame.left_bottom()), - Corner::RightBottom => (Align2::RIGHT_BOTTOM, padded_frame.right_bottom()), - }; - - let text_color = ui.visuals().text_color(); - let galley = painter.layout_no_wrap(text, font_id, text_color); - let rect = anchor.anchor_size(position, galley.size()); - painter.rect_filled( - rect.expand(4.0), - ui.style().visuals.window_corner_radius, - ui.style().visuals.extreme_bg_color.gamma_multiply(0.75), - ); - painter.galley(rect.min, galley, text_color); - } - } - - (cursors, hovered_item_id) - } - - fn paint_grid(&self, ui: &Ui, shapes: &mut Vec<(Shape, f32)>, axis: Axis, fade_range: Rangef) { - let Self { - transform, - // axis_formatters, - grid_spacers, - clamp_grid, - .. - } = self; - - let iaxis = usize::from(axis); - - // Where on the cross-dimension to show the label values - let bounds = transform.bounds(); - let value_cross = 0.0_f64.clamp(bounds.min[1 - iaxis], bounds.max[1 - iaxis]); - - let input = GridInput { - bounds: (bounds.min[iaxis], bounds.max[iaxis]), - base_step_size: transform.dvalue_dpos()[iaxis].abs() * fade_range.min as f64, - }; - let steps = (grid_spacers[iaxis])(input); - - let clamp_range = clamp_grid.then(|| { - let mut tight_bounds = PlotBounds::NOTHING; - for item in &self.items { - let item_bounds = item.bounds(); - tight_bounds.merge_x(&item_bounds); - tight_bounds.merge_y(&item_bounds); - } - tight_bounds - }); - - for step in steps { - let value_main = step.value; - - if let Some(clamp_range) = clamp_range { - match axis { - Axis::X => { - if !clamp_range.range_x().contains(&value_main) { - continue; - } - } - Axis::Y => { - if !clamp_range.range_y().contains(&value_main) { - continue; - } - } - } - } - - let value = match axis { - Axis::X => PlotPoint::new(value_main, value_cross), - Axis::Y => PlotPoint::new(value_cross, value_main), - }; - - let pos_in_gui = transform.position_from_point(&value); - let spacing_in_points = (transform.dpos_dvalue()[iaxis] * step.step_size).abs() as f32; - - if spacing_in_points <= fade_range.min { - continue; // Too close together - } - - let line_strength = remap_clamp(spacing_in_points, fade_range, 0.0..=1.0); - - let line_color = color_from_strength(ui, line_strength); - - let mut p0 = pos_in_gui; - let mut p1 = pos_in_gui; - p0[1 - iaxis] = transform.frame().min[1 - iaxis]; - p1[1 - iaxis] = transform.frame().max[1 - iaxis]; - - if let Some(clamp_range) = clamp_range { - match axis { - Axis::X => { - p0.y = transform.position_from_point_y(clamp_range.min[1]); - p1.y = transform.position_from_point_y(clamp_range.max[1]); - } - Axis::Y => { - p0.x = transform.position_from_point_x(clamp_range.min[0]); - p1.x = transform.position_from_point_x(clamp_range.max[0]); - } - } - } - - shapes.push(( - Shape::line_segment([p0, p1], Stroke::new(1.0, line_color)), - line_strength, - )); - } - } - - fn hover(&self, ui: &Ui, pointer: Pos2, shapes: &mut Vec) -> (Vec, Option) { - let Self { - plot_area_response, - transform, - show_x, - show_y, - label_formatter, - items, - .. - } = self; - - if !show_x && !show_y { - return (Vec::new(), None); - } - - let interact_radius_sq = ui.style().interaction.interact_radius.powi(2); - - let candidates = items.iter().filter(|entry| entry.allow_hover()).filter_map(|item| { - let item = &**item; - let closest = item.find_closest(pointer, transform); - - Some(item).zip(closest) - }); - - // Since many items can have same distance, - // and dist_sq can be zero for some items (e.g. rectangle) - // we pick topmost item within interact radius - let topmost = candidates - .filter(|(_, elem)| elem.dist_sq <= interact_radius_sq) - .next_back(); - - let plot = items::PlotConfig { - ui, - transform, - show_x: *show_x, - show_y: *show_y, - }; - - let mut cursors = Vec::new(); - - let hovered_plot_item_id = if let Some((item, elem)) = topmost { - item.on_hover(plot_area_response, elem, shapes, &mut cursors, &plot, label_formatter); - Some(item.id()) - } else { - let value = transform.value_from_position(pointer); - items::rulers_and_tooltip_at_value(plot_area_response, value, "", &plot, &mut cursors, label_formatter); - None - }; - - (cursors, hovered_plot_item_id) - } -} - /// Returns next bigger power in given base /// e.g. /// ```ignore diff --git a/egui_plot/src/plot.rs b/egui_plot/src/plot.rs new file mode 100644 index 00000000..f3558ce7 --- /dev/null +++ b/egui_plot/src/plot.rs @@ -0,0 +1,1614 @@ +use std::ops::RangeInclusive; +use std::sync::Arc; + +use egui::Color32; +use egui::CursorIcon; +use egui::Id; +use egui::Layout; +use egui::PointerButton; +use egui::Response; +use egui::Sense; +use egui::Shape; +use egui::Stroke; +use egui::TextStyle; +use egui::Ui; +use egui::WidgetText; +use egui::epaint; +use emath::Align2; +use emath::NumExt as _; +use emath::Pos2; +use emath::Rangef; +use emath::Rect; +use emath::Vec2; +use emath::Vec2b; +use emath::remap_clamp; +use emath::vec2; + +use crate::Axis; +use crate::AxisHints; +use crate::BoundsLinkGroups; +use crate::BoundsModification; +use crate::CoordinatesFormatter; +use crate::Corner; +use crate::Cursor; +use crate::CursorLinkGroups; +use crate::GridInput; +use crate::GridMark; +use crate::GridSpacer; +use crate::LabelFormatter; +use crate::Legend; +use crate::LinkedBounds; +use crate::PlotBounds; +use crate::PlotFrameCursors; +use crate::PlotItem; +use crate::PlotMemory; +use crate::PlotPoint; +use crate::PlotResponse; +use crate::PlotTransform; +use crate::PlotUi; +use crate::items; +use crate::items::horizontal_line; +use crate::items::rulers_color; +use crate::items::vertical_line; +use crate::legend::LegendWidget; + +/// A 2D plot, e.g. a graph of a function. +/// +/// [`Plot`] supports multiple lines and points. +/// +/// ``` +/// # egui::__run_test_ui(|ui| { +/// use egui_plot::Line; +/// use egui_plot::Plot; +/// use egui_plot::PlotPoints; +/// +/// let sin: PlotPoints = (0..1000) +/// .map(|i| { +/// let x = i as f64 * 0.01; +/// [x, x.sin()] +/// }) +/// .collect(); +/// let line = Line::new("sin", sin); +/// Plot::new("my_plot") +/// .view_aspect(2.0) +/// .show(ui, |plot_ui| plot_ui.line(line)); +/// # }); +/// ``` +pub struct Plot<'a> { + id_source: Id, + id: Option, + + center_axis: Vec2b, + allow_zoom: Vec2b, + allow_drag: Vec2b, + allow_axis_zoom_drag: Vec2b, + allow_scroll: Vec2b, + allow_double_click_reset: bool, + allow_boxed_zoom: bool, + default_auto_bounds: Vec2b, + min_auto_bounds: PlotBounds, + margin_fraction: Vec2, + pan_pointer_button: PointerButton, + boxed_zoom_pointer_button: PointerButton, + linked_axes: Option<(Id, Vec2b)>, + linked_cursors: Option<(Id, Vec2b)>, + + min_size: Vec2, + width: Option, + height: Option, + data_aspect: Option, + view_aspect: Option, + invert_x: bool, + invert_y: bool, + + reset: bool, + + show_x: bool, + show_y: bool, + label_formatter: LabelFormatter<'a>, + coordinates_formatter: Option<(Corner, CoordinatesFormatter<'a>)>, + x_axes: Vec>, // default x axes + y_axes: Vec>, // default y axes + legend_config: Option, + cursor_color: Option, + show_background: bool, + show_axes: Vec2b, + + show_grid: Vec2b, + grid_spacing: Rangef, + grid_spacers: [GridSpacer<'a>; 2], + clamp_grid: bool, + + sense: Sense, +} + +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 { + id_source: Id::new(id_source), + id: None, + + center_axis: false.into(), + allow_zoom: true.into(), + allow_drag: true.into(), + allow_axis_zoom_drag: true.into(), + allow_scroll: true.into(), + allow_double_click_reset: true, + allow_boxed_zoom: true, + default_auto_bounds: true.into(), + min_auto_bounds: PlotBounds::NOTHING, + margin_fraction: Vec2::splat(0.05), + pan_pointer_button: PointerButton::Primary, + boxed_zoom_pointer_button: PointerButton::Secondary, + linked_axes: None, + linked_cursors: None, + + min_size: Vec2::splat(64.0), + width: None, + height: None, + data_aspect: None, + view_aspect: None, + invert_x: false, + invert_y: false, + + reset: false, + + show_x: true, + show_y: true, + label_formatter: None, + coordinates_formatter: None, + x_axes: vec![AxisHints::new(Axis::X)], + y_axes: vec![AxisHints::new(Axis::Y)], + legend_config: None, + cursor_color: None, + show_background: true, + show_axes: true.into(), + + show_grid: true.into(), + grid_spacing: Rangef::new(8.0, 300.0), + grid_spacers: [crate::log_grid_spacer(10), crate::log_grid_spacer(10)], + clamp_grid: false, + + sense: egui::Sense::click_and_drag(), + } + } + + /// Set an explicit (global) id for the plot. + /// + /// This will override the id set by [`Self::new`]. + /// + /// This is the same `Id` that can be used for [`PlotMemory::load`]. + #[inline] + pub fn id(mut self, id: Id) -> Self { + self.id = Some(id); + self + } + + /// width / height ratio of the data. + /// For instance, it can be useful to set this to `1.0` for when the two + /// axes show the same unit. + /// By default the plot window's aspect ratio is used. + #[inline] + pub fn data_aspect(mut self, data_aspect: f32) -> Self { + self.data_aspect = Some(data_aspect); + self + } + + /// width / height ratio of the plot region. + /// By default no fixed aspect ratio is set (and width/height will fill the + /// ui it is in). + #[inline] + pub fn view_aspect(mut self, view_aspect: f32) -> Self { + self.view_aspect = Some(view_aspect); + self + } + + /// Set whether to invert the x-axis (i.e. positive values go to the left). + /// By default the x-axis is not inverted (i.e. positive values go to the + /// right). + #[inline] + pub fn invert_x(mut self, invert: bool) -> Self { + self.invert_x = invert; + self + } + + /// Set whether to invert the y-axis (i.e. positive values go down). + /// By default the y-axis is not inverted (i.e. positive values go up). + #[inline] + pub fn invert_y(mut self, invert: bool) -> Self { + self.invert_y = invert; + self + } + + /// Width of plot. By default a plot will fill the ui it is in. + /// If you set [`Self::view_aspect`], the width can be calculated from the + /// height. + #[inline] + pub fn width(mut self, width: f32) -> Self { + self.min_size.x = width; + self.width = Some(width); + self + } + + /// Height of plot. By default a plot will fill the ui it is in. + /// If you set [`Self::view_aspect`], the height can be calculated from the + /// width. + #[inline] + pub fn height(mut self, height: f32) -> Self { + self.min_size.y = height; + self.height = Some(height); + self + } + + /// Minimum size of the plot view. + #[inline] + pub fn min_size(mut self, min_size: Vec2) -> Self { + self.min_size = min_size; + self + } + + /// Show the x-value (e.g. when hovering). Default: `true`. + #[inline] + pub fn show_x(mut self, show_x: bool) -> Self { + self.show_x = show_x; + self + } + + /// Show the y-value (e.g. when hovering). Default: `true`. + #[inline] + pub fn show_y(mut self, show_y: bool) -> Self { + self.show_y = show_y; + self + } + + /// Always keep the X-axis centered. Default: `false`. + #[inline] + pub fn center_x_axis(mut self, on: bool) -> Self { + self.center_axis.x = on; + self + } + + /// Always keep the Y-axis centered. Default: `false`. + #[inline] + pub fn center_y_axis(mut self, on: bool) -> Self { + self.center_axis.y = on; + self + } + + /// Whether to allow zooming in the plot. Default: `true`. + /// + /// Note: Allowing zoom in one axis but not the other may lead to unexpected + /// results if used in combination with `data_aspect`. + #[inline] + pub fn allow_zoom(mut self, on: T) -> Self + where + T: Into, + { + self.allow_zoom = on.into(); + self + } + + /// Whether to allow scrolling in the plot. Default: `true`. + #[inline] + pub fn allow_scroll(mut self, on: T) -> Self + where + T: Into, + { + self.allow_scroll = on.into(); + self + } + + /// Whether to allow double clicking to reset the view. + /// Default: `true`. + #[inline] + pub fn allow_double_click_reset(mut self, on: bool) -> Self { + self.allow_double_click_reset = on; + self + } + + /// Set the side margin as a fraction of the plot size. Only used for auto + /// bounds. + /// + /// For instance, a value of `0.1` will add 10% space on both sides. + #[inline] + pub fn set_margin_fraction(mut self, margin_fraction: Vec2) -> Self { + self.margin_fraction = margin_fraction; + self + } + + /// Whether to allow zooming in the plot by dragging out a box with the + /// secondary mouse button. + /// + /// Default: `true`. + /// + /// The button to use is specified by [`Self::boxed_zoom_pointer_button`]. + #[inline] + pub fn allow_boxed_zoom(mut self, on: bool) -> Self { + self.allow_boxed_zoom = on; + self + } + + /// Config the button pointer to use for drag-to-pan. Default: + /// [`Secondary`](PointerButton::Primary) + #[inline] + pub fn pan_pointer_button(mut self, pan_pointer_button: PointerButton) -> Self { + self.pan_pointer_button = pan_pointer_button; + self + } + + /// Config the button pointer to use for boxed zooming. Default: + /// [`Secondary`](PointerButton::Secondary) + #[inline] + pub fn boxed_zoom_pointer_button(mut self, boxed_zoom_pointer_button: PointerButton) -> Self { + self.boxed_zoom_pointer_button = boxed_zoom_pointer_button; + self + } + + /// Whether to allow dragging in the plot to move the bounds. Default: + /// `true`. + /// + /// The button to use is specified by [`Self::pan_pointer_button`]. + #[inline] + pub fn allow_drag(mut self, on: T) -> Self + where + T: Into, + { + self.allow_drag = on.into(); + self + } + + /// Whether to allow dragging in the axis areas to zoom the plot. Default: + /// `true`. + #[inline] + pub fn allow_axis_zoom_drag(mut self, on: T) -> Self + where + T: Into, + { + self.allow_axis_zoom_drag = on.into(); + self + } + + /// Provide a function to customize the on-hover label for the x and y axis + /// + /// ``` + /// # egui::__run_test_ui(|ui| { + /// use egui_plot::Line; + /// use egui_plot::Plot; + /// use egui_plot::PlotPoints; + /// let sin: PlotPoints = (0..1000) + /// .map(|i| { + /// let x = i as f64 * 0.01; + /// [x, x.sin()] + /// }) + /// .collect(); + /// let line = Line::new("sin", sin); + /// Plot::new("my_plot") + /// .view_aspect(2.0) + /// .label_formatter(|name, value| { + /// if !name.is_empty() { + /// format!("{}: {:.*}%", name, 1, value.y) + /// } else { + /// "".to_owned() + /// } + /// }) + /// .show(ui, |plot_ui| plot_ui.line(line)); + /// # }); + /// ``` + #[inline] + pub fn label_formatter(mut self, label_formatter: impl Fn(&str, &PlotPoint) -> String + 'a) -> Self { + self.label_formatter = Some(Box::new(label_formatter)); + self + } + + /// Show the pointer coordinates in the plot. + #[inline] + pub fn coordinates_formatter(mut self, position: Corner, formatter: CoordinatesFormatter<'a>) -> Self { + self.coordinates_formatter = Some((position, formatter)); + self + } + + /// Configure how the grid in the background is spaced apart along the X + /// axis. + /// + /// Default is a log-10 grid, i.e. every plot unit is divided into 10 other + /// units. + /// + /// The function has this signature: + /// ```ignore + /// fn step_sizes(input: GridInput) -> Vec; + /// ``` + /// + /// This function should return all marks along the visible range of the X + /// axis. `step_size` also determines how thick/faint each line is + /// drawn. For example, if x = 80..=230 is visible and you want big + /// marks at steps of 100 and small ones at 25, you can return: + /// ```no_run + /// # use egui_plot::GridMark; + /// vec![ + /// // 100s + /// GridMark { + /// value: 100.0, + /// step_size: 100.0, + /// }, + /// GridMark { + /// value: 200.0, + /// step_size: 100.0, + /// }, + /// // 25s + /// GridMark { + /// value: 125.0, + /// step_size: 25.0, + /// }, + /// GridMark { + /// value: 150.0, + /// step_size: 25.0, + /// }, + /// GridMark { + /// value: 175.0, + /// step_size: 25.0, + /// }, + /// GridMark { + /// value: 225.0, + /// step_size: 25.0, + /// }, + /// ]; + /// # () + /// ``` + /// + /// There are helpers for common cases, see [`crate::log_grid_spacer`] and + /// [`crate::uniform_grid_spacer`]. + #[inline] + pub fn x_grid_spacer(mut self, spacer: impl Fn(GridInput) -> Vec + 'a) -> Self { + self.grid_spacers[0] = Box::new(spacer); + self + } + + /// Default is a log-10 grid, i.e. every plot unit is divided into 10 other + /// units. + /// + /// See [`Self::x_grid_spacer`] for explanation. + #[inline] + pub fn y_grid_spacer(mut self, spacer: impl Fn(GridInput) -> Vec + 'a) -> Self { + self.grid_spacers[1] = Box::new(spacer); + self + } + + /// Set when the grid starts showing. + /// + /// When grid lines are closer than the given minimum, they will be hidden. + /// When they get further apart they will fade in, until the reaches the + /// given maximum, at which point they are fully opaque. + #[inline] + pub fn grid_spacing(mut self, grid_spacing: impl Into) -> Self { + self.grid_spacing = grid_spacing.into(); + self + } + + /// Clamp the grid to only be visible at the range of data where we have + /// values. + /// + /// Default: `false`. + #[inline] + pub fn clamp_grid(mut self, clamp_grid: bool) -> Self { + self.clamp_grid = clamp_grid; + self + } + + /// Set the sense for the plot rect. + /// + /// Default: `Sense::click_and_drag()`. + #[inline] + pub fn sense(mut self, sense: Sense) -> Self { + self.sense = sense; + self + } + + /// Overwrite the starting and reset bounds used for the x axis. + /// Set the `default_auto_bounds` of the x axis to `false`. + /// + /// Panics in debug builds if `min >= max`. + #[inline] + pub fn default_x_bounds(mut self, min: f64, max: f64) -> Self { + debug_assert!(min < max, "`min` must be less than `max` in `default_x_bounds`"); + self.default_auto_bounds.x = false; + self.min_auto_bounds.min[0] = min; + self.min_auto_bounds.max[0] = max; + self + } + + /// Overwrite the starting and reset bounds used for the y axis. + /// Set the `default_auto_bounds` of the y axis to `false`. + /// + /// Panics in debug builds if `min >= max`. + #[inline] + pub fn default_y_bounds(mut self, min: f64, max: f64) -> Self { + debug_assert!(min < max, "`min` must be less than `max` in `default_y_bounds`"); + self.default_auto_bounds.y = false; + self.min_auto_bounds.min[1] = min; + self.min_auto_bounds.max[1] = max; + self + } + + /// Expand bounds to include the given x value. + /// For instance, to always show the y axis, call `plot.include_x(0.0)`. + #[inline] + pub fn include_x(mut self, x: impl Into) -> Self { + self.min_auto_bounds.extend_with_x(x.into()); + self + } + + /// Expand bounds to include the given y value. + /// For instance, to always show the x axis, call `plot.include_y(0.0)`. + #[inline] + pub fn include_y(mut self, y: impl Into) -> Self { + self.min_auto_bounds.extend_with_y(y.into()); + self + } + + /// Set whether the bounds should be automatically set based on data by + /// default. + /// + /// This is enabled by default. + #[inline] + pub fn auto_bounds(mut self, auto_bounds: impl Into) -> Self { + self.default_auto_bounds = auto_bounds.into(); + self + } + + /// Expand bounds to fit all items across the x axis, including values given + /// by `include_x`. + #[deprecated = "Use `auto_bounds` instead"] + #[inline] + pub fn auto_bounds_x(mut self) -> Self { + self.default_auto_bounds.x = true; + self + } + + /// Expand bounds to fit all items across the y axis, including values given + /// by `include_y`. + #[deprecated = "Use `auto_bounds` instead"] + #[inline] + pub fn auto_bounds_y(mut self) -> Self { + self.default_auto_bounds.y = true; + self + } + + /// Show a legend including all named items. + #[inline] + pub fn legend(mut self, legend: Legend) -> Self { + self.legend_config = Some(legend); + self + } + + /// Whether or not to show the background [`Rect`]. + /// + /// Can be useful to disable if the plot is overlaid over existing content. + /// Default: `true`. + #[inline] + pub fn show_background(mut self, show: bool) -> Self { + self.show_background = show; + self + } + + /// Show axis labels and grid tick values on the side of the plot. + /// + /// Default: `true`. + #[inline] + pub fn show_axes(mut self, show: impl Into) -> Self { + self.show_axes = show.into(); + self + } + + /// Show a grid overlay on the plot. + /// + /// Default: `true`. + #[inline] + pub fn show_grid(mut self, show: impl Into) -> Self { + self.show_grid = show.into(); + self + } + + /// Add this plot to an axis link group so that this plot will share the + /// bounds with other plots in the same group. A plot cannot belong to + /// more than one axis group. + #[inline] + pub fn link_axis(mut self, group_id: impl Into, link: impl Into) -> Self { + self.linked_axes = Some((group_id.into(), link.into())); + self + } + + /// Add this plot to a cursor link group so that this plot will share the + /// cursor position with other plots in the same group. A plot cannot + /// belong to more than one cursor group. + #[inline] + pub fn link_cursor(mut self, group_id: impl Into, link: impl Into) -> Self { + self.linked_cursors = Some((group_id.into(), link.into())); + self + } + + /// Round grid positions to full pixels to avoid aliasing. Improves plot + /// appearance but might have an undesired effect when shifting the plot + /// bounds. Enabled by default. + #[inline] + #[deprecated = "This no longer has any effect and is always enabled."] + pub fn sharp_grid_lines(self, _enabled: bool) -> Self { + self + } + + /// Resets the plot. + #[inline] + pub fn reset(mut self) -> Self { + self.reset = true; + self + } + + /// Set the x axis label of the main X-axis. + /// + /// Default: no label. + #[inline] + pub fn x_axis_label(mut self, label: impl Into) -> Self { + if let Some(main) = self.x_axes.first_mut() { + main.label = label.into(); + } + self + } + + /// Set the y axis label of the main Y-axis. + /// + /// Default: no label. + #[inline] + pub fn y_axis_label(mut self, label: impl Into) -> Self { + if let Some(main) = self.y_axes.first_mut() { + main.label = label.into(); + } + self + } + + /// Set the position of the main X-axis. + #[inline] + pub fn x_axis_position(mut self, placement: crate::VPlacement) -> Self { + if let Some(main) = self.x_axes.first_mut() { + main.placement = placement.into(); + } + self + } + + /// Set the position of the main Y-axis. + #[inline] + pub fn y_axis_position(mut self, placement: crate::HPlacement) -> Self { + if let Some(main) = self.y_axes.first_mut() { + main.placement = placement.into(); + } + self + } + + /// Specify custom formatter for ticks on the main X-axis. + /// + /// Arguments of `fmt`: + /// * the grid mark to format + /// * currently shown range on this axis. + #[inline] + pub fn x_axis_formatter(mut self, fmt: impl Fn(GridMark, &RangeInclusive) -> String + 'a) -> Self { + if let Some(main) = self.x_axes.first_mut() { + main.formatter = Arc::new(fmt); + } + self + } + + /// Specify custom formatter for ticks on the main Y-axis. + /// + /// Arguments of `fmt`: + /// * the grid mark to format + /// * currently shown range on this axis. + #[inline] + pub fn y_axis_formatter(mut self, fmt: impl Fn(GridMark, &RangeInclusive) -> String + 'a) -> Self { + if let Some(main) = self.y_axes.first_mut() { + main.formatter = Arc::new(fmt); + } + self + } + + /// Set the minimum width of the main y-axis, in ui points. + /// + /// The width will automatically expand if any tickmark text is wider than + /// this. + #[inline] + pub fn y_axis_min_width(mut self, min_width: f32) -> Self { + if let Some(main) = self.y_axes.first_mut() { + main.min_thickness = min_width; + } + self + } + + /// Set the main Y-axis-width by number of digits + #[inline] + #[deprecated = "Use `y_axis_min_width` instead"] + pub fn y_axis_width(self, digits: usize) -> Self { + self.y_axis_min_width(12.0 * digits as f32) + } + + /// Set custom configuration for X-axis + /// + /// More than one axis may be specified. The first specified axis is + /// considered the main axis. + #[inline] + pub fn custom_x_axes(mut self, hints: Vec>) -> Self { + self.x_axes = hints; + self + } + + /// Set custom configuration for left Y-axis + /// + /// More than one axis may be specified. The first specified axis is + /// considered the main axis. + #[inline] + pub fn custom_y_axes(mut self, hints: Vec>) -> Self { + self.y_axes = hints; + self + } + + /// Set custom cursor color. + /// + /// You may set the color to [`Color32::TRANSPARENT`] to hide the cursors. + #[inline] + pub fn cursor_color(mut self, color: Color32) -> Self { + self.cursor_color = Some(color); + self + } + + /// Interact with and add items to the plot and finally draw it. + pub fn show<'b, R>(self, ui: &mut Ui, build_fn: impl FnOnce(&mut PlotUi<'b>) -> R + 'a) -> PlotResponse { + self.show_dyn(ui, Box::new(build_fn)) + } + + #[expect(clippy::too_many_lines)] // TODO #166: shorten this function + fn show_dyn<'b, R>(self, ui: &mut Ui, build_fn: Box) -> R + 'a>) -> PlotResponse { + let Self { + id_source, + id, + center_axis, + allow_zoom, + allow_drag, + allow_axis_zoom_drag, + allow_scroll, + allow_double_click_reset, + allow_boxed_zoom, + pan_pointer_button, + boxed_zoom_pointer_button, + default_auto_bounds, + min_auto_bounds, + margin_fraction, + width, + height, + mut min_size, + data_aspect, + view_aspect, + invert_x, + invert_y, + mut show_x, + mut show_y, + label_formatter, + coordinates_formatter, + x_axes, + y_axes, + legend_config, + cursor_color, + reset, + show_background, + show_axes, + show_grid, + grid_spacing, + linked_axes, + linked_cursors, + + clamp_grid, + grid_spacers, + sense, + } = self; + + // Disable interaction if ui is disabled. + let allow_zoom = allow_zoom.and(ui.is_enabled()); + let allow_drag = allow_drag.and(ui.is_enabled()); + let allow_scroll = allow_scroll.and(ui.is_enabled()); + + // Determine position of widget. + let pos = ui.available_rect_before_wrap().min; + // Minimum values for screen protection + min_size.x = min_size.x.at_least(1.0); + min_size.y = min_size.y.at_least(1.0); + + // Determine size of widget. + let size = { + let width = width + .unwrap_or_else(|| { + if let (Some(height), Some(aspect)) = (height, view_aspect) { + height * aspect + } else { + ui.available_size_before_wrap().x + } + }) + .at_least(min_size.x); + + let height = height + .unwrap_or_else(|| { + if let Some(aspect) = view_aspect { + width / aspect + } else { + ui.available_size_before_wrap().y + } + }) + .at_least(min_size.y); + vec2(width, height) + }; + + // Determine complete rect of widget. + let complete_rect = Rect { + min: pos, + max: pos + size, + }; + + let plot_id = id.unwrap_or_else(|| ui.make_persistent_id(id_source)); + + let ([x_axis_widgets, y_axis_widgets], plot_rect) = crate::axis_widgets( + PlotMemory::load(ui.ctx(), plot_id).as_ref(), // TODO #164: avoid loading plot memory twice + show_axes, + complete_rect, + [&x_axes, &y_axes], + ); + + // Allocate the plot window. + let response = ui.allocate_rect(plot_rect, sense); + + let x_axis_responses = x_axis_widgets + .iter() + .map(|widget| { + let axis_response = ui.allocate_rect(widget.rect, Sense::drag()); + if allow_axis_zoom_drag.x { + axis_response.on_hover_cursor(CursorIcon::ResizeHorizontal) + } else { + axis_response + } + }) + .collect::>(); + + let y_axis_responses = y_axis_widgets + .iter() + .map(|widget| { + let axis_response = ui.allocate_rect(widget.rect, Sense::drag()); + + if allow_axis_zoom_drag.y { + axis_response.on_hover_cursor(CursorIcon::ResizeVertical) + } else { + axis_response + } + }) + .collect::>(); + + // Load or initialize the memory. + ui.ctx().check_for_id_clash(plot_id, plot_rect, "Plot"); + + let mut mem = if reset { + if let Some((name, _)) = linked_axes.as_ref() { + ui.data_mut(|data| { + let link_groups: &mut BoundsLinkGroups = data.get_temp_mut_or_default(Id::NULL); + link_groups.0.remove(name); + }); + } + None + } else { + PlotMemory::load(ui.ctx(), plot_id) + } + .unwrap_or_else(|| PlotMemory { + auto_bounds: default_auto_bounds, + hovered_legend_item: None, + hidden_items: Default::default(), + transform: PlotTransform::new_with_invert_axis( + plot_rect, + min_auto_bounds, + center_axis, + Vec2b::new(invert_x, invert_y), + ), + last_click_pos_for_zoom: None, + x_axis_thickness: Default::default(), + y_axis_thickness: Default::default(), + }); + + let last_plot_transform = mem.transform; + + // Call the plot build function. + let mut plot_ui = PlotUi { + ctx: ui.ctx().clone(), + items: Vec::new(), + next_auto_color_idx: 0, + last_plot_transform, + last_auto_bounds: mem.auto_bounds, + response, + bounds_modifications: Vec::new(), + }; + let inner = build_fn(&mut plot_ui); + let PlotUi { + mut items, + mut response, + last_plot_transform, + bounds_modifications, + .. + } = plot_ui; + + // Background + if show_background { + ui.painter().with_clip_rect(plot_rect).add(epaint::RectShape::new( + plot_rect, + 2, + ui.visuals().extreme_bg_color, + ui.visuals().widgets.noninteractive.bg_stroke, + egui::StrokeKind::Inside, + )); + } + + // --- Legend --- + let legend = + legend_config.and_then(|config| LegendWidget::try_new(plot_rect, config, &items, &mem.hidden_items)); + // Don't show hover cursor when hovering over legend. + if mem.hovered_legend_item.is_some() { + show_x = false; + show_y = false; + } + // Remove the deselected items. + items.retain(|item| !mem.hidden_items.contains(&item.id())); + // Highlight the hovered items. + if let Some(item_id) = &mem.hovered_legend_item { + items + .iter_mut() + .filter(|entry| &entry.id() == item_id) + .for_each(|entry| entry.highlight()); + } + // Move highlighted items to front. + items.sort_by_key(|item| item.highlighted()); + + // --- Bound computation --- + let mut bounds = *last_plot_transform.bounds(); + + // Find the cursors from other plots we need to draw + let draw_cursors: Vec = if let Some((id, _)) = linked_cursors.as_ref() { + ui.data_mut(|data| { + let frames: &mut CursorLinkGroups = data.get_temp_mut_or_default(Id::NULL); + let cursors = frames.0.entry(*id).or_default(); + + // Look for our previous frame + let index = cursors + .iter() + .enumerate() + .find(|(_, frame)| frame.id == plot_id) + .map(|(i, _)| i); + + // Remove our previous frame and all older frames as these are no longer + // displayed. This avoids unbounded growth, as we add an entry + // each time we draw a plot. + index.map(|index| cursors.drain(0..=index)); + + // Gather all cursors of the remaining frames. This will be all the cursors of + // the other plots in the group. We want to draw these in the + // current plot too. + cursors.iter().flat_map(|frame| frame.cursors.iter().copied()).collect() + }) + } else { + Vec::new() + }; + + // Transfer the bounds from a link group. + if let Some((id, axes)) = linked_axes.as_ref() { + ui.data_mut(|data| { + let link_groups: &mut BoundsLinkGroups = data.get_temp_mut_or_default(Id::NULL); + if let Some(linked_bounds) = link_groups.0.get(id) { + if axes.x { + bounds.set_x(&linked_bounds.bounds); + mem.auto_bounds.x = linked_bounds.auto_bounds.x; + } + if axes.y { + bounds.set_y(&linked_bounds.bounds); + mem.auto_bounds.y = linked_bounds.auto_bounds.y; + } + } + }); + } + + // Allow double-clicking to reset to the initial bounds. + if allow_double_click_reset && response.double_clicked() { + mem.auto_bounds = true.into(); + } + + let any_dynamic_modifications = !bounds_modifications.is_empty(); + // Apply bounds modifications. + for modification in bounds_modifications { + match modification { + BoundsModification::SetX(range) => { + bounds.min[0] = *range.start(); + bounds.max[0] = *range.end(); + mem.auto_bounds.x = false; + } + BoundsModification::SetY(range) => { + bounds.min[1] = *range.start(); + bounds.max[1] = *range.end(); + mem.auto_bounds.y = false; + } + BoundsModification::Translate(delta) => { + let delta = (delta.x as f64, delta.y as f64); + bounds.translate(delta); + mem.auto_bounds = false.into(); + } + BoundsModification::AutoBounds(new_auto_bounds) => { + mem.auto_bounds = new_auto_bounds; + } + BoundsModification::Zoom(zoom_factor, center) => { + bounds.zoom(zoom_factor, center); + mem.auto_bounds = false.into(); + } + } + } + + // Reset bounds to initial bounds if they haven't been modified. + if (!default_auto_bounds.x && !any_dynamic_modifications) || mem.auto_bounds.x { + bounds.set_x(&min_auto_bounds); + } + if (!default_auto_bounds.y && !any_dynamic_modifications) || mem.auto_bounds.y { + bounds.set_y(&min_auto_bounds); + } + + let auto_x = mem.auto_bounds.x && (!min_auto_bounds.is_valid_x() || default_auto_bounds.x); + let auto_y = mem.auto_bounds.y && (!min_auto_bounds.is_valid_y() || default_auto_bounds.y); + + // Set bounds automatically based on content. + if auto_x || auto_y { + for item in &items { + let item_bounds = item.bounds(); + if auto_x { + bounds.merge_x(&item_bounds); + } + if auto_y { + bounds.merge_y(&item_bounds); + } + } + + if auto_x { + bounds.add_relative_margin_x(margin_fraction); + } + + if auto_y { + bounds.add_relative_margin_y(margin_fraction); + } + } + + mem.transform = + PlotTransform::new_with_invert_axis(plot_rect, bounds, center_axis, Vec2b::new(invert_x, invert_y)); + + // Enforce aspect ratio + if let Some(data_aspect) = data_aspect { + if let Some((_, linked_axes)) = &linked_axes { + let change_x = linked_axes.y && !linked_axes.x; + mem.transform + .set_aspect_by_changing_axis(data_aspect as f64, if change_x { Axis::X } else { Axis::Y }); + } else if default_auto_bounds.any() { + mem.transform.set_aspect_by_expanding(data_aspect as f64); + } else { + mem.transform.set_aspect_by_changing_axis(data_aspect as f64, Axis::Y); + } + } + + // Dragging + if allow_drag.any() && response.dragged_by(pan_pointer_button) { + response = response.on_hover_cursor(CursorIcon::Grabbing); + let mut delta = -response.drag_delta(); + if !allow_drag.x { + delta.x = 0.0; + } + if !allow_drag.y { + delta.y = 0.0; + } + mem.transform.translate_bounds((delta.x as f64, delta.y as f64)); + mem.auto_bounds = mem.auto_bounds.and(!allow_drag); + } + + // Drag axes to zoom: + for d in 0..2 { + if allow_axis_zoom_drag[d] { + if let Some(axis_response) = (if d == 0 { &x_axis_responses } else { &y_axis_responses }) + .iter() + .find(|r| r.dragged_by(PointerButton::Primary)) + { + if let Some(drag_start_pos) = ui.input(|i| i.pointer.press_origin()) { + let delta = axis_response.drag_delta(); + + let axis_zoom = 1.0 + (0.02 * delta[d]).clamp(-1.0, 1.0); + + let zoom = if data_aspect.is_some() { + // Zoom both axes equally to maintain aspect ratio: + Vec2::splat(axis_zoom) + } else { + let mut zoom = Vec2::splat(1.0); + zoom[d] = axis_zoom; + zoom + }; + + if zoom != Vec2::splat(1.0) { + let mut zoom_center = plot_rect.center(); + zoom_center[d] = drag_start_pos[d]; + mem.transform.zoom(zoom, zoom_center); + mem.auto_bounds = false.into(); + } + } + } + } + } + + // Zooming + let mut boxed_zoom_rect = None; + if allow_boxed_zoom { + // Save last click to allow boxed zooming + if response.drag_started() && response.dragged_by(boxed_zoom_pointer_button) { + // it would be best for egui that input has a memory of the last click pos + // because it's a common pattern + mem.last_click_pos_for_zoom = response.hover_pos(); + } + let box_start_pos = mem.last_click_pos_for_zoom; + let box_end_pos = response.hover_pos(); + if let (Some(box_start_pos), Some(box_end_pos)) = (box_start_pos, box_end_pos) { + // while dragging prepare a Shape and draw it later on top of the plot + if response.dragged_by(boxed_zoom_pointer_button) { + response = response.on_hover_cursor(CursorIcon::ZoomIn); + let rect = epaint::Rect::from_two_pos(box_start_pos, box_end_pos); + boxed_zoom_rect = Some(( + epaint::RectShape::stroke( + rect, + 0.0, + epaint::Stroke::new(4., Color32::DARK_BLUE), + egui::StrokeKind::Middle, + ), // Outer stroke + epaint::RectShape::stroke( + rect, + 0.0, + epaint::Stroke::new(2., Color32::WHITE), + egui::StrokeKind::Middle, + ), // Inner stroke + )); + } + // when the click is release perform the zoom + if response.drag_stopped() { + let box_start_pos = mem.transform.value_from_position(box_start_pos); + let box_end_pos = mem.transform.value_from_position(box_end_pos); + let new_bounds = PlotBounds { + min: [box_start_pos.x.min(box_end_pos.x), box_start_pos.y.min(box_end_pos.y)], + max: [box_start_pos.x.max(box_end_pos.x), box_start_pos.y.max(box_end_pos.y)], + }; + if new_bounds.is_valid() { + mem.transform.set_bounds(new_bounds); + mem.auto_bounds = false.into(); + } + // reset the boxed zoom state + mem.last_click_pos_for_zoom = None; + } + } + } + + // Note: we catch zoom/pan if the response contains the pointer, even if it + // isn't hovered. For instance: The user is painting another interactive + // widget on top of the plot but they still want to be able to pan/zoom + // the plot. + if let (true, Some(hover_pos)) = (response.contains_pointer(), ui.input(|i| i.pointer.hover_pos())) { + if allow_zoom.any() { + let mut zoom_factor = if data_aspect.is_some() { + Vec2::splat(ui.input(|i| i.zoom_delta())) + } else { + ui.input(|i| i.zoom_delta_2d()) + }; + if !allow_zoom.x { + zoom_factor.x = 1.0; + } + if !allow_zoom.y { + zoom_factor.y = 1.0; + } + if zoom_factor != Vec2::splat(1.0) { + mem.transform.zoom(zoom_factor, hover_pos); + mem.auto_bounds = mem.auto_bounds.and(!allow_zoom); + } + } + if allow_scroll.any() { + let mut scroll_delta = ui.input(|i| i.smooth_scroll_delta); + if !allow_scroll.x { + scroll_delta.x = 0.0; + } + if !allow_scroll.y { + scroll_delta.y = 0.0; + } + if scroll_delta != Vec2::ZERO { + mem.transform + .translate_bounds((-scroll_delta.x as f64, -scroll_delta.y as f64)); + mem.auto_bounds = false.into(); + } + } + } + + // --- transform initialized + + // Add legend widgets to plot + let bounds = mem.transform.bounds(); + let x_axis_range = bounds.range_x(); + let x_steps = Arc::new({ + let input = GridInput { + bounds: (bounds.min[0], bounds.max[0]), + base_step_size: mem.transform.dvalue_dpos()[0].abs() * grid_spacing.min as f64, + }; + (grid_spacers[0])(input) + }); + let y_axis_range = bounds.range_y(); + let y_steps = Arc::new({ + let input = GridInput { + bounds: (bounds.min[1], bounds.max[1]), + base_step_size: mem.transform.dvalue_dpos()[1].abs() * grid_spacing.min as f64, + }; + (grid_spacers[1])(input) + }); + for (i, mut widget) in x_axis_widgets.into_iter().enumerate() { + widget.range = x_axis_range.clone(); + widget.transform = Some(mem.transform); + widget.steps = x_steps.clone(); + let (_response, thickness) = widget.ui(ui, Axis::X); + mem.x_axis_thickness.insert(i, thickness); + } + for (i, mut widget) in y_axis_widgets.into_iter().enumerate() { + widget.range = y_axis_range.clone(); + widget.transform = Some(mem.transform); + widget.steps = y_steps.clone(); + let (_response, thickness) = widget.ui(ui, Axis::Y); + mem.y_axis_thickness.insert(i, thickness); + } + + // Initialize values from functions. + for item in &mut items { + item.initialize(mem.transform.bounds().range_x()); + } + + let prepared = PreparedPlot { + plot_area_response: &response, + items, + show_x, + show_y, + label_formatter, + coordinates_formatter, + show_grid, + grid_spacing, + transform: mem.transform, + draw_cursor_x: linked_cursors.as_ref().is_some_and(|group| group.1.x), + draw_cursor_y: linked_cursors.as_ref().is_some_and(|group| group.1.y), + draw_cursors, + cursor_color, + grid_spacers, + clamp_grid, + }; + + let (plot_cursors, mut hovered_plot_item) = prepared.ui(ui, &response); + + if let Some(boxed_zoom_rect) = boxed_zoom_rect { + ui.painter().with_clip_rect(plot_rect).add(boxed_zoom_rect.0); + ui.painter().with_clip_rect(plot_rect).add(boxed_zoom_rect.1); + } + + if let Some(mut legend) = legend { + ui.add(&mut legend); + mem.hidden_items = legend.hidden_items(); + mem.hovered_legend_item = legend.hovered_item(); + + if let Some(item_id) = &mem.hovered_legend_item { + hovered_plot_item.get_or_insert(*item_id); + } + } + + if let Some((id, _)) = linked_cursors.as_ref() { + // Push the frame we just drew to the list of frames + ui.data_mut(|data| { + let frames: &mut CursorLinkGroups = data.get_temp_mut_or_default(Id::NULL); + let cursors = frames.0.entry(*id).or_default(); + cursors.push(PlotFrameCursors { + id: plot_id, + cursors: plot_cursors, + }); + }); + } + + if let Some((id, _)) = linked_axes.as_ref() { + // Save the linked bounds. + ui.data_mut(|data| { + let link_groups: &mut BoundsLinkGroups = data.get_temp_mut_or_default(Id::NULL); + link_groups.0.insert( + *id, + LinkedBounds { + bounds: *mem.transform.bounds(), + auto_bounds: mem.auto_bounds, + }, + ); + }); + } + + let transform = mem.transform; + mem.store(ui.ctx(), plot_id); + + let response = if show_x || show_y { + response.on_hover_cursor(CursorIcon::Crosshair) + } else { + response + }; + + ui.advance_cursor_after_rect(complete_rect); + + PlotResponse { + inner, + response, + transform, + hovered_plot_item, + } + } +} + +struct PreparedPlot<'a> { + /// The response of the whole plot area + plot_area_response: &'a Response, + items: Vec>, + show_x: bool, + show_y: bool, + label_formatter: LabelFormatter<'a>, + coordinates_formatter: Option<(Corner, CoordinatesFormatter<'a>)>, + // axis_formatters: [AxisFormatter; 2], + transform: PlotTransform, + show_grid: Vec2b, + grid_spacing: Rangef, + grid_spacers: [GridSpacer<'a>; 2], + draw_cursor_x: bool, + draw_cursor_y: bool, + draw_cursors: Vec, + cursor_color: Option, + + clamp_grid: bool, +} + +impl PreparedPlot<'_> { + fn ui(self, ui: &mut Ui, response: &Response) -> (Vec, Option) { + let mut axes_shapes = Vec::new(); + + if self.show_grid.x { + self.paint_grid(ui, &mut axes_shapes, Axis::X, self.grid_spacing); + } + if self.show_grid.y { + self.paint_grid(ui, &mut axes_shapes, Axis::Y, self.grid_spacing); + } + + // 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 transform = &self.transform; + + let mut plot_ui = ui.new_child( + egui::UiBuilder::new() + .max_rect(*transform.frame()) + .layout(Layout::default()), + ); + plot_ui.set_clip_rect(transform.frame().intersect(ui.clip_rect())); + for item in &self.items { + item.shapes(&plot_ui, transform, &mut shapes); + } + + let hover_pos = response.hover_pos(); + let (cursors, hovered_item_id) = if let Some(pointer) = hover_pos { + self.hover(ui, pointer, &mut shapes) + } else { + (Vec::new(), None) + }; + + // Draw cursors + let line_color = self.cursor_color.unwrap_or_else(|| rulers_color(ui)); + + let mut draw_cursor = |cursors: &Vec, always| { + for &cursor in cursors { + match cursor { + Cursor::Horizontal { y } => { + if self.draw_cursor_y || always { + shapes.push(horizontal_line( + transform.position_from_point(&PlotPoint::new(0.0, y)), + &self.transform, + line_color, + )); + } + } + Cursor::Vertical { x } => { + if self.draw_cursor_x || always { + shapes.push(vertical_line( + transform.position_from_point(&PlotPoint::new(x, 0.0)), + &self.transform, + line_color, + )); + } + } + } + } + }; + + draw_cursor(&self.draw_cursors, false); + draw_cursor(&cursors, true); + + let painter = ui.painter().with_clip_rect(*transform.frame()); + painter.extend(shapes); + + // Show coordinates in a corner of the plot: + if let Some((corner, formatter)) = self.coordinates_formatter.as_ref() { + let hover_pos = response.hover_pos(); + if let Some(pointer) = hover_pos { + let font_id = TextStyle::Monospace.resolve(ui.style()); + let coordinate = transform.value_from_position(pointer); + let text = formatter.format(&coordinate, transform.bounds()); + let padded_frame = transform.frame().shrink(4.0); + let (anchor, position) = match corner { + Corner::LeftTop => (Align2::LEFT_TOP, padded_frame.left_top()), + Corner::RightTop => (Align2::RIGHT_TOP, padded_frame.right_top()), + Corner::LeftBottom => (Align2::LEFT_BOTTOM, padded_frame.left_bottom()), + Corner::RightBottom => (Align2::RIGHT_BOTTOM, padded_frame.right_bottom()), + }; + + let text_color = ui.visuals().text_color(); + let galley = painter.layout_no_wrap(text, font_id, text_color); + let rect = anchor.anchor_size(position, galley.size()); + painter.rect_filled( + rect.expand(4.0), + ui.style().visuals.window_corner_radius, + ui.style().visuals.extreme_bg_color.gamma_multiply(0.75), + ); + painter.galley(rect.min, galley, text_color); + } + } + + (cursors, hovered_item_id) + } + + fn paint_grid(&self, ui: &Ui, shapes: &mut Vec<(Shape, f32)>, axis: Axis, fade_range: Rangef) { + let Self { + transform, + // axis_formatters, + grid_spacers, + clamp_grid, + .. + } = self; + + let iaxis = usize::from(axis); + + // Where on the cross-dimension to show the label values + let bounds = transform.bounds(); + let value_cross = 0.0_f64.clamp(bounds.min[1 - iaxis], bounds.max[1 - iaxis]); + + let input = GridInput { + bounds: (bounds.min[iaxis], bounds.max[iaxis]), + base_step_size: transform.dvalue_dpos()[iaxis].abs() * fade_range.min as f64, + }; + let steps = (grid_spacers[iaxis])(input); + + let clamp_range = clamp_grid.then(|| { + let mut tight_bounds = PlotBounds::NOTHING; + for item in &self.items { + let item_bounds = item.bounds(); + tight_bounds.merge_x(&item_bounds); + tight_bounds.merge_y(&item_bounds); + } + tight_bounds + }); + + for step in steps { + let value_main = step.value; + + if let Some(clamp_range) = clamp_range { + match axis { + Axis::X => { + if !clamp_range.range_x().contains(&value_main) { + continue; + } + } + Axis::Y => { + if !clamp_range.range_y().contains(&value_main) { + continue; + } + } + } + } + + let value = match axis { + Axis::X => PlotPoint::new(value_main, value_cross), + Axis::Y => PlotPoint::new(value_cross, value_main), + }; + + let pos_in_gui = transform.position_from_point(&value); + let spacing_in_points = (transform.dpos_dvalue()[iaxis] * step.step_size).abs() as f32; + + if spacing_in_points <= fade_range.min { + continue; // Too close together + } + + let line_strength = remap_clamp(spacing_in_points, fade_range, 0.0..=1.0); + + let line_color = crate::color_from_strength(ui, line_strength); + + let mut p0 = pos_in_gui; + let mut p1 = pos_in_gui; + p0[1 - iaxis] = transform.frame().min[1 - iaxis]; + p1[1 - iaxis] = transform.frame().max[1 - iaxis]; + + if let Some(clamp_range) = clamp_range { + match axis { + Axis::X => { + p0.y = transform.position_from_point_y(clamp_range.min[1]); + p1.y = transform.position_from_point_y(clamp_range.max[1]); + } + Axis::Y => { + p0.x = transform.position_from_point_x(clamp_range.min[0]); + p1.x = transform.position_from_point_x(clamp_range.max[0]); + } + } + } + + shapes.push(( + Shape::line_segment([p0, p1], Stroke::new(1.0, line_color)), + line_strength, + )); + } + } + + fn hover(&self, ui: &Ui, pointer: Pos2, shapes: &mut Vec) -> (Vec, Option) { + let Self { + plot_area_response, + transform, + show_x, + show_y, + label_formatter, + items, + .. + } = self; + + if !show_x && !show_y { + return (Vec::new(), None); + } + + let interact_radius_sq = ui.style().interaction.interact_radius.powi(2); + + let candidates = items.iter().filter(|entry| entry.allow_hover()).filter_map(|item| { + let item = &**item; + let closest = item.find_closest(pointer, transform); + + Some(item).zip(closest) + }); + + // Since many items can have same distance, + // and dist_sq can be zero for some items (e.g. rectangle) + // we pick topmost item within interact radius + let topmost = candidates + .filter(|(_, elem)| elem.dist_sq <= interact_radius_sq) + .next_back(); + + let plot = crate::PlotConfig { + ui, + transform, + show_x: *show_x, + show_y: *show_y, + }; + + let mut cursors = Vec::new(); + + let hovered_plot_item_id = if let Some((item, elem)) = topmost { + item.on_hover(plot_area_response, elem, shapes, &mut cursors, &plot, label_formatter); + Some(item.id()) + } else { + let value = transform.value_from_position(pointer); + items::rulers_and_tooltip_at_value(plot_area_response, value, "", &plot, &mut cursors, label_formatter); + None + }; + + (cursors, hovered_plot_item_id) + } +}