//! Simple plotting library. //! //! ## Feature flags #![cfg_attr(feature = "document-features", doc = document_features::document_features!())] //! use std::{ops::RangeInclusive, sync::Arc}; use egui::ahash::HashMap; use epaint::util::FloatOrd; use epaint::Hsva; use axis::AxisWidget; use items::PlotItem; use legend::LegendWidget; use egui::*; pub use items::{ Arrows, Bar, BarChart, BoxElem, BoxPlot, BoxSpread, HLine, Line, LineStyle, MarkerShape, Orientation, PlotImage, PlotPoint, PlotPoints, Points, Polygon, Text, VLine, }; pub use legend::{Corner, Legend}; pub use transform::{PlotBounds, PlotTransform}; use items::{horizontal_line, rulers_color, vertical_line}; pub use axis::{Axis, AxisHints, HPlacement, Placement, VPlacement}; mod axis; mod items; mod legend; mod transform; type LabelFormatterFn = dyn Fn(&str, &PlotPoint) -> String; type LabelFormatter = Option>; type GridSpacerFn = dyn Fn(GridInput) -> Vec; type GridSpacer = Box; type CoordinatesFormatterFn = dyn Fn(&PlotPoint, &PlotBounds) -> String; /// Specifies the coordinates formatting when passed to [`Plot::coordinates_formatter`]. pub struct CoordinatesFormatter { function: Box, } impl CoordinatesFormatter { /// Create a new formatter based on the pointer coordinate and the plot bounds. pub fn new(function: impl Fn(&PlotPoint, &PlotBounds) -> String + 'static) -> Self { Self { function: Box::new(function), } } /// Show a fixed number of decimal places. pub fn with_decimals(num_decimals: usize) -> Self { Self { function: Box::new(move |value, _| { format!("x: {:.d$}\ny: {:.d$}", value.x, value.y, d = num_decimals) }), } } fn format(&self, value: &PlotPoint, bounds: &PlotBounds) -> String { (self.function)(value, bounds) } } impl Default for CoordinatesFormatter { fn default() -> Self { Self::with_decimals(3) } } // ---------------------------------------------------------------------------- const MIN_LINE_SPACING_IN_POINTS: f64 = 6.0; // TODO(emilk): large enough for a wide label #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct AxisBools { pub x: bool, pub y: bool, } impl AxisBools { #[inline] pub fn new(x: bool, y: bool) -> Self { Self { x, y } } #[inline] pub fn any(&self) -> bool { self.x || self.y } } impl From for AxisBools { #[inline] fn from(val: bool) -> Self { AxisBools { x: val, y: val } } } impl From<[bool; 2]> for AxisBools { #[inline] fn from([x, y]: [bool; 2]) -> Self { AxisBools { x, y } } } /// Information about the plot that has to persist between frames. #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[derive(Clone)] struct PlotMemory { /// Indicates if the user has modified the bounds, for example by moving or zooming, /// or if the bounds should be calculated based by included point or auto bounds. bounds_modified: AxisBools, hovered_entry: Option, hidden_items: ahash::HashSet, last_plot_transform: PlotTransform, /// Allows to remember the first click position when performing a boxed zoom last_click_pos_for_zoom: Option, } #[cfg(feature = "serde")] impl PlotMemory { pub fn load(ctx: &Context, id: Id) -> Option { ctx.data_mut(|d| d.get_persisted(id)) } pub fn store(self, ctx: &Context, id: Id) { ctx.data_mut(|d| d.insert_persisted(id, self)); } } #[cfg(not(feature = "serde"))] impl PlotMemory { pub fn load(ctx: &Context, id: Id) -> Option { ctx.data_mut(|d| d.get_temp(id)) } pub fn store(self, ctx: &Context, id: Id) { ctx.data_mut(|d| d.insert_temp(id, self)); } } // ---------------------------------------------------------------------------- /// Indicates a vertical or horizontal cursor line in plot coordinates. #[derive(Copy, Clone, PartialEq)] enum Cursor { Horizontal { y: f64 }, Vertical { x: f64 }, } /// Contains the cursors drawn for a plot widget in a single frame. #[derive(PartialEq, Clone)] struct PlotFrameCursors { id: Id, cursors: Vec, } #[derive(Default, Clone)] struct CursorLinkGroups(HashMap>); #[derive(Clone)] struct LinkedBounds { bounds: PlotBounds, bounds_modified: AxisBools, } #[derive(Default, Clone)] struct BoundsLinkGroups(HashMap); // ---------------------------------------------------------------------------- /// What [`Plot::show`] returns. pub struct PlotResponse { /// What the user closure returned. pub inner: R, /// The response of the plot. pub response: Response, /// The transform between screen coordinates and plot coordinates. pub transform: PlotTransform, } // ---------------------------------------------------------------------------- /// 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, 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); /// Plot::new("my_plot").view_aspect(2.0).show(ui, |plot_ui| plot_ui.line(line)); /// # }); /// ``` pub struct Plot { id_source: Id, center_axis: AxisBools, allow_zoom: AxisBools, allow_drag: AxisBools, allow_scroll: bool, allow_double_click_reset: bool, allow_boxed_zoom: bool, auto_bounds: AxisBools, min_auto_bounds: PlotBounds, margin_fraction: Vec2, boxed_zoom_pointer_button: PointerButton, linked_axes: Option<(Id, AxisBools)>, linked_cursors: Option<(Id, AxisBools)>, min_size: Vec2, width: Option, height: Option, data_aspect: Option, view_aspect: Option, reset: bool, show_x: bool, show_y: bool, label_formatter: LabelFormatter, coordinates_formatter: Option<(Corner, CoordinatesFormatter)>, x_axes: Vec, // default x axes y_axes: Vec, // default y axes legend_config: Option, show_background: bool, show_axes: AxisBools, show_grid: AxisBools, grid_spacers: [GridSpacer; 2], sharp_grid_lines: bool, clamp_grid: bool, } impl Plot { /// 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), center_axis: false.into(), allow_zoom: true.into(), allow_drag: true.into(), allow_scroll: true, allow_double_click_reset: true, allow_boxed_zoom: true, auto_bounds: false.into(), min_auto_bounds: PlotBounds::NOTHING, margin_fraction: Vec2::splat(0.05), 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, reset: false, show_x: true, show_y: true, label_formatter: None, coordinates_formatter: None, x_axes: vec![Default::default()], y_axes: vec![Default::default()], legend_config: None, show_background: true, show_axes: true.into(), show_grid: true.into(), grid_spacers: [log_grid_spacer(10), log_grid_spacer(10)], sharp_grid_lines: true, clamp_grid: false, } } /// 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. 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). pub fn view_aspect(mut self, view_aspect: f32) -> Self { self.view_aspect = Some(view_aspect); 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. 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. pub fn height(mut self, height: f32) -> Self { self.min_size.y = height; self.height = Some(height); self } /// Minimum size of the plot view. 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`. 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`. pub fn show_y(mut self, show_y: bool) -> Self { self.show_y = show_y; self } /// Always keep the X-axis centered. Default: `false`. pub fn center_x_axis(mut self, on: bool) -> Self { self.center_axis.x = on; self } /// Always keep the Y-axis centered. Default: `false`. 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`. 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`. pub fn allow_scroll(mut self, on: bool) -> Self { self.allow_scroll = on; self } /// Whether to allow double clicking to reset the view. /// Default: `true`. 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. 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`. pub fn allow_boxed_zoom(mut self, on: bool) -> Self { self.allow_boxed_zoom = on; self } /// Config the button pointer to use for boxed zooming. Default: [`Secondary`](PointerButton::Secondary) 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`. pub fn allow_drag(mut self, on: T) -> Self where T: Into, { self.allow_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, 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); /// 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)); /// # }); /// ``` pub fn label_formatter( mut self, label_formatter: impl Fn(&str, &PlotPoint) -> String + 'static, ) -> Self { self.label_formatter = Some(Box::new(label_formatter)); self } /// Show the pointer coordinates in the plot. pub fn coordinates_formatter( mut self, position: Corner, formatter: CoordinatesFormatter, ) -> 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`]. pub fn x_grid_spacer(mut self, spacer: impl Fn(GridInput) -> Vec + 'static) -> 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. pub fn y_grid_spacer(mut self, spacer: impl Fn(GridInput) -> Vec + 'static) -> Self { self.grid_spacers[1] = Box::new(spacer); self } /// Clamp the grid to only be visible at the range of data where we have values. /// /// Default: `false`. pub fn clamp_grid(mut self, clamp_grid: bool) -> Self { self.clamp_grid = clamp_grid; self } /// Expand bounds to include the given x value. /// For instance, to always show the y axis, call `plot.include_x(0.0)`. 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)`. pub fn include_y(mut self, y: impl Into) -> Self { self.min_auto_bounds.extend_with_y(y.into()); self } /// Expand bounds to fit all items across the x axis, including values given by `include_x`. pub fn auto_bounds_x(mut self) -> Self { self.auto_bounds.x = true; self } /// Expand bounds to fit all items across the y axis, including values given by `include_y`. pub fn auto_bounds_y(mut self) -> Self { self.auto_bounds.y = true; self } /// Show a legend including all named items. 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`. 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; 2]`. 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; 2]`. 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. pub fn link_axis(mut self, group_id: impl Into, link_x: bool, link_y: bool) -> Self { self.linked_axes = Some(( group_id.into(), AxisBools { x: link_x, y: link_y, }, )); 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. pub fn link_cursor(mut self, group_id: impl Into, link_x: bool, link_y: bool) -> Self { self.linked_cursors = Some(( group_id.into(), AxisBools { x: link_x, y: link_y, }, )); 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. pub fn sharp_grid_lines(mut self, enabled: bool) -> Self { self.sharp_grid_lines = enabled; self } /// Resets the plot. pub fn reset(mut self) -> Self { self.reset = true; self } /// Set the x axis label of the main X-axis. /// /// Default: no label. 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. 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. 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. 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`: /// * raw tick value as `f64`. /// * maximum requested number of characters per tick label. /// * currently shown range on this axis. pub fn x_axis_formatter( mut self, fmt: impl Fn(f64, usize, &RangeInclusive) -> String + 'static, ) -> 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`: /// * raw tick value as `f64`. /// * maximum requested number of characters per tick label. /// * currently shown range on this axis. pub fn y_axis_formatter( mut self, fmt: impl Fn(f64, usize, &RangeInclusive) -> String + 'static, ) -> Self { if let Some(main) = self.y_axes.first_mut() { main.formatter = Arc::new(fmt); } self } /// Set the main Y-axis-width by number of digits /// /// The default is 5 digits. /// /// > Todo: This is experimental. Changing the font size might break this. pub fn y_axis_width(mut self, digits: usize) -> Self { if let Some(main) = self.y_axes.first_mut() { main.digits = digits; } self } /// Set custom configuration for X-axis /// /// More than one axis may be specified. The first specified axis is considered the main axis. 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. pub fn custom_y_axes(mut self, hints: Vec) -> Self { self.y_axes = hints; self } /// 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 { let Self { id_source, center_axis, allow_zoom, allow_drag, allow_scroll, allow_double_click_reset, allow_boxed_zoom, boxed_zoom_pointer_button: boxed_zoom_pointer, auto_bounds, min_auto_bounds, margin_fraction, width, height, min_size, data_aspect, view_aspect, mut show_x, mut show_y, label_formatter, coordinates_formatter, x_axes, y_axes, legend_config, reset, show_background, show_axes, show_grid, linked_axes, linked_cursors, clamp_grid, grid_spacers, sharp_grid_lines, } = self; // Determine position of widget. let pos = ui.available_rect_before_wrap().min; // 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, }; // Next we want to create this layout. // Incides are only examples. // // left right // +---+---------x----------+ + // | | X-axis 3 | // | +--------------------+ top // | | X-axis 2 | // +-+-+--------------------+-+-+ // |y|y| |y|y| // |-|-| |-|-| // |A|A| |A|A| // y|x|x| Plot Window |x|x| // |i|i| |i|i| // |s|s| |s|s| // |1|0| |2|3| // +-+-+--------------------+-+-+ // | X-axis 0 | | // +--------------------+ | bottom // | X-axis 1 | | // + +--------------------+---+ // let mut plot_rect: Rect = { // Calcuclate the space needed for each axis labels. let mut margin = Margin::ZERO; if show_axes.x { for cfg in &x_axes { match cfg.placement { axis::Placement::LeftBottom => { margin.bottom += cfg.thickness(Axis::X); } axis::Placement::RightTop => { margin.top += cfg.thickness(Axis::X); } } } } if show_axes.y { for cfg in &y_axes { match cfg.placement { axis::Placement::LeftBottom => { margin.left += cfg.thickness(Axis::Y); } axis::Placement::RightTop => { margin.right += cfg.thickness(Axis::Y); } } } } // determine plot rectangle margin.shrink_rect(complete_rect) }; let [mut x_axis_widgets, mut y_axis_widgets] = axis_widgets(show_axes, plot_rect, [&x_axes, &y_axes]); // If too little space, remove axis widgets if plot_rect.width() <= 0.0 || plot_rect.height() <= 0.0 { y_axis_widgets.clear(); x_axis_widgets.clear(); plot_rect = complete_rect; } // Allocate the plot window. let response = ui.allocate_rect(plot_rect, Sense::drag()); let rect = plot_rect; // Load or initialize the memory. let plot_id = ui.make_persistent_id(id_source); ui.ctx().check_for_id_clash(plot_id, rect, "Plot"); let memory = if reset { if let Some((name, _)) = linked_axes.as_ref() { ui.memory_mut(|memory| { let link_groups: &mut BoundsLinkGroups = memory.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 { bounds_modified: false.into(), hovered_entry: None, hidden_items: Default::default(), last_plot_transform: PlotTransform::new( rect, min_auto_bounds, center_axis.x, center_axis.y, ), last_click_pos_for_zoom: None, }); let PlotMemory { mut bounds_modified, mut hovered_entry, mut hidden_items, last_plot_transform, mut last_click_pos_for_zoom, } = memory; // Call the plot build function. let mut plot_ui = PlotUi { items: Vec::new(), next_auto_color_idx: 0, last_plot_transform, response, bounds_modifications: Vec::new(), ctx: ui.ctx().clone(), }; 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(rect) .add(epaint::RectShape::new( rect, Rounding::same(2.0), ui.visuals().extreme_bg_color, ui.visuals().widgets.noninteractive.bg_stroke, )); } // --- Legend --- let legend = legend_config .and_then(|config| LegendWidget::try_new(rect, config, &items, &hidden_items)); // Don't show hover cursor when hovering over legend. if hovered_entry.is_some() { show_x = false; show_y = false; } // Remove the deselected items. items.retain(|item| !hidden_items.contains(item.name())); // Highlight the hovered items. if let Some(hovered_name) = &hovered_entry { items .iter_mut() .filter(|entry| entry.name() == hovered_name) .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.memory_mut(|memory| { let frames: &mut CursorLinkGroups = memory.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.memory_mut(|memory| { let link_groups: &mut BoundsLinkGroups = memory.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); bounds_modified.x = linked_bounds.bounds_modified.x; } if axes.y { bounds.set_y(&linked_bounds.bounds); bounds_modified.y = linked_bounds.bounds_modified.y; } }; }); }; // Allow double clicking to reset to the initial bounds. if allow_double_click_reset && response.double_clicked() { bounds_modified = false.into(); } // Apply bounds modifications. for modification in bounds_modifications { match modification { BoundsModification::Set(new_bounds) => { bounds = new_bounds; bounds_modified = true.into(); } BoundsModification::Translate(delta) => { bounds.translate(delta); bounds_modified = true.into(); } } } // Reset bounds to initial bounds if they haven't been modified. if !bounds_modified.x { bounds.set_x(&min_auto_bounds); } if !bounds_modified.y { bounds.set_y(&min_auto_bounds); } let auto_x = !bounds_modified.x && (!min_auto_bounds.is_valid_x() || auto_bounds.x); let auto_y = !bounds_modified.y && (!min_auto_bounds.is_valid_y() || 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); } } let mut transform = PlotTransform::new(rect, bounds, center_axis.x, center_axis.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; transform.set_aspect_by_changing_axis(data_aspect as f64, change_x); } else if auto_bounds.any() { transform.set_aspect_by_expanding(data_aspect as f64); } else { transform.set_aspect_by_changing_axis(data_aspect as f64, false); } } // Dragging if allow_drag.any() && response.dragged_by(PointerButton::Primary) { 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; } transform.translate_bounds(delta); bounds_modified = allow_drag; } // 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) { // it would be best for egui that input has a memory of the last click pos because it's a common pattern last_click_pos_for_zoom = response.hover_pos(); } let box_start_pos = 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) { 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), ), // Outer stroke epaint::RectShape::stroke( rect, 0.0, epaint::Stroke::new(2., Color32::WHITE), ), // Inner stroke )); } // when the click is release perform the zoom if response.drag_released() { let box_start_pos = transform.value_from_position(box_start_pos); let box_end_pos = 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() { transform.set_bounds(new_bounds); bounds_modified = true.into(); } // reset the boxed zoom state last_click_pos_for_zoom = None; } } } let hover_pos = response.hover_pos(); if let Some(hover_pos) = 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) { transform.zoom(zoom_factor, hover_pos); bounds_modified = allow_zoom; } } if allow_scroll { let scroll_delta = ui.input(|i| i.scroll_delta); if scroll_delta != Vec2::ZERO { transform.translate_bounds(-scroll_delta); bounds_modified = true.into(); } } } // --- transform initialized // Add legend widgets to plot let bounds = 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: transform.dvalue_dpos()[0] * MIN_LINE_SPACING_IN_POINTS * 2.0, }; (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: transform.dvalue_dpos()[1] * MIN_LINE_SPACING_IN_POINTS * 2.0, }; (grid_spacers[1])(input) }); for mut widget in x_axis_widgets { widget.range = x_axis_range.clone(); widget.transform = Some(transform); widget.steps = x_steps.clone(); widget.ui(ui, Axis::X); } for mut widget in y_axis_widgets { widget.range = y_axis_range.clone(); widget.transform = Some(transform); widget.steps = y_steps.clone(); widget.ui(ui, Axis::Y); } // Initialize values from functions. for item in &mut items { item.initialize(transform.bounds().range_x()); } let prepared = PreparedPlot { items, show_x, show_y, label_formatter, coordinates_formatter, show_grid, transform, draw_cursor_x: linked_cursors.as_ref().map_or(false, |group| group.1.x), draw_cursor_y: linked_cursors.as_ref().map_or(false, |group| group.1.y), draw_cursors, grid_spacers, sharp_grid_lines, clamp_grid, }; let plot_cursors = prepared.ui(ui, &response); if let Some(boxed_zoom_rect) = boxed_zoom_rect { ui.painter().with_clip_rect(rect).add(boxed_zoom_rect.0); ui.painter().with_clip_rect(rect).add(boxed_zoom_rect.1); } if let Some(mut legend) = legend { ui.add(&mut legend); hidden_items = legend.hidden_items(); hovered_entry = legend.hovered_entry_name(); } if let Some((id, _)) = linked_cursors.as_ref() { // Push the frame we just drew to the list of frames ui.memory_mut(|memory| { let frames: &mut CursorLinkGroups = memory.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.memory_mut(|memory| { let link_groups: &mut BoundsLinkGroups = memory.data.get_temp_mut_or_default(Id::null()); link_groups.0.insert( *id, LinkedBounds { bounds: *transform.bounds(), bounds_modified, }, ); }); } let memory = PlotMemory { bounds_modified, hovered_entry, hidden_items, last_plot_transform: transform, last_click_pos_for_zoom, }; memory.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, } } } fn axis_widgets( show_axes: AxisBools, plot_rect: Rect, [x_axes, y_axes]: [&[AxisHints]; 2], ) -> [Vec; 2] { let mut x_axis_widgets = Vec::::new(); let mut y_axis_widgets = Vec::::new(); // Widget count per border of plot in order left, top, right, bottom struct NumWidgets { left: usize, top: usize, right: usize, bottom: usize, } let mut num_widgets = NumWidgets { left: 0, top: 0, right: 0, bottom: 0, }; if show_axes.x { for cfg in x_axes { let size_y = Vec2::new(0.0, cfg.thickness(Axis::X)); let rect = match cfg.placement { axis::Placement::LeftBottom => { let off = num_widgets.bottom as f32; num_widgets.bottom += 1; Rect { min: plot_rect.left_bottom() + size_y * off, max: plot_rect.right_bottom() + size_y * (off + 1.0), } } axis::Placement::RightTop => { let off = num_widgets.top as f32; num_widgets.top += 1; Rect { min: plot_rect.left_top() - size_y * (off + 1.0), max: plot_rect.right_top() - size_y * off, } } }; x_axis_widgets.push(AxisWidget::new(cfg.clone(), rect)); } } if show_axes.y { for cfg in y_axes { let size_x = Vec2::new(cfg.thickness(Axis::Y), 0.0); let rect = match cfg.placement { axis::Placement::LeftBottom => { let off = num_widgets.left as f32; num_widgets.left += 1; Rect { min: plot_rect.left_top() - size_x * (off + 1.0), max: plot_rect.left_bottom() - size_x * off, } } axis::Placement::RightTop => { let off = num_widgets.right as f32; num_widgets.right += 1; Rect { min: plot_rect.right_top() + size_x * off, max: plot_rect.right_bottom() + size_x * (off + 1.0), } } }; y_axis_widgets.push(AxisWidget::new(cfg.clone(), rect)); } } [x_axis_widgets, y_axis_widgets] } /// User-requested modifications to the plot bounds. We collect them in the plot build function to later apply /// them at the right time, as other modifications need to happen first. enum BoundsModification { Set(PlotBounds), Translate(Vec2), } /// 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, 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)); } /// Can be used to check if the plot was hovered or clicked. pub fn response(&self) -> &Response { &self.response } /// Returns `true` if the plot area is currently hovered. #[deprecated = "Use plot_ui.response().hovered()"] pub fn plot_hovered(&self) -> bool { self.response.hovered() } /// Returns `true` if the plot was clicked by the primary button. #[deprecated = "Use plot_ui.response().clicked()"] pub fn plot_clicked(&self) -> bool { self.response.clicked() } /// Returns `true` if the plot was clicked by the secondary button. #[deprecated = "Use plot_ui.response().secondary_clicked()"] pub fn plot_secondary_clicked(&self) -> bool { self.response.secondary_clicked() } /// 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 netween 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 /// Input for "grid spacer" functions. /// /// See [`Plot::x_grid_spacer()`] and [`Plot::y_grid_spacer()`]. pub struct GridInput { /// Min/max of the visible data range (the values at the two edges of the plot, /// for the current axis). pub bounds: (f64, f64), /// Recommended (but not required) lower-bound on the step size returned by custom grid spacers. /// /// Computed as the ratio between the diagram's bounds (in plot coordinates) and the viewport /// (in frame/window coordinates), scaled up to represent the minimal possible step. pub base_step_size: f64, } /// One mark (horizontal or vertical line) in the background grid of a plot. #[derive(Debug, Clone, Copy)] pub struct GridMark { /// X or Y value in the plot. pub value: f64, /// The (approximate) distance to the next value of same thickness. /// /// Determines how thick the grid line is painted. It's not important that `step_size` /// matches the difference between two `value`s precisely, but rather that grid marks of /// same thickness have same `step_size`. For example, months can have a different number /// of days, but consistently using a `step_size` of 30 days is a valid approximation. pub step_size: f64, } /// Recursively splits the grid into `base` subdivisions (e.g. 100, 10, 1). /// /// The logarithmic base, expressing how many times each grid unit is subdivided. /// 10 is a typical value, others are possible though. pub fn log_grid_spacer(log_base: i64) -> GridSpacer { let log_base = log_base as f64; let step_sizes = move |input: GridInput| -> Vec { // The distance between two of the thinnest grid lines is "rounded" up // to the next-bigger power of base let smallest_visible_unit = next_power(input.base_step_size, log_base); let step_sizes = [ smallest_visible_unit, smallest_visible_unit * log_base, smallest_visible_unit * log_base * log_base, ]; generate_marks(step_sizes, input.bounds) }; Box::new(step_sizes) } /// Splits the grid into uniform-sized spacings (e.g. 100, 25, 1). /// /// This function should return 3 positive step sizes, designating where the lines in the grid are drawn. /// Lines are thicker for larger step sizes. Ordering of returned value is irrelevant. /// /// Why only 3 step sizes? Three is the number of different line thicknesses that egui typically uses in the grid. /// Ideally, those 3 are not hardcoded values, but depend on the visible range (accessible through `GridInput`). pub fn uniform_grid_spacer(spacer: impl Fn(GridInput) -> [f64; 3] + 'static) -> GridSpacer { let get_marks = move |input: GridInput| -> Vec { let bounds = input.bounds; let step_sizes = spacer(input); generate_marks(step_sizes, bounds) }; Box::new(get_marks) } // ---------------------------------------------------------------------------- struct PreparedPlot { items: Vec>, show_x: bool, show_y: bool, label_formatter: LabelFormatter, coordinates_formatter: Option<(Corner, CoordinatesFormatter)>, // axis_formatters: [AxisFormatter; 2], transform: PlotTransform, show_grid: AxisBools, grid_spacers: [GridSpacer; 2], draw_cursor_x: bool, draw_cursor_y: bool, draw_cursors: Vec, sharp_grid_lines: bool, clamp_grid: bool, } impl PreparedPlot { fn ui(self, ui: &mut Ui, response: &Response) -> Vec { let mut axes_shapes = Vec::new(); if self.show_grid.x { self.paint_grid(ui, &mut axes_shapes, Axis::X); } if self.show_grid.y { self.paint_grid(ui, &mut axes_shapes, Axis::Y); } // 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.child_ui(*transform.frame(), Layout::default()); plot_ui.set_clip_rect(*transform.frame()); for item in &self.items { item.shapes(&mut plot_ui, transform, &mut shapes); } let hover_pos = response.hover_pos(); let cursors = if let Some(pointer) = hover_pos { self.hover(ui, pointer, &mut shapes) } else { Vec::new() }; // Draw cursors let line_color = 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); 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()), }; painter.text(position, anchor, text, font_id, ui.visuals().text_color()); } } cursors } fn paint_grid(&self, ui: &Ui, shapes: &mut Vec<(Shape, f32)>, axis: Axis) { #![allow(clippy::collapsible_else_if)] 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] * MIN_LINE_SPACING_IN_POINTS, }; 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 > MIN_LINE_SPACING_IN_POINTS as f32 { let line_strength = remap_clamp( spacing_in_points, MIN_LINE_SPACING_IN_POINTS as f32..=300.0, 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]); } } } if self.sharp_grid_lines { // Round to avoid aliasing p0 = ui.painter().round_pos_to_pixels(p0); p1 = ui.painter().round_pos_to_pixels(p1); } 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 { let Self { transform, show_x, show_y, label_formatter, items, .. } = self; if !show_x && !show_y { return Vec::new(); } let interact_radius_sq: f32 = (16.0f32).powi(2); let candidates = items.iter().filter_map(|item| { let item = &**item; let closest = item.find_closest(pointer, transform); Some(item).zip(closest) }); let closest = candidates .min_by_key(|(_, elem)| elem.dist_sq.ord()) .filter(|(_, elem)| elem.dist_sq <= interact_radius_sq); let mut cursors = Vec::new(); let plot = items::PlotConfig { ui, transform, show_x: *show_x, show_y: *show_y, }; if let Some((item, elem)) = closest { item.on_hover(elem, shapes, &mut cursors, &plot, label_formatter); } else { let value = transform.value_from_position(pointer); items::rulers_at_value( pointer, value, "", &plot, shapes, &mut cursors, label_formatter, ); } cursors } } /// Returns next bigger power in given base /// e.g. /// ```ignore /// use egui_plot::next_power; /// assert_eq!(next_power(0.01, 10.0), 0.01); /// assert_eq!(next_power(0.02, 10.0), 0.1); /// assert_eq!(next_power(0.2, 10.0), 1); /// ``` fn next_power(value: f64, base: f64) -> f64 { assert_ne!(value, 0.0); // can be negative (typical for Y axis) base.powi(value.abs().log(base).ceil() as i32) } /// Fill in all values between [min, max] which are a multiple of `step_size` fn generate_marks(step_sizes: [f64; 3], bounds: (f64, f64)) -> Vec { let mut steps = vec![]; fill_marks_between(&mut steps, step_sizes[0], bounds); fill_marks_between(&mut steps, step_sizes[1], bounds); fill_marks_between(&mut steps, step_sizes[2], bounds); steps } /// Fill in all values between [min, max] which are a multiple of `step_size` fn fill_marks_between(out: &mut Vec, step_size: f64, (min, max): (f64, f64)) { assert!(max > min); let first = (min / step_size).ceil() as i64; let last = (max / step_size).ceil() as i64; let marks_iter = (first..last).map(|i| { let value = (i as f64) * step_size; GridMark { value, step_size } }); out.extend(marks_iter); } /// Helper for formatting a number so that we always show at least a few decimals, /// unless it is an integer, in which case we never show any decimals. pub fn format_number(number: f64, num_decimals: usize) -> String { let is_integral = number as i64 as f64 == number; if is_integral { // perfect integer - show it as such: format!("{number:.0}") } else { // make sure we tell the user it is not an integer by always showing a decimal or two: format!("{:.*}", num_decimals.at_least(1), number) } } /// Determine a color from a 0-1 strength value. pub fn color_from_strength(ui: &Ui, strength: f32) -> Color32 { let bg = ui.visuals().extreme_bg_color; let fg = ui.visuals().widgets.open.fg_stroke.color; let mix = 0.5 * strength.sqrt(); Color32::from_rgb( lerp((bg.r() as f32)..=(fg.r() as f32), mix) as u8, lerp((bg.g() as f32)..=(fg.g() as f32), mix) as u8, lerp((bg.b() as f32)..=(fg.b() as f32), mix) as u8, ) }