diff --git a/Makefile b/Makefile index 57385c1b..4780bc7a 100644 --- a/Makefile +++ b/Makefile @@ -152,7 +152,7 @@ check_license: # Checks for dependency cycles between modules. check_cycles: - cargo-cycles + cargo-cycles --check-public-api --check-absolute-paths check_linter: python3 ./scripts/lint.py diff --git a/egui_plot/src/axis.rs b/egui_plot/src/axis.rs index f7f7dbc6..c0d87cf4 100644 --- a/egui_plot/src/axis.rs +++ b/egui_plot/src/axis.rs @@ -15,8 +15,12 @@ use egui::WidgetText; use egui::emath::Rot2; use egui::emath::remap_clamp; use egui::epaint::TextShape; +use emath::Vec2b; +use emath::pos2; +use emath::remap; -use super::transform::PlotTransform; +use crate::bounds::PlotBounds; +use crate::bounds::PlotPoint; use crate::colors; use crate::grid::GridMark; use crate::placement::HPlacement; @@ -329,3 +333,277 @@ impl<'a> AxisWidget<'a> { thickness } } + +/// Contains the screen rectangle and the plot bounds and provides methods to +/// transform between them. +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[derive(Clone, Copy, Debug)] +pub struct PlotTransform { + /// The screen rectangle. + frame: Rect, + + /// The plot bounds. + bounds: PlotBounds, + + /// Whether to always center the x-range or y-range of the bounds. + centered: Vec2b, + + /// Whether to always invert the x and/or y axis + inverted_axis: Vec2b, +} + +impl PlotTransform { + pub fn new(frame: Rect, bounds: PlotBounds, center_axis: impl Into) -> Self { + debug_assert!( + 0.0 <= frame.width() && 0.0 <= frame.height(), + "Bad plot frame: {frame:?}" + ); + let center_axis = center_axis.into(); + + // Since the current Y bounds an affect the final X bounds and vice versa, we + // need to keep the original version of the `bounds` before we start + // modifying it. + let mut new_bounds = bounds; + + // Sanitize bounds. + // + // When a given bound axis is "thin" (e.g. width or height is 0) but finite, we + // center the bounds around that value. If the other axis is "fat", we + // reuse its extent for the thin axis, and default to +/- 1.0 otherwise. + if !bounds.is_finite_x() { + new_bounds.set_x(&PlotBounds::new_symmetrical(1.0)); + } else if bounds.width() <= 0.0 { + new_bounds.set_x_center_width( + bounds.center().x, + if bounds.is_valid_y() { bounds.height() } else { 1.0 }, + ); + } + + if !bounds.is_finite_y() { + new_bounds.set_y(&PlotBounds::new_symmetrical(1.0)); + } else if bounds.height() <= 0.0 { + new_bounds.set_y_center_height( + bounds.center().y, + if bounds.is_valid_x() { bounds.width() } else { 1.0 }, + ); + } + + // Scale axes so that the origin is in the center. + if center_axis.x { + new_bounds.make_x_symmetrical(); + } + if center_axis.y { + new_bounds.make_y_symmetrical(); + } + + debug_assert!(new_bounds.is_valid(), "Bad final plot bounds: {new_bounds:?}"); + + Self { + frame, + bounds: new_bounds, + centered: center_axis, + inverted_axis: Vec2b::new(false, false), + } + } + + pub fn new_with_invert_axis( + frame: Rect, + bounds: PlotBounds, + center_axis: impl Into, + invert_axis: impl Into, + ) -> Self { + let mut new = Self::new(frame, bounds, center_axis); + new.inverted_axis = invert_axis.into(); + new + } + + /// ui-space rectangle. + #[inline] + pub fn frame(&self) -> &Rect { + &self.frame + } + + /// Plot-space bounds. + #[inline] + pub fn bounds(&self) -> &PlotBounds { + &self.bounds + } + + #[inline] + pub fn set_bounds(&mut self, bounds: PlotBounds) { + self.bounds = bounds; + } + + pub fn translate_bounds(&mut self, mut delta_pos: (f64, f64)) { + if self.centered.x { + delta_pos.0 = 0.; + } + if self.centered.y { + delta_pos.1 = 0.; + } + delta_pos.0 *= self.dvalue_dpos()[0]; + delta_pos.1 *= self.dvalue_dpos()[1]; + self.bounds.translate((delta_pos.0, delta_pos.1)); + } + + /// Zoom by a relative factor with the given screen position as center. + pub fn zoom(&mut self, zoom_factor: Vec2, center: Pos2) { + let center = self.value_from_position(center); + + let mut new_bounds = self.bounds; + new_bounds.zoom(zoom_factor, center); + + if new_bounds.is_valid() { + self.bounds = new_bounds; + } + } + + pub fn position_from_point_x(&self, value: f64) -> f32 { + remap( + value, + self.bounds.min[0]..=self.bounds.max[0], + if self.inverted_axis[0] { + (self.frame.right() as f64)..=(self.frame.left() as f64) + } else { + (self.frame.left() as f64)..=(self.frame.right() as f64) + }, + ) as f32 + } + + pub fn position_from_point_y(&self, value: f64) -> f32 { + remap( + value, + self.bounds.min[1]..=self.bounds.max[1], + // negated y axis by default + if self.inverted_axis[1] { + (self.frame.top() as f64)..=(self.frame.bottom() as f64) + } else { + (self.frame.bottom() as f64)..=(self.frame.top() as f64) + }, + ) as f32 + } + + /// Screen/ui position from point on plot. + pub fn position_from_point(&self, value: &PlotPoint) -> Pos2 { + pos2(self.position_from_point_x(value.x), self.position_from_point_y(value.y)) + } + + /// Plot point from screen/ui position. + pub fn value_from_position(&self, pos: Pos2) -> PlotPoint { + let x = remap( + pos.x as f64, + if self.inverted_axis[0] { + (self.frame.right() as f64)..=(self.frame.left() as f64) + } else { + (self.frame.left() as f64)..=(self.frame.right() as f64) + }, + self.bounds.range_x(), + ); + let y = remap( + pos.y as f64, + // negated y axis by default + if self.inverted_axis[1] { + (self.frame.top() as f64)..=(self.frame.bottom() as f64) + } else { + (self.frame.bottom() as f64)..=(self.frame.top() as f64) + }, + self.bounds.range_y(), + ); + + PlotPoint::new(x, y) + } + + /// Transform a rectangle of plot values to a screen-coordinate rectangle. + /// + /// This typically means that the rect is mirrored vertically (top becomes + /// bottom and vice versa), since the plot's coordinate system has +Y + /// up, while egui has +Y down. + pub fn rect_from_values(&self, value1: &PlotPoint, value2: &PlotPoint) -> Rect { + let pos1 = self.position_from_point(value1); + let pos2 = self.position_from_point(value2); + + let mut rect = Rect::NOTHING; + rect.extend_with(pos1); + rect.extend_with(pos2); + rect + } + + /// delta position / delta value = how many ui points per step in the X axis + /// in "plot space" + pub fn dpos_dvalue_x(&self) -> f64 { + let flip = if self.inverted_axis[0] { -1.0 } else { 1.0 }; + flip * (self.frame.width() as f64) / self.bounds.width() + } + + /// delta position / delta value = how many ui points per step in the Y axis + /// in "plot space" + pub fn dpos_dvalue_y(&self) -> f64 { + let flip = if self.inverted_axis[1] { 1.0 } else { -1.0 }; + flip * (self.frame.height() as f64) / self.bounds.height() + } + + /// delta position / delta value = how many ui points per step in "plot + /// space" + pub fn dpos_dvalue(&self) -> [f64; 2] { + [self.dpos_dvalue_x(), self.dpos_dvalue_y()] + } + + /// delta value / delta position = how much ground do we cover in "plot + /// space" per ui point? + pub fn dvalue_dpos(&self) -> [f64; 2] { + [1.0 / self.dpos_dvalue_x(), 1.0 / self.dpos_dvalue_y()] + } + + /// scale.x/scale.y ratio. + /// + /// If 1.0, it means the scale factor is the same in both axes. + fn aspect(&self) -> f64 { + let rw = self.frame.width() as f64; + let rh = self.frame.height() as f64; + (self.bounds.width() / rw) / (self.bounds.height() / rh) + } + + /// Sets the aspect ratio by expanding the x- or y-axis. + /// + /// This never contracts, so we don't miss out on any data. + pub(crate) fn set_aspect_by_expanding(&mut self, aspect: f64) { + let current_aspect = self.aspect(); + + let epsilon = 1e-5; + if (current_aspect - aspect).abs() < epsilon { + // Don't make any changes when the aspect is already almost correct. + return; + } + + if current_aspect < aspect { + self.bounds + .expand_x((aspect / current_aspect - 1.0) * self.bounds.width() * 0.5); + } else { + self.bounds + .expand_y((current_aspect / aspect - 1.0) * self.bounds.height() * 0.5); + } + } + + /// Sets the aspect ratio by changing either the X or Y axis (callers + /// choice). + pub(crate) fn set_aspect_by_changing_axis(&mut self, aspect: f64, axis: Axis) { + let current_aspect = self.aspect(); + + let epsilon = 1e-5; + if (current_aspect - aspect).abs() < epsilon { + // Don't make any changes when the aspect is already almost correct. + return; + } + + match axis { + Axis::X => { + self.bounds + .expand_x((aspect / current_aspect - 1.0) * self.bounds.width() * 0.5); + } + Axis::Y => { + self.bounds + .expand_y((current_aspect / aspect - 1.0) * self.bounds.height() * 0.5); + } + } + } +} diff --git a/egui_plot/src/bounds.rs b/egui_plot/src/bounds.rs index eb1af275..9bb09b90 100644 --- a/egui_plot/src/bounds.rs +++ b/egui_plot/src/bounds.rs @@ -2,10 +2,50 @@ use std::ops::RangeInclusive; use ahash::HashMap; use egui::Id; +use emath::Pos2; use emath::Vec2; use emath::Vec2b; -use crate::PlotPoint; +/// A point coordinate in the plot. +/// +/// Uses f64 for improved accuracy to enable plotting +/// large values (e.g. unix time on x axis). +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct PlotPoint { + /// This is often something monotonically increasing, such as time, but + /// doesn't have to be. Goes from left to right. + pub x: f64, + + /// Goes from bottom to top (inverse of everything else in egui!). + pub y: f64, +} + +impl From<[f64; 2]> for PlotPoint { + #[inline] + fn from([x, y]: [f64; 2]) -> Self { + Self { x, y } + } +} + +impl PlotPoint { + #[inline(always)] + pub fn new(x: impl Into, y: impl Into) -> Self { + Self { + x: x.into(), + y: y.into(), + } + } + + #[inline(always)] + pub fn to_pos2(self) -> Pos2 { + Pos2::new(self.x as f32, self.y as f32) + } + + #[inline(always)] + pub fn to_vec2(self) -> Vec2 { + Vec2::new(self.x as f32, self.y as f32) + } +} /// 2D bounding box of f64 precision. /// diff --git a/egui_plot/src/data.rs b/egui_plot/src/data.rs index 4aed1f75..97fc7ab9 100644 --- a/egui_plot/src/data.rs +++ b/egui_plot/src/data.rs @@ -3,52 +3,10 @@ use std::iter::FromIterator; use std::ops::RangeBounds; use std::ops::RangeInclusive; -use emath::Pos2; -use emath::Vec2; use emath::lerp; use crate::bounds::PlotBounds; - -/// A point coordinate in the plot. -/// -/// Uses f64 for improved accuracy to enable plotting -/// large values (e.g. unix time on x axis). -#[derive(Clone, Copy, Debug, PartialEq)] -pub struct PlotPoint { - /// This is often something monotonically increasing, such as time, but - /// doesn't have to be. Goes from left to right. - pub x: f64, - - /// Goes from bottom to top (inverse of everything else in egui!). - pub y: f64, -} - -impl From<[f64; 2]> for PlotPoint { - #[inline] - fn from([x, y]: [f64; 2]) -> Self { - Self { x, y } - } -} - -impl PlotPoint { - #[inline(always)] - pub fn new(x: impl Into, y: impl Into) -> Self { - Self { - x: x.into(), - y: y.into(), - } - } - - #[inline(always)] - pub fn to_pos2(self) -> Pos2 { - Pos2::new(self.x as f32, self.y as f32) - } - - #[inline(always)] - pub fn to_vec2(self) -> Vec2 { - Vec2::new(self.x as f32, self.y as f32) - } -} +use crate::bounds::PlotPoint; /// Represents many [`PlotPoint`]s. /// diff --git a/egui_plot/src/items/arrows.rs b/egui_plot/src/items/arrows.rs index ee2315fc..eecaa09b 100644 --- a/egui_plot/src/items/arrows.rs +++ b/egui_plot/src/items/arrows.rs @@ -1,18 +1,18 @@ use std::ops::RangeInclusive; use egui::Color32; +use egui::Id; use egui::Shape; use egui::Stroke; use egui::Ui; use emath::Rot2; -use super::PlotGeometry; -use crate::Id; -use crate::PlotItem; -use crate::PlotItemBase; -use crate::PlotTransform; +use crate::axis::PlotTransform; use crate::bounds::PlotBounds; use crate::data::PlotPoints; +use crate::items::PlotGeometry; +use crate::items::PlotItem; +use crate::items::PlotItemBase; 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 dbdf8363..a5182896 100644 --- a/egui_plot/src/items/bar_chart.rs +++ b/egui_plot/src/items/bar_chart.rs @@ -2,6 +2,7 @@ use std::ops::RangeInclusive; use egui::Color32; use egui::CornerRadius; +use egui::Id; use egui::Shape; use egui::Stroke; use egui::Ui; @@ -10,19 +11,18 @@ use emath::Float as _; use emath::NumExt as _; use emath::Pos2; -use super::ClosestElem; -use super::PlotGeometry; -use super::add_rulers_and_text; -use crate::Cursor; -use crate::Id; -use crate::PlotConfig; -use crate::PlotItem; -use crate::PlotItemBase; -use crate::PlotTransform; use crate::aesthetics::Orientation; +use crate::axis::PlotTransform; use crate::bounds::PlotBounds; +use crate::bounds::PlotPoint; use crate::colors::highlighted_color; -use crate::data::PlotPoint; +use crate::cursor::Cursor; +use crate::items::ClosestElem; +use crate::items::PlotConfig; +use crate::items::PlotGeometry; +use crate::items::PlotItem; +use crate::items::PlotItemBase; +use crate::items::add_rulers_and_text; use crate::label::LabelFormatter; use crate::math::find_closest_rect; use crate::rect_elem::RectElement; diff --git a/egui_plot/src/items/box_plot.rs b/egui_plot/src/items/box_plot.rs index 0d53137b..3ad9cfe2 100644 --- a/egui_plot/src/items/box_plot.rs +++ b/egui_plot/src/items/box_plot.rs @@ -2,6 +2,7 @@ use std::ops::RangeInclusive; use egui::Color32; use egui::CornerRadius; +use egui::Id; use egui::Shape; use egui::Stroke; use egui::Ui; @@ -9,19 +10,18 @@ use egui::epaint::RectShape; use emath::NumExt as _; use emath::Pos2; -use super::ClosestElem; -use super::PlotGeometry; -use super::add_rulers_and_text; -use crate::Cursor; -use crate::Id; -use crate::PlotConfig; -use crate::PlotItem; -use crate::PlotItemBase; -use crate::PlotTransform; use crate::aesthetics::Orientation; +use crate::axis::PlotTransform; use crate::bounds::PlotBounds; +use crate::bounds::PlotPoint; use crate::colors::highlighted_color; -use crate::data::PlotPoint; +use crate::cursor::Cursor; +use crate::items::ClosestElem; +use crate::items::PlotConfig; +use crate::items::PlotGeometry; +use crate::items::PlotItem; +use crate::items::PlotItemBase; +use crate::items::add_rulers_and_text; use crate::label::LabelFormatter; use crate::math::find_closest_rect; use crate::rect_elem::RectElement; diff --git a/egui_plot/src/items/heatmap.rs b/egui_plot/src/items/heatmap.rs index b1c7aaf9..6af0776e 100644 --- a/egui_plot/src/items/heatmap.rs +++ b/egui_plot/src/items/heatmap.rs @@ -13,16 +13,16 @@ use egui::Vec2; use egui::WidgetText; use emath::Float as _; -use super::ClosestElem; -use super::Cursor; -use super::PlotGeometry; -use super::PlotTransform; -use crate::PlotConfig; -use crate::PlotItem; -use crate::PlotItemBase; +use crate::axis::PlotTransform; use crate::bounds::PlotBounds; +use crate::bounds::PlotPoint; use crate::colors::BASE_COLORS; -use crate::data::PlotPoint; +use crate::cursor::Cursor; +use crate::items::ClosestElem; +use crate::items::PlotConfig; +use crate::items::PlotGeometry; +use crate::items::PlotItem; +use crate::items::PlotItemBase; use crate::label::LabelFormatter; /// Default resolution for heatmap color palette diff --git a/egui_plot/src/items/line.rs b/egui_plot/src/items/line.rs index 5eb3e4db..55596aac 100644 --- a/egui_plot/src/items/line.rs +++ b/egui_plot/src/items/line.rs @@ -1,6 +1,7 @@ use std::ops::RangeInclusive; use egui::Color32; +use egui::Id; use egui::Shape; use egui::Stroke; use egui::Ui; @@ -8,14 +9,13 @@ use egui::epaint::PathStroke; use emath::Pos2; use emath::pos2; -use super::PlotGeometry; -use crate::Id; -use crate::PlotItem; -use crate::PlotItemBase; -use crate::PlotTransform; use crate::aesthetics::LineStyle; +use crate::axis::PlotTransform; use crate::bounds::PlotBounds; -use crate::data::PlotPoint; +use crate::bounds::PlotPoint; +use crate::items::PlotGeometry; +use crate::items::PlotItem; +use crate::items::PlotItemBase; /// A horizontal line in a plot, filling the full width #[derive(Clone, Debug, PartialEq)] diff --git a/egui_plot/src/items/mod.rs b/egui_plot/src/items/mod.rs index 512dd9e5..607cd37d 100644 --- a/egui_plot/src/items/mod.rs +++ b/egui_plot/src/items/mod.rs @@ -3,12 +3,6 @@ use std::ops::RangeInclusive; -pub use arrows::Arrows; -pub use bar_chart::Bar; -pub use bar_chart::BarChart; -pub use box_plot::BoxElem; -pub use box_plot::BoxPlot; -pub use box_plot::BoxSpread; use egui::Align2; use egui::Color32; use egui::Id; @@ -20,23 +14,29 @@ use egui::TextStyle; use egui::Ui; 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; -pub use series::Line; -pub use span::Span; -pub use text::Text; - -use super::Cursor; -use super::PlotTransform; + use crate::aesthetics::Orientation; +use crate::axis::PlotTransform; use crate::bounds::PlotBounds; -use crate::data::PlotPoint; +use crate::bounds::PlotPoint; +use crate::cursor::Cursor; +pub use crate::items::arrows::Arrows; +pub use crate::items::bar_chart::Bar; +pub use crate::items::bar_chart::BarChart; +pub use crate::items::box_plot::BoxElem; +pub use crate::items::box_plot::BoxPlot; +pub use crate::items::box_plot::BoxSpread; +pub use crate::items::heatmap::Heatmap; +pub use crate::items::line::HLine; +pub use crate::items::line::VLine; +pub use crate::items::line::horizontal_line; +pub use crate::items::line::vertical_line; +pub use crate::items::plot_image::PlotImage; +pub use crate::items::points::Points; +pub use crate::items::polygon::Polygon; +pub use crate::items::series::Line; +pub use crate::items::span::Span; +pub use crate::items::text::Text; use crate::label::LabelFormatter; use crate::rect_elem::RectElement; diff --git a/egui_plot/src/items/plot_image.rs b/egui_plot/src/items/plot_image.rs index 46939e7e..d189a8c1 100644 --- a/egui_plot/src/items/plot_image.rs +++ b/egui_plot/src/items/plot_image.rs @@ -2,6 +2,7 @@ use std::ops::RangeInclusive; use egui::Color32; use egui::CornerRadius; +use egui::Id; use egui::ImageOptions; use egui::Shape; use egui::Stroke; @@ -12,13 +13,12 @@ use emath::Rot2; use emath::Vec2; use emath::pos2; -use super::PlotGeometry; -use crate::Id; -use crate::PlotItem; -use crate::PlotItemBase; -use crate::PlotTransform; +use crate::axis::PlotTransform; use crate::bounds::PlotBounds; -use crate::data::PlotPoint; +use crate::bounds::PlotPoint; +use crate::items::PlotGeometry; +use crate::items::PlotItem; +use crate::items::PlotItemBase; /// An image in the plot. #[derive(Clone)] diff --git a/egui_plot/src/items/points.rs b/egui_plot/src/items/points.rs index 51417397..d2e63465 100644 --- a/egui_plot/src/items/points.rs +++ b/egui_plot/src/items/points.rs @@ -1,6 +1,7 @@ use std::ops::RangeInclusive; use egui::Color32; +use egui::Id; use egui::Shape; use egui::Stroke; use egui::Ui; @@ -9,15 +10,14 @@ use emath::Pos2; use emath::pos2; use emath::vec2; -use super::PlotGeometry; -use crate::Id; -use crate::PlotItem; -use crate::PlotItemBase; -use crate::PlotTransform; use crate::aesthetics::MarkerShape; +use crate::axis::PlotTransform; use crate::bounds::PlotBounds; -use crate::data::PlotPoint; +use crate::bounds::PlotPoint; use crate::data::PlotPoints; +use crate::items::PlotGeometry; +use crate::items::PlotItem; +use crate::items::PlotItemBase; 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 0fd82aa0..71de8fd3 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::PlotGeometry; -use crate::PlotItem; -use crate::PlotItemBase; -use crate::PlotTransform; use crate::aesthetics::LineStyle; +use crate::axis::PlotTransform; use crate::bounds::PlotBounds; use crate::colors::DEFAULT_FILL_ALPHA; use crate::data::PlotPoints; +use crate::items::PlotGeometry; +use crate::items::PlotItem; +use crate::items::PlotItemBase; /// A convex polygon. pub struct Polygon<'a> { diff --git a/egui_plot/src/items/series.rs b/egui_plot/src/items/series.rs index b347f96d..6f2be18e 100644 --- a/egui_plot/src/items/series.rs +++ b/egui_plot/src/items/series.rs @@ -2,6 +2,7 @@ use std::ops::RangeInclusive; use std::sync::Arc; use egui::Color32; +use egui::Id; use egui::Mesh; use egui::Rgba; use egui::Shape; @@ -13,16 +14,15 @@ use emath::Pos2; use emath::Rect; use emath::pos2; -use super::PlotGeometry; -use crate::Id; -use crate::PlotItem; -use crate::PlotItemBase; -use crate::PlotTransform; use crate::aesthetics::LineStyle; +use crate::axis::PlotTransform; use crate::bounds::PlotBounds; +use crate::bounds::PlotPoint; use crate::colors::DEFAULT_FILL_ALPHA; -use crate::data::PlotPoint; use crate::data::PlotPoints; +use crate::items::PlotGeometry; +use crate::items::PlotItem; +use crate::items::PlotItemBase; use crate::math::y_intersection; /// A series of values forming a path. diff --git a/egui_plot/src/items/span.rs b/egui_plot/src/items/span.rs index 4fd25a2b..1f5559c0 100644 --- a/egui_plot/src/items/span.rs +++ b/egui_plot/src/items/span.rs @@ -15,15 +15,15 @@ use egui::epaint::TextShape; use egui::pos2; use emath::TSTransform; -use super::PlotGeometry; -use super::PlotItem; -use super::PlotItemBase; -use super::PlotPoint; -use super::PlotTransform; -use crate::Axis; use crate::aesthetics::LineStyle; +use crate::axis::Axis; +use crate::axis::PlotTransform; use crate::bounds::PlotBounds; +use crate::bounds::PlotPoint; use crate::colors::highlighted_color; +use crate::items::PlotGeometry; +use crate::items::PlotItem; +use crate::items::PlotItemBase; 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 0ccaf461..546f1c74 100644 --- a/egui_plot/src/items/text.rs +++ b/egui_plot/src/items/text.rs @@ -1,6 +1,7 @@ use std::ops::RangeInclusive; use egui::Color32; +use egui::Id; use egui::Shape; use egui::Stroke; use egui::TextStyle; @@ -9,13 +10,12 @@ use egui::WidgetText; use egui::epaint::TextShape; use emath::Align2; -use super::PlotGeometry; -use crate::Id; -use crate::PlotItem; -use crate::PlotItemBase; -use crate::PlotTransform; +use crate::axis::PlotTransform; use crate::bounds::PlotBounds; -use crate::data::PlotPoint; +use crate::bounds::PlotPoint; +use crate::items::PlotGeometry; +use crate::items::PlotItem; +use crate::items::PlotItemBase; 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 index f65cce94..f84a1ff9 100644 --- a/egui_plot/src/label.rs +++ b/egui_plot/src/label.rs @@ -1,6 +1,6 @@ use emath::NumExt as _; -use crate::PlotPoint; +use crate::bounds::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. diff --git a/egui_plot/src/lib.rs b/egui_plot/src/lib.rs index 71ad004d..f8e68517 100644 --- a/egui_plot/src/lib.rs +++ b/egui_plot/src/lib.rs @@ -22,29 +22,19 @@ mod memory; mod overlays; mod placement; mod plot; -mod plot_ui; mod rect_elem; -mod transform; mod utils; -pub use bounds::PlotBounds; -use egui::Id; -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::aesthetics::LineStyle; pub use crate::aesthetics::MarkerShape; pub use crate::aesthetics::Orientation; pub use crate::axis::Axis; pub use crate::axis::AxisHints; +pub use crate::axis::PlotTransform; +pub use crate::bounds::PlotBounds; +pub use crate::bounds::PlotPoint; 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::data::PlotPoint; pub use crate::data::PlotPoints; pub use crate::grid::GridInput; pub use crate::grid::GridMark; @@ -73,9 +63,13 @@ pub use crate::items::VLine; pub use crate::label::LabelFormatter; pub use crate::label::format_number; pub use crate::memory::PlotMemory; +pub use crate::overlays::ColorConflictHandling; pub use crate::overlays::CoordinatesFormatter; +pub use crate::overlays::Legend; pub use crate::placement::Corner; +pub use crate::placement::HPlacement; +pub use crate::placement::Placement; +pub use crate::placement::VPlacement; pub use crate::plot::Plot; pub use crate::plot::PlotResponse; -pub use crate::plot_ui::PlotUi; -pub use crate::transform::PlotTransform; +pub use crate::plot::PlotUi; diff --git a/egui_plot/src/math.rs b/egui_plot/src/math.rs index 402e7706..2b2163f4 100644 --- a/egui_plot/src/math.rs +++ b/egui_plot/src/math.rs @@ -1,7 +1,7 @@ use emath::Float as _; use emath::Pos2; -use crate::PlotTransform; +use crate::axis::PlotTransform; use crate::items::ClosestElem; use crate::rect_elem::RectElement; diff --git a/egui_plot/src/memory.rs b/egui_plot/src/memory.rs index f469656a..93417b2b 100644 --- a/egui_plot/src/memory.rs +++ b/egui_plot/src/memory.rs @@ -5,7 +5,7 @@ use egui::Id; use egui::Pos2; use egui::Vec2b; -use crate::PlotTransform; +use crate::axis::PlotTransform; use crate::bounds::PlotBounds; /// Information about the plot that has to persist between frames. diff --git a/egui_plot/src/overlays/coordinates.rs b/egui_plot/src/overlays/coordinates.rs index e60c44ed..3049ed81 100644 --- a/egui_plot/src/overlays/coordinates.rs +++ b/egui_plot/src/overlays/coordinates.rs @@ -1,5 +1,5 @@ use crate::bounds::PlotBounds; -use crate::data::PlotPoint; +use crate::bounds::PlotPoint; type CoordinatesFormatterFn<'a> = dyn Fn(&PlotPoint, &PlotBounds) -> String + 'a; diff --git a/egui_plot/src/overlays/legend.rs b/egui_plot/src/overlays/legend.rs index b512d946..e393373b 100644 --- a/egui_plot/src/overlays/legend.rs +++ b/egui_plot/src/overlays/legend.rs @@ -22,7 +22,7 @@ use egui::epaint::CircleShape; use egui::pos2; use egui::vec2; -use crate::PlotItem; +use crate::items::PlotItem; use crate::placement::Corner; /// How to handle multiple conflicting color for a legend item. @@ -210,7 +210,7 @@ impl LegendEntry { } #[derive(Clone)] -pub(crate) struct LegendWidget { +pub struct LegendWidget { rect: Rect, entries: Vec, config: Legend, diff --git a/egui_plot/src/overlays/mod.rs b/egui_plot/src/overlays/mod.rs index 1af8664d..4ae302da 100644 --- a/egui_plot/src/overlays/mod.rs +++ b/egui_plot/src/overlays/mod.rs @@ -2,6 +2,9 @@ //! coordinates. mod coordinates; -pub mod legend; +mod legend; pub use coordinates::CoordinatesFormatter; +pub use legend::ColorConflictHandling; +pub use legend::Legend; +pub use legend::LegendWidget; diff --git a/egui_plot/src/plot.rs b/egui_plot/src/plot.rs index 53101600..b6fd3085 100644 --- a/egui_plot/src/plot.rs +++ b/egui_plot/src/plot.rs @@ -14,6 +14,7 @@ use egui::Stroke; use egui::TextStyle; use egui::Ui; use egui::WidgetText; +use egui::ecolor::Hsva; use egui::epaint; use emath::Align2; use emath::NumExt as _; @@ -25,32 +26,32 @@ use emath::Vec2b; use emath::remap_clamp; use emath::vec2; -use crate::Axis; -use crate::AxisHints; -use crate::Cursor; -use crate::CursorLinkGroups; -use crate::Legend; -use crate::PlotFrameCursors; -use crate::PlotItem; -use crate::PlotMemory; -use crate::PlotTransform; -use crate::PlotUi; +use crate::axis::Axis; +use crate::axis::AxisHints; use crate::axis::AxisWidget; +use crate::axis::PlotTransform; use crate::bounds::BoundsLinkGroups; use crate::bounds::BoundsModification; use crate::bounds::LinkedBounds; use crate::bounds::PlotBounds; +use crate::bounds::PlotPoint; use crate::colors::rulers_color; -use crate::data::PlotPoint; +use crate::cursor::Cursor; +use crate::cursor::CursorLinkGroups; +use crate::cursor::PlotFrameCursors; use crate::grid::GridInput; use crate::grid::GridMark; use crate::grid::GridSpacer; use crate::items; +use crate::items::PlotItem; +use crate::items::Span; use crate::items::horizontal_line; use crate::items::vertical_line; use crate::label::LabelFormatter; +use crate::memory::PlotMemory; use crate::overlays::CoordinatesFormatter; -use crate::overlays::legend::LegendWidget; +use crate::overlays::Legend; +use crate::overlays::LegendWidget; use crate::placement::Corner; use crate::placement::HPlacement; use crate::placement::VPlacement; @@ -1835,3 +1836,292 @@ pub struct PlotResponse { /// plot (line, marker, etc.) or by hovering the item in the legend. pub hovered_plot_item: Option, } + +/// Provides methods to interact with a plot while building it. It is the single +/// argument of the closure provided to [`Plot::show`]. See [`Plot`] for an +/// example of how to use it. +pub struct PlotUi<'a> { + pub(crate) ctx: egui::Context, + pub(crate) items: Vec>, + pub(crate) next_auto_color_idx: usize, + pub(crate) last_plot_transform: PlotTransform, + pub(crate) last_auto_bounds: Vec2b, + pub(crate) response: Response, + pub(crate) bounds_modifications: Vec, +} + +impl<'a> PlotUi<'a> { + fn auto_color(&mut self) -> Color32 { + let i = self.next_auto_color_idx; + self.next_auto_color_idx += 1; + let golden_ratio = (5.0_f32.sqrt() - 1.0) / 2.0; // 0.61803398875 + let h = i as f32 * golden_ratio; + Hsva::new(h, 0.85, 0.5, 1.0).into() // TODO(#165): OkLab or some other perspective color space + } + + pub fn ctx(&self) -> &egui::Context { + &self.ctx + } + + /// The plot bounds as they were in the last frame. If called on the first + /// frame and the bounds were not further specified in the plot builder, + /// this will return bounds centered on the origin. The bounds do + /// not change until the plot is drawn. + pub fn plot_bounds(&self) -> PlotBounds { + *self.last_plot_transform.bounds() + } + + /// Set the plot bounds. Can be useful for implementing alternative plot + /// navigation methods. + pub fn set_plot_bounds(&mut self, plot_bounds: PlotBounds) { + self.set_plot_bounds_x(plot_bounds.range_x()); + self.set_plot_bounds_y(plot_bounds.range_y()); + } + + /// Set the X bounds. Can be useful for implementing alternative plot + /// navigation methods. + pub fn set_plot_bounds_x(&mut self, range: impl Into>) { + self.bounds_modifications.push(BoundsModification::SetX(range.into())); + } + + /// Set the Y bounds. Can be useful for implementing alternative plot + /// navigation methods. + pub fn set_plot_bounds_y(&mut self, range: impl Into>) { + self.bounds_modifications.push(BoundsModification::SetY(range.into())); + } + + /// Move the plot bounds. Can be useful for implementing alternative plot + /// navigation methods. + pub fn translate_bounds(&mut self, delta_pos: Vec2) { + self.bounds_modifications.push(BoundsModification::Translate(delta_pos)); + } + + /// Whether the plot axes were in auto-bounds mode in the last frame. If + /// called on the first frame, this is the [`Plot`]'s default + /// auto-bounds mode. + pub fn auto_bounds(&self) -> Vec2b { + self.last_auto_bounds + } + + /// Set the auto-bounds mode for the plot axes. + pub fn set_auto_bounds(&mut self, auto_bounds: impl Into) { + self.bounds_modifications + .push(BoundsModification::AutoBounds(auto_bounds.into())); + } + + /// Can be used to check if the plot was hovered or clicked. + pub fn response(&self) -> &Response { + &self.response + } + + /// Scale the plot bounds around a position in plot coordinates. + /// + /// Can be useful for implementing alternative plot navigation methods. + /// + /// The plot bounds are divided by `zoom_factor`, therefore: + /// - `zoom_factor < 1.0` zooms out, i.e., increases the visible range to + /// show more data. + /// - `zoom_factor > 1.0` zooms in, i.e., reduces the visible range to show + /// more detail. + pub fn zoom_bounds(&mut self, zoom_factor: Vec2, center: PlotPoint) { + self.bounds_modifications + .push(BoundsModification::Zoom(zoom_factor, center)); + } + + /// Scale the plot bounds around the hovered position, if any. + /// + /// Can be useful for implementing alternative plot navigation methods. + /// + /// The plot bounds are divided by `zoom_factor`, therefore: + /// - `zoom_factor < 1.0` zooms out, i.e., increases the visible range to + /// show more data. + /// - `zoom_factor > 1.0` zooms in, i.e., reduces the visible range to show + /// more detail. + pub fn zoom_bounds_around_hovered(&mut self, zoom_factor: Vec2) { + if let Some(hover_pos) = self.pointer_coordinate() { + self.zoom_bounds(zoom_factor, hover_pos); + } + } + + /// The pointer position in plot coordinates. Independent of whether the + /// pointer is in the plot area. + pub fn pointer_coordinate(&self) -> Option { + // We need to subtract the drag delta to keep in sync with the frame-delayed + // screen transform: + let last_pos = self.ctx().input(|i| i.pointer.latest_pos())? - self.response.drag_delta(); + let value = self.plot_from_screen(last_pos); + Some(value) + } + + /// The pointer drag delta in plot coordinates. + pub fn pointer_coordinate_drag_delta(&self) -> Vec2 { + let delta = self.response.drag_delta(); + let dp_dv = self.last_plot_transform.dpos_dvalue(); + Vec2::new(delta.x / dp_dv[0] as f32, delta.y / dp_dv[1] as f32) + } + + /// Read the transform between plot coordinates and screen coordinates. + pub fn transform(&self) -> &PlotTransform { + &self.last_plot_transform + } + + /// Transform the plot coordinates to screen coordinates. + pub fn screen_from_plot(&self, position: PlotPoint) -> Pos2 { + self.last_plot_transform.position_from_point(&position) + } + + /// Transform the screen coordinates to plot coordinates. + pub fn plot_from_screen(&self, position: Pos2) -> PlotPoint { + self.last_plot_transform.value_from_position(position) + } + + /// Add an arbitrary item. + pub fn add(&mut self, item: impl PlotItem + 'a) { + self.items.push(Box::new(item)); + } + + /// Add an arbitrary item. + pub fn add_item(&mut self, item: Box) { + self.items.push(item); + } + + /// Add a data line. + pub fn line(&mut self, mut line: crate::Line<'a>) { + if line.series.is_empty() { + return; + } + + // Give the stroke an automatic color if no color has been assigned. + if line.stroke.color == Color32::TRANSPARENT { + line.stroke.color = self.auto_color(); + } + self.items.push(Box::new(line)); + } + + /// Add a polygon. The polygon has to be convex. + pub fn polygon(&mut self, mut polygon: crate::Polygon<'a>) { + if polygon.series.is_empty() { + return; + } + + // Give the stroke an automatic color if no color has been assigned. + if polygon.stroke.color == Color32::TRANSPARENT { + polygon.stroke.color = self.auto_color(); + } + self.items.push(Box::new(polygon)); + } + + /// Add a text. + pub fn text(&mut self, text: crate::Text) { + if text.text.is_empty() { + return; + } + + self.items.push(Box::new(text)); + } + + /// Add data points. + pub fn points(&mut self, mut points: crate::Points<'a>) { + if points.series.is_empty() { + return; + } + + // Give the points an automatic color if no color has been assigned. + if points.color == Color32::TRANSPARENT { + points.color = self.auto_color(); + } + self.items.push(Box::new(points)); + } + + /// Add arrows. + pub fn arrows(&mut self, mut arrows: crate::Arrows<'a>) { + if arrows.origins.is_empty() || arrows.tips.is_empty() { + return; + } + + // Give the arrows an automatic color if no color has been assigned. + if arrows.color == Color32::TRANSPARENT { + arrows.color = self.auto_color(); + } + self.items.push(Box::new(arrows)); + } + + /// Add an image. + pub fn image(&mut self, image: crate::PlotImage) { + self.items.push(Box::new(image)); + } + + /// Add a horizontal line. + /// Can be useful e.g. to show min/max bounds or similar. + /// Always fills the full width of the plot. + pub fn hline(&mut self, mut hline: crate::HLine) { + if hline.stroke.color == Color32::TRANSPARENT { + hline.stroke.color = self.auto_color(); + } + self.items.push(Box::new(hline)); + } + + /// Add a vertical line. + /// Can be useful e.g. to show min/max bounds or similar. + /// Always fills the full height of the plot. + pub fn vline(&mut self, mut vline: crate::VLine) { + if vline.stroke.color == Color32::TRANSPARENT { + vline.stroke.color = self.auto_color(); + } + self.items.push(Box::new(vline)); + } + + /// Add an axis-aligned span. + /// + /// Spans fill the space between two values on one axis. If both the fill + /// and border colors are transparent, a color is auto-assigned. + pub fn span(&mut self, mut span: Span) { + let fill_is_transparent = span.fill_color() == Color32::TRANSPARENT; + let border_is_transparent = span.border_color_value() == Color32::TRANSPARENT; + + // If no color was provided, automatically assign a color to the span + if fill_is_transparent && border_is_transparent { + let auto_color = self.auto_color(); + span = span.fill(auto_color.gamma_multiply(0.15)).border_color(auto_color); + } else if border_is_transparent && !fill_is_transparent { + let fill_color = span.fill_color(); + span = span.border_color(fill_color); + } + + self.items.push(Box::new(span)); + } + + /// Add a box plot diagram. + pub fn box_plot(&mut self, mut box_plot: crate::BoxPlot) { + if box_plot.boxes.is_empty() { + return; + } + + // Give the elements an automatic color if no color has been assigned. + if PlotItem::color(&box_plot) == Color32::TRANSPARENT { + box_plot = box_plot.color(self.auto_color()); + } + self.items.push(Box::new(box_plot)); + } + + /// Add a bar chart. + pub fn bar_chart(&mut self, mut chart: crate::BarChart) { + if chart.bars.is_empty() { + return; + } + + // Give the elements an automatic color if no color has been assigned. + if PlotItem::color(&chart) == Color32::TRANSPARENT { + chart = chart.color(self.auto_color()); + } + self.items.push(Box::new(chart)); + } + + /// Add a heatmap. + pub fn heatmap(&mut self, heatmap: crate::Heatmap) { + if heatmap.values.is_empty() { + return; + } + self.items.push(Box::new(heatmap)); + } +} diff --git a/egui_plot/src/plot_ui.rs b/egui_plot/src/plot_ui.rs deleted file mode 100644 index d1f316fa..00000000 --- a/egui_plot/src/plot_ui.rs +++ /dev/null @@ -1,306 +0,0 @@ -use std::ops::RangeInclusive; - -use egui::Color32; -use egui::Pos2; -use egui::Response; -use egui::Vec2; -use egui::Vec2b; -use egui::epaint::Hsva; - -#[expect(unused_imports)] // for links in docstrings -use crate::Plot; -use crate::PlotItem; -use crate::PlotTransform; -use crate::Span; -use crate::bounds::BoundsModification; -use crate::bounds::PlotBounds; -use crate::data::PlotPoint; - -/// Provides methods to interact with a plot while building it. It is the single -/// argument of the closure provided to [`Plot::show`]. See [`Plot`] for an -/// example of how to use it. -pub struct PlotUi<'a> { - pub(crate) ctx: egui::Context, - pub(crate) items: Vec>, - pub(crate) next_auto_color_idx: usize, - pub(crate) last_plot_transform: PlotTransform, - pub(crate) last_auto_bounds: Vec2b, - pub(crate) response: Response, - pub(crate) bounds_modifications: Vec, -} - -impl<'a> PlotUi<'a> { - fn auto_color(&mut self) -> Color32 { - let i = self.next_auto_color_idx; - self.next_auto_color_idx += 1; - let golden_ratio = (5.0_f32.sqrt() - 1.0) / 2.0; // 0.61803398875 - let h = i as f32 * golden_ratio; - Hsva::new(h, 0.85, 0.5, 1.0).into() // TODO(#165): OkLab or some other perspective color space - } - - pub fn ctx(&self) -> &egui::Context { - &self.ctx - } - - /// The plot bounds as they were in the last frame. If called on the first - /// frame and the bounds were not further specified in the plot builder, - /// this will return bounds centered on the origin. The bounds do - /// not change until the plot is drawn. - pub fn plot_bounds(&self) -> PlotBounds { - *self.last_plot_transform.bounds() - } - - /// Set the plot bounds. Can be useful for implementing alternative plot - /// navigation methods. - pub fn set_plot_bounds(&mut self, plot_bounds: PlotBounds) { - self.set_plot_bounds_x(plot_bounds.range_x()); - self.set_plot_bounds_y(plot_bounds.range_y()); - } - - /// Set the X bounds. Can be useful for implementing alternative plot - /// navigation methods. - pub fn set_plot_bounds_x(&mut self, range: impl Into>) { - self.bounds_modifications.push(BoundsModification::SetX(range.into())); - } - - /// Set the Y bounds. Can be useful for implementing alternative plot - /// navigation methods. - pub fn set_plot_bounds_y(&mut self, range: impl Into>) { - self.bounds_modifications.push(BoundsModification::SetY(range.into())); - } - - /// Move the plot bounds. Can be useful for implementing alternative plot - /// navigation methods. - pub fn translate_bounds(&mut self, delta_pos: Vec2) { - self.bounds_modifications.push(BoundsModification::Translate(delta_pos)); - } - - /// Whether the plot axes were in auto-bounds mode in the last frame. If - /// called on the first frame, this is the [`Plot`]'s default - /// auto-bounds mode. - pub fn auto_bounds(&self) -> Vec2b { - self.last_auto_bounds - } - - /// Set the auto-bounds mode for the plot axes. - pub fn set_auto_bounds(&mut self, auto_bounds: impl Into) { - self.bounds_modifications - .push(BoundsModification::AutoBounds(auto_bounds.into())); - } - - /// Can be used to check if the plot was hovered or clicked. - pub fn response(&self) -> &Response { - &self.response - } - - /// Scale the plot bounds around a position in plot coordinates. - /// - /// Can be useful for implementing alternative plot navigation methods. - /// - /// The plot bounds are divided by `zoom_factor`, therefore: - /// - `zoom_factor < 1.0` zooms out, i.e., increases the visible range to - /// show more data. - /// - `zoom_factor > 1.0` zooms in, i.e., reduces the visible range to show - /// more detail. - pub fn zoom_bounds(&mut self, zoom_factor: Vec2, center: PlotPoint) { - self.bounds_modifications - .push(BoundsModification::Zoom(zoom_factor, center)); - } - - /// Scale the plot bounds around the hovered position, if any. - /// - /// Can be useful for implementing alternative plot navigation methods. - /// - /// The plot bounds are divided by `zoom_factor`, therefore: - /// - `zoom_factor < 1.0` zooms out, i.e., increases the visible range to - /// show more data. - /// - `zoom_factor > 1.0` zooms in, i.e., reduces the visible range to show - /// more detail. - pub fn zoom_bounds_around_hovered(&mut self, zoom_factor: Vec2) { - if let Some(hover_pos) = self.pointer_coordinate() { - self.zoom_bounds(zoom_factor, hover_pos); - } - } - - /// The pointer position in plot coordinates. Independent of whether the - /// pointer is in the plot area. - pub fn pointer_coordinate(&self) -> Option { - // We need to subtract the drag delta to keep in sync with the frame-delayed - // screen transform: - let last_pos = self.ctx().input(|i| i.pointer.latest_pos())? - self.response.drag_delta(); - let value = self.plot_from_screen(last_pos); - Some(value) - } - - /// The pointer drag delta in plot coordinates. - pub fn pointer_coordinate_drag_delta(&self) -> Vec2 { - let delta = self.response.drag_delta(); - let dp_dv = self.last_plot_transform.dpos_dvalue(); - Vec2::new(delta.x / dp_dv[0] as f32, delta.y / dp_dv[1] as f32) - } - - /// Read the transform between plot coordinates and screen coordinates. - pub fn transform(&self) -> &PlotTransform { - &self.last_plot_transform - } - - /// Transform the plot coordinates to screen coordinates. - pub fn screen_from_plot(&self, position: PlotPoint) -> Pos2 { - self.last_plot_transform.position_from_point(&position) - } - - /// Transform the screen coordinates to plot coordinates. - pub fn plot_from_screen(&self, position: Pos2) -> PlotPoint { - self.last_plot_transform.value_from_position(position) - } - - /// Add an arbitrary item. - pub fn add(&mut self, item: impl PlotItem + 'a) { - self.items.push(Box::new(item)); - } - - /// Add an arbitrary item. - pub fn add_item(&mut self, item: Box) { - self.items.push(item); - } - - /// Add a data line. - pub fn line(&mut self, mut line: crate::Line<'a>) { - if line.series.is_empty() { - return; - } - - // Give the stroke an automatic color if no color has been assigned. - if line.stroke.color == Color32::TRANSPARENT { - line.stroke.color = self.auto_color(); - } - self.items.push(Box::new(line)); - } - - /// Add a polygon. The polygon has to be convex. - pub fn polygon(&mut self, mut polygon: crate::Polygon<'a>) { - if polygon.series.is_empty() { - return; - } - - // Give the stroke an automatic color if no color has been assigned. - if polygon.stroke.color == Color32::TRANSPARENT { - polygon.stroke.color = self.auto_color(); - } - self.items.push(Box::new(polygon)); - } - - /// Add a text. - pub fn text(&mut self, text: crate::Text) { - if text.text.is_empty() { - return; - } - - self.items.push(Box::new(text)); - } - - /// Add data points. - pub fn points(&mut self, mut points: crate::Points<'a>) { - if points.series.is_empty() { - return; - } - - // Give the points an automatic color if no color has been assigned. - if points.color == Color32::TRANSPARENT { - points.color = self.auto_color(); - } - self.items.push(Box::new(points)); - } - - /// Add arrows. - pub fn arrows(&mut self, mut arrows: crate::Arrows<'a>) { - if arrows.origins.is_empty() || arrows.tips.is_empty() { - return; - } - - // Give the arrows an automatic color if no color has been assigned. - if arrows.color == Color32::TRANSPARENT { - arrows.color = self.auto_color(); - } - self.items.push(Box::new(arrows)); - } - - /// Add an image. - pub fn image(&mut self, image: crate::PlotImage) { - self.items.push(Box::new(image)); - } - - /// Add a horizontal line. - /// Can be useful e.g. to show min/max bounds or similar. - /// Always fills the full width of the plot. - pub fn hline(&mut self, mut hline: crate::HLine) { - if hline.stroke.color == Color32::TRANSPARENT { - hline.stroke.color = self.auto_color(); - } - self.items.push(Box::new(hline)); - } - - /// Add a vertical line. - /// Can be useful e.g. to show min/max bounds or similar. - /// Always fills the full height of the plot. - pub fn vline(&mut self, mut vline: crate::VLine) { - if vline.stroke.color == Color32::TRANSPARENT { - vline.stroke.color = self.auto_color(); - } - self.items.push(Box::new(vline)); - } - - /// Add an axis-aligned span. - /// - /// Spans fill the space between two values on one axis. If both the fill - /// and border colors are transparent, a color is auto-assigned. - pub fn span(&mut self, mut span: Span) { - let fill_is_transparent = span.fill_color() == Color32::TRANSPARENT; - let border_is_transparent = span.border_color_value() == Color32::TRANSPARENT; - - // If no color was provided, automatically assign a color to the span - if fill_is_transparent && border_is_transparent { - let auto_color = self.auto_color(); - span = span.fill(auto_color.gamma_multiply(0.15)).border_color(auto_color); - } else if border_is_transparent && !fill_is_transparent { - let fill_color = span.fill_color(); - span = span.border_color(fill_color); - } - - self.items.push(Box::new(span)); - } - - /// Add a box plot diagram. - pub fn box_plot(&mut self, mut box_plot: crate::BoxPlot) { - if box_plot.boxes.is_empty() { - return; - } - - // Give the elements an automatic color if no color has been assigned. - if PlotItem::color(&box_plot) == Color32::TRANSPARENT { - box_plot = box_plot.color(self.auto_color()); - } - self.items.push(Box::new(box_plot)); - } - - /// Add a bar chart. - pub fn bar_chart(&mut self, mut chart: crate::BarChart) { - if chart.bars.is_empty() { - return; - } - - // Give the elements an automatic color if no color has been assigned. - if PlotItem::color(&chart) == Color32::TRANSPARENT { - chart = chart.color(self.auto_color()); - } - self.items.push(Box::new(chart)); - } - - /// Add a heatmap. - pub fn heatmap(&mut self, heatmap: crate::Heatmap) { - if heatmap.values.is_empty() { - return; - } - self.items.push(Box::new(heatmap)); - } -} diff --git a/egui_plot/src/rect_elem.rs b/egui_plot/src/rect_elem.rs index f3c059fa..7827f548 100644 --- a/egui_plot/src/rect_elem.rs +++ b/egui_plot/src/rect_elem.rs @@ -1,7 +1,7 @@ -use super::PlotPoint; use crate::aesthetics::Orientation; +use crate::axis::PlotTransform; use crate::bounds::PlotBounds; -use crate::transform::PlotTransform; +use crate::bounds::PlotPoint; /// Trait that abstracts from rectangular 'Value'-like elements, such as bars or /// boxes diff --git a/egui_plot/src/transform.rs b/egui_plot/src/transform.rs deleted file mode 100644 index a7c1b542..00000000 --- a/egui_plot/src/transform.rs +++ /dev/null @@ -1,284 +0,0 @@ -use egui::Pos2; -use egui::Rect; -use egui::Vec2; -use egui::Vec2b; -use egui::pos2; -use egui::remap; - -use super::PlotPoint; -use crate::Axis; -use crate::bounds::PlotBounds; - -/// Contains the screen rectangle and the plot bounds and provides methods to -/// transform between them. -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[derive(Clone, Copy, Debug)] -pub struct PlotTransform { - /// The screen rectangle. - frame: Rect, - - /// The plot bounds. - bounds: PlotBounds, - - /// Whether to always center the x-range or y-range of the bounds. - centered: Vec2b, - - /// Whether to always invert the x and/or y axis - inverted_axis: Vec2b, -} - -impl PlotTransform { - pub fn new(frame: Rect, bounds: PlotBounds, center_axis: impl Into) -> Self { - debug_assert!( - 0.0 <= frame.width() && 0.0 <= frame.height(), - "Bad plot frame: {frame:?}" - ); - let center_axis = center_axis.into(); - - // Since the current Y bounds an affect the final X bounds and vice versa, we - // need to keep the original version of the `bounds` before we start - // modifying it. - let mut new_bounds = bounds; - - // Sanitize bounds. - // - // When a given bound axis is "thin" (e.g. width or height is 0) but finite, we - // center the bounds around that value. If the other axis is "fat", we - // reuse its extent for the thin axis, and default to +/- 1.0 otherwise. - if !bounds.is_finite_x() { - new_bounds.set_x(&PlotBounds::new_symmetrical(1.0)); - } else if bounds.width() <= 0.0 { - new_bounds.set_x_center_width( - bounds.center().x, - if bounds.is_valid_y() { bounds.height() } else { 1.0 }, - ); - } - - if !bounds.is_finite_y() { - new_bounds.set_y(&PlotBounds::new_symmetrical(1.0)); - } else if bounds.height() <= 0.0 { - new_bounds.set_y_center_height( - bounds.center().y, - if bounds.is_valid_x() { bounds.width() } else { 1.0 }, - ); - } - - // Scale axes so that the origin is in the center. - if center_axis.x { - new_bounds.make_x_symmetrical(); - } - if center_axis.y { - new_bounds.make_y_symmetrical(); - } - - debug_assert!(new_bounds.is_valid(), "Bad final plot bounds: {new_bounds:?}"); - - Self { - frame, - bounds: new_bounds, - centered: center_axis, - inverted_axis: Vec2b::new(false, false), - } - } - - pub fn new_with_invert_axis( - frame: Rect, - bounds: PlotBounds, - center_axis: impl Into, - invert_axis: impl Into, - ) -> Self { - let mut new = Self::new(frame, bounds, center_axis); - new.inverted_axis = invert_axis.into(); - new - } - - /// ui-space rectangle. - #[inline] - pub fn frame(&self) -> &Rect { - &self.frame - } - - /// Plot-space bounds. - #[inline] - pub fn bounds(&self) -> &PlotBounds { - &self.bounds - } - - #[inline] - pub fn set_bounds(&mut self, bounds: PlotBounds) { - self.bounds = bounds; - } - - pub fn translate_bounds(&mut self, mut delta_pos: (f64, f64)) { - if self.centered.x { - delta_pos.0 = 0.; - } - if self.centered.y { - delta_pos.1 = 0.; - } - delta_pos.0 *= self.dvalue_dpos()[0]; - delta_pos.1 *= self.dvalue_dpos()[1]; - self.bounds.translate((delta_pos.0, delta_pos.1)); - } - - /// Zoom by a relative factor with the given screen position as center. - pub fn zoom(&mut self, zoom_factor: Vec2, center: Pos2) { - let center = self.value_from_position(center); - - let mut new_bounds = self.bounds; - new_bounds.zoom(zoom_factor, center); - - if new_bounds.is_valid() { - self.bounds = new_bounds; - } - } - - pub fn position_from_point_x(&self, value: f64) -> f32 { - remap( - value, - self.bounds.min[0]..=self.bounds.max[0], - if self.inverted_axis[0] { - (self.frame.right() as f64)..=(self.frame.left() as f64) - } else { - (self.frame.left() as f64)..=(self.frame.right() as f64) - }, - ) as f32 - } - - pub fn position_from_point_y(&self, value: f64) -> f32 { - remap( - value, - self.bounds.min[1]..=self.bounds.max[1], - // negated y axis by default - if self.inverted_axis[1] { - (self.frame.top() as f64)..=(self.frame.bottom() as f64) - } else { - (self.frame.bottom() as f64)..=(self.frame.top() as f64) - }, - ) as f32 - } - - /// Screen/ui position from point on plot. - pub fn position_from_point(&self, value: &PlotPoint) -> Pos2 { - pos2(self.position_from_point_x(value.x), self.position_from_point_y(value.y)) - } - - /// Plot point from screen/ui position. - pub fn value_from_position(&self, pos: Pos2) -> PlotPoint { - let x = remap( - pos.x as f64, - if self.inverted_axis[0] { - (self.frame.right() as f64)..=(self.frame.left() as f64) - } else { - (self.frame.left() as f64)..=(self.frame.right() as f64) - }, - self.bounds.range_x(), - ); - let y = remap( - pos.y as f64, - // negated y axis by default - if self.inverted_axis[1] { - (self.frame.top() as f64)..=(self.frame.bottom() as f64) - } else { - (self.frame.bottom() as f64)..=(self.frame.top() as f64) - }, - self.bounds.range_y(), - ); - - PlotPoint::new(x, y) - } - - /// Transform a rectangle of plot values to a screen-coordinate rectangle. - /// - /// This typically means that the rect is mirrored vertically (top becomes - /// bottom and vice versa), since the plot's coordinate system has +Y - /// up, while egui has +Y down. - pub fn rect_from_values(&self, value1: &PlotPoint, value2: &PlotPoint) -> Rect { - let pos1 = self.position_from_point(value1); - let pos2 = self.position_from_point(value2); - - let mut rect = Rect::NOTHING; - rect.extend_with(pos1); - rect.extend_with(pos2); - rect - } - - /// delta position / delta value = how many ui points per step in the X axis - /// in "plot space" - pub fn dpos_dvalue_x(&self) -> f64 { - let flip = if self.inverted_axis[0] { -1.0 } else { 1.0 }; - flip * (self.frame.width() as f64) / self.bounds.width() - } - - /// delta position / delta value = how many ui points per step in the Y axis - /// in "plot space" - pub fn dpos_dvalue_y(&self) -> f64 { - let flip = if self.inverted_axis[1] { 1.0 } else { -1.0 }; - flip * (self.frame.height() as f64) / self.bounds.height() - } - - /// delta position / delta value = how many ui points per step in "plot - /// space" - pub fn dpos_dvalue(&self) -> [f64; 2] { - [self.dpos_dvalue_x(), self.dpos_dvalue_y()] - } - - /// delta value / delta position = how much ground do we cover in "plot - /// space" per ui point? - pub fn dvalue_dpos(&self) -> [f64; 2] { - [1.0 / self.dpos_dvalue_x(), 1.0 / self.dpos_dvalue_y()] - } - - /// scale.x/scale.y ratio. - /// - /// If 1.0, it means the scale factor is the same in both axes. - fn aspect(&self) -> f64 { - let rw = self.frame.width() as f64; - let rh = self.frame.height() as f64; - (self.bounds.width() / rw) / (self.bounds.height() / rh) - } - - /// Sets the aspect ratio by expanding the x- or y-axis. - /// - /// This never contracts, so we don't miss out on any data. - pub(crate) fn set_aspect_by_expanding(&mut self, aspect: f64) { - let current_aspect = self.aspect(); - - let epsilon = 1e-5; - if (current_aspect - aspect).abs() < epsilon { - // Don't make any changes when the aspect is already almost correct. - return; - } - - if current_aspect < aspect { - self.bounds - .expand_x((aspect / current_aspect - 1.0) * self.bounds.width() * 0.5); - } else { - self.bounds - .expand_y((current_aspect / aspect - 1.0) * self.bounds.height() * 0.5); - } - } - - /// Sets the aspect ratio by changing either the X or Y axis (callers - /// choice). - pub(crate) fn set_aspect_by_changing_axis(&mut self, aspect: f64, axis: Axis) { - let current_aspect = self.aspect(); - - let epsilon = 1e-5; - if (current_aspect - aspect).abs() < epsilon { - // Don't make any changes when the aspect is already almost correct. - return; - } - - match axis { - Axis::X => { - self.bounds - .expand_x((aspect / current_aspect - 1.0) * self.bounds.width() * 0.5); - } - Axis::Y => { - self.bounds - .expand_y((current_aspect / aspect - 1.0) * self.bounds.height() * 0.5); - } - } - } -}