egui/crates/egui_plot/src/lib.rs

1745 lines
58 KiB
Rust

//! Simple plotting library for [`egui`](https://github.com/emilk/egui).
//!
//! Check out [`Plot`] for how to get started.
//!
//! ## Feature flags
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
//!
mod axis;
mod items;
mod legend;
mod memory;
mod plot_ui;
mod transform;
use std::{ops::RangeInclusive, sync::Arc};
use egui::ahash::HashMap;
use egui::*;
use epaint::{util::FloatOrd, Hsva};
pub use crate::{
axis::{Axis, AxisHints, HPlacement, Placement, VPlacement},
items::{
Arrows, Bar, BarChart, BoxElem, BoxPlot, BoxSpread, HLine, Line, LineStyle, MarkerShape,
Orientation, PlotImage, PlotItem, PlotPoint, PlotPoints, Points, Polygon, Text, VLine,
},
legend::{Corner, Legend},
memory::PlotMemory,
plot_ui::PlotUi,
transform::{PlotBounds, PlotTransform},
};
use axis::AxisWidget;
use items::{horizontal_line, rulers_color, vertical_line};
use legend::LegendWidget;
type LabelFormatterFn = dyn Fn(&str, &PlotPoint) -> String;
pub type LabelFormatter = Option<Box<LabelFormatterFn>>;
type GridSpacerFn = dyn Fn(GridInput) -> Vec<GridMark>;
type GridSpacer = Box<GridSpacerFn>;
type CoordinatesFormatterFn = dyn Fn(&PlotPoint, &PlotBounds) -> String;
/// Specifies the coordinates formatting when passed to [`Plot::coordinates_formatter`].
pub struct CoordinatesFormatter {
function: Box<CoordinatesFormatterFn>,
}
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)
}
}
// ----------------------------------------------------------------------------
/// Indicates a vertical or horizontal cursor line in plot coordinates.
#[derive(Copy, Clone, PartialEq)]
pub 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<Cursor>,
}
#[derive(Default, Clone)]
struct CursorLinkGroups(HashMap<Id, Vec<PlotFrameCursors>>);
#[derive(Clone)]
struct LinkedBounds {
bounds: PlotBounds,
auto_bounds: Vec2b,
}
#[derive(Default, Clone)]
struct BoundsLinkGroups(HashMap<Id, LinkedBounds>);
// ----------------------------------------------------------------------------
/// What [`Plot::show`] returns.
pub struct PlotResponse<R> {
/// 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,
/// The id of a currently hovered item if any.
///
/// This is `None` if either no item was hovered, or the hovered item didn't provide an id.
pub hovered_plot_item: Option<Id>,
}
// ----------------------------------------------------------------------------
/// 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,
id: Option<Id>,
center_axis: Vec2b,
allow_zoom: Vec2b,
allow_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,
boxed_zoom_pointer_button: PointerButton,
linked_axes: Option<(Id, Vec2b)>,
linked_cursors: Option<(Id, Vec2b)>,
min_size: Vec2,
width: Option<f32>,
height: Option<f32>,
data_aspect: Option<f32>,
view_aspect: Option<f32>,
reset: bool,
show_x: bool,
show_y: bool,
label_formatter: LabelFormatter,
coordinates_formatter: Option<(Corner, CoordinatesFormatter)>,
x_axes: Vec<AxisHints>, // default x axes
y_axes: Vec<AxisHints>, // default y axes
legend_config: Option<Legend>,
show_background: bool,
show_axes: Vec2b,
show_grid: Vec2b,
grid_spacing: Rangef,
grid_spacers: [GridSpacer; 2],
sharp_grid_lines: bool,
clamp_grid: bool,
sense: Sense,
}
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),
id: None,
center_axis: false.into(),
allow_zoom: true.into(),
allow_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),
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![AxisHints::new(Axis::X)],
y_axes: vec![AxisHints::new(Axis::Y)],
legend_config: 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)],
sharp_grid_lines: true,
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
}
/// 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<T>(mut self, on: T) -> Self
where
T: Into<Vec2b>,
{
self.allow_zoom = on.into();
self
}
/// Whether to allow scrolling in the plot. Default: `true`.
#[inline]
pub fn allow_scroll<T>(mut self, on: T) -> Self
where
T: Into<Vec2b>,
{
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`.
#[inline]
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)
#[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`.
#[inline]
pub fn allow_drag<T>(mut self, on: T) -> Self
where
T: Into<Vec2b>,
{
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<GridMark>;
/// ```
///
/// 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<GridMark> + '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.
#[inline]
pub fn y_grid_spacer(mut self, spacer: impl Fn(GridInput) -> Vec<GridMark> + 'static) -> 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<Rangef>) -> 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
}
/// 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<f64>) -> 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<f64>) -> 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: Vec2b) -> Self {
self.default_auto_bounds = auto_bounds;
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<Vec2b>) -> 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<Vec2b>) -> 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<Id>, link_x: bool, link_y: bool) -> Self {
self.linked_axes = Some((
group_id.into(),
Vec2b {
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.
#[inline]
pub fn link_cursor(mut self, group_id: impl Into<Id>, link_x: bool, link_y: bool) -> Self {
self.linked_cursors = Some((
group_id.into(),
Vec2b {
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.
#[inline]
pub fn sharp_grid_lines(mut self, enabled: bool) -> Self {
self.sharp_grid_lines = enabled;
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<WidgetText>) -> 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<WidgetText>) -> 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
/// * maximum requested number of characters per tick label.
/// * currently shown range on this axis.
pub fn x_axis_formatter(
mut self,
fmt: impl Fn(GridMark, usize, &RangeInclusive<f64>) -> 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`:
/// * the grid mark to format
/// * maximum requested number of characters per tick label.
/// * currently shown range on this axis.
pub fn y_axis_formatter(
mut self,
fmt: impl Fn(GridMark, usize, &RangeInclusive<f64>) -> 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.
#[inline]
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.
#[inline]
pub fn custom_x_axes(mut self, hints: Vec<AxisHints>) -> 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<AxisHints>) -> Self {
self.y_axes = hints;
self
}
/// Interact with and add items to the plot and finally draw it.
pub fn show<R>(self, ui: &mut Ui, build_fn: impl FnOnce(&mut PlotUi) -> R) -> PlotResponse<R> {
self.show_dyn(ui, Box::new(build_fn))
}
fn show_dyn<'a, R>(
self,
ui: &mut Ui,
build_fn: Box<dyn FnOnce(&mut PlotUi) -> R + 'a>,
) -> PlotResponse<R> {
let Self {
id_source,
id,
center_axis,
allow_zoom,
allow_drag,
allow_scroll,
allow_double_click_reset,
allow_boxed_zoom,
boxed_zoom_pointer_button,
default_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,
grid_spacing,
linked_axes,
linked_cursors,
clamp_grid,
grid_spacers,
sharp_grid_lines,
sense,
} = 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,
};
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: 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);
// 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(plot_rect, min_auto_bounds, center_axis.x, center_axis.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,
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(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.name()));
// Highlight the hovered items.
if let Some(hovered_name) = &mem.hovered_legend_item {
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<Cursor> = 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();
}
// Apply bounds modifications.
for modification in bounds_modifications {
match modification {
BoundsModification::Set(new_bounds) => {
bounds = new_bounds;
mem.auto_bounds = false.into();
}
BoundsModification::Translate(delta) => {
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 mem.auto_bounds.x {
bounds.set_x(&min_auto_bounds);
}
if 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(plot_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;
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(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;
}
mem.transform.translate_bounds(delta);
mem.auto_bounds = !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_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),
), // 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_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;
}
}
}
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) {
mem.transform.zoom(zoom_factor, hover_pos);
mem.auto_bounds = !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);
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 {
items,
show_x,
show_y,
label_formatter,
coordinates_formatter,
show_grid,
grid_spacing,
transform: mem.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, 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_name();
}
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(
mem: Option<&PlotMemory>,
show_axes: Vec2b,
complete_rect: Rect,
[x_axes, y_axes]: [&[AxisHints]; 2],
) -> ([Vec<AxisWidget>; 2], Rect) {
// Next we want to create this layout.
// Indices 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 x_axis_widgets = Vec::<AxisWidget>::new();
let mut y_axis_widgets = Vec::<AxisWidget>::new();
// Will shrink as we add more axes.
let mut rect_left = complete_rect;
if show_axes.x {
// We will fix this later, once we know how much space the y axes take up.
let initial_x_range = complete_rect.x_range();
for (i, cfg) in x_axes.iter().enumerate().rev() {
let mut height = cfg.thickness(Axis::X);
if let Some(mem) = mem {
// If the labels took up too much space the previous frame, give them more space now:
height = height.max(mem.x_axis_thickness.get(&i).copied().unwrap_or_default());
}
let rect = match VPlacement::from(cfg.placement) {
VPlacement::Bottom => {
let bottom = rect_left.bottom();
*rect_left.bottom_mut() -= height;
let top = rect_left.bottom();
Rect::from_x_y_ranges(initial_x_range, top..=bottom)
}
VPlacement::Top => {
let top = rect_left.top();
*rect_left.top_mut() += height;
let bottom = rect_left.top();
Rect::from_x_y_ranges(initial_x_range, top..=bottom)
}
};
x_axis_widgets.push(AxisWidget::new(cfg.clone(), rect));
}
}
if show_axes.y {
// We know this, since we've already allocated space for the x axes.
let plot_y_range = rect_left.y_range();
for (i, cfg) in y_axes.iter().enumerate().rev() {
let mut width = cfg.thickness(Axis::Y);
if let Some(mem) = mem {
// If the labels took up too much space the previous frame, give them more space now:
width = width.max(mem.y_axis_thickness.get(&i).copied().unwrap_or_default());
}
let rect = match HPlacement::from(cfg.placement) {
HPlacement::Left => {
let left = rect_left.left();
*rect_left.left_mut() += width;
let right = rect_left.left();
Rect::from_x_y_ranges(left..=right, plot_y_range)
}
HPlacement::Right => {
let right = rect_left.right();
*rect_left.right_mut() -= width;
let left = rect_left.right();
Rect::from_x_y_ranges(left..=right, plot_y_range)
}
};
y_axis_widgets.push(AxisWidget::new(cfg.clone(), rect));
}
}
let mut plot_rect = rect_left;
// 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;
}
// Bow that we know the final x_range of the plot_rect,
// assign it to the x_axis_widgets (they are currently too wide):
for widget in &mut x_axis_widgets {
widget.rect = Rect::from_x_y_ranges(plot_rect.x_range(), widget.rect.y_range());
}
([x_axis_widgets, y_axis_widgets], plot_rect)
}
/// 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),
AutoBounds(Vec2b),
Zoom(Vec2, PlotPoint),
}
// ----------------------------------------------------------------------------
// 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.
///
/// Always positive.
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<GridMark> {
// handle degenerate cases
if input.base_step_size.abs() < f64::EPSILON {
return Vec::new();
}
// 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<GridMark> {
let bounds = input.bounds;
let step_sizes = spacer(input);
generate_marks(step_sizes, bounds)
};
Box::new(get_marks)
}
// ----------------------------------------------------------------------------
struct PreparedPlot {
items: Vec<Box<dyn PlotItem>>,
show_x: bool,
show_y: bool,
label_formatter: LabelFormatter,
coordinates_formatter: Option<(Corner, CoordinatesFormatter)>,
// axis_formatters: [AxisFormatter; 2],
transform: PlotTransform,
show_grid: Vec2b,
grid_spacing: Rangef,
grid_spacers: [GridSpacer; 2],
draw_cursor_x: bool,
draw_cursor_y: bool,
draw_cursors: Vec<Cursor>,
sharp_grid_lines: bool,
clamp_grid: bool,
}
impl PreparedPlot {
fn ui(self, ui: &mut Ui, response: &Response) -> (Vec<Cursor>, Option<Id>) {
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.child_ui(*transform.frame(), 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 = rulers_color(ui);
let mut draw_cursor = |cursors: &Vec<Cursor>, 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, hovered_item_id)
}
fn paint_grid(&self, ui: &Ui, shapes: &mut Vec<(Shape, f32)>, axis: Axis, fade_range: Rangef) {
#![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].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]);
}
}
}
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<Shape>) -> (Vec<Cursor>, Option<Id>) {
let Self {
transform,
show_x,
show_y,
label_formatter,
items,
..
} = self;
if !show_x && !show_y {
return (Vec::new(), None);
}
let interact_radius_sq = (16.0_f32).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 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)) = closest {
item.on_hover(elem, shapes, &mut cursors, &plot, label_formatter);
item.id()
} else {
let value = transform.value_from_position(pointer);
items::rulers_at_value(
pointer,
value,
"",
&plot,
shapes,
&mut cursors,
label_formatter,
);
None
};
(cursors, hovered_plot_item_id)
}
}
/// 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 {
debug_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<GridMark> {
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<GridMark>, step_size: f64, (min, max): (f64, f64)) {
debug_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 base_color = ui.visuals().text_color();
base_color.gamma_multiply(strength.sqrt())
}