diff --git a/Makefile b/Makefile index 5f60b44d..07235765 100644 --- a/Makefile +++ b/Makefile @@ -119,6 +119,7 @@ run_checks: check_no_commented_out_code \ checks_no_unfinished \ check_shear \ check_deny \ + check_linter \ check_pub_change_intentional # Performs a compilation check of all crates in the workspace, without building. @@ -153,6 +154,9 @@ check_license: check_cycles: cargo-cycles +check_linter: + python3 ./scripts/lint.py + check_pub_change_intentional: if [ "$$(cargo public-api diff latest -p egui_plot -sss --deny all)" != "" ]; then \ echo "--------------------------------"; \ diff --git a/egui_plot/src/axis.rs b/egui_plot/src/axis.rs index d3b64ff3..f7f7dbc6 100644 --- a/egui_plot/src/axis.rs +++ b/egui_plot/src/axis.rs @@ -16,8 +16,12 @@ use egui::emath::Rot2; use egui::emath::remap_clamp; use egui::epaint::TextShape; -use super::GridMark; use super::transform::PlotTransform; +use crate::colors; +use crate::grid::GridMark; +use crate::placement::HPlacement; +use crate::placement::Placement; +use crate::placement::VPlacement; // Gap between tick labels and axis label in units of the axis label height const AXIS_LABEL_GAP: f32 = 0.25; @@ -44,70 +48,6 @@ impl From for usize { } } -/// Placement of the horizontal X-Axis. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum VPlacement { - Top, - Bottom, -} - -/// Placement of the vertical Y-Axis. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum HPlacement { - Left, - Right, -} - -/// Placement of an axis. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Placement { - /// Bottom for X-axis, or left for Y-axis. - LeftBottom, - - /// Top for x-axis and right for y-axis. - RightTop, -} - -impl From for Placement { - #[inline] - fn from(placement: HPlacement) -> Self { - match placement { - HPlacement::Left => Self::LeftBottom, - HPlacement::Right => Self::RightTop, - } - } -} - -impl From for HPlacement { - #[inline] - fn from(placement: Placement) -> Self { - match placement { - Placement::LeftBottom => Self::Left, - Placement::RightTop => Self::Right, - } - } -} - -impl From for Placement { - #[inline] - fn from(placement: VPlacement) -> Self { - match placement { - VPlacement::Top => Self::RightTop, - VPlacement::Bottom => Self::LeftBottom, - } - } -} - -impl From for VPlacement { - #[inline] - fn from(placement: Placement) -> Self { - match placement { - Placement::LeftBottom => Self::Bottom, - Placement::RightTop => Self::Top, - } - } -} - /// Axis configuration. /// /// Used to configure axis label and ticks. @@ -330,7 +270,7 @@ impl<'a> AxisWidget<'a> { // Fade in labels as they get further apart: let strength = remap_clamp(spacing_in_points, label_spacing, 0.0..=1.0); - let text_color = super::color_from_strength(ui, strength); + let text_color = colors::color_from_strength(ui, strength); let galley = painter.layout_no_wrap(text, font_id.clone(), text_color); let galley_size = match axis { Axis::X => galley.size(), diff --git a/egui_plot/src/bounds.rs b/egui_plot/src/bounds.rs new file mode 100644 index 00000000..eb1af275 --- /dev/null +++ b/egui_plot/src/bounds.rs @@ -0,0 +1,283 @@ +use std::ops::RangeInclusive; + +use ahash::HashMap; +use egui::Id; +use emath::Vec2; +use emath::Vec2b; + +use crate::PlotPoint; + +/// 2D bounding box of f64 precision. +/// +/// The range of data values we show. +#[derive(Clone, Copy, PartialEq, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct PlotBounds { + pub(crate) min: [f64; 2], + pub(crate) max: [f64; 2], +} + +impl PlotBounds { + pub const NOTHING: Self = Self { + min: [f64::INFINITY; 2], + max: [-f64::INFINITY; 2], + }; + + #[inline] + pub fn from_min_max(min: [f64; 2], max: [f64; 2]) -> Self { + Self { min, max } + } + + #[inline] + pub fn min(&self) -> [f64; 2] { + self.min + } + + #[inline] + pub fn max(&self) -> [f64; 2] { + self.max + } + + #[inline] + pub fn new_symmetrical(half_extent: f64) -> Self { + Self { + min: [-half_extent; 2], + max: [half_extent; 2], + } + } + + #[inline] + pub fn is_finite(&self) -> bool { + self.min[0].is_finite() && self.min[1].is_finite() && self.max[0].is_finite() && self.max[1].is_finite() + } + + #[inline] + pub fn is_finite_x(&self) -> bool { + self.min[0].is_finite() && self.max[0].is_finite() + } + + #[inline] + pub fn is_finite_y(&self) -> bool { + self.min[1].is_finite() && self.max[1].is_finite() + } + + #[inline] + pub fn is_valid(&self) -> bool { + self.is_finite() && self.width() > 0.0 && self.height() > 0.0 + } + + #[inline] + pub fn is_valid_x(&self) -> bool { + self.is_finite_x() && self.width() > 0.0 + } + + #[inline] + pub fn is_valid_y(&self) -> bool { + self.is_finite_y() && self.height() > 0.0 + } + + #[inline] + pub fn width(&self) -> f64 { + self.max[0] - self.min[0] + } + + #[inline] + pub fn height(&self) -> f64 { + self.max[1] - self.min[1] + } + + #[inline] + pub fn center(&self) -> PlotPoint { + [ + emath::fast_midpoint(self.min[0], self.max[0]), + emath::fast_midpoint(self.min[1], self.max[1]), + ] + .into() + } + + /// Expand to include the given (x,y) value + #[inline] + pub fn extend_with(&mut self, value: &PlotPoint) { + self.extend_with_x(value.x); + self.extend_with_y(value.y); + } + + /// Expand to include the given x coordinate + #[inline] + pub fn extend_with_x(&mut self, x: f64) { + self.min[0] = self.min[0].min(x); + self.max[0] = self.max[0].max(x); + } + + /// Expand to include the given y coordinate + #[inline] + pub fn extend_with_y(&mut self, y: f64) { + self.min[1] = self.min[1].min(y); + self.max[1] = self.max[1].max(y); + } + + #[inline] + fn clamp_to_finite(&mut self) { + for d in 0..2 { + self.min[d] = self.min[d].clamp(f64::MIN, f64::MAX); + if self.min[d].is_nan() { + self.min[d] = 0.0; + } + + self.max[d] = self.max[d].clamp(f64::MIN, f64::MAX); + if self.max[d].is_nan() { + self.max[d] = 0.0; + } + } + } + + #[inline] + pub fn expand_x(&mut self, pad: f64) { + if pad.is_finite() { + self.min[0] -= pad; + self.max[0] += pad; + self.clamp_to_finite(); + } + } + + #[inline] + pub fn expand_y(&mut self, pad: f64) { + if pad.is_finite() { + self.min[1] -= pad; + self.max[1] += pad; + self.clamp_to_finite(); + } + } + + #[inline] + pub fn merge_x(&mut self, other: &Self) { + self.min[0] = self.min[0].min(other.min[0]); + self.max[0] = self.max[0].max(other.max[0]); + } + + #[inline] + pub fn merge_y(&mut self, other: &Self) { + self.min[1] = self.min[1].min(other.min[1]); + self.max[1] = self.max[1].max(other.max[1]); + } + + #[inline] + pub fn set_x(&mut self, other: &Self) { + self.min[0] = other.min[0]; + self.max[0] = other.max[0]; + } + + #[inline] + pub fn set_x_center_width(&mut self, x: f64, width: f64) { + self.min[0] = x - width / 2.0; + self.max[0] = x + width / 2.0; + } + + #[inline] + pub fn set_y(&mut self, other: &Self) { + self.min[1] = other.min[1]; + self.max[1] = other.max[1]; + } + + #[inline] + pub fn set_y_center_height(&mut self, y: f64, height: f64) { + self.min[1] = y - height / 2.0; + self.max[1] = y + height / 2.0; + } + + #[inline] + pub fn merge(&mut self, other: &Self) { + self.min[0] = self.min[0].min(other.min[0]); + self.min[1] = self.min[1].min(other.min[1]); + self.max[0] = self.max[0].max(other.max[0]); + self.max[1] = self.max[1].max(other.max[1]); + } + + #[inline] + pub fn translate_x(&mut self, delta: f64) { + if delta.is_finite() { + self.min[0] += delta; + self.max[0] += delta; + self.clamp_to_finite(); + } + } + + #[inline] + pub fn translate_y(&mut self, delta: f64) { + if delta.is_finite() { + self.min[1] += delta; + self.max[1] += delta; + self.clamp_to_finite(); + } + } + + #[inline] + pub fn translate(&mut self, delta: (f64, f64)) { + self.translate_x(delta.0); + self.translate_y(delta.1); + } + + #[inline] + pub fn zoom(&mut self, zoom_factor: Vec2, center: PlotPoint) { + self.min[0] = center.x + (self.min[0] - center.x) / (zoom_factor.x as f64); + self.max[0] = center.x + (self.max[0] - center.x) / (zoom_factor.x as f64); + self.min[1] = center.y + (self.min[1] - center.y) / (zoom_factor.y as f64); + self.max[1] = center.y + (self.max[1] - center.y) / (zoom_factor.y as f64); + } + + #[inline] + pub fn add_relative_margin_x(&mut self, margin_fraction: Vec2) { + let width = self.width().max(0.0); + self.expand_x(margin_fraction.x as f64 * width); + } + + #[inline] + pub fn add_relative_margin_y(&mut self, margin_fraction: Vec2) { + let height = self.height().max(0.0); + self.expand_y(margin_fraction.y as f64 * height); + } + + #[inline] + pub fn range_x(&self) -> RangeInclusive { + self.min[0]..=self.max[0] + } + + #[inline] + pub fn range_y(&self) -> RangeInclusive { + self.min[1]..=self.max[1] + } + + #[inline] + pub fn make_x_symmetrical(&mut self) { + let x_abs = self.min[0].abs().max(self.max[0].abs()); + self.min[0] = -x_abs; + self.max[0] = x_abs; + } + + #[inline] + pub fn make_y_symmetrical(&mut self) { + let y_abs = self.min[1].abs().max(self.max[1].abs()); + self.min[1] = -y_abs; + self.max[1] = y_abs; + } +} + +#[derive(Clone)] +pub struct LinkedBounds { + pub bounds: PlotBounds, + pub auto_bounds: Vec2b, +} + +#[derive(Default, Clone)] +pub struct BoundsLinkGroups(pub HashMap); + +/// 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. +pub enum BoundsModification { + SetX(RangeInclusive), + SetY(RangeInclusive), + Translate(Vec2), + AutoBounds(Vec2b), + Zoom(Vec2, PlotPoint), +} diff --git a/egui_plot/src/colors.rs b/egui_plot/src/colors.rs new file mode 100644 index 00000000..56d21a8f --- /dev/null +++ b/egui_plot/src/colors.rs @@ -0,0 +1,51 @@ +use egui::Color32; +use egui::Rgba; +use egui::Stroke; +use egui::Ui; +use emath::NumExt as _; + +pub(crate) fn rulers_color(ui: &Ui) -> Color32 { + if ui.visuals().dark_mode { + Color32::from_gray(100).additive() + } else { + Color32::from_black_alpha(180) + } +} + +pub(crate) fn highlighted_color(mut stroke: Stroke, fill: Color32) -> (Stroke, Color32) { + stroke.width *= 2.0; + + let mut fill = Rgba::from(fill); + if fill.is_additive() { + // Make slightly brighter + fill = 1.3 * fill; + } else { + // Make more opaque: + let fill_alpha = (2.0 * fill.a()).at_most(1.0); + fill = fill.to_opaque().multiply(fill_alpha); + } + + (stroke, fill.into()) +} + +pub const DEFAULT_FILL_ALPHA: f32 = 0.05; + +/// Default base colors. Used for now only in heatmap palette. +pub const BASE_COLORS: [Color32; 10] = [ + Color32::from_rgb(48, 18, 59), + Color32::from_rgb(35, 106, 141), + Color32::from_rgb(30, 160, 140), + Color32::from_rgb(88, 200, 98), + Color32::from_rgb(164, 223, 39), + Color32::from_rgb(228, 223, 14), + Color32::from_rgb(250, 187, 13), + Color32::from_rgb(246, 135, 8), + Color32::from_rgb(213, 68, 2), + Color32::from_rgb(122, 4, 2), +]; + +/// 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()) +} diff --git a/egui_plot/src/cursor.rs b/egui_plot/src/cursor.rs new file mode 100644 index 00000000..a56effc2 --- /dev/null +++ b/egui_plot/src/cursor.rs @@ -0,0 +1,28 @@ +use ahash::HashMap; +use egui::Id; + +/// Indicates a vertical or horizontal cursor line in plot coordinates. +#[derive(Copy, Clone, PartialEq)] +pub enum Cursor { + /// Horizontal cursor line at the given y-coordinate. + Horizontal { + /// Y-coordinate of the horizontal cursor line. + y: f64, + }, + + /// Vertical cursor line at the given x-coordinate. + Vertical { + /// X-coordinate of the vertical cursor line. + x: f64, + }, +} + +/// Contains the cursors drawn for a plot widget in a single frame. +#[derive(PartialEq, Clone)] +pub(crate) struct PlotFrameCursors { + pub(crate) id: Id, + pub(crate) cursors: Vec, +} + +#[derive(Default, Clone)] +pub(crate) struct CursorLinkGroups(pub(crate) HashMap>); diff --git a/egui_plot/src/grid.rs b/egui_plot/src/grid.rs new file mode 100644 index 00000000..509c7545 --- /dev/null +++ b/egui_plot/src/grid.rs @@ -0,0 +1,205 @@ +use std::cmp::Ordering; + +type GridSpacerFn<'a> = dyn Fn(GridInput) -> Vec + 'a; +pub type GridSpacer<'a> = Box>; + +/// Input for "grid spacer" functions. +/// +/// See [`crate::Plot::x_grid_spacer()`] and [`crate::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, PartialEq)] +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<'static> { + let log_base = log_base as f64; + let step_sizes = move |input: GridInput| -> Vec { + // 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<'a>(spacer: impl Fn(GridInput) -> [f64; 3] + 'a) -> GridSpacer<'a> { + 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) +} + +/// 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, "Bad input"); // 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); + + // Remove duplicates: + // This can happen because we have overlapping steps, e.g.: + // step_size[0] = 10 => [-10, 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, + // 110, 120] step_size[1] = 100 => [ 0, + // 100 ] step_size[2] = 1000 => [ 0 + // ] + + steps.sort_by(|a, b| cmp_f64(a.value, b.value)); + + let min_step = step_sizes.iter().fold(f64::INFINITY, |a, &b| a.min(b)); + let eps = 0.1 * min_step; // avoid putting two ticks too closely together + + let mut deduplicated: Vec = Vec::with_capacity(steps.len()); + for step in steps { + if let Some(last) = deduplicated.last_mut() { + if (last.value - step.value).abs() < eps { + // Keep the one with the largest step size + if last.step_size < step.step_size { + *last = step; + } + continue; + } + } + deduplicated.push(step); + } + + deduplicated +} + +#[test] +fn test_generate_marks() { + fn approx_eq(a: &GridMark, b: &GridMark) -> bool { + (a.value - b.value).abs() < 1e-10 && a.step_size == b.step_size + } + + let gm = |value, step_size| GridMark { value, step_size }; + + let marks = generate_marks([0.01, 0.1, 1.0], (2.855, 3.015)); + let expected = vec![ + gm(2.86, 0.01), + gm(2.87, 0.01), + gm(2.88, 0.01), + gm(2.89, 0.01), + gm(2.90, 0.1), + gm(2.91, 0.01), + gm(2.92, 0.01), + gm(2.93, 0.01), + gm(2.94, 0.01), + gm(2.95, 0.01), + gm(2.96, 0.01), + gm(2.97, 0.01), + gm(2.98, 0.01), + gm(2.99, 0.01), + gm(3.00, 1.), + gm(3.01, 0.01), + ]; + + let mut problem = if marks.len() == expected.len() { + None + } else { + Some(format!( + "Different lengths: got {}, expected {}", + marks.len(), + expected.len() + )) + }; + + for (i, (a, b)) in marks.iter().zip(&expected).enumerate() { + if !approx_eq(a, b) { + problem = Some(format!("Mismatch at index {i}: {a:?} != {b:?}")); + break; + } + } + + if let Some(problem) = problem { + panic!("Test failed: {problem}. Got: {marks:#?}, expected: {expected:#?}"); + } +} + +fn cmp_f64(a: f64, b: f64) -> Ordering { + match a.partial_cmp(&b) { + Some(ord) => ord, + None => a.is_nan().cmp(&b.is_nan()), + } +} + +/// 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)) { + debug_assert!(min <= max, "Bad plot bounds: min: {min}, max: {max}"); + 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); +} diff --git a/egui_plot/src/items/arrows.rs b/egui_plot/src/items/arrows.rs index a717e1b9..eaa97049 100644 --- a/egui_plot/src/items/arrows.rs +++ b/egui_plot/src/items/arrows.rs @@ -7,12 +7,12 @@ use egui::Ui; use emath::Rot2; use crate::Id; -use crate::PlotBounds; -use crate::PlotGeometry; use crate::PlotItem; use crate::PlotItemBase; -use crate::PlotPoints; use crate::PlotTransform; +use crate::bounds::PlotBounds; +use crate::values::PlotGeometry; +use crate::values::PlotPoints; impl<'a> Arrows<'a> { pub fn new(name: impl Into, origins: impl Into>, tips: impl Into>) -> Self { diff --git a/egui_plot/src/items/bar_chart.rs b/egui_plot/src/items/bar_chart.rs index c08fe4c5..e7bf7351 100644 --- a/egui_plot/src/items/bar_chart.rs +++ b/egui_plot/src/items/bar_chart.rs @@ -11,21 +11,21 @@ use emath::NumExt as _; use emath::Pos2; use super::add_rulers_and_text; -use super::find_closest_rect; -use super::rect_elem::RectElement; -use super::rect_elem::highlighted_color; -use crate::ClosestElem; use crate::Cursor; use crate::Id; -use crate::LabelFormatter; -use crate::Orientation; -use crate::PlotBounds; use crate::PlotConfig; -use crate::PlotGeometry; use crate::PlotItem; use crate::PlotItemBase; -use crate::PlotPoint; use crate::PlotTransform; +use crate::bounds::PlotBounds; +use crate::colors::highlighted_color; +use crate::label::LabelFormatter; +use crate::math::find_closest_rect; +use crate::rect_elem::RectElement; +use crate::values::ClosestElem; +use crate::values::Orientation; +use crate::values::PlotGeometry; +use crate::values::PlotPoint; /// A bar chart. pub struct BarChart { @@ -414,6 +414,6 @@ impl RectElement for Bar { Orientation::Vertical => scale[1], }; let decimals = ((-scale.abs().log10()).ceil().at_least(0.0) as usize).at_most(6); - crate::format_number(self.value, decimals) + crate::label::format_number(self.value, decimals) } } diff --git a/egui_plot/src/items/box_plot.rs b/egui_plot/src/items/box_plot.rs index fc0c780e..08463a47 100644 --- a/egui_plot/src/items/box_plot.rs +++ b/egui_plot/src/items/box_plot.rs @@ -10,21 +10,21 @@ use emath::NumExt as _; use emath::Pos2; use super::add_rulers_and_text; -use super::find_closest_rect; -use super::rect_elem::RectElement; -use super::rect_elem::highlighted_color; -use crate::ClosestElem; use crate::Cursor; use crate::Id; -use crate::LabelFormatter; -use crate::Orientation; -use crate::PlotBounds; use crate::PlotConfig; -use crate::PlotGeometry; use crate::PlotItem; use crate::PlotItemBase; -use crate::PlotPoint; use crate::PlotTransform; +use crate::bounds::PlotBounds; +use crate::colors::highlighted_color; +use crate::label::LabelFormatter; +use crate::math::find_closest_rect; +use crate::rect_elem::RectElement; +use crate::values::ClosestElem; +use crate::values::Orientation; +use crate::values::PlotGeometry; +use crate::values::PlotPoint; /// A diagram containing a series of [`BoxElem`] elements. pub struct BoxPlot { diff --git a/egui_plot/src/items/heatmap.rs b/egui_plot/src/items/heatmap.rs index bd1e731b..4f34e721 100644 --- a/egui_plot/src/items/heatmap.rs +++ b/egui_plot/src/items/heatmap.rs @@ -14,29 +14,16 @@ use egui::WidgetText; use emath::Float as _; use super::Cursor; -use super::LabelFormatter; -use super::PlotBounds; use super::PlotTransform; -use crate::ClosestElem; use crate::PlotConfig; -use crate::PlotGeometry; use crate::PlotItem; use crate::PlotItemBase; -use crate::PlotPoint; - -/// Default base colors for heatmap palette -pub const BASE_COLORS: [Color32; 10] = [ - Color32::from_rgb(48, 18, 59), - Color32::from_rgb(35, 106, 141), - Color32::from_rgb(30, 160, 140), - Color32::from_rgb(88, 200, 98), - Color32::from_rgb(164, 223, 39), - Color32::from_rgb(228, 223, 14), - Color32::from_rgb(250, 187, 13), - Color32::from_rgb(246, 135, 8), - Color32::from_rgb(213, 68, 2), - Color32::from_rgb(122, 4, 2), -]; +use crate::bounds::PlotBounds; +use crate::colors::BASE_COLORS; +use crate::label::LabelFormatter; +use crate::values::ClosestElem; +use crate::values::PlotGeometry; +use crate::values::PlotPoint; /// Default resolution for heatmap color palette pub const DEFAULT_RESOLUTION: usize = 128; diff --git a/egui_plot/src/items/line.rs b/egui_plot/src/items/line.rs index 14fb3745..2bd67848 100644 --- a/egui_plot/src/items/line.rs +++ b/egui_plot/src/items/line.rs @@ -5,15 +5,17 @@ use egui::Shape; use egui::Stroke; use egui::Ui; use egui::epaint::PathStroke; +use emath::Pos2; +use emath::pos2; use crate::Id; -use crate::LineStyle; -use crate::PlotBounds; -use crate::PlotGeometry; use crate::PlotItem; use crate::PlotItemBase; -use crate::PlotPoint; use crate::PlotTransform; +use crate::bounds::PlotBounds; +use crate::values::LineStyle; +use crate::values::PlotGeometry; +use crate::values::PlotPoint; /// A horizontal line in a plot, filling the full width #[derive(Clone, Debug, PartialEq)] @@ -284,3 +286,19 @@ impl PlotItem for VLine { bounds } } + +pub fn vertical_line(pointer: Pos2, transform: &PlotTransform, line_color: Color32) -> Shape { + let frame = transform.frame(); + Shape::line_segment( + [pos2(pointer.x, frame.top()), pos2(pointer.x, frame.bottom())], + (1.0, line_color), + ) +} + +pub fn horizontal_line(pointer: Pos2, transform: &PlotTransform, line_color: Color32) -> Shape { + let frame = transform.frame(); + Shape::line_segment( + [pos2(frame.left(), pointer.y), pos2(frame.right(), pointer.y)], + (1.0, line_color), + ) +} diff --git a/egui_plot/src/items/mod.rs b/egui_plot/src/items/mod.rs index a4e6b41b..007a30e0 100644 --- a/egui_plot/src/items/mod.rs +++ b/egui_plot/src/items/mod.rs @@ -1,4 +1,4 @@ -//! Contains items that can be added to a plot. +//! Contains items that can be added to a plot at some plot coordinates. #![expect(clippy::type_complexity)] // TODO(#163): simplify some of the callback types with type aliases use std::ops::RangeInclusive; @@ -18,31 +18,30 @@ use egui::Pos2; use egui::Shape; use egui::TextStyle; use egui::Ui; -use egui::pos2; use egui::vec2; use emath::Float as _; pub use heatmap::Heatmap; pub use line::HLine; pub use line::VLine; +pub use line::horizontal_line; +pub use line::vertical_line; pub use plot_image::PlotImage; pub use points::Points; pub use polygon::Polygon; -use rect_elem::RectElement; pub use series::Line; pub use span::Span; pub use text::Text; -pub use values::ClosestElem; -pub use values::LineStyle; -pub use values::MarkerShape; -pub use values::Orientation; -pub use values::PlotGeometry; -pub use values::PlotPoint; -pub use values::PlotPoints; use super::Cursor; -use super::LabelFormatter; -use super::PlotBounds; use super::PlotTransform; +use crate::bounds::PlotBounds; +use crate::label::LabelFormatter; +use crate::rect_elem::RectElement; +use crate::values::ClosestElem; +use crate::values::LineStyle; +use crate::values::Orientation; +use crate::values::PlotGeometry; +use crate::values::PlotPoint; mod arrows; mod bar_chart; @@ -52,13 +51,9 @@ mod line; mod plot_image; mod points; mod polygon; -mod rect_elem; mod series; mod span; mod text; -mod values; - -const DEFAULT_FILL_ALPHA: f32 = 0.05; /// Base data shared by all plot items. #[derive(Clone, Debug, PartialEq, Eq)] @@ -203,42 +198,9 @@ pub trait PlotItem { } } -// ---------------------------------------------------------------------------- - -/// Returns the x-coordinate of a possible intersection between a line segment -/// from `p1` to `p2` and a horizontal line at the given y-coordinate. -fn y_intersection(p1: &Pos2, p2: &Pos2, y: f32) -> Option { - ((p1.y > y && p2.y < y) || (p1.y < y && p2.y > y)) - .then_some(((y * (p1.x - p2.x)) - (p1.x * p2.y - p1.y * p2.x)) / (p1.y - p2.y)) -} - // ---------------------------------------------------------------------------- // Helper functions -pub(crate) fn rulers_color(ui: &Ui) -> Color32 { - if ui.visuals().dark_mode { - Color32::from_gray(100).additive() - } else { - Color32::from_black_alpha(180) - } -} - -pub(crate) fn vertical_line(pointer: Pos2, transform: &PlotTransform, line_color: Color32) -> Shape { - let frame = transform.frame(); - Shape::line_segment( - [pos2(pointer.x, frame.top()), pos2(pointer.x, frame.bottom())], - (1.0, line_color), - ) -} - -pub(crate) fn horizontal_line(pointer: Pos2, transform: &PlotTransform, line_color: Color32) -> Shape { - let frame = transform.frame(); - Shape::line_segment( - [pos2(frame.left(), pointer.y), pos2(frame.right(), pointer.y)], - (1.0, line_color), - ) -} - fn add_rulers_and_text( elem: &dyn RectElement, plot: &PlotConfig<'_>, @@ -364,23 +326,3 @@ pub(super) fn rulers_and_tooltip_at_value( ui.label(text); }); } - -fn find_closest_rect<'a, T>( - rects: impl IntoIterator, - point: Pos2, - transform: &PlotTransform, -) -> Option -where - T: 'a + RectElement, -{ - rects - .into_iter() - .enumerate() - .map(|(index, bar)| { - let bar_rect = transform.rect_from_values(&bar.bounds_min(), &bar.bounds_max()); - let dist_sq = bar_rect.distance_sq_to_pos(point); - - ClosestElem { index, dist_sq } - }) - .min_by_key(|e| e.dist_sq.ord()) -} diff --git a/egui_plot/src/items/plot_image.rs b/egui_plot/src/items/plot_image.rs index 338caa7d..be0b41a4 100644 --- a/egui_plot/src/items/plot_image.rs +++ b/egui_plot/src/items/plot_image.rs @@ -13,12 +13,12 @@ use emath::Vec2; use emath::pos2; use crate::Id; -use crate::PlotBounds; -use crate::PlotGeometry; use crate::PlotItem; use crate::PlotItemBase; -use crate::PlotPoint; use crate::PlotTransform; +use crate::bounds::PlotBounds; +use crate::values::PlotGeometry; +use crate::values::PlotPoint; /// An image in the plot. #[derive(Clone)] diff --git a/egui_plot/src/items/points.rs b/egui_plot/src/items/points.rs index 0d0150f6..24a02b08 100644 --- a/egui_plot/src/items/points.rs +++ b/egui_plot/src/items/points.rs @@ -10,14 +10,14 @@ use emath::pos2; use emath::vec2; use crate::Id; -use crate::MarkerShape; -use crate::PlotBounds; -use crate::PlotGeometry; use crate::PlotItem; use crate::PlotItemBase; -use crate::PlotPoint; -use crate::PlotPoints; use crate::PlotTransform; +use crate::bounds::PlotBounds; +use crate::values::MarkerShape; +use crate::values::PlotGeometry; +use crate::values::PlotPoint; +use crate::values::PlotPoints; impl<'a> Points<'a> { pub fn new(name: impl Into, series: impl Into>) -> Self { diff --git a/egui_plot/src/items/polygon.rs b/egui_plot/src/items/polygon.rs index 3c95ab1f..9e431b48 100644 --- a/egui_plot/src/items/polygon.rs +++ b/egui_plot/src/items/polygon.rs @@ -7,14 +7,14 @@ use egui::Stroke; use egui::Ui; use egui::epaint::PathStroke; -use super::DEFAULT_FILL_ALPHA; -use crate::LineStyle; -use crate::PlotBounds; -use crate::PlotGeometry; use crate::PlotItem; use crate::PlotItemBase; -use crate::PlotPoints; use crate::PlotTransform; +use crate::bounds::PlotBounds; +use crate::colors::DEFAULT_FILL_ALPHA; +use crate::values::LineStyle; +use crate::values::PlotGeometry; +use crate::values::PlotPoints; /// A convex polygon. pub struct Polygon<'a> { diff --git a/egui_plot/src/items/series.rs b/egui_plot/src/items/series.rs index edd9cdf7..a3e88f48 100644 --- a/egui_plot/src/items/series.rs +++ b/egui_plot/src/items/series.rs @@ -13,17 +13,17 @@ use emath::Pos2; use emath::Rect; use emath::pos2; -use super::DEFAULT_FILL_ALPHA; -use super::y_intersection; use crate::Id; -use crate::LineStyle; -use crate::PlotBounds; -use crate::PlotGeometry; use crate::PlotItem; use crate::PlotItemBase; -use crate::PlotPoint; -use crate::PlotPoints; use crate::PlotTransform; +use crate::bounds::PlotBounds; +use crate::colors::DEFAULT_FILL_ALPHA; +use crate::math::y_intersection; +use crate::values::LineStyle; +use crate::values::PlotGeometry; +use crate::values::PlotPoint; +use crate::values::PlotPoints; /// A series of values forming a path. pub struct Line<'a> { diff --git a/egui_plot/src/items/span.rs b/egui_plot/src/items/span.rs index 191c6890..c990d4d5 100644 --- a/egui_plot/src/items/span.rs +++ b/egui_plot/src/items/span.rs @@ -16,14 +16,14 @@ use egui::pos2; use emath::TSTransform; use super::LineStyle; -use super::PlotBounds; use super::PlotGeometry; use super::PlotItem; use super::PlotItemBase; use super::PlotPoint; use super::PlotTransform; -use super::rect_elem::highlighted_color; use crate::Axis; +use crate::bounds::PlotBounds; +use crate::colors::highlighted_color; use crate::utils::find_name_candidate; /// Padding between the label of the span and both the edge of the view and the diff --git a/egui_plot/src/items/text.rs b/egui_plot/src/items/text.rs index f760126c..b0632f20 100644 --- a/egui_plot/src/items/text.rs +++ b/egui_plot/src/items/text.rs @@ -10,12 +10,12 @@ use egui::epaint::TextShape; use emath::Align2; use crate::Id; -use crate::PlotBounds; -use crate::PlotGeometry; use crate::PlotItem; use crate::PlotItemBase; -use crate::PlotPoint; use crate::PlotTransform; +use crate::bounds::PlotBounds; +use crate::values::PlotGeometry; +use crate::values::PlotPoint; impl Text { pub fn new(name: impl Into, position: PlotPoint, text: impl Into) -> Self { diff --git a/egui_plot/src/label.rs b/egui_plot/src/label.rs new file mode 100644 index 00000000..f65cce94 --- /dev/null +++ b/egui_plot/src/label.rs @@ -0,0 +1,22 @@ +use emath::NumExt as _; + +use crate::PlotPoint; + +/// 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) + } +} + +type LabelFormatterFn<'a> = dyn Fn(&str, &PlotPoint) -> String + 'a; + +/// Optional label formatter function for customizing hover labels. +pub type LabelFormatter<'a> = Option>>; diff --git a/egui_plot/src/lib.rs b/egui_plot/src/lib.rs index a3e1078c..9b728806 100644 --- a/egui_plot/src/lib.rs +++ b/egui_plot/src/lib.rs @@ -9,401 +9,72 @@ //! mod axis; +mod bounds; +mod colors; +mod cursor; +mod grid; mod items; -mod legend; +mod label; +mod math; mod memory; +mod overlays; +mod placement; mod plot; mod plot_ui; +mod rect_elem; mod transform; mod utils; +mod values; -use std::cmp::Ordering; -use std::ops::RangeInclusive; - -use ahash::HashMap; -use egui::Color32; +pub use bounds::PlotBounds; use egui::Id; -use egui::NumExt as _; -use egui::Response; -use egui::Ui; -use egui::Vec2; -use egui::Vec2b; +pub use overlays::legend::ColorConflictHandling; +pub use overlays::legend::Legend; +pub use placement::HPlacement; +pub use placement::Placement; +pub use placement::VPlacement; pub use crate::axis::Axis; pub use crate::axis::AxisHints; -pub use crate::axis::HPlacement; -pub use crate::axis::Placement; -pub use crate::axis::VPlacement; +pub use crate::colors::color_from_strength; +pub use crate::cursor::Cursor; +pub(crate) use crate::cursor::CursorLinkGroups; +pub(crate) use crate::cursor::PlotFrameCursors; +pub use crate::grid::GridInput; +pub use crate::grid::GridMark; +pub use crate::grid::log_grid_spacer; +pub use crate::grid::uniform_grid_spacer; pub use crate::items::Arrows; pub use crate::items::Bar; pub use crate::items::BarChart; pub use crate::items::BoxElem; pub use crate::items::BoxPlot; pub use crate::items::BoxSpread; -pub use crate::items::ClosestElem; pub use crate::items::HLine; pub use crate::items::Heatmap; pub use crate::items::Line; -pub use crate::items::LineStyle; -pub use crate::items::MarkerShape; -pub use crate::items::Orientation; pub use crate::items::PlotConfig; -pub use crate::items::PlotGeometry; pub use crate::items::PlotImage; pub use crate::items::PlotItem; pub use crate::items::PlotItemBase; -pub use crate::items::PlotPoint; -pub use crate::items::PlotPoints; pub use crate::items::Points; pub use crate::items::Polygon; pub use crate::items::Span; pub use crate::items::Text; pub use crate::items::VLine; -pub use crate::legend::ColorConflictHandling; -pub use crate::legend::Corner; -pub use crate::legend::Legend; +pub use crate::label::LabelFormatter; +pub use crate::label::format_number; pub use crate::memory::PlotMemory; +pub use crate::overlays::CoordinatesFormatter; +pub use crate::placement::Corner; pub use crate::plot::Plot; +pub use crate::plot::PlotResponse; pub use crate::plot_ui::PlotUi; -pub use crate::transform::PlotBounds; pub use crate::transform::PlotTransform; - -type LabelFormatterFn<'a> = dyn Fn(&str, &PlotPoint) -> String + 'a; - -/// Optional label formatter function for customizing hover labels. -pub type LabelFormatter<'a> = Option>>; - -type GridSpacerFn<'a> = dyn Fn(GridInput) -> Vec + 'a; -type GridSpacer<'a> = Box>; - -type CoordinatesFormatterFn<'a> = dyn Fn(&PlotPoint, &PlotBounds) -> String + 'a; - -/// Specifies the coordinates formatting when passed to -/// [`Plot::coordinates_formatter`]. -pub struct CoordinatesFormatter<'a> { - function: Box>, -} - -impl<'a> CoordinatesFormatter<'a> { - /// Create a new formatter based on the pointer coordinate and the plot - /// bounds. - pub fn new(function: impl Fn(&PlotPoint, &PlotBounds) -> String + 'a) -> 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 cursor line at the given y-coordinate. - Horizontal { - /// Y-coordinate of the horizontal cursor line. - y: f64, - }, - - /// Vertical cursor line at the given x-coordinate. - Vertical { - /// X-coordinate of the vertical cursor line. - 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, - auto_bounds: Vec2b, -} - -#[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, - - /// The id of a currently hovered item if any. - /// - /// This is `None` if either no item was hovered. - /// A plot item can be hovered either by hovering its representation in the - /// plot (line, marker, etc.) or by hovering the item in the legend. - pub hovered_plot_item: Option, -} - -// ---------------------------------------------------------------------------- - -/// 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 { - SetX(RangeInclusive), - SetY(RangeInclusive), - 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, PartialEq)] -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<'static> { - let log_base = log_base as f64; - let step_sizes = move |input: GridInput| -> Vec { - // 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<'a>(spacer: impl Fn(GridInput) -> [f64; 3] + 'a) -> GridSpacer<'a> { - 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) -} - -// ---------------------------------------------------------------------------- - -/// 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, "Bad input"); // 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); - - // Remove duplicates: - // This can happen because we have overlapping steps, e.g.: - // step_size[0] = 10 => [-10, 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, - // 110, 120] step_size[1] = 100 => [ 0, - // 100 ] step_size[2] = 1000 => [ 0 - // ] - - steps.sort_by(|a, b| cmp_f64(a.value, b.value)); - - let min_step = step_sizes.iter().fold(f64::INFINITY, |a, &b| a.min(b)); - let eps = 0.1 * min_step; // avoid putting two ticks too closely together - - let mut deduplicated: Vec = Vec::with_capacity(steps.len()); - for step in steps { - if let Some(last) = deduplicated.last_mut() { - if (last.value - step.value).abs() < eps { - // Keep the one with the largest step size - if last.step_size < step.step_size { - *last = step; - } - continue; - } - } - deduplicated.push(step); - } - - deduplicated -} - -#[test] -fn test_generate_marks() { - fn approx_eq(a: &GridMark, b: &GridMark) -> bool { - (a.value - b.value).abs() < 1e-10 && a.step_size == b.step_size - } - - let gm = |value, step_size| GridMark { value, step_size }; - - let marks = generate_marks([0.01, 0.1, 1.0], (2.855, 3.015)); - let expected = vec![ - gm(2.86, 0.01), - gm(2.87, 0.01), - gm(2.88, 0.01), - gm(2.89, 0.01), - gm(2.90, 0.1), - gm(2.91, 0.01), - gm(2.92, 0.01), - gm(2.93, 0.01), - gm(2.94, 0.01), - gm(2.95, 0.01), - gm(2.96, 0.01), - gm(2.97, 0.01), - gm(2.98, 0.01), - gm(2.99, 0.01), - gm(3.00, 1.), - gm(3.01, 0.01), - ]; - - let mut problem = if marks.len() == expected.len() { - None - } else { - Some(format!( - "Different lengths: got {}, expected {}", - marks.len(), - expected.len() - )) - }; - - for (i, (a, b)) in marks.iter().zip(&expected).enumerate() { - if !approx_eq(a, b) { - problem = Some(format!("Mismatch at index {i}: {a:?} != {b:?}")); - break; - } - } - - if let Some(problem) = problem { - panic!("Test failed: {problem}. Got: {marks:#?}, expected: {expected:#?}"); - } -} - -fn cmp_f64(a: f64, b: f64) -> Ordering { - match a.partial_cmp(&b) { - Some(ord) => ord, - None => a.is_nan().cmp(&b.is_nan()), - } -} - -/// 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)) { - debug_assert!(min <= max, "Bad plot bounds: min: {min}, max: {max}"); - 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()) -} +pub use crate::values::ClosestElem; +pub use crate::values::LineStyle; +pub use crate::values::MarkerShape; +pub use crate::values::Orientation; +pub use crate::values::PlotGeometry; +pub use crate::values::PlotPoint; +pub use crate::values::PlotPoints; diff --git a/egui_plot/src/math.rs b/egui_plot/src/math.rs new file mode 100644 index 00000000..cdfdd9ef --- /dev/null +++ b/egui_plot/src/math.rs @@ -0,0 +1,33 @@ +use emath::Float as _; +use emath::Pos2; + +use crate::ClosestElem; +use crate::PlotTransform; +use crate::rect_elem::RectElement; + +/// Returns the x-coordinate of a possible intersection between a line segment +/// from `p1` to `p2` and a horizontal line at the given y-coordinate. +pub fn y_intersection(p1: &Pos2, p2: &Pos2, y: f32) -> Option { + ((p1.y > y && p2.y < y) || (p1.y < y && p2.y > y)) + .then_some(((y * (p1.x - p2.x)) - (p1.x * p2.y - p1.y * p2.x)) / (p1.y - p2.y)) +} + +pub fn find_closest_rect<'a, T>( + rects: impl IntoIterator, + point: Pos2, + transform: &PlotTransform, +) -> Option +where + T: 'a + RectElement, +{ + rects + .into_iter() + .enumerate() + .map(|(index, bar)| { + let bar_rect = transform.rect_from_values(&bar.bounds_min(), &bar.bounds_max()); + let dist_sq = bar_rect.distance_sq_to_pos(point); + + ClosestElem { index, dist_sq } + }) + .min_by_key(|e| e.dist_sq.ord()) +} diff --git a/egui_plot/src/memory.rs b/egui_plot/src/memory.rs index dec5cc03..f469656a 100644 --- a/egui_plot/src/memory.rs +++ b/egui_plot/src/memory.rs @@ -5,8 +5,8 @@ use egui::Id; use egui::Pos2; use egui::Vec2b; -use crate::PlotBounds; use crate::PlotTransform; +use crate::bounds::PlotBounds; /// Information about the plot that has to persist between frames. #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] diff --git a/egui_plot/src/overlays/coordinates.rs b/egui_plot/src/overlays/coordinates.rs new file mode 100644 index 00000000..4b687a38 --- /dev/null +++ b/egui_plot/src/overlays/coordinates.rs @@ -0,0 +1,37 @@ +use crate::bounds::PlotBounds; +use crate::values::PlotPoint; + +type CoordinatesFormatterFn<'a> = dyn Fn(&PlotPoint, &PlotBounds) -> String + 'a; + +/// Specifies the coordinates formatting when passed to +/// [`crate::Plot::coordinates_formatter`]. +pub struct CoordinatesFormatter<'a> { + function: Box>, +} + +impl<'a> CoordinatesFormatter<'a> { + /// Create a new formatter based on the pointer coordinate and the plot + /// bounds. + pub fn new(function: impl Fn(&PlotPoint, &PlotBounds) -> String + 'a) -> 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)), + } + } + + pub(crate) fn format(&self, value: &PlotPoint, bounds: &PlotBounds) -> String { + (self.function)(value, bounds) + } +} + +impl Default for CoordinatesFormatter<'_> { + fn default() -> Self { + Self::with_decimals(3) + } +} diff --git a/egui_plot/src/legend.rs b/egui_plot/src/overlays/legend.rs similarity index 96% rename from egui_plot/src/legend.rs rename to egui_plot/src/overlays/legend.rs index eea606df..b512d946 100644 --- a/egui_plot/src/legend.rs +++ b/egui_plot/src/overlays/legend.rs @@ -22,25 +22,8 @@ use egui::epaint::CircleShape; use egui::pos2; use egui::vec2; -use super::items::PlotItem; - -/// Where to place the plot legend. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub enum Corner { - LeftTop, - RightTop, - LeftBottom, - RightBottom, -} - -impl Corner { - pub fn all() -> impl Iterator { - [Self::LeftTop, Self::RightTop, Self::LeftBottom, Self::RightBottom] - .iter() - .copied() - } -} +use crate::PlotItem; +use crate::placement::Corner; /// How to handle multiple conflicting color for a legend item. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -227,7 +210,7 @@ impl LegendEntry { } #[derive(Clone)] -pub(super) struct LegendWidget { +pub(crate) struct LegendWidget { rect: Rect, entries: Vec, config: Legend, @@ -236,7 +219,7 @@ pub(super) struct LegendWidget { impl LegendWidget { /// Create a new legend from items, the names of items that are hidden and /// the style of the text. Returns `None` if the legend has no entries. - pub(super) fn try_new<'a>( + pub(crate) fn try_new<'a>( rect: Rect, config: Legend, items: &[Box], diff --git a/egui_plot/src/overlays/mod.rs b/egui_plot/src/overlays/mod.rs new file mode 100644 index 00000000..1af8664d --- /dev/null +++ b/egui_plot/src/overlays/mod.rs @@ -0,0 +1,7 @@ +//! Contains widgets that can be added to a plot at some fixed screen +//! coordinates. + +mod coordinates; +pub mod legend; + +pub use coordinates::CoordinatesFormatter; diff --git a/egui_plot/src/placement.rs b/egui_plot/src/placement.rs new file mode 100644 index 00000000..be7782ae --- /dev/null +++ b/egui_plot/src/placement.rs @@ -0,0 +1,81 @@ +/// Placement of the horizontal X-Axis. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum VPlacement { + Top, + Bottom, +} + +/// Placement of the vertical Y-Axis. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HPlacement { + Left, + Right, +} + +/// Placement of an axis. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Placement { + /// Bottom for X-axis, or left for Y-axis. + LeftBottom, + + /// Top for x-axis and right for y-axis. + RightTop, +} + +impl From for Placement { + #[inline] + fn from(placement: HPlacement) -> Self { + match placement { + HPlacement::Left => Self::LeftBottom, + HPlacement::Right => Self::RightTop, + } + } +} + +impl From for HPlacement { + #[inline] + fn from(placement: Placement) -> Self { + match placement { + Placement::LeftBottom => Self::Left, + Placement::RightTop => Self::Right, + } + } +} + +impl From for Placement { + #[inline] + fn from(placement: VPlacement) -> Self { + match placement { + VPlacement::Top => Self::RightTop, + VPlacement::Bottom => Self::LeftBottom, + } + } +} + +impl From for VPlacement { + #[inline] + fn from(placement: Placement) -> Self { + match placement { + Placement::LeftBottom => Self::Bottom, + Placement::RightTop => Self::Top, + } + } +} + +/// Where to place the plot legend. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub enum Corner { + LeftTop, + RightTop, + LeftBottom, + RightBottom, +} + +impl Corner { + pub fn all() -> impl Iterator { + [Self::LeftTop, Self::RightTop, Self::LeftBottom, Self::RightBottom] + .iter() + .copied() + } +} diff --git a/egui_plot/src/plot.rs b/egui_plot/src/plot.rs index 664dc9ff..7a88ea00 100644 --- a/egui_plot/src/plot.rs +++ b/egui_plot/src/plot.rs @@ -27,34 +27,33 @@ use emath::vec2; use crate::Axis; use crate::AxisHints; -use crate::BoundsLinkGroups; -use crate::BoundsModification; -use crate::CoordinatesFormatter; -use crate::Corner; use crate::Cursor; use crate::CursorLinkGroups; -use crate::GridInput; -use crate::GridMark; -use crate::GridSpacer; -use crate::HPlacement; -use crate::LabelFormatter; use crate::Legend; -use crate::LinkedBounds; -use crate::PlotBounds; use crate::PlotFrameCursors; use crate::PlotItem; use crate::PlotMemory; -use crate::PlotPoint; -use crate::PlotResponse; use crate::PlotTransform; use crate::PlotUi; -use crate::VPlacement; use crate::axis::AxisWidget; +use crate::bounds::BoundsLinkGroups; +use crate::bounds::BoundsModification; +use crate::bounds::LinkedBounds; +use crate::bounds::PlotBounds; +use crate::colors::rulers_color; +use crate::grid::GridInput; +use crate::grid::GridMark; +use crate::grid::GridSpacer; use crate::items; use crate::items::horizontal_line; -use crate::items::rulers_color; use crate::items::vertical_line; -use crate::legend::LegendWidget; +use crate::label::LabelFormatter; +use crate::overlays::CoordinatesFormatter; +use crate::overlays::legend::LegendWidget; +use crate::placement::Corner; +use crate::placement::HPlacement; +use crate::placement::VPlacement; +use crate::values::PlotPoint; /// Combined axis widgets: `[x_axis_widgets, y_axis_widgets]` type AxisWidgets<'a> = [Vec>; 2]; @@ -177,7 +176,7 @@ impl<'a> Plot<'a> { show_grid: true.into(), grid_spacing: Rangef::new(8.0, 300.0), - grid_spacers: [crate::log_grid_spacer(10), crate::log_grid_spacer(10)], + grid_spacers: [crate::grid::log_grid_spacer(10), crate::grid::log_grid_spacer(10)], clamp_grid: false, sense: egui::Sense::click_and_drag(), @@ -466,8 +465,8 @@ impl<'a> Plot<'a> { /// # () /// ``` /// - /// There are helpers for common cases, see [`crate::log_grid_spacer`] and - /// [`crate::uniform_grid_spacer`]. + /// There are helpers for common cases, see [`crate::grid::log_grid_spacer`] + /// and [`crate::grid::uniform_grid_spacer`]. #[inline] pub fn x_grid_spacer(mut self, spacer: impl Fn(GridInput) -> Vec + 'a) -> Self { self.grid_spacers[0] = Box::new(spacer); @@ -1479,7 +1478,7 @@ impl<'a> Plot<'a> { let line_strength = remap_clamp(spacing_in_points, self.grid_spacing, 0.0..=1.0); - let line_color = crate::color_from_strength(ui, line_strength); + let line_color = crate::colors::color_from_strength(ui, line_strength); let mut p0 = pos_in_gui; let mut p1 = pos_in_gui; @@ -1817,3 +1816,22 @@ fn axis_widgets<'a>( ([x_axis_widgets, y_axis_widgets], plot_rect) } + +/// 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, + + /// The id of a currently hovered item if any. + /// + /// This is `None` if either no item was hovered. + /// A plot item can be hovered either by hovering its representation in the + /// plot (line, marker, etc.) or by hovering the item in the legend. + pub hovered_plot_item: Option, +} diff --git a/egui_plot/src/plot_ui.rs b/egui_plot/src/plot_ui.rs index c41bcd67..6dcf2dd6 100644 --- a/egui_plot/src/plot_ui.rs +++ b/egui_plot/src/plot_ui.rs @@ -7,14 +7,14 @@ use egui::Vec2; use egui::Vec2b; use egui::epaint::Hsva; -use crate::BoundsModification; #[expect(unused_imports)] // for links in docstrings use crate::Plot; -use crate::PlotBounds; use crate::PlotItem; -use crate::PlotPoint; use crate::PlotTransform; use crate::Span; +use crate::bounds::BoundsModification; +use crate::bounds::PlotBounds; +use crate::values::PlotPoint; /// Provides methods to interact with a plot while building it. It is the single /// argument of the closure provided to [`Plot::show`]. See [`Plot`] for an diff --git a/egui_plot/src/items/rect_elem.rs b/egui_plot/src/rect_elem.rs similarity index 75% rename from egui_plot/src/items/rect_elem.rs rename to egui_plot/src/rect_elem.rs index 99afcd68..61d18ce5 100644 --- a/egui_plot/src/items/rect_elem.rs +++ b/egui_plot/src/rect_elem.rs @@ -1,11 +1,6 @@ -use egui::emath::NumExt as _; -use egui::epaint::Color32; -use egui::epaint::Rgba; -use egui::epaint::Stroke; - use super::Orientation; use super::PlotPoint; -use crate::transform::PlotBounds; +use crate::bounds::PlotBounds; use crate::transform::PlotTransform; /// Trait that abstracts from rectangular 'Value'-like elements, such as bars or @@ -60,19 +55,3 @@ pub(super) trait RectElement { // ---------------------------------------------------------------------------- // Helper functions - -pub(super) fn highlighted_color(mut stroke: Stroke, fill: Color32) -> (Stroke, Color32) { - stroke.width *= 2.0; - - let mut fill = Rgba::from(fill); - if fill.is_additive() { - // Make slightly brighter - fill = 1.3 * fill; - } else { - // Make more opaque: - let fill_alpha = (2.0 * fill.a()).at_most(1.0); - fill = fill.to_opaque().multiply(fill_alpha); - } - - (stroke, fill.into()) -} diff --git a/egui_plot/src/transform.rs b/egui_plot/src/transform.rs index 0025074b..a7c1b542 100644 --- a/egui_plot/src/transform.rs +++ b/egui_plot/src/transform.rs @@ -1,5 +1,3 @@ -use std::ops::RangeInclusive; - use egui::Pos2; use egui::Rect; use egui::Vec2; @@ -9,261 +7,7 @@ use egui::remap; use super::PlotPoint; use crate::Axis; - -/// 2D bounding box of f64 precision. -/// -/// The range of data values we show. -#[derive(Clone, Copy, PartialEq, Debug)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct PlotBounds { - pub(crate) min: [f64; 2], - pub(crate) max: [f64; 2], -} - -impl PlotBounds { - pub const NOTHING: Self = Self { - min: [f64::INFINITY; 2], - max: [-f64::INFINITY; 2], - }; - - #[inline] - pub fn from_min_max(min: [f64; 2], max: [f64; 2]) -> Self { - Self { min, max } - } - - #[inline] - pub fn min(&self) -> [f64; 2] { - self.min - } - - #[inline] - pub fn max(&self) -> [f64; 2] { - self.max - } - - #[inline] - pub fn new_symmetrical(half_extent: f64) -> Self { - Self { - min: [-half_extent; 2], - max: [half_extent; 2], - } - } - - #[inline] - pub fn is_finite(&self) -> bool { - self.min[0].is_finite() && self.min[1].is_finite() && self.max[0].is_finite() && self.max[1].is_finite() - } - - #[inline] - pub fn is_finite_x(&self) -> bool { - self.min[0].is_finite() && self.max[0].is_finite() - } - - #[inline] - pub fn is_finite_y(&self) -> bool { - self.min[1].is_finite() && self.max[1].is_finite() - } - - #[inline] - pub fn is_valid(&self) -> bool { - self.is_finite() && self.width() > 0.0 && self.height() > 0.0 - } - - #[inline] - pub fn is_valid_x(&self) -> bool { - self.is_finite_x() && self.width() > 0.0 - } - - #[inline] - pub fn is_valid_y(&self) -> bool { - self.is_finite_y() && self.height() > 0.0 - } - - #[inline] - pub fn width(&self) -> f64 { - self.max[0] - self.min[0] - } - - #[inline] - pub fn height(&self) -> f64 { - self.max[1] - self.min[1] - } - - #[inline] - pub fn center(&self) -> PlotPoint { - [ - emath::fast_midpoint(self.min[0], self.max[0]), - emath::fast_midpoint(self.min[1], self.max[1]), - ] - .into() - } - - /// Expand to include the given (x,y) value - #[inline] - pub fn extend_with(&mut self, value: &PlotPoint) { - self.extend_with_x(value.x); - self.extend_with_y(value.y); - } - - /// Expand to include the given x coordinate - #[inline] - pub fn extend_with_x(&mut self, x: f64) { - self.min[0] = self.min[0].min(x); - self.max[0] = self.max[0].max(x); - } - - /// Expand to include the given y coordinate - #[inline] - pub fn extend_with_y(&mut self, y: f64) { - self.min[1] = self.min[1].min(y); - self.max[1] = self.max[1].max(y); - } - - #[inline] - fn clamp_to_finite(&mut self) { - for d in 0..2 { - self.min[d] = self.min[d].clamp(f64::MIN, f64::MAX); - if self.min[d].is_nan() { - self.min[d] = 0.0; - } - - self.max[d] = self.max[d].clamp(f64::MIN, f64::MAX); - if self.max[d].is_nan() { - self.max[d] = 0.0; - } - } - } - - #[inline] - pub fn expand_x(&mut self, pad: f64) { - if pad.is_finite() { - self.min[0] -= pad; - self.max[0] += pad; - self.clamp_to_finite(); - } - } - - #[inline] - pub fn expand_y(&mut self, pad: f64) { - if pad.is_finite() { - self.min[1] -= pad; - self.max[1] += pad; - self.clamp_to_finite(); - } - } - - #[inline] - pub fn merge_x(&mut self, other: &Self) { - self.min[0] = self.min[0].min(other.min[0]); - self.max[0] = self.max[0].max(other.max[0]); - } - - #[inline] - pub fn merge_y(&mut self, other: &Self) { - self.min[1] = self.min[1].min(other.min[1]); - self.max[1] = self.max[1].max(other.max[1]); - } - - #[inline] - pub fn set_x(&mut self, other: &Self) { - self.min[0] = other.min[0]; - self.max[0] = other.max[0]; - } - - #[inline] - pub fn set_x_center_width(&mut self, x: f64, width: f64) { - self.min[0] = x - width / 2.0; - self.max[0] = x + width / 2.0; - } - - #[inline] - pub fn set_y(&mut self, other: &Self) { - self.min[1] = other.min[1]; - self.max[1] = other.max[1]; - } - - #[inline] - pub fn set_y_center_height(&mut self, y: f64, height: f64) { - self.min[1] = y - height / 2.0; - self.max[1] = y + height / 2.0; - } - - #[inline] - pub fn merge(&mut self, other: &Self) { - self.min[0] = self.min[0].min(other.min[0]); - self.min[1] = self.min[1].min(other.min[1]); - self.max[0] = self.max[0].max(other.max[0]); - self.max[1] = self.max[1].max(other.max[1]); - } - - #[inline] - pub fn translate_x(&mut self, delta: f64) { - if delta.is_finite() { - self.min[0] += delta; - self.max[0] += delta; - self.clamp_to_finite(); - } - } - - #[inline] - pub fn translate_y(&mut self, delta: f64) { - if delta.is_finite() { - self.min[1] += delta; - self.max[1] += delta; - self.clamp_to_finite(); - } - } - - #[inline] - pub fn translate(&mut self, delta: (f64, f64)) { - self.translate_x(delta.0); - self.translate_y(delta.1); - } - - #[inline] - pub fn zoom(&mut self, zoom_factor: Vec2, center: PlotPoint) { - self.min[0] = center.x + (self.min[0] - center.x) / (zoom_factor.x as f64); - self.max[0] = center.x + (self.max[0] - center.x) / (zoom_factor.x as f64); - self.min[1] = center.y + (self.min[1] - center.y) / (zoom_factor.y as f64); - self.max[1] = center.y + (self.max[1] - center.y) / (zoom_factor.y as f64); - } - - #[inline] - pub fn add_relative_margin_x(&mut self, margin_fraction: Vec2) { - let width = self.width().max(0.0); - self.expand_x(margin_fraction.x as f64 * width); - } - - #[inline] - pub fn add_relative_margin_y(&mut self, margin_fraction: Vec2) { - let height = self.height().max(0.0); - self.expand_y(margin_fraction.y as f64 * height); - } - - #[inline] - pub fn range_x(&self) -> RangeInclusive { - self.min[0]..=self.max[0] - } - - #[inline] - pub fn range_y(&self) -> RangeInclusive { - self.min[1]..=self.max[1] - } - - #[inline] - pub fn make_x_symmetrical(&mut self) { - let x_abs = self.min[0].abs().max(self.max[0].abs()); - self.min[0] = -x_abs; - self.max[0] = x_abs; - } - - #[inline] - pub fn make_y_symmetrical(&mut self) { - let y_abs = self.min[1].abs().max(self.max[1].abs()); - self.min[1] = -y_abs; - self.max[1] = y_abs; - } -} +use crate::bounds::PlotBounds; /// Contains the screen rectangle and the plot bounds and provides methods to /// transform between them. diff --git a/egui_plot/src/items/values.rs b/egui_plot/src/values.rs similarity index 99% rename from egui_plot/src/items/values.rs rename to egui_plot/src/values.rs index 06a3f809..c08823b7 100644 --- a/egui_plot/src/items/values.rs +++ b/egui_plot/src/values.rs @@ -12,7 +12,7 @@ use egui::epaint::PathStroke; use egui::lerp; use egui::pos2; -use crate::transform::PlotBounds; +use crate::bounds::PlotBounds; /// A point coordinate in the plot. ///