From 18943b83e88908675d21d4dcbcb6a93a723553db Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 15 Jul 2024 18:45:19 +0200 Subject: [PATCH] Move `egui_plot` to its own repo (#4828) * Part of https://github.com/emilk/egui/issues/4705 `egui_plot` can now be found at https://github.com/emilk/egui_plot --- .github/workflows/labels.yml | 2 +- ARCHITECTURE.md | 5 +- CHANGELOG.md | 2 +- CODEOWNERS | 1 - Cargo.lock | 32 - Cargo.toml | 2 - RELEASES.md | 3 +- crates/egui_demo_lib/Cargo.toml | 3 +- crates/egui_demo_lib/src/demo/context_menu.rs | 124 +- .../src/demo/demo_app_windows.rs | 1 - crates/egui_demo_lib/src/demo/mod.rs | 1 - crates/egui_demo_lib/src/demo/plot_demo.rs | 1083 --------- .../egui_demo_lib/src/demo/widget_gallery.rs | 23 - crates/egui_plot/CHANGELOG.md | 92 - crates/egui_plot/Cargo.toml | 47 - crates/egui_plot/README.md | 12 +- crates/egui_plot/src/axis.rs | 398 ---- crates/egui_plot/src/items/bar.rs | 197 -- crates/egui_plot/src/items/box_elem.rs | 296 --- crates/egui_plot/src/items/mod.rs | 2104 ----------------- crates/egui_plot/src/items/rect_elem.rs | 73 - crates/egui_plot/src/items/values.rs | 434 ---- crates/egui_plot/src/legend.rs | 324 --- crates/egui_plot/src/lib.rs | 1857 --------------- crates/egui_plot/src/memory.rs | 81 - crates/egui_plot/src/plot_ui.rs | 235 -- crates/egui_plot/src/transform.rs | 509 ---- examples/custom_plot_manipulation/Cargo.toml | 23 - examples/custom_plot_manipulation/README.md | 7 - .../custom_plot_manipulation/screenshot.png | Bin 39733 -> 0 bytes examples/custom_plot_manipulation/src/main.rs | 129 - examples/save_plot/Cargo.toml | 24 - examples/save_plot/README.md | 7 - examples/save_plot/screenshot.png | Bin 6467 -> 0 bytes examples/save_plot/src/main.rs | 75 - scripts/generate_changelog.py | 1 - 36 files changed, 11 insertions(+), 8196 deletions(-) delete mode 100644 crates/egui_demo_lib/src/demo/plot_demo.rs delete mode 100644 crates/egui_plot/CHANGELOG.md delete mode 100644 crates/egui_plot/Cargo.toml delete mode 100644 crates/egui_plot/src/axis.rs delete mode 100644 crates/egui_plot/src/items/bar.rs delete mode 100644 crates/egui_plot/src/items/box_elem.rs delete mode 100644 crates/egui_plot/src/items/mod.rs delete mode 100644 crates/egui_plot/src/items/rect_elem.rs delete mode 100644 crates/egui_plot/src/items/values.rs delete mode 100644 crates/egui_plot/src/legend.rs delete mode 100644 crates/egui_plot/src/lib.rs delete mode 100644 crates/egui_plot/src/memory.rs delete mode 100644 crates/egui_plot/src/plot_ui.rs delete mode 100644 crates/egui_plot/src/transform.rs delete mode 100644 examples/custom_plot_manipulation/Cargo.toml delete mode 100644 examples/custom_plot_manipulation/README.md delete mode 100644 examples/custom_plot_manipulation/screenshot.png delete mode 100644 examples/custom_plot_manipulation/src/main.rs delete mode 100644 examples/save_plot/Cargo.toml delete mode 100644 examples/save_plot/README.md delete mode 100644 examples/save_plot/screenshot.png delete mode 100644 examples/save_plot/src/main.rs diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index d3f5cd14e5d..a79f65da4fa 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -29,4 +29,4 @@ jobs: with: mode: minimum count: 1 - labels: "CI, dependencies, docs and examples, ecolor, eframe, egui_extras, egui_glow, egui_plot, egui-wgpu, egui-winit, egui, epaint, exclude from changelog, typo" + labels: "CI, dependencies, docs and examples, ecolor, eframe, egui_extras, egui_glow, egui-wgpu, egui-winit, egui, epaint, exclude from changelog, typo" diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index c9f314f7d60..81229ec0519 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -5,7 +5,7 @@ Also see [`CONTRIBUTING.md`](CONTRIBUTING.md) for what to do before opening a PR ## Crate overview -The crates in this repository are: `egui, emath, epaint, egui_extras, egui_plot, egui-winit, egui_glow, egui_demo_lib, egui_demo_app`. +The crates in this repository are: `egui, emath, epaint, egui_extras, egui-winit, egui_glow, egui_demo_lib, egui_demo_app`. ### `egui`: The main GUI library. Example code: `if ui.button("Click me").clicked() { … }` @@ -24,9 +24,6 @@ Depends on `emath`. ### `egui_extras` This adds additional features on top of `egui`. -### `egui_plot` -Plotting for `egui`. - ### `egui-winit` This crates provides bindings between [`egui`](https://github.com/emilk/egui) and [winit](https://crates.io/crates/winit). diff --git a/CHANGELOG.md b/CHANGELOG.md index fec256ebf6d..ac42b232bff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # egui changelog All notable changes to the `egui` crate will be documented in this file. -NOTE: this is just the changelog for the core `egui` crate. [`eframe`](crates/eframe/CHANGELOG.md), [`egui_plot`](crates/egui_plot/CHANGELOG.md), [`ecolor`](crates/ecolor/CHANGELOG.md), [`epaint`](crates/epaint/CHANGELOG.md), [`egui-winit`](crates/egui-winit/CHANGELOG.md), [`egui_glow`](crates/egui_glow/CHANGELOG.md) and [`egui-wgpu`](crates/egui-wgpu/CHANGELOG.md) have their own changelogs! +NOTE: this is just the changelog for the core `egui` crate. [`eframe`](crates/eframe/CHANGELOG.md), [`ecolor`](crates/ecolor/CHANGELOG.md), [`epaint`](crates/epaint/CHANGELOG.md), [`egui-winit`](crates/egui-winit/CHANGELOG.md), [`egui_glow`](crates/egui_glow/CHANGELOG.md) and [`egui-wgpu`](crates/egui-wgpu/CHANGELOG.md) have their own changelogs! This file is updated upon each release. Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. diff --git a/CODEOWNERS b/CODEOWNERS index 432612e90d1..40b72a3140a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,2 +1 @@ -/crates/egui_plot @Bromeon @EmbersArc /crates/egui-wgpu @Wumpf diff --git a/Cargo.lock b/Cargo.lock index 12c7560b907..8cc7f53f362 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1059,15 +1059,6 @@ dependencies = [ "env_logger", ] -[[package]] -name = "custom_plot_manipulation" -version = "0.1.0" -dependencies = [ - "eframe", - "egui_plot", - "env_logger", -] - [[package]] name = "custom_window_frame" version = "0.1.0" @@ -1302,7 +1293,6 @@ dependencies = [ "document-features", "egui", "egui_extras", - "egui_plot", "log", "serde", "unicode_names2", @@ -1348,17 +1338,6 @@ dependencies = [ "winit", ] -[[package]] -name = "egui_plot" -version = "0.28.1" -dependencies = [ - "ahash", - "document-features", - "egui", - "emath", - "serde", -] - [[package]] name = "ehttp" version = "0.5.0" @@ -3288,17 +3267,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "save_plot" -version = "0.1.0" -dependencies = [ - "eframe", - "egui_plot", - "env_logger", - "image", - "rfd", -] - [[package]] name = "scoped-tls" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index 77416706487..a8575f468be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,6 @@ members = [ "crates/egui_demo_lib", "crates/egui_extras", "crates/egui_glow", - "crates/egui_plot", "crates/egui-wgpu", "crates/egui-winit", "crates/egui", @@ -58,7 +57,6 @@ emath = { version = "0.28.1", path = "crates/emath", default-features = false } ecolor = { version = "0.28.1", path = "crates/ecolor", default-features = false } epaint = { version = "0.28.1", path = "crates/epaint", default-features = false } egui = { version = "0.28.1", path = "crates/egui", default-features = false } -egui_plot = { version = "0.28.1", path = "crates/egui_plot", default-features = false } egui-winit = { version = "0.28.1", path = "crates/egui-winit", default-features = false } egui_extras = { version = "0.28.1", path = "crates/egui_extras", default-features = false } egui-wgpu = { version = "0.28.1", path = "crates/egui-wgpu", default-features = false } diff --git a/RELEASES.md b/RELEASES.md index c173d934941..055de76ceb6 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -44,6 +44,7 @@ We don't update the MSRV in a patch release, unless we really, really need to. - check the in-browser profiler * [ ] check the color test * [ ] update `eframe_template` and test +* [ ] update `egui_plot` and test * [ ] update `egui_tiles` and test * [ ] test with Rerun * [ ] `./scripts/check.sh` @@ -76,7 +77,6 @@ I usually do this all on the `master` branch, but doing it in a release branch i (cd crates/ecolor && cargo publish --quiet) && echo "✅ ecolor" (cd crates/epaint && cargo publish --quiet) && echo "✅ epaint" (cd crates/egui && cargo publish --quiet) && echo "✅ egui" -(cd crates/egui_plot && cargo publish --quiet) && echo "✅ egui_plot" (cd crates/egui-winit && cargo publish --quiet) && echo "✅ egui-winit" (cd crates/egui_extras && cargo publish --quiet) && echo "✅ egui_extras" (cd crates/egui-wgpu && cargo publish --quiet) && echo "✅ egui-wgpu" @@ -94,4 +94,5 @@ I usually do this all on the `master` branch, but doing it in a release branch i ## After release * [ ] publish new `eframe_template` +* [ ] publish new `egui_plot` * [ ] publish new `egui_tiles` diff --git a/crates/egui_demo_lib/Cargo.toml b/crates/egui_demo_lib/Cargo.toml index 7da0cbf85bd..88c2e85557c 100644 --- a/crates/egui_demo_lib/Cargo.toml +++ b/crates/egui_demo_lib/Cargo.toml @@ -34,7 +34,7 @@ default = [] chrono = ["egui_extras/datepicker", "dep:chrono"] ## Allow serialization using [`serde`](https://docs.rs/serde). -serde = ["egui/serde", "egui_plot/serde", "dep:serde", "egui_extras/serde"] +serde = ["egui/serde", "dep:serde", "egui_extras/serde"] ## Enable better syntax highlighting using [`syntect`](https://docs.rs/syntect). syntect = ["egui_extras/syntect"] @@ -43,7 +43,6 @@ syntect = ["egui_extras/syntect"] [dependencies] egui = { workspace = true, default-features = false, features = ["color-hex"] } egui_extras = { workspace = true, features = ["default"] } -egui_plot = { workspace = true, features = ["default"] } log.workspace = true unicode_names2 = { version = "0.6.0", default-features = false } # this old version has fewer dependencies diff --git a/crates/egui_demo_lib/src/demo/context_menu.rs b/crates/egui_demo_lib/src/demo/context_menu.rs index 61cf2c18b73..2dce3e76304 100644 --- a/crates/egui_demo_lib/src/demo/context_menu.rs +++ b/crates/egui_demo_lib/src/demo/context_menu.rs @@ -1,51 +1,6 @@ -use egui::Vec2b; - -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Default, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -enum Plot { - Sin, - Bell, - Sigmoid, -} - -fn gaussian(x: f64) -> f64 { - let var: f64 = 2.0; - f64::exp(-(x / var).powi(2)) / (var * f64::sqrt(std::f64::consts::TAU)) -} - -fn sigmoid(x: f64) -> f64 { - -1.0 + 2.0 / (1.0 + f64::exp(-x)) -} - -#[derive(Clone, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct ContextMenus { - plot: Plot, - show_axes: Vec2b, - allow_drag: bool, - allow_zoom: bool, - allow_scroll: bool, - center_x_axis: bool, - center_y_axis: bool, - width: f32, - height: f32, -} - -impl Default for ContextMenus { - fn default() -> Self { - Self { - plot: Plot::Sin, - show_axes: Vec2b::TRUE, - allow_drag: true, - allow_zoom: true, - allow_scroll: true, - center_x_axis: false, - center_y_axis: false, - width: 400.0, - height: 200.0, - } - } -} +pub struct ContextMenus {} impl crate::Demo for ContextMenus { fn name(&self) -> &'static str { @@ -66,8 +21,10 @@ impl crate::View for ContextMenus { fn ui(&mut self, ui: &mut egui::Ui) { ui.horizontal(|ui| { ui.menu_button("Click for menu", Self::nested_menus); + ui.button("Right-click for menu") .context_menu(Self::nested_menus); + if ui.ctx().is_context_menu_open() { ui.label("Context menu is open"); } else { @@ -75,49 +32,6 @@ impl crate::View for ContextMenus { } }); - ui.separator(); - - ui.label("Right-click plot to edit it!"); - ui.horizontal(|ui| { - self.example_plot(ui).context_menu(|ui| { - ui.menu_button("Plot", |ui| { - if ui.radio_value(&mut self.plot, Plot::Sin, "Sin").clicked() - || ui - .radio_value(&mut self.plot, Plot::Bell, "Gaussian") - .clicked() - || ui - .radio_value(&mut self.plot, Plot::Sigmoid, "Sigmoid") - .clicked() - { - ui.close_menu(); - } - }); - egui::Grid::new("button_grid").show(ui, |ui| { - ui.add( - egui::DragValue::new(&mut self.width) - .range(0.0..=f32::INFINITY) - .speed(1.0) - .prefix("Width: "), - ); - ui.add( - egui::DragValue::new(&mut self.height) - .range(0.0..=f32::INFINITY) - .speed(1.0) - .prefix("Height: "), - ); - ui.end_row(); - ui.checkbox(&mut self.show_axes[0], "x-Axis"); - ui.checkbox(&mut self.show_axes[1], "y-Axis"); - ui.end_row(); - if ui.checkbox(&mut self.allow_drag, "Drag").changed() - || ui.checkbox(&mut self.allow_zoom, "Zoom").changed() - || ui.checkbox(&mut self.allow_scroll, "Scroll").changed() - { - ui.close_menu(); - } - }); - }); - }); ui.vertical_centered(|ui| { ui.add(crate::egui_github_link_file!()); }); @@ -125,36 +39,6 @@ impl crate::View for ContextMenus { } impl ContextMenus { - fn example_plot(&self, ui: &mut egui::Ui) -> egui::Response { - use egui_plot::{Line, PlotPoints}; - let n = 128; - let line = Line::new( - (0..=n) - .map(|i| { - use std::f64::consts::TAU; - let x = egui::remap(i as f64, 0.0..=n as f64, -TAU..=TAU); - match self.plot { - Plot::Sin => [x, x.sin()], - Plot::Bell => [x, 10.0 * gaussian(x)], - Plot::Sigmoid => [x, sigmoid(x)], - } - }) - .collect::(), - ); - egui_plot::Plot::new("example_plot") - .show_axes(self.show_axes) - .allow_drag(self.allow_drag) - .allow_zoom(self.allow_zoom) - .allow_scroll(self.allow_scroll) - .center_x_axis(self.center_x_axis) - .center_x_axis(self.center_y_axis) - .width(self.width) - .height(self.height) - .data_aspect(1.0) - .show(ui, |plot_ui| plot_ui.line(line)) - .response - } - fn nested_menus(ui: &mut egui::Ui) { ui.set_max_width(200.0); // To make sure we wrap long text diff --git a/crates/egui_demo_lib/src/demo/demo_app_windows.rs b/crates/egui_demo_lib/src/demo/demo_app_windows.rs index 8856d695a6f..160ab2e6a60 100644 --- a/crates/egui_demo_lib/src/demo/demo_app_windows.rs +++ b/crates/egui_demo_lib/src/demo/demo_app_windows.rs @@ -36,7 +36,6 @@ impl Default for Demos { Box::::default(), Box::::default(), Box::::default(), - Box::::default(), Box::::default(), Box::::default(), Box::::default(), diff --git a/crates/egui_demo_lib/src/demo/mod.rs b/crates/egui_demo_lib/src/demo/mod.rs index a010f670305..e5d7e595931 100644 --- a/crates/egui_demo_lib/src/demo/mod.rs +++ b/crates/egui_demo_lib/src/demo/mod.rs @@ -22,7 +22,6 @@ pub mod painting; pub mod pan_zoom; pub mod panels; pub mod password; -pub mod plot_demo; pub mod scrolling; pub mod sliders; pub mod strip_demo; diff --git a/crates/egui_demo_lib/src/demo/plot_demo.rs b/crates/egui_demo_lib/src/demo/plot_demo.rs deleted file mode 100644 index d9046a0b8be..00000000000 --- a/crates/egui_demo_lib/src/demo/plot_demo.rs +++ /dev/null @@ -1,1083 +0,0 @@ -use std::f64::consts::TAU; -use std::ops::RangeInclusive; - -use egui::*; - -use egui_plot::{ - Arrows, AxisHints, Bar, BarChart, BoxElem, BoxPlot, BoxSpread, CoordinatesFormatter, Corner, - GridInput, GridMark, HLine, Legend, Line, LineStyle, MarkerShape, Plot, PlotImage, PlotPoint, - PlotPoints, PlotResponse, Points, Polygon, Text, VLine, -}; - -// ---------------------------------------------------------------------------- - -#[derive(PartialEq, Eq)] -enum Panel { - Lines, - Markers, - Legend, - Charts, - Items, - Interaction, - CustomAxes, - LinkedAxes, -} - -impl Default for Panel { - fn default() -> Self { - Self::Lines - } -} - -// ---------------------------------------------------------------------------- - -#[derive(PartialEq, Default)] -pub struct PlotDemo { - line_demo: LineDemo, - marker_demo: MarkerDemo, - legend_demo: LegendDemo, - charts_demo: ChartsDemo, - items_demo: ItemsDemo, - interaction_demo: InteractionDemo, - custom_axes_demo: CustomAxesDemo, - linked_axes_demo: LinkedAxesDemo, - open_panel: Panel, -} - -impl crate::Demo for PlotDemo { - fn name(&self) -> &'static str { - "🗠 Plot" - } - - fn show(&mut self, ctx: &Context, open: &mut bool) { - use crate::View as _; - Window::new(self.name()) - .open(open) - .default_size(vec2(400.0, 400.0)) - .vscroll(false) - .show(ctx, |ui| self.ui(ui)); - } -} - -impl crate::View for PlotDemo { - fn ui(&mut self, ui: &mut Ui) { - ui.horizontal(|ui| { - egui::reset_button(ui, self, "Reset"); - ui.collapsing("Instructions", |ui| { - ui.label("Pan by dragging, or scroll (+ shift = horizontal)."); - ui.label("Box zooming: Right click to zoom in and zoom out using a selection."); - if cfg!(target_arch = "wasm32") { - ui.label("Zoom with ctrl / ⌘ + pointer wheel, or with pinch gesture."); - } else if cfg!(target_os = "macos") { - ui.label("Zoom with ctrl / ⌘ + scroll."); - } else { - ui.label("Zoom with ctrl + scroll."); - } - ui.label("Reset view with double-click."); - }); - ui.add(crate::egui_github_link_file!()); - }); - ui.separator(); - ui.horizontal(|ui| { - ui.selectable_value(&mut self.open_panel, Panel::Lines, "Lines"); - ui.selectable_value(&mut self.open_panel, Panel::Markers, "Markers"); - ui.selectable_value(&mut self.open_panel, Panel::Legend, "Legend"); - ui.selectable_value(&mut self.open_panel, Panel::Charts, "Charts"); - ui.selectable_value(&mut self.open_panel, Panel::Items, "Items"); - ui.selectable_value(&mut self.open_panel, Panel::Interaction, "Interaction"); - ui.selectable_value(&mut self.open_panel, Panel::CustomAxes, "Custom Axes"); - ui.selectable_value(&mut self.open_panel, Panel::LinkedAxes, "Linked Axes"); - }); - ui.separator(); - - match self.open_panel { - Panel::Lines => { - self.line_demo.ui(ui); - } - Panel::Markers => { - self.marker_demo.ui(ui); - } - Panel::Legend => { - self.legend_demo.ui(ui); - } - Panel::Charts => { - self.charts_demo.ui(ui); - } - Panel::Items => { - self.items_demo.ui(ui); - } - Panel::Interaction => { - self.interaction_demo.ui(ui); - } - Panel::CustomAxes => { - self.custom_axes_demo.ui(ui); - } - Panel::LinkedAxes => { - self.linked_axes_demo.ui(ui); - } - } - } -} - -// ---------------------------------------------------------------------------- - -#[derive(Copy, Clone, PartialEq)] -struct LineDemo { - animate: bool, - time: f64, - circle_radius: f64, - circle_center: Pos2, - square: bool, - proportional: bool, - coordinates: bool, - show_axes: bool, - show_grid: bool, - line_style: LineStyle, -} - -impl Default for LineDemo { - fn default() -> Self { - Self { - animate: !cfg!(debug_assertions), - time: 0.0, - circle_radius: 1.5, - circle_center: Pos2::new(0.0, 0.0), - square: false, - proportional: true, - coordinates: true, - show_axes: true, - show_grid: true, - line_style: LineStyle::Solid, - } - } -} - -impl LineDemo { - fn options_ui(&mut self, ui: &mut Ui) { - let Self { - animate, - time: _, - circle_radius, - circle_center, - square, - proportional, - coordinates, - show_axes, - show_grid, - line_style, - } = self; - - ui.horizontal(|ui| { - ui.group(|ui| { - ui.vertical(|ui| { - ui.label("Circle:"); - ui.add( - egui::DragValue::new(circle_radius) - .speed(0.1) - .range(0.0..=f64::INFINITY) - .prefix("r: "), - ); - ui.horizontal(|ui| { - ui.add( - egui::DragValue::new(&mut circle_center.x) - .speed(0.1) - .prefix("x: "), - ); - ui.add( - egui::DragValue::new(&mut circle_center.y) - .speed(1.0) - .prefix("y: "), - ); - }); - }); - }); - - ui.vertical(|ui| { - ui.checkbox(show_axes, "Show axes"); - ui.checkbox(show_grid, "Show grid"); - ui.checkbox(coordinates, "Show coordinates on hover") - .on_hover_text("Can take a custom formatting function."); - }); - - ui.vertical(|ui| { - ui.style_mut().wrap_mode = Some(TextWrapMode::Extend); - ui.checkbox(animate, "Animate"); - ui.checkbox(square, "Square view") - .on_hover_text("Always keep the viewport square."); - ui.checkbox(proportional, "Proportional data axes") - .on_hover_text("Tick are the same size on both axes."); - - ComboBox::from_label("Line style") - .selected_text(line_style.to_string()) - .show_ui(ui, |ui| { - for style in &[ - LineStyle::Solid, - LineStyle::dashed_dense(), - LineStyle::dashed_loose(), - LineStyle::dotted_dense(), - LineStyle::dotted_loose(), - ] { - ui.selectable_value(line_style, *style, style.to_string()); - } - }); - }); - }); - } - - fn circle(&self) -> Line { - let n = 512; - let circle_points: PlotPoints = (0..=n) - .map(|i| { - let t = remap(i as f64, 0.0..=(n as f64), 0.0..=TAU); - let r = self.circle_radius; - [ - r * t.cos() + self.circle_center.x as f64, - r * t.sin() + self.circle_center.y as f64, - ] - }) - .collect(); - Line::new(circle_points) - .color(Color32::from_rgb(100, 200, 100)) - .style(self.line_style) - .name("circle") - } - - fn sin(&self) -> Line { - let time = self.time; - Line::new(PlotPoints::from_explicit_callback( - move |x| 0.5 * (2.0 * x).sin() * time.sin(), - .., - 512, - )) - .color(Color32::from_rgb(200, 100, 100)) - .style(self.line_style) - .name("wave") - } - - fn thingy(&self) -> Line { - let time = self.time; - Line::new(PlotPoints::from_parametric_callback( - move |t| ((2.0 * t + time).sin(), (3.0 * t).sin()), - 0.0..=TAU, - 256, - )) - .color(Color32::from_rgb(100, 150, 250)) - .style(self.line_style) - .name("x = sin(2t), y = sin(3t)") - } -} - -impl LineDemo { - fn ui(&mut self, ui: &mut Ui) -> Response { - self.options_ui(ui); - - if self.animate { - ui.ctx().request_repaint(); - self.time += ui.input(|i| i.unstable_dt).at_most(1.0 / 30.0) as f64; - }; - let mut plot = Plot::new("lines_demo") - .legend(Legend::default()) - .show_axes(self.show_axes) - .show_grid(self.show_grid); - if self.square { - plot = plot.view_aspect(1.0); - } - if self.proportional { - plot = plot.data_aspect(1.0); - } - if self.coordinates { - plot = plot.coordinates_formatter(Corner::LeftBottom, CoordinatesFormatter::default()); - } - plot.show(ui, |plot_ui| { - plot_ui.line(self.circle()); - plot_ui.line(self.sin()); - plot_ui.line(self.thingy()); - }) - .response - } -} - -// ---------------------------------------------------------------------------- - -#[derive(PartialEq)] -struct MarkerDemo { - fill_markers: bool, - marker_radius: f32, - automatic_colors: bool, - marker_color: Color32, -} - -impl Default for MarkerDemo { - fn default() -> Self { - Self { - fill_markers: true, - marker_radius: 5.0, - automatic_colors: true, - marker_color: Color32::GREEN, - } - } -} - -impl MarkerDemo { - fn markers(&self) -> Vec { - MarkerShape::all() - .enumerate() - .map(|(i, marker)| { - let y_offset = i as f64 * 0.5 + 1.0; - let mut points = Points::new(vec![ - [1.0, 0.0 + y_offset], - [2.0, 0.5 + y_offset], - [3.0, 0.0 + y_offset], - [4.0, 0.5 + y_offset], - [5.0, 0.0 + y_offset], - [6.0, 0.5 + y_offset], - ]) - .name(format!("{marker:?}")) - .filled(self.fill_markers) - .radius(self.marker_radius) - .shape(marker); - - if !self.automatic_colors { - points = points.color(self.marker_color); - } - - points - }) - .collect() - } - - fn ui(&mut self, ui: &mut Ui) -> Response { - ui.horizontal(|ui| { - ui.checkbox(&mut self.fill_markers, "Fill"); - ui.add( - egui::DragValue::new(&mut self.marker_radius) - .speed(0.1) - .range(0.0..=f64::INFINITY) - .prefix("Radius: "), - ); - ui.checkbox(&mut self.automatic_colors, "Automatic colors"); - if !self.automatic_colors { - ui.color_edit_button_srgba(&mut self.marker_color); - } - }); - - let markers_plot = Plot::new("markers_demo") - .data_aspect(1.0) - .legend(Legend::default()); - markers_plot - .show(ui, |plot_ui| { - for marker in self.markers() { - plot_ui.points(marker); - } - }) - .response - } -} - -// ---------------------------------------------------------------------------- - -#[derive(Default, PartialEq)] -struct LegendDemo { - config: Legend, -} - -impl LegendDemo { - fn line_with_slope(slope: f64) -> Line { - Line::new(PlotPoints::from_explicit_callback( - move |x| slope * x, - .., - 100, - )) - } - - fn sin() -> Line { - Line::new(PlotPoints::from_explicit_callback( - move |x| x.sin(), - .., - 100, - )) - } - - fn cos() -> Line { - Line::new(PlotPoints::from_explicit_callback( - move |x| x.cos(), - .., - 100, - )) - } - - fn ui(&mut self, ui: &mut Ui) -> Response { - let Self { config } = self; - - egui::Grid::new("settings").show(ui, |ui| { - ui.label("Text style:"); - ui.horizontal(|ui| { - let all_text_styles = ui.style().text_styles(); - for style in all_text_styles { - ui.selectable_value(&mut config.text_style, style.clone(), style.to_string()); - } - }); - ui.end_row(); - - ui.label("Position:"); - ui.horizontal(|ui| { - Corner::all().for_each(|position| { - ui.selectable_value(&mut config.position, position, format!("{position:?}")); - }); - }); - ui.end_row(); - - ui.label("Opacity:"); - ui.add( - egui::DragValue::new(&mut config.background_alpha) - .speed(0.02) - .range(0.0..=1.0), - ); - ui.end_row(); - }); - let legend_plot = Plot::new("legend_demo") - .legend(config.clone()) - .data_aspect(1.0); - legend_plot - .show(ui, |plot_ui| { - plot_ui.line(Self::line_with_slope(0.5).name("lines")); - plot_ui.line(Self::line_with_slope(1.0).name("lines")); - plot_ui.line(Self::line_with_slope(2.0).name("lines")); - plot_ui.line(Self::sin().name("sin(x)")); - plot_ui.line(Self::cos().name("cos(x)")); - }) - .response - } -} - -// ---------------------------------------------------------------------------- - -#[derive(PartialEq, Default)] -struct CustomAxesDemo {} - -impl CustomAxesDemo { - const MINS_PER_DAY: f64 = 24.0 * 60.0; - const MINS_PER_H: f64 = 60.0; - - fn logistic_fn() -> Line { - fn days(min: f64) -> f64 { - CustomAxesDemo::MINS_PER_DAY * min - } - - let values = PlotPoints::from_explicit_callback( - move |x| 1.0 / (1.0 + (-2.5 * (x / Self::MINS_PER_DAY - 2.0)).exp()), - days(0.0)..days(5.0), - 100, - ); - Line::new(values) - } - - #[allow(clippy::needless_pass_by_value)] - fn x_grid(input: GridInput) -> Vec { - // Note: this always fills all possible marks. For optimization, `input.bounds` - // could be used to decide when the low-interval grids (minutes) should be added. - - let mut marks = vec![]; - - let (min, max) = input.bounds; - let min = min.floor() as i32; - let max = max.ceil() as i32; - - for i in min..=max { - let step_size = if i % Self::MINS_PER_DAY as i32 == 0 { - // 1 day - Self::MINS_PER_DAY - } else if i % Self::MINS_PER_H as i32 == 0 { - // 1 hour - Self::MINS_PER_H - } else if i % 5 == 0 { - // 5min - 5.0 - } else { - // skip grids below 5min - continue; - }; - - marks.push(GridMark { - value: i as f64, - step_size, - }); - } - - marks - } - - #[allow(clippy::unused_self)] - fn ui(&mut self, ui: &mut Ui) -> Response { - const MINS_PER_DAY: f64 = CustomAxesDemo::MINS_PER_DAY; - const MINS_PER_H: f64 = CustomAxesDemo::MINS_PER_H; - - fn day(x: f64) -> f64 { - (x / MINS_PER_DAY).floor() - } - - fn hour(x: f64) -> f64 { - (x.rem_euclid(MINS_PER_DAY) / MINS_PER_H).floor() - } - - fn minute(x: f64) -> f64 { - x.rem_euclid(MINS_PER_H).floor() - } - - fn percent(y: f64) -> f64 { - 100.0 * y - } - - let time_formatter = |mark: GridMark, _range: &RangeInclusive| { - let minutes = mark.value; - if minutes < 0.0 || 5.0 * MINS_PER_DAY <= minutes { - // No labels outside value bounds - String::new() - } else if is_approx_integer(minutes / MINS_PER_DAY) { - // Days - format!("Day {}", day(minutes)) - } else { - // Hours and minutes - format!("{h}:{m:02}", h = hour(minutes), m = minute(minutes)) - } - }; - - let percentage_formatter = |mark: GridMark, _range: &RangeInclusive| { - let percent = 100.0 * mark.value; - if is_approx_zero(percent) { - String::new() // skip zero - } else if is_approx_integer(percent) { - // Display only integer percentages - format!("{percent:.0}%") - } else { - String::new() - } - }; - - let label_fmt = |_s: &str, val: &PlotPoint| { - format!( - "Day {d}, {h}:{m:02}\n{p:.2}%", - d = day(val.x), - h = hour(val.x), - m = minute(val.x), - p = percent(val.y) - ) - }; - - ui.label("Zoom in on the X-axis to see hours and minutes"); - - let x_axes = vec![ - AxisHints::new_x().label("Time").formatter(time_formatter), - AxisHints::new_x().label("Value"), - ]; - let y_axes = vec![ - AxisHints::new_y() - .label("Percent") - .formatter(percentage_formatter), - AxisHints::new_y() - .label("Absolute") - .placement(egui_plot::HPlacement::Right), - ]; - Plot::new("custom_axes") - .data_aspect(2.0 * MINS_PER_DAY as f32) - .custom_x_axes(x_axes) - .custom_y_axes(y_axes) - .x_grid_spacer(Self::x_grid) - .label_formatter(label_fmt) - .show(ui, |plot_ui| { - plot_ui.line(Self::logistic_fn()); - }) - .response - } -} - -// ---------------------------------------------------------------------------- - -#[derive(PartialEq)] -struct LinkedAxesDemo { - link_x: bool, - link_y: bool, - link_cursor_x: bool, - link_cursor_y: bool, -} - -impl Default for LinkedAxesDemo { - fn default() -> Self { - Self { - link_x: true, - link_y: true, - link_cursor_x: true, - link_cursor_y: true, - } - } -} - -impl LinkedAxesDemo { - fn line_with_slope(slope: f64) -> Line { - Line::new(PlotPoints::from_explicit_callback( - move |x| slope * x, - .., - 100, - )) - } - - fn sin() -> Line { - Line::new(PlotPoints::from_explicit_callback( - move |x| x.sin(), - .., - 100, - )) - } - - fn cos() -> Line { - Line::new(PlotPoints::from_explicit_callback( - move |x| x.cos(), - .., - 100, - )) - } - - fn configure_plot(plot_ui: &mut egui_plot::PlotUi) { - plot_ui.line(Self::line_with_slope(0.5)); - plot_ui.line(Self::line_with_slope(1.0)); - plot_ui.line(Self::line_with_slope(2.0)); - plot_ui.line(Self::sin()); - plot_ui.line(Self::cos()); - } - - fn ui(&mut self, ui: &mut Ui) -> Response { - ui.horizontal(|ui| { - ui.label("Linked axes:"); - ui.checkbox(&mut self.link_x, "X"); - ui.checkbox(&mut self.link_y, "Y"); - }); - ui.horizontal(|ui| { - ui.label("Linked cursors:"); - ui.checkbox(&mut self.link_cursor_x, "X"); - ui.checkbox(&mut self.link_cursor_y, "Y"); - }); - - let link_group_id = ui.id().with("linked_demo"); - ui.horizontal(|ui| { - Plot::new("left-top") - .data_aspect(1.0) - .width(250.0) - .height(250.0) - .link_axis(link_group_id, self.link_x, self.link_y) - .link_cursor(link_group_id, self.link_cursor_x, self.link_cursor_y) - .show(ui, Self::configure_plot); - Plot::new("right-top") - .data_aspect(2.0) - .width(150.0) - .height(250.0) - .y_axis_label("y") - .y_axis_position(egui_plot::HPlacement::Right) - .link_axis(link_group_id, self.link_x, self.link_y) - .link_cursor(link_group_id, self.link_cursor_x, self.link_cursor_y) - .show(ui, Self::configure_plot); - }); - Plot::new("left-bottom") - .data_aspect(0.5) - .width(250.0) - .height(150.0) - .x_axis_label("x") - .link_axis(link_group_id, self.link_x, self.link_y) - .link_cursor(link_group_id, self.link_cursor_x, self.link_cursor_y) - .show(ui, Self::configure_plot) - .response - } -} - -// ---------------------------------------------------------------------------- - -#[derive(PartialEq, Default)] -struct ItemsDemo { - texture: Option, -} - -impl ItemsDemo { - fn ui(&mut self, ui: &mut Ui) -> Response { - let n = 100; - let mut sin_values: Vec<_> = (0..=n) - .map(|i| remap(i as f64, 0.0..=n as f64, -TAU..=TAU)) - .map(|i| [i, i.sin()]) - .collect(); - - let line = Line::new(sin_values.split_off(n / 2)).fill(-1.5); - let polygon = Polygon::new(PlotPoints::from_parametric_callback( - |t| (4.0 * t.sin() + 2.0 * t.cos(), 4.0 * t.cos() + 2.0 * t.sin()), - 0.0..TAU, - 100, - )); - let points = Points::new(sin_values).stems(-1.5).radius(1.0); - - let arrows = { - let pos_radius = 8.0; - let tip_radius = 7.0; - let arrow_origins = PlotPoints::from_parametric_callback( - |t| (pos_radius * t.sin(), pos_radius * t.cos()), - 0.0..TAU, - 36, - ); - let arrow_tips = PlotPoints::from_parametric_callback( - |t| (tip_radius * t.sin(), tip_radius * t.cos()), - 0.0..TAU, - 36, - ); - Arrows::new(arrow_origins, arrow_tips) - }; - - let texture: &egui::TextureHandle = self.texture.get_or_insert_with(|| { - ui.ctx() - .load_texture("plot_demo", egui::ColorImage::example(), Default::default()) - }); - let image = PlotImage::new( - texture, - PlotPoint::new(0.0, 10.0), - 5.0 * vec2(texture.aspect_ratio(), 1.0), - ); - - let plot = Plot::new("items_demo") - .legend(Legend::default().position(Corner::RightBottom)) - .show_x(false) - .show_y(false) - .data_aspect(1.0); - plot.show(ui, |plot_ui| { - plot_ui.hline(HLine::new(9.0).name("Lines horizontal")); - plot_ui.hline(HLine::new(-9.0).name("Lines horizontal")); - plot_ui.vline(VLine::new(9.0).name("Lines vertical")); - plot_ui.vline(VLine::new(-9.0).name("Lines vertical")); - plot_ui.line(line.name("Line with fill")); - plot_ui.polygon(polygon.name("Convex polygon")); - plot_ui.points(points.name("Points with stems")); - plot_ui.text(Text::new(PlotPoint::new(-3.0, -3.0), "wow").name("Text")); - plot_ui.text(Text::new(PlotPoint::new(-2.0, 2.5), "so graph").name("Text")); - plot_ui.text(Text::new(PlotPoint::new(3.0, 3.0), "much color").name("Text")); - plot_ui.text(Text::new(PlotPoint::new(2.5, -2.0), "such plot").name("Text")); - plot_ui.image(image.name("Image")); - plot_ui.arrows(arrows.name("Arrows")); - }) - .response - } -} - -// ---------------------------------------------------------------------------- - -#[derive(Default, PartialEq)] -struct InteractionDemo {} - -impl InteractionDemo { - #[allow(clippy::unused_self)] - fn ui(&mut self, ui: &mut Ui) -> Response { - let id = ui.make_persistent_id("interaction_demo"); - - // This demonstrates how to read info about the plot _before_ showing it: - let plot_memory = egui_plot::PlotMemory::load(ui.ctx(), id); - if let Some(plot_memory) = plot_memory { - let bounds = plot_memory.bounds(); - ui.label(format!( - "plot bounds: min: {:.02?}, max: {:.02?}", - bounds.min(), - bounds.max() - )); - } - - let plot = Plot::new("interaction_demo").id(id).height(300.0); - - let PlotResponse { - response, - inner: (screen_pos, pointer_coordinate, pointer_coordinate_drag_delta, bounds, hovered), - hovered_plot_item, - .. - } = plot.show(ui, |plot_ui| { - plot_ui.line( - Line::new(PlotPoints::from_explicit_callback( - move |x| x.sin(), - .., - 100, - )) - .color(Color32::RED) - .id(egui::Id::new("sin")), - ); - plot_ui.line( - Line::new(PlotPoints::from_explicit_callback( - move |x| x.cos(), - .., - 100, - )) - .color(Color32::BLUE) - .id(egui::Id::new("cos")), - ); - - ( - plot_ui.screen_from_plot(PlotPoint::new(0.0, 0.0)), - plot_ui.pointer_coordinate(), - plot_ui.pointer_coordinate_drag_delta(), - plot_ui.plot_bounds(), - plot_ui.response().hovered(), - ) - }); - - ui.label(format!( - "plot bounds: min: {:.02?}, max: {:.02?}", - bounds.min(), - bounds.max() - )); - ui.label(format!( - "origin in screen coordinates: x: {:.02}, y: {:.02}", - screen_pos.x, screen_pos.y - )); - ui.label(format!("plot hovered: {hovered}")); - let coordinate_text = if let Some(coordinate) = pointer_coordinate { - format!("x: {:.02}, y: {:.02}", coordinate.x, coordinate.y) - } else { - "None".to_owned() - }; - ui.label(format!("pointer coordinate: {coordinate_text}")); - let coordinate_text = format!( - "x: {:.02}, y: {:.02}", - pointer_coordinate_drag_delta.x, pointer_coordinate_drag_delta.y - ); - ui.label(format!("pointer coordinate drag delta: {coordinate_text}")); - - let hovered_item = if hovered_plot_item == Some(egui::Id::new("sin")) { - "red sin" - } else if hovered_plot_item == Some(egui::Id::new("cos")) { - "blue cos" - } else { - "none" - }; - ui.label(format!("hovered plot item: {hovered_item}")); - - response - } -} - -// ---------------------------------------------------------------------------- - -#[derive(PartialEq, Eq)] -enum Chart { - GaussBars, - StackedBars, - BoxPlot, -} - -impl Default for Chart { - fn default() -> Self { - Self::GaussBars - } -} - -#[derive(PartialEq)] -struct ChartsDemo { - chart: Chart, - vertical: bool, - allow_zoom: Vec2b, - allow_drag: Vec2b, - allow_scroll: Vec2b, -} - -impl Default for ChartsDemo { - fn default() -> Self { - Self { - vertical: true, - chart: Chart::default(), - allow_zoom: true.into(), - allow_drag: true.into(), - allow_scroll: true.into(), - } - } -} - -impl ChartsDemo { - fn ui(&mut self, ui: &mut Ui) -> Response { - ui.horizontal(|ui| { - ui.vertical(|ui| { - ui.label("Type:"); - ui.horizontal(|ui| { - ui.selectable_value(&mut self.chart, Chart::GaussBars, "Histogram"); - ui.selectable_value(&mut self.chart, Chart::StackedBars, "Stacked Bar Chart"); - ui.selectable_value(&mut self.chart, Chart::BoxPlot, "Box Plot"); - }); - ui.label("Orientation:"); - ui.horizontal(|ui| { - ui.selectable_value(&mut self.vertical, true, "Vertical"); - ui.selectable_value(&mut self.vertical, false, "Horizontal"); - }); - }); - ui.vertical(|ui| { - ui.group(|ui| { - ui.add_enabled_ui(self.chart != Chart::StackedBars, |ui| { - ui.horizontal(|ui| { - ui.label("Allow zoom:"); - ui.checkbox(&mut self.allow_zoom.x, "X"); - ui.checkbox(&mut self.allow_zoom.y, "Y"); - }); - }); - ui.horizontal(|ui| { - ui.label("Allow drag:"); - ui.checkbox(&mut self.allow_drag.x, "X"); - ui.checkbox(&mut self.allow_drag.y, "Y"); - }); - ui.horizontal(|ui| { - ui.label("Allow scroll:"); - ui.checkbox(&mut self.allow_scroll.x, "X"); - ui.checkbox(&mut self.allow_scroll.y, "Y"); - }); - }); - }); - }); - match self.chart { - Chart::GaussBars => self.bar_gauss(ui), - Chart::StackedBars => self.bar_stacked(ui), - Chart::BoxPlot => self.box_plot(ui), - } - } - - fn bar_gauss(&self, ui: &mut Ui) -> Response { - let mut chart = BarChart::new( - (-395..=395) - .step_by(10) - .map(|x| x as f64 * 0.01) - .map(|x| { - ( - x, - (-x * x / 2.0).exp() / (2.0 * std::f64::consts::PI).sqrt(), - ) - }) - // The 10 factor here is purely for a nice 1:1 aspect ratio - .map(|(x, f)| Bar::new(x, f * 10.0).width(0.095)) - .collect(), - ) - .color(Color32::LIGHT_BLUE) - .name("Normal Distribution"); - if !self.vertical { - chart = chart.horizontal(); - } - - Plot::new("Normal Distribution Demo") - .legend(Legend::default()) - .clamp_grid(true) - .allow_zoom(self.allow_zoom) - .allow_drag(self.allow_drag) - .allow_scroll(self.allow_scroll) - .show(ui, |plot_ui| plot_ui.bar_chart(chart)) - .response - } - - fn bar_stacked(&self, ui: &mut Ui) -> Response { - let mut chart1 = BarChart::new(vec![ - Bar::new(0.5, 1.0).name("Day 1"), - Bar::new(1.5, 3.0).name("Day 2"), - Bar::new(2.5, 1.0).name("Day 3"), - Bar::new(3.5, 2.0).name("Day 4"), - Bar::new(4.5, 4.0).name("Day 5"), - ]) - .width(0.7) - .name("Set 1"); - - let mut chart2 = BarChart::new(vec![ - Bar::new(0.5, 1.0), - Bar::new(1.5, 1.5), - Bar::new(2.5, 0.1), - Bar::new(3.5, 0.7), - Bar::new(4.5, 0.8), - ]) - .width(0.7) - .name("Set 2") - .stack_on(&[&chart1]); - - let mut chart3 = BarChart::new(vec![ - Bar::new(0.5, -0.5), - Bar::new(1.5, 1.0), - Bar::new(2.5, 0.5), - Bar::new(3.5, -1.0), - Bar::new(4.5, 0.3), - ]) - .width(0.7) - .name("Set 3") - .stack_on(&[&chart1, &chart2]); - - let mut chart4 = BarChart::new(vec![ - Bar::new(0.5, 0.5), - Bar::new(1.5, 1.0), - Bar::new(2.5, 0.5), - Bar::new(3.5, -0.5), - Bar::new(4.5, -0.5), - ]) - .width(0.7) - .name("Set 4") - .stack_on(&[&chart1, &chart2, &chart3]); - - if !self.vertical { - chart1 = chart1.horizontal(); - chart2 = chart2.horizontal(); - chart3 = chart3.horizontal(); - chart4 = chart4.horizontal(); - } - - Plot::new("Stacked Bar Chart Demo") - .legend(Legend::default()) - .data_aspect(1.0) - .allow_drag(self.allow_drag) - .show(ui, |plot_ui| { - plot_ui.bar_chart(chart1); - plot_ui.bar_chart(chart2); - plot_ui.bar_chart(chart3); - plot_ui.bar_chart(chart4); - }) - .response - } - - fn box_plot(&self, ui: &mut Ui) -> Response { - let yellow = Color32::from_rgb(248, 252, 168); - let mut box1 = BoxPlot::new(vec![ - BoxElem::new(0.5, BoxSpread::new(1.5, 2.2, 2.5, 2.6, 3.1)).name("Day 1"), - BoxElem::new(2.5, BoxSpread::new(0.4, 1.0, 1.1, 1.4, 2.1)).name("Day 2"), - BoxElem::new(4.5, BoxSpread::new(1.7, 2.0, 2.2, 2.5, 2.9)).name("Day 3"), - ]) - .name("Experiment A"); - - let mut box2 = BoxPlot::new(vec![ - BoxElem::new(1.0, BoxSpread::new(0.2, 0.5, 1.0, 2.0, 2.7)).name("Day 1"), - BoxElem::new(3.0, BoxSpread::new(1.5, 1.7, 2.1, 2.9, 3.3)) - .name("Day 2: interesting") - .stroke(Stroke::new(1.5, yellow)) - .fill(yellow.linear_multiply(0.2)), - BoxElem::new(5.0, BoxSpread::new(1.3, 2.0, 2.3, 2.9, 4.0)).name("Day 3"), - ]) - .name("Experiment B"); - - let mut box3 = BoxPlot::new(vec![ - BoxElem::new(1.5, BoxSpread::new(2.1, 2.2, 2.6, 2.8, 3.0)).name("Day 1"), - BoxElem::new(3.5, BoxSpread::new(1.3, 1.5, 1.9, 2.2, 2.4)).name("Day 2"), - BoxElem::new(5.5, BoxSpread::new(0.2, 0.4, 1.0, 1.3, 1.5)).name("Day 3"), - ]) - .name("Experiment C"); - - if !self.vertical { - box1 = box1.horizontal(); - box2 = box2.horizontal(); - box3 = box3.horizontal(); - } - - Plot::new("Box Plot Demo") - .legend(Legend::default()) - .allow_zoom(self.allow_zoom) - .allow_drag(self.allow_drag) - .show(ui, |plot_ui| { - plot_ui.box_plot(box1); - plot_ui.box_plot(box2); - plot_ui.box_plot(box3); - }) - .response - } -} - -fn is_approx_zero(val: f64) -> bool { - val.abs() < 1e-6 -} - -fn is_approx_integer(val: f64) -> bool { - val.fract().abs() < 1e-6 -} diff --git a/crates/egui_demo_lib/src/demo/widget_gallery.rs b/crates/egui_demo_lib/src/demo/widget_gallery.rs index 2f59c7caee0..f942c8626d6 100644 --- a/crates/egui_demo_lib/src/demo/widget_gallery.rs +++ b/crates/egui_demo_lib/src/demo/widget_gallery.rs @@ -248,10 +248,6 @@ impl WidgetGallery { }); ui.end_row(); - ui.add(doc_link_label_with_crate("egui_plot", "Plot", "plot")); - example_plot(ui); - ui.end_row(); - ui.hyperlink_to( "Custom widget:", super::toggle_switch::url_to_file_source_code(), @@ -264,25 +260,6 @@ impl WidgetGallery { } } -fn example_plot(ui: &mut egui::Ui) -> egui::Response { - use egui_plot::{Line, PlotPoints}; - let n = 128; - let line_points: PlotPoints = (0..=n) - .map(|i| { - use std::f64::consts::TAU; - let x = egui::remap(i as f64, 0.0..=n as f64, -TAU..=TAU); - [x, x.sin()] - }) - .collect(); - let line = Line::new(line_points); - egui_plot::Plot::new("example_plot") - .height(32.0) - .show_axes(false) - .data_aspect(1.0) - .show(ui, |plot_ui| plot_ui.line(line)) - .response -} - fn doc_link_label<'a>(title: &'a str, search_term: &'a str) -> impl egui::Widget + 'a { doc_link_label_with_crate("egui", title, search_term) } diff --git a/crates/egui_plot/CHANGELOG.md b/crates/egui_plot/CHANGELOG.md deleted file mode 100644 index d94eb7978f7..00000000000 --- a/crates/egui_plot/CHANGELOG.md +++ /dev/null @@ -1,92 +0,0 @@ -# Changelog for egui_plot -All notable changes to the `egui_plot` integration will be noted in this file. - -This file is updated upon each release. -Changes since the last release can be found at or by running the `scripts/generate_changelog.py` script. - - -## 0.28.1 - 2024-07-05 -Nothing new - - -## 0.28.0 - 2024-07-03 -### ⭐ Added -* Hide all other series when alt-clicking in the legend [#4549](https://github.com/emilk/egui/pull/4549) by [@abey79](https://github.com/abey79) - -### 🔧 Changed -* `Plot::Items:allow_hover` give possibility to masked the interaction on hovered item [#2558](https://github.com/emilk/egui/pull/2558) by [@haricot](https://github.com/haricot) -* Expose `ClosestElem` and `PlotConfig` [#4380](https://github.com/emilk/egui/pull/4380) by [@Narcha](https://github.com/Narcha) -* Introduce lifetime to `egui_plot::Plot` to replace `'static` fields [#4435](https://github.com/emilk/egui/pull/4435) by [@Fabus1184](https://github.com/Fabus1184) -* Plot now respects the `interact_radius` set in the UI's style [#4520](https://github.com/emilk/egui/pull/4520) by [@YgorSouza](https://github.com/YgorSouza) -* Improve behavior of plot auto-bounds with reduced data [#4632](https://github.com/emilk/egui/pull/4632) by [@abey79](https://github.com/abey79) -* Improve default formatter of tick-marks [#4738](https://github.com/emilk/egui/pull/4738) by [@emilk](https://github.com/emilk) - -### 🐛 Fixed -* Disable interaction for `ScrollArea` and `Plot` when UI is disabled [#4457](https://github.com/emilk/egui/pull/4457) by [@varphone](https://github.com/varphone) -* Make sure plot size is positive [#4429](https://github.com/emilk/egui/pull/4429) by [@rustbasic](https://github.com/rustbasic) -* Use `f64` for translate [#4637](https://github.com/emilk/egui/pull/4637) by [@Its-Just-Nans](https://github.com/Its-Just-Nans) -* Clamp plot zoom values to valid range [#4695](https://github.com/emilk/egui/pull/4695) by [@Its-Just-Nans](https://github.com/Its-Just-Nans) -* Fix plot bounds of empty plots [#4741](https://github.com/emilk/egui/pull/4741) by [@emilk](https://github.com/emilk) - - -## 0.27.2 - 2024-04-02 -* Allow zoom/pan a plot as long as it contains the mouse cursor [#4292](https://github.com/emilk/egui/pull/4292) -* Prevent plot from resetting one axis while zooming/dragging the other [#4252](https://github.com/emilk/egui/pull/4252) (thanks [@YgorSouza](https://github.com/YgorSouza)!) -* egui_plot: Fix the same plot tick label being painted multiple times [#4307](https://github.com/emilk/egui/pull/4307) - - -## 0.27.1 - 2024-03-29 -* Nothing new - - -## 0.27.0 - 2024-03-26 -* Add `sense` option to `Plot` [#4052](https://github.com/emilk/egui/pull/4052) (thanks [@AmesingFlank](https://github.com/AmesingFlank)!) -* Plot widget - allow disabling scroll for x and y separately [#4051](https://github.com/emilk/egui/pull/4051) (thanks [@YgorSouza](https://github.com/YgorSouza)!) -* Fix panic when the base step size is set to 0 [#4078](https://github.com/emilk/egui/pull/4078) (thanks [@abey79](https://github.com/abey79)!) -* Expose `PlotGeometry` in public API [#4193](https://github.com/emilk/egui/pull/4193) (thanks [@dwuertz](https://github.com/dwuertz)!) - - -## 0.26.2 - 2024-02-14 -* Nothing new - - -## 0.26.1 - 2024-02-11 -* Nothing new - - -## 0.26.0 - 2024-02-05 -* Make `egui_plot::PlotMemory` public [#3871](https://github.com/emilk/egui/pull/3871) -* Customizable spacing of grid and axis label spacing [#3896](https://github.com/emilk/egui/pull/3896) -* Change default plot line thickness from 1.0 to 1.5 [#3918](https://github.com/emilk/egui/pull/3918) -* Automatically expand plot axes thickness to fit their labels [#3921](https://github.com/emilk/egui/pull/3921) -* Plot items now have optional id which is returned in the plot's response when hovered [#3920](https://github.com/emilk/egui/pull/3920) (thanks [@Wumpf](https://github.com/Wumpf)!) -* Parallel tessellation with opt-in `rayon` feature [#3934](https://github.com/emilk/egui/pull/3934) -* Make `egui_plot::PlotItem` a public trait [#3943](https://github.com/emilk/egui/pull/3943) -* Fix clip rect for plot items [#3955](https://github.com/emilk/egui/pull/3955) (thanks [@YgorSouza](https://github.com/YgorSouza)!) - - -## 0.25.0 - 2024-01-08 -* Fix plot auto-bounds unset by default [#3722](https://github.com/emilk/egui/pull/3722) (thanks [@abey79](https://github.com/abey79)!) -* Add methods to zoom a `Plot` programmatically [#2714](https://github.com/emilk/egui/pull/2714) (thanks [@YgorSouza](https://github.com/YgorSouza)!) -* Add a public API for overriding plot legend traces' visibilities [#3534](https://github.com/emilk/egui/pull/3534) (thanks [@jayzhudev](https://github.com/jayzhudev)!) - - -## 0.24.1 - 2024-12-03 -* Fix plot auto-bounds default [#3722](https://github.com/emilk/egui/pull/3722) (thanks [@abey79](https://github.com/abey79)!) - - -## 0.24.0 - 2023-11-23 -* Add `emath::Vec2b`, replacing `egui_plot::AxisBools` [#3543](https://github.com/emilk/egui/pull/3543) -* Add `auto_bounds/set_auto_bounds` to `PlotUi` [#3587](https://github.com/emilk/egui/pull/3587) [#3586](https://github.com/emilk/egui/pull/3586) (thanks [@abey79](https://github.com/abey79)!) -* Update MSRV to Rust 1.72 [#3595](https://github.com/emilk/egui/pull/3595) - - -## 0.23.0 - 2023-09-27 - Initial release, after being forked out from `egui` -* Draw axis labels and ticks outside of plotting window [#2284](https://github.com/emilk/egui/pull/2284) (thanks [@JohannesProgrammiert](https://github.com/JohannesProgrammiert)!) -* Add `PlotUi::response()` to replace `plot_clicked()` etc [#3223](https://github.com/emilk/egui/pull/3223) -* Add rotation feature to plot images [#3121](https://github.com/emilk/egui/pull/3121) (thanks [@ThundR67](https://github.com/ThundR67)!) -* Plot items: Image rotation and size in plot coordinates, polygon fill color [#3182](https://github.com/emilk/egui/pull/3182) (thanks [@s-nie](https://github.com/s-nie)!) -* Add method to specify `tip_size` of plot arrows [#3138](https://github.com/emilk/egui/pull/3138) (thanks [@nagua](https://github.com/nagua)!) -* Better handle additive colors in plots [#3387](https://github.com/emilk/egui/pull/3387) -* Fix auto_bounds when only one axis has restricted navigation [#3171](https://github.com/emilk/egui/pull/3171) (thanks [@KoffeinFlummi](https://github.com/KoffeinFlummi)!) -* Fix plot formatter not taking closures [#3260](https://github.com/emilk/egui/pull/3260) (thanks [@Wumpf](https://github.com/Wumpf)!) diff --git a/crates/egui_plot/Cargo.toml b/crates/egui_plot/Cargo.toml deleted file mode 100644 index 9717fd0525a..00000000000 --- a/crates/egui_plot/Cargo.toml +++ /dev/null @@ -1,47 +0,0 @@ -[package] -name = "egui_plot" -version.workspace = true -authors = [ - "Emil Ernerfeldt ", # https://github.com/emilk - "Jan Haller ", # https://github.com/Bromeon - "Sven Niederberger ", # https://github.com/EmbersArc -] -description = "Immediate mode plotting for the egui GUI library" -edition.workspace = true -rust-version.workspace = true -homepage = "https://github.com/emilk/egui" -license.workspace = true -readme = "README.md" -repository = "https://github.com/emilk/egui" -categories = ["visualization", "gui"] -keywords = ["egui", "plot", "plotting"] -include = ["../LICENSE-APACHE", "../LICENSE-MIT", "**/*.rs", "Cargo.toml"] - -[lints] -workspace = true - -[package.metadata.docs.rs] -all-features = true - -[lib] - - -[features] -default = [] - - -## Allow serialization using [`serde`](https://docs.rs/serde). -serde = ["dep:serde", "egui/serde"] - - -[dependencies] -egui = { workspace = true, default-features = false } -emath = { workspace = true, default-features = false } - -ahash.workspace = true - -#! ### Optional dependencies -## Enable this when generating docs. -document-features = { workspace = true, optional = true } - -serde = { workspace = true, optional = true } diff --git a/crates/egui_plot/README.md b/crates/egui_plot/README.md index f0bc44ac363..654511a12d5 100644 --- a/crates/egui_plot/README.md +++ b/crates/egui_plot/README.md @@ -1,11 +1 @@ -# egui_plot - -[![Latest version](https://img.shields.io/crates/v/egui_plot.svg)](https://crates.io/crates/egui_plot) -[![Documentation](https://docs.rs/egui_plot/badge.svg)](https://docs.rs/egui_plot) -[![unsafe forbidden](https://img.shields.io/badge/unsafe-forbidden-success.svg)](https://github.com/rust-secure-code/safety-dance/) -![MIT](https://img.shields.io/badge/license-MIT-blue.svg) -![Apache](https://img.shields.io/badge/license-Apache-blue.svg) - -Immediate mode plotting for [`egui`](https://github.com/emilk/egui). - -[**Looking for a maintainer!**](https://github.com/emilk/egui/issues/4705) +`egui_plot` has been moved to diff --git a/crates/egui_plot/src/axis.rs b/crates/egui_plot/src/axis.rs deleted file mode 100644 index 79925115bee..00000000000 --- a/crates/egui_plot/src/axis.rs +++ /dev/null @@ -1,398 +0,0 @@ -use std::{fmt::Debug, ops::RangeInclusive, sync::Arc}; - -use egui::{ - emath::{remap_clamp, Rot2}, - epaint::TextShape, - Pos2, Rangef, Rect, Response, Sense, TextStyle, TextWrapMode, Ui, Vec2, WidgetText, -}; - -use super::{transform::PlotTransform, GridMark}; - -pub(super) type AxisFormatterFn<'a> = dyn Fn(GridMark, &RangeInclusive) -> String + 'a; - -/// X or Y axis. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Axis { - /// Horizontal X-Axis - X = 0, - - /// Vertical Y-axis - Y = 1, -} - -impl From for usize { - #[inline] - fn from(value: Axis) -> Self { - match value { - Axis::X => 0, - Axis::Y => 1, - } - } -} - -/// 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. -#[derive(Clone)] -pub struct AxisHints<'a> { - pub(super) label: WidgetText, - pub(super) formatter: Arc>, - pub(super) min_thickness: f32, - pub(super) placement: Placement, - pub(super) label_spacing: Rangef, -} - -// TODO(JohannesProgrammiert): this just a guess. It might cease to work if a user changes font size. -const LINE_HEIGHT: f32 = 12.0; - -impl<'a> AxisHints<'a> { - /// Initializes a default axis configuration for the X axis. - pub fn new_x() -> Self { - Self::new(Axis::X) - } - - /// Initializes a default axis configuration for the Y axis. - pub fn new_y() -> Self { - Self::new(Axis::Y) - } - - /// Initializes a default axis configuration for the specified axis. - /// - /// `label` is empty. - /// `formatter` is default float to string formatter. - pub fn new(axis: Axis) -> Self { - Self { - label: Default::default(), - formatter: Arc::new(Self::default_formatter), - min_thickness: 14.0, - placement: Placement::LeftBottom, - label_spacing: match axis { - Axis::X => Rangef::new(60.0, 80.0), // labels can get pretty wide - Axis::Y => Rangef::new(20.0, 30.0), // text isn't very high - }, - } - } - - /// Specify custom formatter for ticks. - /// - /// The first parameter of `formatter` is the raw tick value as `f64`. - /// The second parameter of `formatter` is the currently shown range on this axis. - pub fn formatter( - mut self, - fmt: impl Fn(GridMark, &RangeInclusive) -> String + 'a, - ) -> Self { - self.formatter = Arc::new(fmt); - self - } - - fn default_formatter(mark: GridMark, _range: &RangeInclusive) -> String { - // Example: If the step to the next tick is `0.01`, we should use 2 decimals of precision: - let num_decimals = -mark.step_size.log10().round() as usize; - - emath::format_with_decimals_in_range(mark.value, num_decimals..=num_decimals) - } - - /// Specify axis label. - /// - /// The default is 'x' for x-axes and 'y' for y-axes. - #[inline] - pub fn label(mut self, label: impl Into) -> Self { - self.label = label.into(); - self - } - - /// Specify minimum thickness of the axis - #[inline] - pub fn min_thickness(mut self, min_thickness: f32) -> Self { - self.min_thickness = min_thickness; - self - } - - /// Specify maximum number of digits for ticks. - #[inline] - #[deprecated = "Use `min_thickness` instead"] - pub fn max_digits(self, digits: usize) -> Self { - self.min_thickness(12.0 * digits as f32) - } - - /// Specify the placement of the axis. - /// - /// For X-axis, use [`VPlacement`]. - /// For Y-axis, use [`HPlacement`]. - #[inline] - pub fn placement(mut self, placement: impl Into) -> Self { - self.placement = placement.into(); - self - } - - /// Set the minimum spacing between labels - /// - /// When labels get closer together than the given minimum, then they become invisible. - /// When they get further apart than the max, they are at full opacity. - /// - /// Labels can never be closer together than the [`crate::Plot::grid_spacing`] setting. - #[inline] - pub fn label_spacing(mut self, range: impl Into) -> Self { - self.label_spacing = range.into(); - self - } - - pub(super) fn thickness(&self, axis: Axis) -> f32 { - match axis { - Axis::X => self.min_thickness.max(if self.label.is_empty() { - 1.0 * LINE_HEIGHT - } else { - 3.0 * LINE_HEIGHT - }), - Axis::Y => { - self.min_thickness - + if self.label.is_empty() { - 0.0 - } else { - LINE_HEIGHT - } - } - } - } -} - -#[derive(Clone)] -pub(super) struct AxisWidget<'a> { - pub range: RangeInclusive, - pub hints: AxisHints<'a>, - - /// The region where we draw the axis labels. - pub rect: Rect, - pub transform: Option, - pub steps: Arc>, -} - -impl<'a> AxisWidget<'a> { - /// if `rect` has width or height == 0, it will be automatically calculated from ticks and text. - pub fn new(hints: AxisHints<'a>, rect: Rect) -> Self { - Self { - range: (0.0..=0.0), - hints, - rect, - transform: None, - steps: Default::default(), - } - } - - /// Returns the actual thickness of the axis. - pub fn ui(self, ui: &mut Ui, axis: Axis) -> (Response, f32) { - let response = ui.allocate_rect(self.rect, Sense::hover()); - - if !ui.is_rect_visible(response.rect) { - return (response, 0.0); - } - - let visuals = ui.style().visuals.clone(); - - { - let text = self.hints.label; - let galley = text.into_galley( - ui, - Some(TextWrapMode::Extend), - f32::INFINITY, - TextStyle::Body, - ); - let text_color = visuals - .override_text_color - .unwrap_or_else(|| ui.visuals().text_color()); - let angle: f32 = match axis { - Axis::X => 0.0, - Axis::Y => -std::f32::consts::TAU * 0.25, - }; - // select text_pos and angle depending on placement and orientation of widget - let text_pos = match self.hints.placement { - Placement::LeftBottom => match axis { - Axis::X => { - let pos = response.rect.center_bottom(); - Pos2 { - x: pos.x - galley.size().x / 2.0, - y: pos.y - galley.size().y * 1.25, - } - } - Axis::Y => { - let pos = response.rect.left_center(); - Pos2 { - x: pos.x, - y: pos.y + galley.size().x / 2.0, - } - } - }, - Placement::RightTop => match axis { - Axis::X => { - let pos = response.rect.center_top(); - Pos2 { - x: pos.x - galley.size().x / 2.0, - y: pos.y + galley.size().y * 0.25, - } - } - Axis::Y => { - let pos = response.rect.right_center(); - Pos2 { - x: pos.x - galley.size().y * 1.5, - y: pos.y + galley.size().x / 2.0, - } - } - }, - }; - - ui.painter() - .add(TextShape::new(text_pos, galley, text_color).with_angle(angle)); - } - - let font_id = TextStyle::Body.resolve(ui.style()); - let Some(transform) = self.transform else { - return (response, 0.0); - }; - - let label_spacing = self.hints.label_spacing; - - let mut thickness: f32 = 0.0; - - // Add tick labels: - for step in self.steps.iter() { - let text = (self.hints.formatter)(*step, &self.range); - if !text.is_empty() { - let spacing_in_points = - (transform.dpos_dvalue()[usize::from(axis)] * step.step_size).abs() as f32; - - if spacing_in_points <= label_spacing.min { - // Labels are too close together - don't paint them. - continue; - } - - // 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 galley = ui - .painter() - .layout_no_wrap(text, font_id.clone(), text_color); - - if spacing_in_points < galley.size()[axis as usize] { - continue; // the galley won't fit (likely too wide on the X axis). - } - - match axis { - Axis::X => { - thickness = thickness.max(galley.size().y); - - let projected_point = super::PlotPoint::new(step.value, 0.0); - let center_x = transform.position_from_point(&projected_point).x; - let y = match VPlacement::from(self.hints.placement) { - VPlacement::Bottom => self.rect.min.y, - VPlacement::Top => self.rect.max.y - galley.size().y, - }; - let pos = Pos2::new(center_x - galley.size().x / 2.0, y); - ui.painter().add(TextShape::new(pos, galley, text_color)); - } - Axis::Y => { - thickness = thickness.max(galley.size().x); - - let projected_point = super::PlotPoint::new(0.0, step.value); - let center_y = transform.position_from_point(&projected_point).y; - - match HPlacement::from(self.hints.placement) { - HPlacement::Left => { - let angle = 0.0; // TODO(emilk): allow users to rotate text - - if angle == 0.0 { - let x = self.rect.max.x - galley.size().x; - let pos = Pos2::new(x, center_y - galley.size().y / 2.0); - ui.painter().add(TextShape::new(pos, galley, text_color)); - } else { - let right = Pos2::new( - self.rect.max.x, - center_y - galley.size().y / 2.0, - ); - let width = galley.size().x; - let left = - right - Rot2::from_angle(angle) * Vec2::new(width, 0.0); - - ui.painter().add( - TextShape::new(left, galley, text_color).with_angle(angle), - ); - } - } - HPlacement::Right => { - let x = self.rect.min.x; - let pos = Pos2::new(x, center_y - galley.size().y / 2.0); - ui.painter().add(TextShape::new(pos, galley, text_color)); - } - }; - } - }; - } - } - - (response, thickness) - } -} diff --git a/crates/egui_plot/src/items/bar.rs b/crates/egui_plot/src/items/bar.rs deleted file mode 100644 index 9b86b52ef17..00000000000 --- a/crates/egui_plot/src/items/bar.rs +++ /dev/null @@ -1,197 +0,0 @@ -use egui::emath::NumExt; -use egui::epaint::{Color32, RectShape, Rounding, Shape, Stroke}; - -use super::{add_rulers_and_text, highlighted_color, Orientation, PlotConfig, RectElement}; -use crate::{BarChart, Cursor, PlotPoint, PlotTransform}; - -/// One bar in a [`BarChart`]. Potentially floating, allowing stacked bar charts. -/// Width can be changed to allow variable-width histograms. -#[derive(Clone, Debug, PartialEq)] -pub struct Bar { - /// Name of plot element in the diagram (annotated by default formatter) - pub name: String, - - /// Which direction the bar faces in the diagram - pub orientation: Orientation, - - /// Position on the argument (input) axis -- X if vertical, Y if horizontal - pub argument: f64, - - /// Position on the value (output) axis -- Y if vertical, X if horizontal - pub value: f64, - - /// For stacked bars, this denotes where the bar starts. None if base axis - pub base_offset: Option, - - /// Thickness of the bar - pub bar_width: f64, - - /// Line width and color - pub stroke: Stroke, - - /// Fill color - pub fill: Color32, -} - -impl Bar { - /// Create a bar. Its `orientation` is set by its [`BarChart`] parent. - /// - /// - `argument`: Position on the argument axis (X if vertical, Y if horizontal). - /// - `value`: Height of the bar (if vertical). - /// - /// By default the bar is vertical and its base is at zero. - pub fn new(argument: f64, height: f64) -> Self { - Self { - argument, - value: height, - orientation: Orientation::default(), - name: Default::default(), - base_offset: None, - bar_width: 0.5, - stroke: Stroke::new(1.0, Color32::TRANSPARENT), - fill: Color32::TRANSPARENT, - } - } - - /// Name of this bar chart element. - #[allow(clippy::needless_pass_by_value)] - #[inline] - pub fn name(mut self, name: impl ToString) -> Self { - self.name = name.to_string(); - self - } - - /// Add a custom stroke. - #[inline] - pub fn stroke(mut self, stroke: impl Into) -> Self { - self.stroke = stroke.into(); - self - } - - /// Add a custom fill color. - #[inline] - pub fn fill(mut self, color: impl Into) -> Self { - self.fill = color.into(); - self - } - - /// Offset the base of the bar. - /// This offset is on the Y axis for a vertical bar - /// and on the X axis for a horizontal bar. - #[inline] - pub fn base_offset(mut self, offset: f64) -> Self { - self.base_offset = Some(offset); - self - } - - /// Set the bar width. - #[inline] - pub fn width(mut self, width: f64) -> Self { - self.bar_width = width; - self - } - - /// Set orientation of the element as vertical. Argument axis is X. - #[inline] - pub fn vertical(mut self) -> Self { - self.orientation = Orientation::Vertical; - self - } - - /// Set orientation of the element as horizontal. Argument axis is Y. - #[inline] - pub fn horizontal(mut self) -> Self { - self.orientation = Orientation::Horizontal; - self - } - - pub(super) fn lower(&self) -> f64 { - if self.value.is_sign_positive() { - self.base_offset.unwrap_or(0.0) - } else { - self.base_offset.map_or(self.value, |o| o + self.value) - } - } - - pub(super) fn upper(&self) -> f64 { - if self.value.is_sign_positive() { - self.base_offset.map_or(self.value, |o| o + self.value) - } else { - self.base_offset.unwrap_or(0.0) - } - } - - pub(super) fn add_shapes( - &self, - transform: &PlotTransform, - highlighted: bool, - shapes: &mut Vec, - ) { - let (stroke, fill) = if highlighted { - highlighted_color(self.stroke, self.fill) - } else { - (self.stroke, self.fill) - }; - - let rect = transform.rect_from_values(&self.bounds_min(), &self.bounds_max()); - let rect = Shape::Rect(RectShape::new(rect, Rounding::ZERO, fill, stroke)); - - shapes.push(rect); - } - - pub(super) fn add_rulers_and_text( - &self, - parent: &BarChart, - plot: &PlotConfig<'_>, - shapes: &mut Vec, - cursors: &mut Vec, - ) { - let text: Option = parent - .element_formatter - .as_ref() - .map(|fmt| fmt(self, parent)); - - add_rulers_and_text(self, plot, text, shapes, cursors); - } -} - -impl RectElement for Bar { - fn name(&self) -> &str { - self.name.as_str() - } - - fn bounds_min(&self) -> PlotPoint { - self.point_at(self.argument - self.bar_width / 2.0, self.lower()) - } - - fn bounds_max(&self) -> PlotPoint { - self.point_at(self.argument + self.bar_width / 2.0, self.upper()) - } - - fn values_with_ruler(&self) -> Vec { - let base = self.base_offset.unwrap_or(0.0); - let value_center = self.point_at(self.argument, base + self.value); - - let mut ruler_positions = vec![value_center]; - - if let Some(offset) = self.base_offset { - ruler_positions.push(self.point_at(self.argument, offset)); - } - - ruler_positions - } - - fn orientation(&self) -> Orientation { - self.orientation - } - - fn default_values_format(&self, transform: &PlotTransform) -> String { - let scale = transform.dvalue_dpos(); - let scale = match self.orientation { - Orientation::Horizontal => scale[0], - 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) - } -} diff --git a/crates/egui_plot/src/items/box_elem.rs b/crates/egui_plot/src/items/box_elem.rs deleted file mode 100644 index 90755146653..00000000000 --- a/crates/egui_plot/src/items/box_elem.rs +++ /dev/null @@ -1,296 +0,0 @@ -use egui::emath::NumExt as _; -use egui::epaint::{Color32, RectShape, Rounding, Shape, Stroke}; - -use crate::{BoxPlot, Cursor, PlotPoint, PlotTransform}; - -use super::{add_rulers_and_text, highlighted_color, Orientation, PlotConfig, RectElement}; - -/// Contains the values of a single box in a box plot. -#[derive(Clone, Debug, PartialEq)] -pub struct BoxSpread { - /// Value of lower whisker (typically minimum). - /// - /// The whisker is not drawn if `lower_whisker >= quartile1`. - pub lower_whisker: f64, - - /// Value of lower box threshold (typically 25% quartile) - pub quartile1: f64, - - /// Value of middle line in box (typically median) - pub median: f64, - - /// Value of upper box threshold (typically 75% quartile) - pub quartile3: f64, - - /// Value of upper whisker (typically maximum) - /// - /// The whisker is not drawn if `upper_whisker <= quartile3`. - pub upper_whisker: f64, -} - -impl BoxSpread { - pub fn new( - lower_whisker: f64, - quartile1: f64, - median: f64, - quartile3: f64, - upper_whisker: f64, - ) -> Self { - Self { - lower_whisker, - quartile1, - median, - quartile3, - upper_whisker, - } - } -} - -/// A box in a [`BoxPlot`] diagram. This is a low level graphical element; it will not compute quartiles and whiskers, -/// letting one use their preferred formula. Use [`Points`][`super::Points`] to draw the outliers. -#[derive(Clone, Debug, PartialEq)] -pub struct BoxElem { - /// Name of plot element in the diagram (annotated by default formatter). - pub name: String, - - /// Which direction the box faces in the diagram. - pub orientation: Orientation, - - /// Position on the argument (input) axis -- X if vertical, Y if horizontal. - pub argument: f64, - - /// Values of the box - pub spread: BoxSpread, - - /// Thickness of the box - pub box_width: f64, - - /// Width of the whisker at minimum/maximum - pub whisker_width: f64, - - /// Line width and color - pub stroke: Stroke, - - /// Fill color - pub fill: Color32, -} - -impl BoxElem { - /// Create a box element. Its `orientation` is set by its [`BoxPlot`] parent. - /// - /// Check [`BoxElem`] fields for detailed description. - pub fn new(argument: f64, spread: BoxSpread) -> Self { - Self { - argument, - orientation: Orientation::default(), - name: String::default(), - spread, - box_width: 0.25, - whisker_width: 0.15, - stroke: Stroke::new(1.0, Color32::TRANSPARENT), - fill: Color32::TRANSPARENT, - } - } - - /// Name of this box element. - #[allow(clippy::needless_pass_by_value)] - #[inline] - pub fn name(mut self, name: impl ToString) -> Self { - self.name = name.to_string(); - self - } - - /// Add a custom stroke. - #[inline] - pub fn stroke(mut self, stroke: impl Into) -> Self { - self.stroke = stroke.into(); - self - } - - /// Add a custom fill color. - #[inline] - pub fn fill(mut self, color: impl Into) -> Self { - self.fill = color.into(); - self - } - - /// Set the box width. - #[inline] - pub fn box_width(mut self, width: f64) -> Self { - self.box_width = width; - self - } - - /// Set the whisker width. - #[inline] - pub fn whisker_width(mut self, width: f64) -> Self { - self.whisker_width = width; - self - } - - /// Set orientation of the element as vertical. Argument axis is X. - #[inline] - pub fn vertical(mut self) -> Self { - self.orientation = Orientation::Vertical; - self - } - - /// Set orientation of the element as horizontal. Argument axis is Y. - #[inline] - pub fn horizontal(mut self) -> Self { - self.orientation = Orientation::Horizontal; - self - } - - pub(super) fn add_shapes( - &self, - transform: &PlotTransform, - highlighted: bool, - shapes: &mut Vec, - ) { - let (stroke, fill) = if highlighted { - highlighted_color(self.stroke, self.fill) - } else { - (self.stroke, self.fill) - }; - - let rect = transform.rect_from_values( - &self.point_at(self.argument - self.box_width / 2.0, self.spread.quartile1), - &self.point_at(self.argument + self.box_width / 2.0, self.spread.quartile3), - ); - let rect = Shape::Rect(RectShape::new(rect, Rounding::ZERO, fill, stroke)); - shapes.push(rect); - - let line_between = |v1, v2| { - Shape::line_segment( - [ - transform.position_from_point(&v1), - transform.position_from_point(&v2), - ], - stroke, - ) - }; - let median = line_between( - self.point_at(self.argument - self.box_width / 2.0, self.spread.median), - self.point_at(self.argument + self.box_width / 2.0, self.spread.median), - ); - shapes.push(median); - - if self.spread.upper_whisker > self.spread.quartile3 { - let high_whisker = line_between( - self.point_at(self.argument, self.spread.quartile3), - self.point_at(self.argument, self.spread.upper_whisker), - ); - shapes.push(high_whisker); - if self.box_width > 0.0 { - let high_whisker_end = line_between( - self.point_at( - self.argument - self.whisker_width / 2.0, - self.spread.upper_whisker, - ), - self.point_at( - self.argument + self.whisker_width / 2.0, - self.spread.upper_whisker, - ), - ); - shapes.push(high_whisker_end); - } - } - - if self.spread.lower_whisker < self.spread.quartile1 { - let low_whisker = line_between( - self.point_at(self.argument, self.spread.quartile1), - self.point_at(self.argument, self.spread.lower_whisker), - ); - shapes.push(low_whisker); - if self.box_width > 0.0 { - let low_whisker_end = line_between( - self.point_at( - self.argument - self.whisker_width / 2.0, - self.spread.lower_whisker, - ), - self.point_at( - self.argument + self.whisker_width / 2.0, - self.spread.lower_whisker, - ), - ); - shapes.push(low_whisker_end); - } - } - } - - pub(super) fn add_rulers_and_text( - &self, - parent: &BoxPlot, - plot: &PlotConfig<'_>, - shapes: &mut Vec, - cursors: &mut Vec, - ) { - let text: Option = parent - .element_formatter - .as_ref() - .map(|fmt| fmt(self, parent)); - - add_rulers_and_text(self, plot, text, shapes, cursors); - } -} - -impl RectElement for BoxElem { - fn name(&self) -> &str { - self.name.as_str() - } - - fn bounds_min(&self) -> PlotPoint { - let argument = self.argument - self.box_width.max(self.whisker_width) / 2.0; - let value = self.spread.lower_whisker; - self.point_at(argument, value) - } - - fn bounds_max(&self) -> PlotPoint { - let argument = self.argument + self.box_width.max(self.whisker_width) / 2.0; - let value = self.spread.upper_whisker; - self.point_at(argument, value) - } - - fn values_with_ruler(&self) -> Vec { - let median = self.point_at(self.argument, self.spread.median); - let q1 = self.point_at(self.argument, self.spread.quartile1); - let q3 = self.point_at(self.argument, self.spread.quartile3); - let upper = self.point_at(self.argument, self.spread.upper_whisker); - let lower = self.point_at(self.argument, self.spread.lower_whisker); - - vec![median, q1, q3, upper, lower] - } - - fn orientation(&self) -> Orientation { - self.orientation - } - - fn corner_value(&self) -> PlotPoint { - self.point_at(self.argument, self.spread.upper_whisker) - } - - fn default_values_format(&self, transform: &PlotTransform) -> String { - let scale = transform.dvalue_dpos(); - let scale = match self.orientation { - Orientation::Horizontal => scale[0], - Orientation::Vertical => scale[1], - }; - let y_decimals = ((-scale.abs().log10()).ceil().at_least(0.0) as usize) - .at_most(6) - .at_least(1); - format!( - "Max = {max:.decimals$}\ - \nQuartile 3 = {q3:.decimals$}\ - \nMedian = {med:.decimals$}\ - \nQuartile 1 = {q1:.decimals$}\ - \nMin = {min:.decimals$}", - max = self.spread.upper_whisker, - q3 = self.spread.quartile3, - med = self.spread.median, - q1 = self.spread.quartile1, - min = self.spread.lower_whisker, - decimals = y_decimals - ) - } -} diff --git a/crates/egui_plot/src/items/mod.rs b/crates/egui_plot/src/items/mod.rs deleted file mode 100644 index 8e31c1fb610..00000000000 --- a/crates/egui_plot/src/items/mod.rs +++ /dev/null @@ -1,2104 +0,0 @@ -//! Contains items that can be added to a plot. -#![allow(clippy::type_complexity)] // TODO(emilk): simplify some of the callback types with type aliases - -use std::ops::RangeInclusive; - -use epaint::{emath::Rot2, Mesh}; - -use crate::*; - -use super::{Cursor, LabelFormatter, PlotBounds, PlotTransform}; -use rect_elem::*; - -pub use bar::Bar; -pub use box_elem::{BoxElem, BoxSpread}; -pub use values::{ - ClosestElem, LineStyle, MarkerShape, Orientation, PlotGeometry, PlotPoint, PlotPoints, -}; - -mod bar; -mod box_elem; -mod rect_elem; -mod values; - -const DEFAULT_FILL_ALPHA: f32 = 0.05; - -/// Container to pass-through several parameters related to plot visualization -pub struct PlotConfig<'a> { - pub ui: &'a Ui, - pub transform: &'a PlotTransform, - pub show_x: bool, - pub show_y: bool, -} - -/// Trait shared by things that can be drawn in the plot. -pub trait PlotItem { - fn shapes(&self, ui: &Ui, transform: &PlotTransform, shapes: &mut Vec); - - /// For plot-items which are generated based on x values (plotting functions). - fn initialize(&mut self, x_range: RangeInclusive); - - fn name(&self) -> &str; - - fn color(&self) -> Color32; - - fn highlight(&mut self); - - fn highlighted(&self) -> bool; - - /// Can the user hover this item? - fn allow_hover(&self) -> bool; - - fn geometry(&self) -> PlotGeometry<'_>; - - fn bounds(&self) -> PlotBounds; - - fn id(&self) -> Option; - - fn find_closest(&self, point: Pos2, transform: &PlotTransform) -> Option { - match self.geometry() { - PlotGeometry::None => None, - - PlotGeometry::Points(points) => points - .iter() - .enumerate() - .map(|(index, value)| { - let pos = transform.position_from_point(value); - let dist_sq = point.distance_sq(pos); - ClosestElem { index, dist_sq } - }) - .min_by_key(|e| e.dist_sq.ord()), - - PlotGeometry::Rects => { - panic!("If the PlotItem is made of rects, it should implement find_closest()") - } - } - } - - fn on_hover( - &self, - elem: ClosestElem, - shapes: &mut Vec, - cursors: &mut Vec, - plot: &PlotConfig<'_>, - label_formatter: &LabelFormatter<'_>, - ) { - let points = match self.geometry() { - PlotGeometry::Points(points) => points, - PlotGeometry::None => { - panic!("If the PlotItem has no geometry, on_hover() must not be called") - } - PlotGeometry::Rects => { - panic!("If the PlotItem is made of rects, it should implement on_hover()") - } - }; - - let line_color = if plot.ui.visuals().dark_mode { - Color32::from_gray(100).additive() - } else { - Color32::from_black_alpha(180) - }; - - // this method is only called, if the value is in the result set of find_closest() - let value = points[elem.index]; - let pointer = plot.transform.position_from_point(&value); - shapes.push(Shape::circle_filled(pointer, 3.0, line_color)); - - rulers_at_value( - pointer, - value, - self.name(), - plot, - shapes, - cursors, - label_formatter, - ); - } -} - -// ---------------------------------------------------------------------------- - -/// A horizontal line in a plot, filling the full width -#[derive(Clone, Debug, PartialEq)] -pub struct HLine { - pub(super) y: f64, - pub(super) stroke: Stroke, - pub(super) name: String, - pub(super) highlight: bool, - pub(super) allow_hover: bool, - pub(super) style: LineStyle, - id: Option, -} - -impl HLine { - pub fn new(y: impl Into) -> Self { - Self { - y: y.into(), - stroke: Stroke::new(1.0, Color32::TRANSPARENT), - name: String::default(), - highlight: false, - allow_hover: true, - style: LineStyle::Solid, - id: None, - } - } - - /// Highlight this line in the plot by scaling up the line. - #[inline] - pub fn highlight(mut self, highlight: bool) -> Self { - self.highlight = highlight; - self - } - - /// Allowed hovering this item in the plot. Default: `true`. - #[inline] - pub fn allow_hover(mut self, hovering: bool) -> Self { - self.allow_hover = hovering; - self - } - - /// Add a stroke. - #[inline] - pub fn stroke(mut self, stroke: impl Into) -> Self { - self.stroke = stroke.into(); - self - } - - /// Stroke width. A high value means the plot thickens. - #[inline] - pub fn width(mut self, width: impl Into) -> Self { - self.stroke.width = width.into(); - self - } - - /// Stroke color. Default is `Color32::TRANSPARENT` which means a color will be auto-assigned. - #[inline] - pub fn color(mut self, color: impl Into) -> Self { - self.stroke.color = color.into(); - self - } - - /// Set the line's style. Default is `LineStyle::Solid`. - #[inline] - pub fn style(mut self, style: LineStyle) -> Self { - self.style = style; - self - } - - /// Name of this horizontal line. - /// - /// This name will show up in the plot legend, if legends are turned on. - /// - /// Multiple plot items may share the same name, in which case they will also share an entry in - /// the legend. - #[allow(clippy::needless_pass_by_value)] - #[inline] - pub fn name(mut self, name: impl ToString) -> Self { - self.name = name.to_string(); - self - } - - /// Set the line's id which is used to identify it in the plot's response. - #[inline] - pub fn id(mut self, id: Id) -> Self { - self.id = Some(id); - self - } -} - -impl PlotItem for HLine { - fn shapes(&self, ui: &Ui, transform: &PlotTransform, shapes: &mut Vec) { - let Self { - y, - stroke, - highlight, - style, - .. - } = self; - - // Round to minimize aliasing: - let points = vec![ - ui.painter().round_pos_to_pixels( - transform.position_from_point(&PlotPoint::new(transform.bounds().min[0], *y)), - ), - ui.painter().round_pos_to_pixels( - transform.position_from_point(&PlotPoint::new(transform.bounds().max[0], *y)), - ), - ]; - style.style_line(points, *stroke, *highlight, shapes); - } - - fn initialize(&mut self, _x_range: RangeInclusive) {} - - fn name(&self) -> &str { - &self.name - } - - fn color(&self) -> Color32 { - self.stroke.color - } - - fn highlight(&mut self) { - self.highlight = true; - } - - fn highlighted(&self) -> bool { - self.highlight - } - - fn allow_hover(&self) -> bool { - self.allow_hover - } - - fn geometry(&self) -> PlotGeometry<'_> { - PlotGeometry::None - } - - fn bounds(&self) -> PlotBounds { - let mut bounds = PlotBounds::NOTHING; - bounds.min[1] = self.y; - bounds.max[1] = self.y; - bounds - } - - fn id(&self) -> Option { - self.id - } -} - -/// A vertical line in a plot, filling the full width -#[derive(Clone, Debug, PartialEq)] -pub struct VLine { - pub(super) x: f64, - pub(super) stroke: Stroke, - pub(super) name: String, - pub(super) highlight: bool, - pub(super) allow_hover: bool, - pub(super) style: LineStyle, - id: Option, -} - -impl VLine { - pub fn new(x: impl Into) -> Self { - Self { - x: x.into(), - stroke: Stroke::new(1.0, Color32::TRANSPARENT), - name: String::default(), - highlight: false, - allow_hover: true, - style: LineStyle::Solid, - id: None, - } - } - - /// Highlight this line in the plot by scaling up the line. - #[inline] - pub fn highlight(mut self, highlight: bool) -> Self { - self.highlight = highlight; - self - } - - /// Allowed hovering this item in the plot. Default: `true`. - #[inline] - pub fn allow_hover(mut self, hovering: bool) -> Self { - self.allow_hover = hovering; - self - } - - /// Add a stroke. - #[inline] - pub fn stroke(mut self, stroke: impl Into) -> Self { - self.stroke = stroke.into(); - self - } - - /// Stroke width. A high value means the plot thickens. - #[inline] - pub fn width(mut self, width: impl Into) -> Self { - self.stroke.width = width.into(); - self - } - - /// Stroke color. Default is `Color32::TRANSPARENT` which means a color will be auto-assigned. - #[inline] - pub fn color(mut self, color: impl Into) -> Self { - self.stroke.color = color.into(); - self - } - - /// Set the line's style. Default is `LineStyle::Solid`. - #[inline] - pub fn style(mut self, style: LineStyle) -> Self { - self.style = style; - self - } - - /// Name of this vertical line. - /// - /// This name will show up in the plot legend, if legends are turned on. - /// - /// Multiple plot items may share the same name, in which case they will also share an entry in - /// the legend. - #[allow(clippy::needless_pass_by_value)] - #[inline] - pub fn name(mut self, name: impl ToString) -> Self { - self.name = name.to_string(); - self - } - - /// Set the line's id which is used to identify it in the plot's response. - #[inline] - pub fn id(mut self, id: Id) -> Self { - self.id = Some(id); - self - } -} - -impl PlotItem for VLine { - fn shapes(&self, ui: &Ui, transform: &PlotTransform, shapes: &mut Vec) { - let Self { - x, - stroke, - highlight, - style, - .. - } = self; - - // Round to minimize aliasing: - let points = vec![ - ui.painter().round_pos_to_pixels( - transform.position_from_point(&PlotPoint::new(*x, transform.bounds().min[1])), - ), - ui.painter().round_pos_to_pixels( - transform.position_from_point(&PlotPoint::new(*x, transform.bounds().max[1])), - ), - ]; - style.style_line(points, *stroke, *highlight, shapes); - } - - fn initialize(&mut self, _x_range: RangeInclusive) {} - - fn name(&self) -> &str { - &self.name - } - - fn color(&self) -> Color32 { - self.stroke.color - } - - fn highlight(&mut self) { - self.highlight = true; - } - - fn highlighted(&self) -> bool { - self.highlight - } - - fn allow_hover(&self) -> bool { - self.allow_hover - } - - fn geometry(&self) -> PlotGeometry<'_> { - PlotGeometry::None - } - - fn bounds(&self) -> PlotBounds { - let mut bounds = PlotBounds::NOTHING; - bounds.min[0] = self.x; - bounds.max[0] = self.x; - bounds - } - - fn id(&self) -> Option { - self.id - } -} - -/// A series of values forming a path. -pub struct Line { - pub(super) series: PlotPoints, - pub(super) stroke: Stroke, - pub(super) name: String, - pub(super) highlight: bool, - pub(super) allow_hover: bool, - pub(super) fill: Option, - pub(super) style: LineStyle, - id: Option, -} - -impl Line { - pub fn new(series: impl Into) -> Self { - Self { - series: series.into(), - stroke: Stroke::new(1.5, Color32::TRANSPARENT), // Note: a stroke of 1.0 (or less) can look bad on low-dpi-screens - name: Default::default(), - highlight: false, - allow_hover: true, - fill: None, - style: LineStyle::Solid, - id: None, - } - } - - /// Highlight this line in the plot by scaling up the line. - #[inline] - pub fn highlight(mut self, highlight: bool) -> Self { - self.highlight = highlight; - self - } - - /// Allowed hovering this item in the plot. Default: `true`. - #[inline] - pub fn allow_hover(mut self, hovering: bool) -> Self { - self.allow_hover = hovering; - self - } - - /// Add a stroke. - #[inline] - pub fn stroke(mut self, stroke: impl Into) -> Self { - self.stroke = stroke.into(); - self - } - - /// Stroke width. A high value means the plot thickens. - #[inline] - pub fn width(mut self, width: impl Into) -> Self { - self.stroke.width = width.into(); - self - } - - /// Stroke color. Default is `Color32::TRANSPARENT` which means a color will be auto-assigned. - #[inline] - pub fn color(mut self, color: impl Into) -> Self { - self.stroke.color = color.into(); - self - } - - /// Fill the area between this line and a given horizontal reference line. - #[inline] - pub fn fill(mut self, y_reference: impl Into) -> Self { - self.fill = Some(y_reference.into()); - self - } - - /// Set the line's style. Default is `LineStyle::Solid`. - #[inline] - pub fn style(mut self, style: LineStyle) -> Self { - self.style = style; - self - } - - /// Name of this line. - /// - /// This name will show up in the plot legend, if legends are turned on. - /// - /// Multiple plot items may share the same name, in which case they will also share an entry in - /// the legend. - #[allow(clippy::needless_pass_by_value)] - #[inline] - pub fn name(mut self, name: impl ToString) -> Self { - self.name = name.to_string(); - self - } - - /// Set the line's id which is used to identify it in the plot's response. - #[inline] - pub fn id(mut self, id: Id) -> Self { - self.id = Some(id); - self - } -} - -/// 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)) -} - -impl PlotItem for Line { - fn shapes(&self, _ui: &Ui, transform: &PlotTransform, shapes: &mut Vec) { - let Self { - series, - stroke, - highlight, - mut fill, - style, - .. - } = self; - - let values_tf: Vec<_> = series - .points() - .iter() - .map(|v| transform.position_from_point(v)) - .collect(); - let n_values = values_tf.len(); - - // Fill the area between the line and a reference line, if required. - if n_values < 2 { - fill = None; - } - if let Some(y_reference) = fill { - let mut fill_alpha = DEFAULT_FILL_ALPHA; - if *highlight { - fill_alpha = (2.0 * fill_alpha).at_most(1.0); - } - let y = transform - .position_from_point(&PlotPoint::new(0.0, y_reference)) - .y; - let fill_color = Rgba::from(stroke.color) - .to_opaque() - .multiply(fill_alpha) - .into(); - let mut mesh = Mesh::default(); - let expected_intersections = 20; - mesh.reserve_triangles((n_values - 1) * 2); - mesh.reserve_vertices(n_values * 2 + expected_intersections); - values_tf.windows(2).for_each(|w| { - let i = mesh.vertices.len() as u32; - mesh.colored_vertex(w[0], fill_color); - mesh.colored_vertex(pos2(w[0].x, y), fill_color); - if let Some(x) = y_intersection(&w[0], &w[1], y) { - let point = pos2(x, y); - mesh.colored_vertex(point, fill_color); - mesh.add_triangle(i, i + 1, i + 2); - mesh.add_triangle(i + 2, i + 3, i + 4); - } else { - mesh.add_triangle(i, i + 1, i + 2); - mesh.add_triangle(i + 1, i + 2, i + 3); - } - }); - let last = values_tf[n_values - 1]; - mesh.colored_vertex(last, fill_color); - mesh.colored_vertex(pos2(last.x, y), fill_color); - shapes.push(Shape::Mesh(mesh)); - } - style.style_line(values_tf, *stroke, *highlight, shapes); - } - - fn initialize(&mut self, x_range: RangeInclusive) { - self.series.generate_points(x_range); - } - - fn name(&self) -> &str { - self.name.as_str() - } - - fn color(&self) -> Color32 { - self.stroke.color - } - - fn highlight(&mut self) { - self.highlight = true; - } - - fn highlighted(&self) -> bool { - self.highlight - } - - fn allow_hover(&self) -> bool { - self.allow_hover - } - - fn geometry(&self) -> PlotGeometry<'_> { - PlotGeometry::Points(self.series.points()) - } - - fn bounds(&self) -> PlotBounds { - self.series.bounds() - } - - fn id(&self) -> Option { - self.id - } -} - -/// A convex polygon. -pub struct Polygon { - pub(super) series: PlotPoints, - pub(super) stroke: Stroke, - pub(super) name: String, - pub(super) highlight: bool, - pub(super) allow_hover: bool, - pub(super) fill_color: Option, - pub(super) style: LineStyle, - id: Option, -} - -impl Polygon { - pub fn new(series: impl Into) -> Self { - Self { - series: series.into(), - stroke: Stroke::new(1.0, Color32::TRANSPARENT), - name: Default::default(), - highlight: false, - allow_hover: true, - fill_color: None, - style: LineStyle::Solid, - id: None, - } - } - - /// Highlight this polygon in the plot by scaling up the stroke and reducing the fill - /// transparency. - #[inline] - pub fn highlight(mut self, highlight: bool) -> Self { - self.highlight = highlight; - self - } - - /// Allowed hovering this item in the plot. Default: `true`. - #[inline] - pub fn allow_hover(mut self, hovering: bool) -> Self { - self.allow_hover = hovering; - self - } - - /// Add a custom stroke. - #[inline] - pub fn stroke(mut self, stroke: impl Into) -> Self { - self.stroke = stroke.into(); - self - } - - /// Set the stroke width. - #[inline] - pub fn width(mut self, width: impl Into) -> Self { - self.stroke.width = width.into(); - self - } - - /// Fill color. Defaults to the stroke color with added transparency. - #[inline] - pub fn fill_color(mut self, color: impl Into) -> Self { - self.fill_color = Some(color.into()); - self - } - - /// Set the outline's style. Default is `LineStyle::Solid`. - #[inline] - pub fn style(mut self, style: LineStyle) -> Self { - self.style = style; - self - } - - /// Name of this polygon. - /// - /// This name will show up in the plot legend, if legends are turned on. - /// - /// Multiple plot items may share the same name, in which case they will also share an entry in - /// the legend. - #[allow(clippy::needless_pass_by_value)] - #[inline] - pub fn name(mut self, name: impl ToString) -> Self { - self.name = name.to_string(); - self - } - - /// Set the polygon's id which is used to identify it in the plot's response. - #[inline] - pub fn id(mut self, id: Id) -> Self { - self.id = Some(id); - self - } -} - -impl PlotItem for Polygon { - fn shapes(&self, _ui: &Ui, transform: &PlotTransform, shapes: &mut Vec) { - let Self { - series, - stroke, - highlight, - fill_color, - style, - .. - } = self; - - let mut values_tf: Vec<_> = series - .points() - .iter() - .map(|v| transform.position_from_point(v)) - .collect(); - - let fill_color = fill_color.unwrap_or(stroke.color.linear_multiply(DEFAULT_FILL_ALPHA)); - - let shape = Shape::convex_polygon(values_tf.clone(), fill_color, Stroke::NONE); - shapes.push(shape); - values_tf.push(*values_tf.first().unwrap()); - style.style_line(values_tf, *stroke, *highlight, shapes); - } - - fn initialize(&mut self, x_range: RangeInclusive) { - self.series.generate_points(x_range); - } - - fn name(&self) -> &str { - self.name.as_str() - } - - fn color(&self) -> Color32 { - self.stroke.color - } - - fn highlight(&mut self) { - self.highlight = true; - } - - fn highlighted(&self) -> bool { - self.highlight - } - - fn allow_hover(&self) -> bool { - self.allow_hover - } - - fn geometry(&self) -> PlotGeometry<'_> { - PlotGeometry::Points(self.series.points()) - } - - fn bounds(&self) -> PlotBounds { - self.series.bounds() - } - - fn id(&self) -> Option { - self.id - } -} - -/// Text inside the plot. -#[derive(Clone)] -pub struct Text { - pub(super) text: WidgetText, - pub(super) position: PlotPoint, - pub(super) name: String, - pub(super) highlight: bool, - pub(super) allow_hover: bool, - pub(super) color: Color32, - pub(super) anchor: Align2, - id: Option, -} - -impl Text { - pub fn new(position: PlotPoint, text: impl Into) -> Self { - Self { - text: text.into(), - position, - name: Default::default(), - highlight: false, - allow_hover: true, - color: Color32::TRANSPARENT, - anchor: Align2::CENTER_CENTER, - id: None, - } - } - - /// Highlight this text in the plot by drawing a rectangle around it. - #[inline] - pub fn highlight(mut self, highlight: bool) -> Self { - self.highlight = highlight; - self - } - - /// Allowed hovering this item in the plot. Default: `true`. - #[inline] - pub fn allow_hover(mut self, hovering: bool) -> Self { - self.allow_hover = hovering; - self - } - - /// Text color. - #[inline] - pub fn color(mut self, color: impl Into) -> Self { - self.color = color.into(); - self - } - - /// Anchor position of the text. Default is `Align2::CENTER_CENTER`. - #[inline] - pub fn anchor(mut self, anchor: Align2) -> Self { - self.anchor = anchor; - self - } - - /// Name of this text. - /// - /// This name will show up in the plot legend, if legends are turned on. - /// - /// Multiple plot items may share the same name, in which case they will also share an entry in - /// the legend. - #[allow(clippy::needless_pass_by_value)] - #[inline] - pub fn name(mut self, name: impl ToString) -> Self { - self.name = name.to_string(); - self - } - - /// Set the text's id which is used to identify it in the plot's response. - #[inline] - pub fn id(mut self, id: Id) -> Self { - self.id = Some(id); - self - } -} - -impl PlotItem for Text { - fn shapes(&self, ui: &Ui, transform: &PlotTransform, shapes: &mut Vec) { - let color = if self.color == Color32::TRANSPARENT { - ui.style().visuals.text_color() - } else { - self.color - }; - - let galley = self.text.clone().into_galley( - ui, - Some(egui::TextWrapMode::Extend), - f32::INFINITY, - TextStyle::Small, - ); - - let pos = transform.position_from_point(&self.position); - let rect = self.anchor.anchor_size(pos, galley.size()); - - shapes.push(epaint::TextShape::new(rect.min, galley, color).into()); - - if self.highlight { - shapes.push(Shape::rect_stroke( - rect.expand(2.0), - 1.0, - Stroke::new(0.5, color), - )); - } - } - - fn initialize(&mut self, _x_range: RangeInclusive) {} - - fn name(&self) -> &str { - self.name.as_str() - } - - fn color(&self) -> Color32 { - self.color - } - - fn highlight(&mut self) { - self.highlight = true; - } - - fn highlighted(&self) -> bool { - self.highlight - } - - fn allow_hover(&self) -> bool { - self.allow_hover - } - - fn geometry(&self) -> PlotGeometry<'_> { - PlotGeometry::None - } - - fn bounds(&self) -> PlotBounds { - let mut bounds = PlotBounds::NOTHING; - bounds.extend_with(&self.position); - bounds - } - - fn id(&self) -> Option { - self.id - } -} - -/// A set of points. -pub struct Points { - pub(super) series: PlotPoints, - - pub(super) shape: MarkerShape, - - /// Color of the marker. `Color32::TRANSPARENT` means that it will be picked automatically. - pub(super) color: Color32, - - /// Whether to fill the marker. Does not apply to all types. - pub(super) filled: bool, - - /// The maximum extent of the marker from its center. - pub(super) radius: f32, - - pub(super) name: String, - - pub(super) highlight: bool, - - pub(super) allow_hover: bool, - - pub(super) stems: Option, - id: Option, -} - -impl Points { - pub fn new(series: impl Into) -> Self { - Self { - series: series.into(), - shape: MarkerShape::Circle, - color: Color32::TRANSPARENT, - filled: true, - radius: 1.0, - name: Default::default(), - highlight: false, - allow_hover: true, - stems: None, - id: None, - } - } - - /// Set the shape of the markers. - #[inline] - pub fn shape(mut self, shape: MarkerShape) -> Self { - self.shape = shape; - self - } - - /// Highlight these points in the plot by scaling up their markers. - #[inline] - pub fn highlight(mut self, highlight: bool) -> Self { - self.highlight = highlight; - self - } - - /// Allowed hovering this item in the plot. Default: `true`. - #[inline] - pub fn allow_hover(mut self, hovering: bool) -> Self { - self.allow_hover = hovering; - self - } - - /// Set the marker's color. - #[inline] - pub fn color(mut self, color: impl Into) -> Self { - self.color = color.into(); - self - } - - /// Whether to fill the marker. - #[inline] - pub fn filled(mut self, filled: bool) -> Self { - self.filled = filled; - self - } - - /// Whether to add stems between the markers and a horizontal reference line. - #[inline] - pub fn stems(mut self, y_reference: impl Into) -> Self { - self.stems = Some(y_reference.into()); - self - } - - /// Set the maximum extent of the marker around its position, in ui points. - #[inline] - pub fn radius(mut self, radius: impl Into) -> Self { - self.radius = radius.into(); - self - } - - /// Name of this set of points. - /// - /// This name will show up in the plot legend, if legends are turned on. - /// - /// Multiple plot items may share the same name, in which case they will also share an entry in - /// the legend. - #[allow(clippy::needless_pass_by_value)] - #[inline] - pub fn name(mut self, name: impl ToString) -> Self { - self.name = name.to_string(); - self - } - - /// Set the points' id which is used to identify them in the plot's response. - #[inline] - pub fn id(mut self, id: Id) -> Self { - self.id = Some(id); - self - } -} - -impl PlotItem for Points { - fn shapes(&self, _ui: &Ui, transform: &PlotTransform, shapes: &mut Vec) { - let sqrt_3 = 3_f32.sqrt(); - let frac_sqrt_3_2 = 3_f32.sqrt() / 2.0; - let frac_1_sqrt_2 = 1.0 / 2_f32.sqrt(); - - let Self { - series, - shape, - color, - filled, - mut radius, - highlight, - stems, - .. - } = self; - - let stroke_size = radius / 5.0; - - let default_stroke = Stroke::new(stroke_size, *color); - let mut stem_stroke = default_stroke; - let (fill, stroke) = if *filled { - (*color, Stroke::NONE) - } else { - (Color32::TRANSPARENT, default_stroke) - }; - - if *highlight { - radius *= 2f32.sqrt(); - stem_stroke.width *= 2.0; - } - - let y_reference = stems.map(|y| transform.position_from_point(&PlotPoint::new(0.0, y)).y); - - series - .points() - .iter() - .map(|value| transform.position_from_point(value)) - .for_each(|center| { - let tf = |dx: f32, dy: f32| -> Pos2 { center + radius * vec2(dx, dy) }; - - if let Some(y) = y_reference { - let stem = Shape::line_segment([center, pos2(center.x, y)], stem_stroke); - shapes.push(stem); - } - - match shape { - MarkerShape::Circle => { - shapes.push(Shape::Circle(epaint::CircleShape { - center, - radius, - fill, - stroke, - })); - } - MarkerShape::Diamond => { - let points = vec![ - tf(0.0, 1.0), // bottom - tf(-1.0, 0.0), // left - tf(0.0, -1.0), // top - tf(1.0, 0.0), // right - ]; - shapes.push(Shape::convex_polygon(points, fill, stroke)); - } - MarkerShape::Square => { - let points = vec![ - tf(-frac_1_sqrt_2, frac_1_sqrt_2), - tf(-frac_1_sqrt_2, -frac_1_sqrt_2), - tf(frac_1_sqrt_2, -frac_1_sqrt_2), - tf(frac_1_sqrt_2, frac_1_sqrt_2), - ]; - shapes.push(Shape::convex_polygon(points, fill, stroke)); - } - MarkerShape::Cross => { - let diagonal1 = [ - tf(-frac_1_sqrt_2, -frac_1_sqrt_2), - tf(frac_1_sqrt_2, frac_1_sqrt_2), - ]; - let diagonal2 = [ - tf(frac_1_sqrt_2, -frac_1_sqrt_2), - tf(-frac_1_sqrt_2, frac_1_sqrt_2), - ]; - shapes.push(Shape::line_segment(diagonal1, default_stroke)); - shapes.push(Shape::line_segment(diagonal2, default_stroke)); - } - MarkerShape::Plus => { - let horizontal = [tf(-1.0, 0.0), tf(1.0, 0.0)]; - let vertical = [tf(0.0, -1.0), tf(0.0, 1.0)]; - shapes.push(Shape::line_segment(horizontal, default_stroke)); - shapes.push(Shape::line_segment(vertical, default_stroke)); - } - MarkerShape::Up => { - let points = - vec![tf(0.0, -1.0), tf(0.5 * sqrt_3, 0.5), tf(-0.5 * sqrt_3, 0.5)]; - shapes.push(Shape::convex_polygon(points, fill, stroke)); - } - MarkerShape::Down => { - let points = vec![ - tf(0.0, 1.0), - tf(-0.5 * sqrt_3, -0.5), - tf(0.5 * sqrt_3, -0.5), - ]; - shapes.push(Shape::convex_polygon(points, fill, stroke)); - } - MarkerShape::Left => { - let points = - vec![tf(-1.0, 0.0), tf(0.5, -0.5 * sqrt_3), tf(0.5, 0.5 * sqrt_3)]; - shapes.push(Shape::convex_polygon(points, fill, stroke)); - } - MarkerShape::Right => { - let points = vec![ - tf(1.0, 0.0), - tf(-0.5, 0.5 * sqrt_3), - tf(-0.5, -0.5 * sqrt_3), - ]; - shapes.push(Shape::convex_polygon(points, fill, stroke)); - } - MarkerShape::Asterisk => { - let vertical = [tf(0.0, -1.0), tf(0.0, 1.0)]; - let diagonal1 = [tf(-frac_sqrt_3_2, 0.5), tf(frac_sqrt_3_2, -0.5)]; - let diagonal2 = [tf(-frac_sqrt_3_2, -0.5), tf(frac_sqrt_3_2, 0.5)]; - shapes.push(Shape::line_segment(vertical, default_stroke)); - shapes.push(Shape::line_segment(diagonal1, default_stroke)); - shapes.push(Shape::line_segment(diagonal2, default_stroke)); - } - } - }); - } - - fn initialize(&mut self, x_range: RangeInclusive) { - self.series.generate_points(x_range); - } - - fn name(&self) -> &str { - self.name.as_str() - } - - fn color(&self) -> Color32 { - self.color - } - - fn highlight(&mut self) { - self.highlight = true; - } - - fn highlighted(&self) -> bool { - self.highlight - } - - fn allow_hover(&self) -> bool { - self.allow_hover - } - - fn geometry(&self) -> PlotGeometry<'_> { - PlotGeometry::Points(self.series.points()) - } - - fn bounds(&self) -> PlotBounds { - self.series.bounds() - } - - fn id(&self) -> Option { - self.id - } -} - -/// A set of arrows. -pub struct Arrows { - pub(super) origins: PlotPoints, - pub(super) tips: PlotPoints, - pub(super) tip_length: Option, - pub(super) color: Color32, - pub(super) name: String, - pub(super) highlight: bool, - pub(super) allow_hover: bool, - id: Option, -} - -impl Arrows { - pub fn new(origins: impl Into, tips: impl Into) -> Self { - Self { - origins: origins.into(), - tips: tips.into(), - tip_length: None, - color: Color32::TRANSPARENT, - name: Default::default(), - highlight: false, - allow_hover: true, - id: None, - } - } - - /// Highlight these arrows in the plot. - #[inline] - pub fn highlight(mut self, highlight: bool) -> Self { - self.highlight = highlight; - self - } - - /// Allowed hovering this item in the plot. Default: `true`. - #[inline] - pub fn allow_hover(mut self, hovering: bool) -> Self { - self.allow_hover = hovering; - self - } - - /// Set the length of the arrow tips - #[inline] - pub fn tip_length(mut self, tip_length: f32) -> Self { - self.tip_length = Some(tip_length); - self - } - - /// Set the arrows' color. - #[inline] - pub fn color(mut self, color: impl Into) -> Self { - self.color = color.into(); - self - } - - /// Name of this set of arrows. - /// - /// This name will show up in the plot legend, if legends are turned on. - /// - /// Multiple plot items may share the same name, in which case they will also share an entry in - /// the legend. - #[allow(clippy::needless_pass_by_value)] - #[inline] - pub fn name(mut self, name: impl ToString) -> Self { - self.name = name.to_string(); - self - } - - /// Set the arrows' id which is used to identify them in the plot's response. - #[inline] - pub fn id(mut self, id: Id) -> Self { - self.id = Some(id); - self - } -} - -impl PlotItem for Arrows { - fn shapes(&self, _ui: &Ui, transform: &PlotTransform, shapes: &mut Vec) { - use crate::emath::*; - let Self { - origins, - tips, - tip_length, - color, - highlight, - .. - } = self; - let stroke = Stroke::new(if *highlight { 2.0 } else { 1.0 }, *color); - origins - .points() - .iter() - .zip(tips.points().iter()) - .map(|(origin, tip)| { - ( - transform.position_from_point(origin), - transform.position_from_point(tip), - ) - }) - .for_each(|(origin, tip)| { - let vector = tip - origin; - let rot = Rot2::from_angle(std::f32::consts::TAU / 10.0); - let tip_length = if let Some(tip_length) = tip_length { - *tip_length - } else { - vector.length() / 4.0 - }; - let tip = origin + vector; - let dir = vector.normalized(); - shapes.push(Shape::line_segment([origin, tip], stroke)); - shapes.push(Shape::line( - vec![ - tip - tip_length * (rot.inverse() * dir), - tip, - tip - tip_length * (rot * dir), - ], - stroke, - )); - }); - } - - fn initialize(&mut self, _x_range: RangeInclusive) { - self.origins - .generate_points(f64::NEG_INFINITY..=f64::INFINITY); - self.tips.generate_points(f64::NEG_INFINITY..=f64::INFINITY); - } - - fn name(&self) -> &str { - self.name.as_str() - } - - fn color(&self) -> Color32 { - self.color - } - - fn highlight(&mut self) { - self.highlight = true; - } - - fn highlighted(&self) -> bool { - self.highlight - } - - fn allow_hover(&self) -> bool { - self.allow_hover - } - - fn geometry(&self) -> PlotGeometry<'_> { - PlotGeometry::Points(self.origins.points()) - } - - fn bounds(&self) -> PlotBounds { - self.origins.bounds() - } - - fn id(&self) -> Option { - self.id - } -} - -/// An image in the plot. -#[derive(Clone)] -pub struct PlotImage { - pub(super) position: PlotPoint, - pub(super) texture_id: TextureId, - pub(super) uv: Rect, - pub(super) size: Vec2, - pub(crate) rotation: f64, - pub(super) bg_fill: Color32, - pub(super) tint: Color32, - pub(super) highlight: bool, - pub(super) allow_hover: bool, - pub(super) name: String, - id: Option, -} - -impl PlotImage { - /// Create a new image with position and size in plot coordinates. - pub fn new( - texture_id: impl Into, - center_position: PlotPoint, - size: impl Into, - ) -> Self { - Self { - position: center_position, - name: Default::default(), - highlight: false, - allow_hover: true, - texture_id: texture_id.into(), - uv: Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), - size: size.into(), - rotation: 0.0, - bg_fill: Default::default(), - tint: Color32::WHITE, - id: None, - } - } - - /// Highlight this image in the plot. - #[inline] - pub fn highlight(mut self, highlight: bool) -> Self { - self.highlight = highlight; - self - } - - /// Allowed hovering this item in the plot. Default: `true`. - #[inline] - pub fn allow_hover(mut self, hovering: bool) -> Self { - self.allow_hover = hovering; - self - } - - /// Select UV range. Default is (0,0) in top-left, (1,1) bottom right. - #[inline] - pub fn uv(mut self, uv: impl Into) -> Self { - self.uv = uv.into(); - self - } - - /// A solid color to put behind the image. Useful for transparent images. - #[inline] - pub fn bg_fill(mut self, bg_fill: impl Into) -> Self { - self.bg_fill = bg_fill.into(); - self - } - - /// Multiply image color with this. Default is WHITE (no tint). - #[inline] - pub fn tint(mut self, tint: impl Into) -> Self { - self.tint = tint.into(); - self - } - - /// Name of this image. - /// - /// This name will show up in the plot legend, if legends are turned on. - /// - /// Multiple plot items may share the same name, in which case they will also share an entry in - /// the legend. - #[allow(clippy::needless_pass_by_value)] - #[inline] - pub fn name(mut self, name: impl ToString) -> Self { - self.name = name.to_string(); - self - } - - /// Rotate the image counter-clockwise around its center by an angle in radians. - #[inline] - pub fn rotate(mut self, angle: f64) -> Self { - self.rotation = angle; - self - } -} - -impl PlotItem for PlotImage { - fn shapes(&self, ui: &Ui, transform: &PlotTransform, shapes: &mut Vec) { - let Self { - position, - rotation, - texture_id, - uv, - size, - bg_fill, - tint, - highlight, - .. - } = self; - let image_screen_rect = { - let left_top = PlotPoint::new( - position.x - 0.5 * size.x as f64, - position.y - 0.5 * size.y as f64, - ); - let right_bottom = PlotPoint::new( - position.x + 0.5 * size.x as f64, - position.y + 0.5 * size.y as f64, - ); - let left_top_screen = transform.position_from_point(&left_top); - let right_bottom_screen = transform.position_from_point(&right_bottom); - Rect::from_two_pos(left_top_screen, right_bottom_screen) - }; - let screen_rotation = -*rotation as f32; - - egui::paint_texture_at( - ui.painter(), - image_screen_rect, - &ImageOptions { - uv: *uv, - bg_fill: *bg_fill, - tint: *tint, - rotation: Some((Rot2::from_angle(screen_rotation), Vec2::splat(0.5))), - rounding: Rounding::ZERO, - }, - &(*texture_id, image_screen_rect.size()).into(), - ); - if *highlight { - let center = image_screen_rect.center(); - let rotation = Rot2::from_angle(screen_rotation); - let outline = [ - image_screen_rect.right_bottom(), - image_screen_rect.right_top(), - image_screen_rect.left_top(), - image_screen_rect.left_bottom(), - ] - .iter() - .map(|point| center + rotation * (*point - center)) - .collect(); - shapes.push(Shape::closed_line( - outline, - Stroke::new(1.0, ui.visuals().strong_text_color()), - )); - } - } - - fn initialize(&mut self, _x_range: RangeInclusive) {} - - fn name(&self) -> &str { - self.name.as_str() - } - - fn color(&self) -> Color32 { - Color32::TRANSPARENT - } - - fn highlight(&mut self) { - self.highlight = true; - } - - fn highlighted(&self) -> bool { - self.highlight - } - - fn allow_hover(&self) -> bool { - self.allow_hover - } - - fn geometry(&self) -> PlotGeometry<'_> { - PlotGeometry::None - } - - fn bounds(&self) -> PlotBounds { - let mut bounds = PlotBounds::NOTHING; - let left_top = PlotPoint::new( - self.position.x as f32 - self.size.x / 2.0, - self.position.y as f32 - self.size.y / 2.0, - ); - let right_bottom = PlotPoint::new( - self.position.x as f32 + self.size.x / 2.0, - self.position.y as f32 + self.size.y / 2.0, - ); - bounds.extend_with(&left_top); - bounds.extend_with(&right_bottom); - bounds - } - - fn id(&self) -> Option { - self.id - } -} - -// ---------------------------------------------------------------------------- - -/// A bar chart. -pub struct BarChart { - pub(super) bars: Vec, - pub(super) default_color: Color32, - pub(super) name: String, - - /// A custom element formatter - pub(super) element_formatter: Option String>>, - - highlight: bool, - allow_hover: bool, - id: Option, -} - -impl BarChart { - /// Create a bar chart. It defaults to vertically oriented elements. - pub fn new(bars: Vec) -> Self { - Self { - bars, - default_color: Color32::TRANSPARENT, - name: String::new(), - element_formatter: None, - highlight: false, - allow_hover: true, - id: None, - } - } - - /// Set the default color. It is set on all elements that do not already have a specific color. - /// This is the color that shows up in the legend. - /// It can be overridden at the bar level (see [[`Bar`]]). - /// Default is `Color32::TRANSPARENT` which means a color will be auto-assigned. - #[inline] - pub fn color(mut self, color: impl Into) -> Self { - let plot_color = color.into(); - self.default_color = plot_color; - for b in &mut self.bars { - if b.fill == Color32::TRANSPARENT && b.stroke.color == Color32::TRANSPARENT { - b.fill = plot_color.linear_multiply(0.2); - b.stroke.color = plot_color; - } - } - self - } - - /// Name of this chart. - /// - /// This name will show up in the plot legend, if legends are turned on. Multiple charts may - /// share the same name, in which case they will also share an entry in the legend. - #[allow(clippy::needless_pass_by_value)] - #[inline] - pub fn name(mut self, name: impl ToString) -> Self { - self.name = name.to_string(); - self - } - - /// Set all elements to be in a vertical orientation. - /// Argument axis will be X and bar values will be on the Y axis. - #[inline] - pub fn vertical(mut self) -> Self { - for b in &mut self.bars { - b.orientation = Orientation::Vertical; - } - self - } - - /// Set all elements to be in a horizontal orientation. - /// Argument axis will be Y and bar values will be on the X axis. - #[inline] - pub fn horizontal(mut self) -> Self { - for b in &mut self.bars { - b.orientation = Orientation::Horizontal; - } - self - } - - /// Set the width (thickness) of all its elements. - #[inline] - pub fn width(mut self, width: f64) -> Self { - for b in &mut self.bars { - b.bar_width = width; - } - self - } - - /// Highlight all plot elements. - #[inline] - pub fn highlight(mut self, highlight: bool) -> Self { - self.highlight = highlight; - self - } - - /// Allowed hovering this item in the plot. Default: `true`. - #[inline] - pub fn allow_hover(mut self, hovering: bool) -> Self { - self.allow_hover = hovering; - self - } - - /// Add a custom way to format an element. - /// Can be used to display a set number of decimals or custom labels. - #[inline] - pub fn element_formatter(mut self, formatter: Box String>) -> Self { - self.element_formatter = Some(formatter); - self - } - - /// Stacks the bars on top of another chart. - /// Positive values are stacked on top of other positive values. - /// Negative values are stacked below other negative values. - #[inline] - pub fn stack_on(mut self, others: &[&Self]) -> Self { - for (index, bar) in self.bars.iter_mut().enumerate() { - let new_base_offset = if bar.value.is_sign_positive() { - others - .iter() - .filter_map(|other_chart| other_chart.bars.get(index).map(|bar| bar.upper())) - .max_by_key(|value| value.ord()) - } else { - others - .iter() - .filter_map(|other_chart| other_chart.bars.get(index).map(|bar| bar.lower())) - .min_by_key(|value| value.ord()) - }; - - if let Some(value) = new_base_offset { - bar.base_offset = Some(value); - } - } - self - } - - /// Set the bar chart's id which is used to identify it in the plot's response. - #[inline] - pub fn id(mut self, id: Id) -> Self { - self.id = Some(id); - self - } -} - -impl PlotItem for BarChart { - fn shapes(&self, _ui: &Ui, transform: &PlotTransform, shapes: &mut Vec) { - for b in &self.bars { - b.add_shapes(transform, self.highlight, shapes); - } - } - - fn initialize(&mut self, _x_range: RangeInclusive) { - // nothing to do - } - - fn name(&self) -> &str { - self.name.as_str() - } - - fn color(&self) -> Color32 { - self.default_color - } - - fn highlight(&mut self) { - self.highlight = true; - } - - fn highlighted(&self) -> bool { - self.highlight - } - - fn allow_hover(&self) -> bool { - self.allow_hover - } - - fn geometry(&self) -> PlotGeometry<'_> { - PlotGeometry::Rects - } - - fn bounds(&self) -> PlotBounds { - let mut bounds = PlotBounds::NOTHING; - for b in &self.bars { - bounds.merge(&b.bounds()); - } - bounds - } - - fn find_closest(&self, point: Pos2, transform: &PlotTransform) -> Option { - find_closest_rect(&self.bars, point, transform) - } - - fn on_hover( - &self, - elem: ClosestElem, - shapes: &mut Vec, - cursors: &mut Vec, - plot: &PlotConfig<'_>, - _: &LabelFormatter<'_>, - ) { - let bar = &self.bars[elem.index]; - - bar.add_shapes(plot.transform, true, shapes); - bar.add_rulers_and_text(self, plot, shapes, cursors); - } - - fn id(&self) -> Option { - self.id - } -} - -/// A diagram containing a series of [`BoxElem`] elements. -pub struct BoxPlot { - pub(super) boxes: Vec, - pub(super) default_color: Color32, - pub(super) name: String, - - /// A custom element formatter - pub(super) element_formatter: Option String>>, - - highlight: bool, - allow_hover: bool, - id: Option, -} - -impl BoxPlot { - /// Create a plot containing multiple `boxes`. It defaults to vertically oriented elements. - pub fn new(boxes: Vec) -> Self { - Self { - boxes, - default_color: Color32::TRANSPARENT, - name: String::new(), - element_formatter: None, - highlight: false, - allow_hover: true, - id: None, - } - } - - /// Set the default color. It is set on all elements that do not already have a specific color. - /// This is the color that shows up in the legend. - /// It can be overridden at the element level (see [`BoxElem`]). - /// Default is `Color32::TRANSPARENT` which means a color will be auto-assigned. - #[inline] - pub fn color(mut self, color: impl Into) -> Self { - let plot_color = color.into(); - self.default_color = plot_color; - for box_elem in &mut self.boxes { - if box_elem.fill == Color32::TRANSPARENT - && box_elem.stroke.color == Color32::TRANSPARENT - { - box_elem.fill = plot_color.linear_multiply(0.2); - box_elem.stroke.color = plot_color; - } - } - self - } - - /// Name of this box plot diagram. - /// - /// This name will show up in the plot legend, if legends are turned on. Multiple series may - /// share the same name, in which case they will also share an entry in the legend. - #[allow(clippy::needless_pass_by_value)] - #[inline] - pub fn name(mut self, name: impl ToString) -> Self { - self.name = name.to_string(); - self - } - - /// Set all elements to be in a vertical orientation. - /// Argument axis will be X and values will be on the Y axis. - #[inline] - pub fn vertical(mut self) -> Self { - for box_elem in &mut self.boxes { - box_elem.orientation = Orientation::Vertical; - } - self - } - - /// Set all elements to be in a horizontal orientation. - /// Argument axis will be Y and values will be on the X axis. - #[inline] - pub fn horizontal(mut self) -> Self { - for box_elem in &mut self.boxes { - box_elem.orientation = Orientation::Horizontal; - } - self - } - - /// Highlight all plot elements. - #[inline] - pub fn highlight(mut self, highlight: bool) -> Self { - self.highlight = highlight; - self - } - - /// Allowed hovering this item in the plot. Default: `true`. - #[inline] - pub fn allow_hover(mut self, hovering: bool) -> Self { - self.allow_hover = hovering; - self - } - - /// Add a custom way to format an element. - /// Can be used to display a set number of decimals or custom labels. - #[inline] - pub fn element_formatter(mut self, formatter: Box String>) -> Self { - self.element_formatter = Some(formatter); - self - } - - /// Set the box plot's id which is used to identify it in the plot's response. - #[inline] - pub fn id(mut self, id: Id) -> Self { - self.id = Some(id); - self - } -} - -impl PlotItem for BoxPlot { - fn shapes(&self, _ui: &Ui, transform: &PlotTransform, shapes: &mut Vec) { - for b in &self.boxes { - b.add_shapes(transform, self.highlight, shapes); - } - } - - fn initialize(&mut self, _x_range: RangeInclusive) { - // nothing to do - } - - fn name(&self) -> &str { - self.name.as_str() - } - - fn color(&self) -> Color32 { - self.default_color - } - - fn highlight(&mut self) { - self.highlight = true; - } - - fn highlighted(&self) -> bool { - self.highlight - } - - fn allow_hover(&self) -> bool { - self.allow_hover - } - - fn geometry(&self) -> PlotGeometry<'_> { - PlotGeometry::Rects - } - - fn bounds(&self) -> PlotBounds { - let mut bounds = PlotBounds::NOTHING; - for b in &self.boxes { - bounds.merge(&b.bounds()); - } - bounds - } - - fn find_closest(&self, point: Pos2, transform: &PlotTransform) -> Option { - find_closest_rect(&self.boxes, point, transform) - } - - fn on_hover( - &self, - elem: ClosestElem, - shapes: &mut Vec, - cursors: &mut Vec, - plot: &PlotConfig<'_>, - _: &LabelFormatter<'_>, - ) { - let box_plot = &self.boxes[elem.index]; - - box_plot.add_shapes(plot.transform, true, shapes); - box_plot.add_rulers_and_text(self, plot, shapes, cursors); - } - - fn id(&self) -> Option { - self.id - } -} - -// ---------------------------------------------------------------------------- -// 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<'_>, - text: Option, - shapes: &mut Vec, - cursors: &mut Vec, -) { - let orientation = elem.orientation(); - let show_argument = plot.show_x && orientation == Orientation::Vertical - || plot.show_y && orientation == Orientation::Horizontal; - let show_values = plot.show_y && orientation == Orientation::Vertical - || plot.show_x && orientation == Orientation::Horizontal; - - // Rulers for argument (usually vertical) - if show_argument { - for pos in elem.arguments_with_ruler() { - cursors.push(match orientation { - Orientation::Horizontal => Cursor::Horizontal { y: pos.y }, - Orientation::Vertical => Cursor::Vertical { x: pos.x }, - }); - } - } - - // Rulers for values (usually horizontal) - if show_values { - for pos in elem.values_with_ruler() { - cursors.push(match orientation { - Orientation::Horizontal => Cursor::Vertical { x: pos.x }, - Orientation::Vertical => Cursor::Horizontal { y: pos.y }, - }); - } - } - - // Text - let text = text.unwrap_or({ - let mut text = elem.name().to_owned(); // could be empty - - if show_values { - text.push('\n'); - text.push_str(&elem.default_values_format(plot.transform)); - } - - text - }); - - let font_id = TextStyle::Body.resolve(plot.ui.style()); - - let corner_value = elem.corner_value(); - plot.ui.fonts(|f| { - shapes.push(Shape::text( - f, - plot.transform.position_from_point(&corner_value) + vec2(3.0, -2.0), - Align2::LEFT_BOTTOM, - text, - font_id, - plot.ui.visuals().text_color(), - )); - }); -} - -/// Draws a cross of horizontal and vertical ruler at the `pointer` position. -/// `value` is used to for text displaying X/Y coordinates. -#[allow(clippy::too_many_arguments)] -pub(super) fn rulers_at_value( - pointer: Pos2, - value: PlotPoint, - name: &str, - plot: &PlotConfig<'_>, - shapes: &mut Vec, - cursors: &mut Vec, - label_formatter: &LabelFormatter<'_>, -) { - if plot.show_x { - cursors.push(Cursor::Vertical { x: value.x }); - } - if plot.show_y { - cursors.push(Cursor::Horizontal { y: value.y }); - } - - let prefix = if name.is_empty() { - String::new() - } else { - format!("{name}\n") - }; - - let text = { - let scale = plot.transform.dvalue_dpos(); - let x_decimals = ((-scale[0].abs().log10()).ceil().at_least(0.0) as usize).clamp(1, 6); - let y_decimals = ((-scale[1].abs().log10()).ceil().at_least(0.0) as usize).clamp(1, 6); - if let Some(custom_label) = label_formatter { - custom_label(name, &value) - } else if plot.show_x && plot.show_y { - format!( - "{}x = {:.*}\ny = {:.*}", - prefix, x_decimals, value.x, y_decimals, value.y - ) - } else if plot.show_x { - format!("{}x = {:.*}", prefix, x_decimals, value.x) - } else if plot.show_y { - format!("{}y = {:.*}", prefix, y_decimals, value.y) - } else { - unreachable!() - } - }; - - let font_id = TextStyle::Body.resolve(plot.ui.style()); - plot.ui.fonts(|f| { - shapes.push(Shape::text( - f, - pointer + vec2(3.0, -2.0), - Align2::LEFT_BOTTOM, - text, - font_id, - plot.ui.visuals().text_color(), - )); - }); -} - -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/crates/egui_plot/src/items/rect_elem.rs b/crates/egui_plot/src/items/rect_elem.rs deleted file mode 100644 index b83da7c3dbb..00000000000 --- a/crates/egui_plot/src/items/rect_elem.rs +++ /dev/null @@ -1,73 +0,0 @@ -use egui::emath::NumExt as _; -use egui::epaint::{Color32, Rgba, Stroke}; - -use crate::transform::{PlotBounds, PlotTransform}; - -use super::{Orientation, PlotPoint}; - -/// Trait that abstracts from rectangular 'Value'-like elements, such as bars or boxes -pub(super) trait RectElement { - fn name(&self) -> &str; - - fn bounds_min(&self) -> PlotPoint; - - fn bounds_max(&self) -> PlotPoint; - - fn bounds(&self) -> PlotBounds { - let mut bounds = PlotBounds::NOTHING; - bounds.extend_with(&self.bounds_min()); - bounds.extend_with(&self.bounds_max()); - bounds - } - - /// At which argument (input; usually X) there is a ruler (usually vertical) - fn arguments_with_ruler(&self) -> Vec { - // Default: one at center - vec![self.bounds().center()] - } - - /// At which value (output; usually Y) there is a ruler (usually horizontal) - fn values_with_ruler(&self) -> Vec; - - /// The diagram's orientation (vertical/horizontal) - fn orientation(&self) -> Orientation; - - /// Get X/Y-value for (argument, value) pair, taking into account orientation - fn point_at(&self, argument: f64, value: f64) -> PlotPoint { - match self.orientation() { - Orientation::Horizontal => PlotPoint::new(value, argument), - Orientation::Vertical => PlotPoint::new(argument, value), - } - } - - /// Right top of the rectangle (position of text) - fn corner_value(&self) -> PlotPoint { - //self.point_at(self.position + self.width / 2.0, value) - PlotPoint { - x: self.bounds_max().x, - y: self.bounds_max().y, - } - } - - /// Debug formatting for hovered-over value, if none is specified by the user - fn default_values_format(&self, transform: &PlotTransform) -> String; -} - -// ---------------------------------------------------------------------------- -// 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/crates/egui_plot/src/items/values.rs b/crates/egui_plot/src/items/values.rs deleted file mode 100644 index dbbb59bb41a..00000000000 --- a/crates/egui_plot/src/items/values.rs +++ /dev/null @@ -1,434 +0,0 @@ -use std::ops::{Bound, RangeBounds, RangeInclusive}; - -use egui::{Pos2, Shape, Stroke, Vec2}; - -use crate::transform::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) - } -} - -// ---------------------------------------------------------------------------- - -/// Solid, dotted, dashed, etc. -#[derive(Debug, PartialEq, Clone, Copy)] -pub enum LineStyle { - Solid, - Dotted { spacing: f32 }, - Dashed { length: f32 }, -} - -impl LineStyle { - pub fn dashed_loose() -> Self { - Self::Dashed { length: 10.0 } - } - - pub fn dashed_dense() -> Self { - Self::Dashed { length: 5.0 } - } - - pub fn dotted_loose() -> Self { - Self::Dotted { spacing: 10.0 } - } - - pub fn dotted_dense() -> Self { - Self::Dotted { spacing: 5.0 } - } - - pub(super) fn style_line( - &self, - line: Vec, - mut stroke: Stroke, - highlight: bool, - shapes: &mut Vec, - ) { - match line.len() { - 0 => {} - 1 => { - let mut radius = stroke.width / 2.0; - if highlight { - radius *= 2f32.sqrt(); - } - shapes.push(Shape::circle_filled(line[0], radius, stroke.color)); - } - _ => { - match self { - Self::Solid => { - if highlight { - stroke.width *= 2.0; - } - shapes.push(Shape::line(line, stroke)); - } - Self::Dotted { spacing } => { - // Take the stroke width for the radius even though it's not "correct", otherwise - // the dots would become too small. - let mut radius = stroke.width; - if highlight { - radius *= 2f32.sqrt(); - } - shapes.extend(Shape::dotted_line(&line, stroke.color, *spacing, radius)); - } - Self::Dashed { length } => { - if highlight { - stroke.width *= 2.0; - } - let golden_ratio = (5.0_f32.sqrt() - 1.0) / 2.0; // 0.61803398875 - shapes.extend(Shape::dashed_line( - &line, - stroke, - *length, - length * golden_ratio, - )); - } - } - } - } - } -} - -impl std::fmt::Display for LineStyle { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Solid => write!(f, "Solid"), - Self::Dotted { spacing } => write!(f, "Dotted({spacing} px)"), - Self::Dashed { length } => write!(f, "Dashed({length} px)"), - } - } -} - -// ---------------------------------------------------------------------------- - -/// Determines whether a plot element is vertically or horizontally oriented. -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum Orientation { - Horizontal, - Vertical, -} - -impl Default for Orientation { - fn default() -> Self { - Self::Vertical - } -} - -// ---------------------------------------------------------------------------- - -/// Represents many [`PlotPoint`]s. -/// -/// These can be an owned `Vec` or generated with a function. -pub enum PlotPoints { - Owned(Vec), - Generator(ExplicitGenerator), - // Borrowed(&[PlotPoint]), // TODO(EmbersArc): Lifetimes are tricky in this case. -} - -impl Default for PlotPoints { - fn default() -> Self { - Self::Owned(Vec::new()) - } -} - -impl From<[f64; 2]> for PlotPoints { - fn from(coordinate: [f64; 2]) -> Self { - Self::new(vec![coordinate]) - } -} - -impl From> for PlotPoints { - fn from(coordinates: Vec<[f64; 2]>) -> Self { - Self::new(coordinates) - } -} - -impl FromIterator<[f64; 2]> for PlotPoints { - fn from_iter>(iter: T) -> Self { - Self::Owned(iter.into_iter().map(|point| point.into()).collect()) - } -} - -impl PlotPoints { - pub fn new(points: Vec<[f64; 2]>) -> Self { - Self::from_iter(points) - } - - pub fn points(&self) -> &[PlotPoint] { - match self { - Self::Owned(points) => points.as_slice(), - Self::Generator(_) => &[], - } - } - - /// Draw a line based on a function `y=f(x)`, a range (which can be infinite) for x and the number of points. - pub fn from_explicit_callback( - function: impl Fn(f64) -> f64 + 'static, - x_range: impl RangeBounds, - points: usize, - ) -> Self { - let start = match x_range.start_bound() { - Bound::Included(x) | Bound::Excluded(x) => *x, - Bound::Unbounded => f64::NEG_INFINITY, - }; - let end = match x_range.end_bound() { - Bound::Included(x) | Bound::Excluded(x) => *x, - Bound::Unbounded => f64::INFINITY, - }; - let x_range = start..=end; - - let generator = ExplicitGenerator { - function: Box::new(function), - x_range, - points, - }; - - Self::Generator(generator) - } - - /// Draw a line based on a function `(x,y)=f(t)`, a range for t and the number of points. - /// The range may be specified as start..end or as start..=end. - pub fn from_parametric_callback( - function: impl Fn(f64) -> (f64, f64), - t_range: impl RangeBounds, - points: usize, - ) -> Self { - let start = match t_range.start_bound() { - Bound::Included(x) => x, - Bound::Excluded(_) => unreachable!(), - Bound::Unbounded => panic!("The range for parametric functions must be bounded!"), - }; - let end = match t_range.end_bound() { - Bound::Included(x) | Bound::Excluded(x) => x, - Bound::Unbounded => panic!("The range for parametric functions must be bounded!"), - }; - let last_point_included = matches!(t_range.end_bound(), Bound::Included(_)); - let increment = if last_point_included { - (end - start) / (points - 1) as f64 - } else { - (end - start) / points as f64 - }; - (0..points) - .map(|i| { - let t = start + i as f64 * increment; - function(t).into() - }) - .collect() - } - - /// From a series of y-values. - /// The x-values will be the indices of these values - pub fn from_ys_f32(ys: &[f32]) -> Self { - ys.iter() - .enumerate() - .map(|(i, &y)| [i as f64, y as f64]) - .collect() - } - - /// From a series of y-values. - /// The x-values will be the indices of these values - pub fn from_ys_f64(ys: &[f64]) -> Self { - ys.iter().enumerate().map(|(i, &y)| [i as f64, y]).collect() - } - - /// Returns true if there are no data points available and there is no function to generate any. - pub(crate) fn is_empty(&self) -> bool { - match self { - Self::Owned(points) => points.is_empty(), - Self::Generator(_) => false, - } - } - - /// If initialized with a generator function, this will generate `n` evenly spaced points in the - /// given range. - pub(super) fn generate_points(&mut self, x_range: RangeInclusive) { - if let Self::Generator(generator) = self { - *self = Self::range_intersection(&x_range, &generator.x_range) - .map(|intersection| { - let increment = - (intersection.end() - intersection.start()) / (generator.points - 1) as f64; - (0..generator.points) - .map(|i| { - let x = intersection.start() + i as f64 * increment; - let y = (generator.function)(x); - [x, y] - }) - .collect() - }) - .unwrap_or_default(); - } - } - - /// Returns the intersection of two ranges if they intersect. - fn range_intersection( - range1: &RangeInclusive, - range2: &RangeInclusive, - ) -> Option> { - let start = range1.start().max(*range2.start()); - let end = range1.end().min(*range2.end()); - (start < end).then_some(start..=end) - } - - pub(super) fn bounds(&self) -> PlotBounds { - match self { - Self::Owned(points) => { - let mut bounds = PlotBounds::NOTHING; - for point in points { - bounds.extend_with(point); - } - bounds - } - Self::Generator(generator) => generator.estimate_bounds(), - } - } -} - -// ---------------------------------------------------------------------------- - -/// Circle, Diamond, Square, Cross, … -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum MarkerShape { - Circle, - Diamond, - Square, - Cross, - Plus, - Up, - Down, - Left, - Right, - Asterisk, -} - -impl MarkerShape { - /// Get a vector containing all marker shapes. - pub fn all() -> impl ExactSizeIterator { - [ - Self::Circle, - Self::Diamond, - Self::Square, - Self::Cross, - Self::Plus, - Self::Up, - Self::Down, - Self::Left, - Self::Right, - Self::Asterisk, - ] - .iter() - .copied() - } -} - -// ---------------------------------------------------------------------------- - -/// Query the points of the plot, for geometric relations like closest checks -pub enum PlotGeometry<'a> { - /// No geometry based on single elements (examples: text, image, horizontal/vertical line) - None, - - /// Point values (X-Y graphs) - Points(&'a [PlotPoint]), - - /// Rectangles (examples: boxes or bars) - // Has currently no data, as it would require copying rects or iterating a list of pointers. - // Instead, geometry-based functions are directly implemented in the respective PlotItem impl. - Rects, -} - -// ---------------------------------------------------------------------------- - -/// Describes a function y = f(x) with an optional range for x and a number of points. -pub struct ExplicitGenerator { - function: Box f64>, - x_range: RangeInclusive, - points: usize, -} - -impl ExplicitGenerator { - fn estimate_bounds(&self) -> PlotBounds { - let mut bounds = PlotBounds::NOTHING; - - let mut add_x = |x: f64| { - // avoid infinities, as we cannot auto-bound on them! - if x.is_finite() { - bounds.extend_with_x(x); - } - let y = (self.function)(x); - if y.is_finite() { - bounds.extend_with_y(y); - } - }; - - let min_x = *self.x_range.start(); - let max_x = *self.x_range.end(); - - add_x(min_x); - add_x(max_x); - - if min_x.is_finite() && max_x.is_finite() { - // Sample some points in the interval: - const N: u32 = 8; - for i in 1..N { - let t = i as f64 / (N - 1) as f64; - let x = crate::lerp(min_x..=max_x, t); - add_x(x); - } - } else { - // Try adding some points anyway: - for x in [-1, 0, 1] { - let x = x as f64; - if min_x <= x && x <= max_x { - add_x(x); - } - } - } - - bounds - } -} - -// ---------------------------------------------------------------------------- - -/// Result of [`super::PlotItem::find_closest()`] search, identifies an element inside the item for immediate use -pub struct ClosestElem { - /// Position of hovered-over value (or bar/box-plot/…) in `PlotItem` - pub index: usize, - - /// Squared distance from the mouse cursor (needed to compare against other `PlotItems`, which might be nearer) - pub dist_sq: f32, -} diff --git a/crates/egui_plot/src/legend.rs b/crates/egui_plot/src/legend.rs deleted file mode 100644 index e7eb0793b3f..00000000000 --- a/crates/egui_plot/src/legend.rs +++ /dev/null @@ -1,324 +0,0 @@ -use std::{collections::BTreeMap, string::String}; - -use crate::*; - -use super::items::PlotItem; - -/// Where to place the plot legend. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Corner { - LeftTop, - RightTop, - LeftBottom, - RightBottom, -} - -impl Corner { - pub fn all() -> impl Iterator { - [ - Self::LeftTop, - Self::RightTop, - Self::LeftBottom, - Self::RightBottom, - ] - .iter() - .copied() - } -} - -/// The configuration for a plot legend. -#[derive(Clone, PartialEq)] -pub struct Legend { - pub text_style: TextStyle, - pub background_alpha: f32, - pub position: Corner, - - /// Used for overriding the `hidden_items` set in [`LegendWidget`]. - hidden_items: Option>, -} - -impl Default for Legend { - fn default() -> Self { - Self { - text_style: TextStyle::Body, - background_alpha: 0.75, - position: Corner::RightTop, - - hidden_items: None, - } - } -} - -impl Legend { - /// Which text style to use for the legend. Default: `TextStyle::Body`. - #[inline] - pub fn text_style(mut self, style: TextStyle) -> Self { - self.text_style = style; - self - } - - /// The alpha of the legend background. Default: `0.75`. - #[inline] - pub fn background_alpha(mut self, alpha: f32) -> Self { - self.background_alpha = alpha; - self - } - - /// In which corner to place the legend. Default: `Corner::RightTop`. - #[inline] - pub fn position(mut self, corner: Corner) -> Self { - self.position = corner; - self - } - - /// Specifies hidden items in the legend configuration to override the existing ones. This - /// allows the legend traces' visibility to be controlled from the application code. - #[inline] - pub fn hidden_items(mut self, hidden_items: I) -> Self - where - I: IntoIterator, - { - self.hidden_items = Some(hidden_items.into_iter().collect()); - self - } -} - -#[derive(Clone)] -struct LegendEntry { - color: Color32, - checked: bool, - hovered: bool, -} - -impl LegendEntry { - fn new(color: Color32, checked: bool) -> Self { - Self { - color, - checked, - hovered: false, - } - } - - fn ui(&self, ui: &mut Ui, text: String, text_style: &TextStyle) -> Response { - let Self { - color, - checked, - hovered: _, - } = self; - - let font_id = text_style.resolve(ui.style()); - - let galley = ui.fonts(|f| f.layout_delayed_color(text, font_id, f32::INFINITY)); - - let icon_size = galley.size().y; - let icon_spacing = icon_size / 5.0; - let total_extra = vec2(icon_size + icon_spacing, 0.0); - - let desired_size = total_extra + galley.size(); - let (rect, response) = ui.allocate_exact_size(desired_size, Sense::click()); - - response.widget_info(|| { - WidgetInfo::selected( - WidgetType::Checkbox, - ui.is_enabled(), - *checked, - galley.text(), - ) - }); - - let visuals = ui.style().interact(&response); - let label_on_the_left = ui.layout().horizontal_placement() == Align::RIGHT; - - let icon_position_x = if label_on_the_left { - rect.right() - icon_size / 2.0 - } else { - rect.left() + icon_size / 2.0 - }; - let icon_position = pos2(icon_position_x, rect.center().y); - let icon_rect = Rect::from_center_size(icon_position, vec2(icon_size, icon_size)); - - let painter = ui.painter(); - - painter.add(epaint::CircleShape { - center: icon_rect.center(), - radius: icon_size * 0.5, - fill: visuals.bg_fill, - stroke: visuals.bg_stroke, - }); - - if *checked { - let fill = if *color == Color32::TRANSPARENT { - ui.visuals().noninteractive().fg_stroke.color - } else { - *color - }; - painter.add(epaint::Shape::circle_filled( - icon_rect.center(), - icon_size * 0.4, - fill, - )); - } - - let text_position_x = if label_on_the_left { - rect.right() - icon_size - icon_spacing - galley.size().x - } else { - rect.left() + icon_size + icon_spacing - }; - - let text_position = pos2(text_position_x, rect.center().y - 0.5 * galley.size().y); - painter.galley(text_position, galley, visuals.text_color()); - - response - } -} - -#[derive(Clone)] -pub(super) struct LegendWidget { - rect: Rect, - entries: BTreeMap, - config: Legend, -} - -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( - rect: Rect, - config: Legend, - items: &[Box], - hidden_items: &ahash::HashSet, // Existing hidden items in the plot memory. - ) -> Option { - // If `config.hidden_items` is not `None`, it is used. - let hidden_items = config.hidden_items.as_ref().unwrap_or(hidden_items); - - // Collect the legend entries. If multiple items have the same name, they share a - // checkbox. If their colors don't match, we pick a neutral color for the checkbox. - let mut entries: BTreeMap = BTreeMap::new(); - items - .iter() - .filter(|item| !item.name().is_empty()) - .for_each(|item| { - entries - .entry(item.name().to_owned()) - .and_modify(|entry| { - if entry.color != item.color() { - // Multiple items with different colors - entry.color = Color32::TRANSPARENT; - } - }) - .or_insert_with(|| { - let color = item.color(); - let checked = !hidden_items.contains(item.name()); - LegendEntry::new(color, checked) - }); - }); - (!entries.is_empty()).then_some(Self { - rect, - entries, - config, - }) - } - - // Get the names of the hidden items. - pub fn hidden_items(&self) -> ahash::HashSet { - self.entries - .iter() - .filter(|(_, entry)| !entry.checked) - .map(|(name, _)| name.clone()) - .collect() - } - - // Get the name of the hovered items. - pub fn hovered_item_name(&self) -> Option { - self.entries - .iter() - .find(|(_, entry)| entry.hovered) - .map(|(name, _)| name.to_string()) - } -} - -impl Widget for &mut LegendWidget { - fn ui(self, ui: &mut Ui) -> Response { - let LegendWidget { - rect, - entries, - config, - } = self; - - let main_dir = match config.position { - Corner::LeftTop | Corner::RightTop => Direction::TopDown, - Corner::LeftBottom | Corner::RightBottom => Direction::BottomUp, - }; - let cross_align = match config.position { - Corner::LeftTop | Corner::LeftBottom => Align::LEFT, - Corner::RightTop | Corner::RightBottom => Align::RIGHT, - }; - let layout = Layout::from_main_dir_and_cross_align(main_dir, cross_align); - let legend_pad = 4.0; - let legend_rect = rect.shrink(legend_pad); - let mut legend_ui = ui.child_ui(legend_rect, layout, None); - legend_ui - .scope(|ui| { - let background_frame = Frame { - inner_margin: vec2(8.0, 4.0).into(), - rounding: ui.style().visuals.window_rounding, - shadow: epaint::Shadow::NONE, - fill: ui.style().visuals.extreme_bg_color, - stroke: ui.style().visuals.window_stroke(), - ..Default::default() - } - .multiply_with_opacity(config.background_alpha); - background_frame - .show(ui, |ui| { - let mut focus_on_item = None; - - let response_union = entries - .iter_mut() - .map(|(name, entry)| { - let response = entry.ui(ui, name.clone(), &config.text_style); - - // Handle interactions. Alt-clicking must be deferred to end of loop - // since it may affect all entries. - handle_interaction_on_legend_item(&response, entry); - if response.clicked() && ui.input(|r| r.modifiers.alt) { - focus_on_item = Some(name.clone()); - } - - response - }) - .reduce(|r1, r2| r1.union(r2)) - .unwrap(); - - if let Some(focus_on_item) = focus_on_item { - handle_focus_on_legend_item(&focus_on_item, entries); - } - - response_union - }) - .inner - }) - .inner - } -} - -/// Handle per-entry interactions. -fn handle_interaction_on_legend_item(response: &Response, entry: &mut LegendEntry) { - entry.checked ^= response.clicked_by(PointerButton::Primary); - entry.hovered = response.hovered(); -} - -/// Handle alt-click interaction (which may affect all entries). -fn handle_focus_on_legend_item( - clicked_entry_name: &str, - entries: &mut BTreeMap, -) { - // if all other items are already hidden, we show everything - let is_focus_item_only_visible = entries - .iter() - .all(|(name, entry)| !entry.checked || (clicked_entry_name == name)); - - // either show everything or show only the focus item - for (name, entry) in entries.iter_mut() { - entry.checked = is_focus_item_only_visible || clicked_entry_name == name; - } -} diff --git a/crates/egui_plot/src/lib.rs b/crates/egui_plot/src/lib.rs deleted file mode 100644 index aba679300e8..00000000000 --- a/crates/egui_plot/src/lib.rs +++ /dev/null @@ -1,1857 +0,0 @@ -//! Simple plotting library for [`egui`](https://github.com/emilk/egui). -//! -//! Check out [`Plot`] for how to get started. -//! -//! [**Looking for maintainer!**](https://github.com/emilk/egui/issues/4705) -//! -//! ## Feature flags -#![cfg_attr(feature = "document-features", doc = document_features::document_features!())] -//! - -mod axis; -mod items; -mod legend; -mod memory; -mod plot_ui; -mod transform; - -use std::{cmp::Ordering, ops::RangeInclusive, sync::Arc}; - -use ahash::HashMap; -use egui::*; -use emath::Float as _; -use epaint::Hsva; - -pub use crate::{ - axis::{Axis, AxisHints, HPlacement, Placement, VPlacement}, - items::{ - Arrows, Bar, BarChart, BoxElem, BoxPlot, BoxSpread, ClosestElem, HLine, Line, LineStyle, - MarkerShape, Orientation, PlotConfig, PlotGeometry, PlotImage, PlotItem, PlotPoint, - PlotPoints, Points, Polygon, Text, VLine, - }, - legend::{Corner, Legend}, - memory::PlotMemory, - plot_ui::PlotUi, - transform::{PlotBounds, PlotTransform}, -}; - -use axis::AxisWidget; -use items::{horizontal_line, rulers_color, vertical_line}; -use legend::LegendWidget; - -type LabelFormatterFn<'a> = dyn Fn(&str, &PlotPoint) -> String + 'a; -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 { y: f64 }, - Vertical { x: f64 }, -} - -/// Contains the cursors drawn for a plot widget in a single frame. -#[derive(PartialEq, Clone)] -struct PlotFrameCursors { - id: Id, - cursors: Vec, -} - -#[derive(Default, Clone)] -struct CursorLinkGroups(HashMap>); - -#[derive(Clone)] -struct LinkedBounds { - bounds: PlotBounds, - 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, or the hovered item didn't provide an id. - pub hovered_plot_item: Option, -} - -// ---------------------------------------------------------------------------- - -/// A 2D plot, e.g. a graph of a function. -/// -/// [`Plot`] supports multiple lines and points. -/// -/// ``` -/// # egui::__run_test_ui(|ui| { -/// use egui_plot::{Line, Plot, PlotPoints}; -/// -/// let sin: PlotPoints = (0..1000).map(|i| { -/// let x = i as f64 * 0.01; -/// [x, x.sin()] -/// }).collect(); -/// let line = Line::new(sin); -/// Plot::new("my_plot").view_aspect(2.0).show(ui, |plot_ui| plot_ui.line(line)); -/// # }); -/// ``` -pub struct Plot<'a> { - id_source: Id, - id: Option, - - center_axis: Vec2b, - allow_zoom: Vec2b, - allow_drag: Vec2b, - allow_scroll: Vec2b, - allow_double_click_reset: bool, - allow_boxed_zoom: bool, - default_auto_bounds: Vec2b, - min_auto_bounds: PlotBounds, - margin_fraction: Vec2, - boxed_zoom_pointer_button: PointerButton, - linked_axes: Option<(Id, Vec2b)>, - linked_cursors: Option<(Id, Vec2b)>, - - min_size: Vec2, - width: Option, - height: Option, - data_aspect: Option, - view_aspect: Option, - - reset: bool, - - show_x: bool, - show_y: bool, - label_formatter: LabelFormatter<'a>, - coordinates_formatter: Option<(Corner, CoordinatesFormatter<'a>)>, - x_axes: Vec>, // default x axes - y_axes: Vec>, // default y axes - legend_config: Option, - show_background: bool, - show_axes: Vec2b, - - show_grid: Vec2b, - grid_spacing: Rangef, - grid_spacers: [GridSpacer<'a>; 2], - sharp_grid_lines: bool, - clamp_grid: bool, - - sense: Sense, -} - -impl<'a> Plot<'a> { - /// Give a unique id for each plot within the same [`Ui`]. - pub fn new(id_source: impl std::hash::Hash) -> Self { - Self { - id_source: Id::new(id_source), - id: None, - - center_axis: false.into(), - allow_zoom: true.into(), - allow_drag: true.into(), - allow_scroll: true.into(), - allow_double_click_reset: true, - allow_boxed_zoom: true, - default_auto_bounds: true.into(), - min_auto_bounds: PlotBounds::NOTHING, - margin_fraction: Vec2::splat(0.05), - boxed_zoom_pointer_button: PointerButton::Secondary, - linked_axes: None, - linked_cursors: None, - - min_size: Vec2::splat(64.0), - width: None, - height: None, - data_aspect: None, - view_aspect: None, - - reset: false, - - show_x: true, - show_y: true, - label_formatter: None, - coordinates_formatter: None, - x_axes: vec![AxisHints::new(Axis::X)], - y_axes: vec![AxisHints::new(Axis::Y)], - legend_config: None, - show_background: true, - show_axes: true.into(), - - show_grid: true.into(), - grid_spacing: Rangef::new(8.0, 300.0), - grid_spacers: [log_grid_spacer(10), log_grid_spacer(10)], - sharp_grid_lines: true, - clamp_grid: false, - - sense: egui::Sense::click_and_drag(), - } - } - - /// Set an explicit (global) id for the plot. - /// - /// This will override the id set by [`Self::new`]. - /// - /// This is the same `Id` that can be used for [`PlotMemory::load`]. - #[inline] - pub fn id(mut self, id: Id) -> Self { - self.id = Some(id); - self - } - - /// width / height ratio of the data. - /// For instance, it can be useful to set this to `1.0` for when the two axes show the same - /// unit. - /// By default the plot window's aspect ratio is used. - #[inline] - pub fn data_aspect(mut self, data_aspect: f32) -> Self { - self.data_aspect = Some(data_aspect); - self - } - - /// width / height ratio of the plot region. - /// By default no fixed aspect ratio is set (and width/height will fill the ui it is in). - #[inline] - pub fn view_aspect(mut self, view_aspect: f32) -> Self { - self.view_aspect = Some(view_aspect); - self - } - - /// Width of plot. By default a plot will fill the ui it is in. - /// If you set [`Self::view_aspect`], the width can be calculated from the height. - #[inline] - pub fn width(mut self, width: f32) -> Self { - self.min_size.x = width; - self.width = Some(width); - self - } - - /// Height of plot. By default a plot will fill the ui it is in. - /// If you set [`Self::view_aspect`], the height can be calculated from the width. - #[inline] - pub fn height(mut self, height: f32) -> Self { - self.min_size.y = height; - self.height = Some(height); - self - } - - /// Minimum size of the plot view. - #[inline] - pub fn min_size(mut self, min_size: Vec2) -> Self { - self.min_size = min_size; - self - } - - /// Show the x-value (e.g. when hovering). Default: `true`. - #[inline] - pub fn show_x(mut self, show_x: bool) -> Self { - self.show_x = show_x; - self - } - - /// Show the y-value (e.g. when hovering). Default: `true`. - #[inline] - pub fn show_y(mut self, show_y: bool) -> Self { - self.show_y = show_y; - self - } - - /// Always keep the X-axis centered. Default: `false`. - #[inline] - pub fn center_x_axis(mut self, on: bool) -> Self { - self.center_axis.x = on; - self - } - - /// Always keep the Y-axis centered. Default: `false`. - #[inline] - pub fn center_y_axis(mut self, on: bool) -> Self { - self.center_axis.y = on; - self - } - - /// Whether to allow zooming in the plot. Default: `true`. - /// - /// Note: Allowing zoom in one axis but not the other may lead to unexpected results if used in combination with `data_aspect`. - #[inline] - pub fn allow_zoom(mut self, on: T) -> Self - where - T: Into, - { - self.allow_zoom = on.into(); - self - } - - /// Whether to allow scrolling in the plot. Default: `true`. - #[inline] - pub fn allow_scroll(mut self, on: T) -> Self - where - T: Into, - { - self.allow_scroll = on.into(); - self - } - - /// Whether to allow double clicking to reset the view. - /// Default: `true`. - #[inline] - pub fn allow_double_click_reset(mut self, on: bool) -> Self { - self.allow_double_click_reset = on; - self - } - - /// Set the side margin as a fraction of the plot size. Only used for auto bounds. - /// - /// For instance, a value of `0.1` will add 10% space on both sides. - #[inline] - pub fn set_margin_fraction(mut self, margin_fraction: Vec2) -> Self { - self.margin_fraction = margin_fraction; - self - } - - /// Whether to allow zooming in the plot by dragging out a box with the secondary mouse button. - /// - /// Default: `true`. - #[inline] - pub fn allow_boxed_zoom(mut self, on: bool) -> Self { - self.allow_boxed_zoom = on; - self - } - - /// Config the button pointer to use for boxed zooming. Default: [`Secondary`](PointerButton::Secondary) - #[inline] - pub fn boxed_zoom_pointer_button(mut self, boxed_zoom_pointer_button: PointerButton) -> Self { - self.boxed_zoom_pointer_button = boxed_zoom_pointer_button; - self - } - - /// Whether to allow dragging in the plot to move the bounds. Default: `true`. - #[inline] - pub fn allow_drag(mut self, on: T) -> Self - where - T: Into, - { - self.allow_drag = on.into(); - self - } - - /// Provide a function to customize the on-hover label for the x and y axis - /// - /// ``` - /// # egui::__run_test_ui(|ui| { - /// use egui_plot::{Line, Plot, PlotPoints}; - /// let sin: PlotPoints = (0..1000).map(|i| { - /// let x = i as f64 * 0.01; - /// [x, x.sin()] - /// }).collect(); - /// let line = Line::new(sin); - /// Plot::new("my_plot").view_aspect(2.0) - /// .label_formatter(|name, value| { - /// if !name.is_empty() { - /// format!("{}: {:.*}%", name, 1, value.y) - /// } else { - /// "".to_owned() - /// } - /// }) - /// .show(ui, |plot_ui| plot_ui.line(line)); - /// # }); - /// ``` - pub fn label_formatter( - mut self, - label_formatter: impl Fn(&str, &PlotPoint) -> String + 'a, - ) -> Self { - self.label_formatter = Some(Box::new(label_formatter)); - self - } - - /// Show the pointer coordinates in the plot. - pub fn coordinates_formatter( - mut self, - position: Corner, - formatter: CoordinatesFormatter<'a>, - ) -> Self { - self.coordinates_formatter = Some((position, formatter)); - self - } - - /// Configure how the grid in the background is spaced apart along the X axis. - /// - /// Default is a log-10 grid, i.e. every plot unit is divided into 10 other units. - /// - /// The function has this signature: - /// ```ignore - /// fn step_sizes(input: GridInput) -> Vec; - /// ``` - /// - /// This function should return all marks along the visible range of the X axis. - /// `step_size` also determines how thick/faint each line is drawn. - /// For example, if x = 80..=230 is visible and you want big marks at steps of - /// 100 and small ones at 25, you can return: - /// ```no_run - /// # use egui_plot::GridMark; - /// vec![ - /// // 100s - /// GridMark { value: 100.0, step_size: 100.0 }, - /// GridMark { value: 200.0, step_size: 100.0 }, - /// - /// // 25s - /// GridMark { value: 125.0, step_size: 25.0 }, - /// GridMark { value: 150.0, step_size: 25.0 }, - /// GridMark { value: 175.0, step_size: 25.0 }, - /// GridMark { value: 225.0, step_size: 25.0 }, - /// ]; - /// # () - /// ``` - /// - /// There are helpers for common cases, see [`log_grid_spacer`] and [`uniform_grid_spacer`]. - #[inline] - pub fn x_grid_spacer(mut self, spacer: impl Fn(GridInput) -> Vec + 'a) -> Self { - self.grid_spacers[0] = Box::new(spacer); - self - } - - /// Default is a log-10 grid, i.e. every plot unit is divided into 10 other units. - /// - /// See [`Self::x_grid_spacer`] for explanation. - #[inline] - pub fn y_grid_spacer(mut self, spacer: impl Fn(GridInput) -> Vec + 'a) -> Self { - self.grid_spacers[1] = Box::new(spacer); - self - } - - /// Set when the grid starts showing. - /// - /// When grid lines are closer than the given minimum, they will be hidden. - /// When they get further apart they will fade in, until the reaches the given maximum, - /// at which point they are fully opaque. - #[inline] - pub fn grid_spacing(mut self, grid_spacing: impl Into) -> Self { - self.grid_spacing = grid_spacing.into(); - self - } - - /// Clamp the grid to only be visible at the range of data where we have values. - /// - /// Default: `false`. - #[inline] - pub fn clamp_grid(mut self, clamp_grid: bool) -> Self { - self.clamp_grid = clamp_grid; - self - } - - /// Set the sense for the plot rect. - /// - /// Default: `Sense::click_and_drag()`. - #[inline] - pub fn sense(mut self, sense: Sense) -> Self { - self.sense = sense; - self - } - - /// Expand bounds to include the given x value. - /// For instance, to always show the y axis, call `plot.include_x(0.0)`. - #[inline] - pub fn include_x(mut self, x: impl Into) -> Self { - self.min_auto_bounds.extend_with_x(x.into()); - self - } - - /// Expand bounds to include the given y value. - /// For instance, to always show the x axis, call `plot.include_y(0.0)`. - #[inline] - pub fn include_y(mut self, y: impl Into) -> Self { - self.min_auto_bounds.extend_with_y(y.into()); - self - } - - /// Set whether the bounds should be automatically set based on data by default. - /// - /// This is enabled by default. - #[inline] - pub fn auto_bounds(mut self, auto_bounds: Vec2b) -> Self { - self.default_auto_bounds = auto_bounds; - self - } - - /// Expand bounds to fit all items across the x axis, including values given by `include_x`. - #[deprecated = "Use `auto_bounds` instead"] - #[inline] - pub fn auto_bounds_x(mut self) -> Self { - self.default_auto_bounds.x = true; - self - } - - /// Expand bounds to fit all items across the y axis, including values given by `include_y`. - #[deprecated = "Use `auto_bounds` instead"] - #[inline] - pub fn auto_bounds_y(mut self) -> Self { - self.default_auto_bounds.y = true; - self - } - - /// Show a legend including all named items. - #[inline] - pub fn legend(mut self, legend: Legend) -> Self { - self.legend_config = Some(legend); - self - } - - /// Whether or not to show the background [`Rect`]. - /// - /// Can be useful to disable if the plot is overlaid over existing content. - /// Default: `true`. - #[inline] - pub fn show_background(mut self, show: bool) -> Self { - self.show_background = show; - self - } - - /// Show axis labels and grid tick values on the side of the plot. - /// - /// Default: `true`. - #[inline] - pub fn show_axes(mut self, show: impl Into) -> Self { - self.show_axes = show.into(); - self - } - - /// Show a grid overlay on the plot. - /// - /// Default: `true`. - #[inline] - pub fn show_grid(mut self, show: impl Into) -> Self { - self.show_grid = show.into(); - self - } - - /// Add this plot to an axis link group so that this plot will share the bounds with other plots in the - /// same group. A plot cannot belong to more than one axis group. - #[inline] - pub fn link_axis(mut self, group_id: impl Into, link_x: bool, link_y: bool) -> Self { - self.linked_axes = Some(( - group_id.into(), - Vec2b { - x: link_x, - y: link_y, - }, - )); - self - } - - /// Add this plot to a cursor link group so that this plot will share the cursor position with other plots - /// in the same group. A plot cannot belong to more than one cursor group. - #[inline] - pub fn link_cursor(mut self, group_id: impl Into, link_x: bool, link_y: bool) -> Self { - self.linked_cursors = Some(( - group_id.into(), - Vec2b { - x: link_x, - y: link_y, - }, - )); - self - } - - /// Round grid positions to full pixels to avoid aliasing. Improves plot appearance but might have an - /// undesired effect when shifting the plot bounds. Enabled by default. - #[inline] - pub fn sharp_grid_lines(mut self, enabled: bool) -> Self { - self.sharp_grid_lines = enabled; - self - } - - /// Resets the plot. - #[inline] - pub fn reset(mut self) -> Self { - self.reset = true; - self - } - - /// Set the x axis label of the main X-axis. - /// - /// Default: no label. - #[inline] - pub fn x_axis_label(mut self, label: impl Into) -> Self { - if let Some(main) = self.x_axes.first_mut() { - main.label = label.into(); - } - self - } - - /// Set the y axis label of the main Y-axis. - /// - /// Default: no label. - #[inline] - pub fn y_axis_label(mut self, label: impl Into) -> Self { - if let Some(main) = self.y_axes.first_mut() { - main.label = label.into(); - } - self - } - - /// Set the position of the main X-axis. - #[inline] - pub fn x_axis_position(mut self, placement: axis::VPlacement) -> Self { - if let Some(main) = self.x_axes.first_mut() { - main.placement = placement.into(); - } - self - } - - /// Set the position of the main Y-axis. - #[inline] - pub fn y_axis_position(mut self, placement: axis::HPlacement) -> Self { - if let Some(main) = self.y_axes.first_mut() { - main.placement = placement.into(); - } - self - } - - /// Specify custom formatter for ticks on the main X-axis. - /// - /// Arguments of `fmt`: - /// * the grid mark to format - /// * currently shown range on this axis. - pub fn x_axis_formatter( - mut self, - fmt: impl Fn(GridMark, &RangeInclusive) -> String + 'a, - ) -> Self { - if let Some(main) = self.x_axes.first_mut() { - main.formatter = Arc::new(fmt); - } - self - } - - /// Specify custom formatter for ticks on the main Y-axis. - /// - /// Arguments of `fmt`: - /// * the grid mark to format - /// * currently shown range on this axis. - pub fn y_axis_formatter( - mut self, - fmt: impl Fn(GridMark, &RangeInclusive) -> String + 'a, - ) -> Self { - if let Some(main) = self.y_axes.first_mut() { - main.formatter = Arc::new(fmt); - } - self - } - - /// Set the minimum width of the main y-axis, in ui points. - /// - /// The width will automatically expand if any tickmark text is wider than this. - #[inline] - pub fn y_axis_min_width(mut self, min_width: f32) -> Self { - if let Some(main) = self.y_axes.first_mut() { - main.min_thickness = min_width; - } - self - } - - /// Set the main Y-axis-width by number of digits - #[inline] - #[deprecated = "Use `y_axis_min_width` instead"] - pub fn y_axis_width(self, digits: usize) -> Self { - self.y_axis_min_width(12.0 * digits as f32) - } - - /// Set custom configuration for X-axis - /// - /// More than one axis may be specified. The first specified axis is considered the main axis. - #[inline] - pub fn custom_x_axes(mut self, hints: Vec>) -> Self { - self.x_axes = hints; - self - } - - /// Set custom configuration for left Y-axis - /// - /// More than one axis may be specified. The first specified axis is considered the main axis. - #[inline] - pub fn custom_y_axes(mut self, hints: Vec>) -> Self { - self.y_axes = hints; - self - } - - /// Interact with and add items to the plot and finally draw it. - pub fn show( - self, - ui: &mut Ui, - build_fn: impl FnOnce(&mut PlotUi) -> R + 'a, - ) -> PlotResponse { - self.show_dyn(ui, Box::new(build_fn)) - } - - fn show_dyn( - self, - ui: &mut Ui, - build_fn: Box R + 'a>, - ) -> PlotResponse { - let Self { - id_source, - id, - center_axis, - allow_zoom, - allow_drag, - allow_scroll, - allow_double_click_reset, - allow_boxed_zoom, - boxed_zoom_pointer_button, - default_auto_bounds, - min_auto_bounds, - margin_fraction, - width, - height, - mut min_size, - data_aspect, - view_aspect, - mut show_x, - mut show_y, - label_formatter, - coordinates_formatter, - x_axes, - y_axes, - legend_config, - reset, - show_background, - show_axes, - show_grid, - grid_spacing, - linked_axes, - linked_cursors, - - clamp_grid, - grid_spacers, - sharp_grid_lines, - sense, - } = self; - - // Disable interaction if ui is disabled. - let allow_zoom = allow_zoom.and(ui.is_enabled()); - let allow_drag = allow_drag.and(ui.is_enabled()); - let allow_scroll = allow_scroll.and(ui.is_enabled()); - - // Determine position of widget. - let pos = ui.available_rect_before_wrap().min; - // Minimum values for screen protection - min_size.x = min_size.x.at_least(1.0); - min_size.y = min_size.y.at_least(1.0); - - // Determine size of widget. - let size = { - let width = width - .unwrap_or_else(|| { - if let (Some(height), Some(aspect)) = (height, view_aspect) { - height * aspect - } else { - ui.available_size_before_wrap().x - } - }) - .at_least(min_size.x); - - let height = height - .unwrap_or_else(|| { - if let Some(aspect) = view_aspect { - width / aspect - } else { - ui.available_size_before_wrap().y - } - }) - .at_least(min_size.y); - vec2(width, height) - }; - - // Determine complete rect of widget. - let complete_rect = Rect { - min: pos, - max: pos + size, - }; - - let plot_id = id.unwrap_or_else(|| ui.make_persistent_id(id_source)); - - let ([x_axis_widgets, y_axis_widgets], plot_rect) = axis_widgets( - PlotMemory::load(ui.ctx(), plot_id).as_ref(), // TODO(emilk): avoid loading plot memory twice - show_axes, - complete_rect, - [&x_axes, &y_axes], - ); - - // Allocate the plot window. - let response = ui.allocate_rect(plot_rect, sense); - - // Load or initialize the memory. - ui.ctx().check_for_id_clash(plot_id, plot_rect, "Plot"); - - let mut mem = if reset { - if let Some((name, _)) = linked_axes.as_ref() { - ui.data_mut(|data| { - let link_groups: &mut BoundsLinkGroups = data.get_temp_mut_or_default(Id::NULL); - link_groups.0.remove(name); - }); - }; - None - } else { - PlotMemory::load(ui.ctx(), plot_id) - } - .unwrap_or_else(|| PlotMemory { - auto_bounds: default_auto_bounds, - hovered_legend_item: None, - hidden_items: Default::default(), - transform: PlotTransform::new(plot_rect, min_auto_bounds, center_axis.x, center_axis.y), - last_click_pos_for_zoom: None, - x_axis_thickness: Default::default(), - y_axis_thickness: Default::default(), - }); - - let last_plot_transform = mem.transform; - - // Call the plot build function. - let mut plot_ui = PlotUi { - ctx: ui.ctx().clone(), - items: Vec::new(), - next_auto_color_idx: 0, - last_plot_transform, - last_auto_bounds: mem.auto_bounds, - response, - bounds_modifications: Vec::new(), - }; - let inner = build_fn(&mut plot_ui); - let PlotUi { - mut items, - mut response, - last_plot_transform, - bounds_modifications, - .. - } = plot_ui; - - // Background - if show_background { - ui.painter() - .with_clip_rect(plot_rect) - .add(epaint::RectShape::new( - plot_rect, - Rounding::same(2.0), - ui.visuals().extreme_bg_color, - ui.visuals().widgets.noninteractive.bg_stroke, - )); - } - - // --- Legend --- - let legend = legend_config - .and_then(|config| LegendWidget::try_new(plot_rect, config, &items, &mem.hidden_items)); - // Don't show hover cursor when hovering over legend. - if mem.hovered_legend_item.is_some() { - show_x = false; - show_y = false; - } - // Remove the deselected items. - items.retain(|item| !mem.hidden_items.contains(item.name())); - // Highlight the hovered items. - if let Some(hovered_name) = &mem.hovered_legend_item { - items - .iter_mut() - .filter(|entry| entry.name() == hovered_name) - .for_each(|entry| entry.highlight()); - } - // Move highlighted items to front. - items.sort_by_key(|item| item.highlighted()); - - // --- Bound computation --- - let mut bounds = *last_plot_transform.bounds(); - - // Find the cursors from other plots we need to draw - let draw_cursors: Vec = if let Some((id, _)) = linked_cursors.as_ref() { - ui.data_mut(|data| { - let frames: &mut CursorLinkGroups = data.get_temp_mut_or_default(Id::NULL); - let cursors = frames.0.entry(*id).or_default(); - - // Look for our previous frame - let index = cursors - .iter() - .enumerate() - .find(|(_, frame)| frame.id == plot_id) - .map(|(i, _)| i); - - // Remove our previous frame and all older frames as these are no longer displayed. This avoids - // unbounded growth, as we add an entry each time we draw a plot. - index.map(|index| cursors.drain(0..=index)); - - // Gather all cursors of the remaining frames. This will be all the cursors of the - // other plots in the group. We want to draw these in the current plot too. - cursors - .iter() - .flat_map(|frame| frame.cursors.iter().copied()) - .collect() - }) - } else { - Vec::new() - }; - - // Transfer the bounds from a link group. - if let Some((id, axes)) = linked_axes.as_ref() { - ui.data_mut(|data| { - let link_groups: &mut BoundsLinkGroups = data.get_temp_mut_or_default(Id::NULL); - if let Some(linked_bounds) = link_groups.0.get(id) { - if axes.x { - bounds.set_x(&linked_bounds.bounds); - mem.auto_bounds.x = linked_bounds.auto_bounds.x; - } - if axes.y { - bounds.set_y(&linked_bounds.bounds); - mem.auto_bounds.y = linked_bounds.auto_bounds.y; - } - }; - }); - }; - - // Allow double-clicking to reset to the initial bounds. - if allow_double_click_reset && response.double_clicked() { - mem.auto_bounds = true.into(); - } - - // Apply bounds modifications. - for modification in bounds_modifications { - match modification { - BoundsModification::Set(new_bounds) => { - bounds = new_bounds; - mem.auto_bounds = false.into(); - } - BoundsModification::Translate(delta) => { - let delta = (delta.x as f64, delta.y as f64); - bounds.translate(delta); - mem.auto_bounds = false.into(); - } - BoundsModification::AutoBounds(new_auto_bounds) => { - mem.auto_bounds = new_auto_bounds; - } - BoundsModification::Zoom(zoom_factor, center) => { - bounds.zoom(zoom_factor, center); - mem.auto_bounds = false.into(); - } - } - } - - // Reset bounds to initial bounds if they haven't been modified. - if mem.auto_bounds.x { - bounds.set_x(&min_auto_bounds); - } - if mem.auto_bounds.y { - bounds.set_y(&min_auto_bounds); - } - - let auto_x = mem.auto_bounds.x && (!min_auto_bounds.is_valid_x() || default_auto_bounds.x); - let auto_y = mem.auto_bounds.y && (!min_auto_bounds.is_valid_y() || default_auto_bounds.y); - - // Set bounds automatically based on content. - if auto_x || auto_y { - for item in &items { - let item_bounds = item.bounds(); - if auto_x { - bounds.merge_x(&item_bounds); - } - if auto_y { - bounds.merge_y(&item_bounds); - } - } - - if auto_x { - bounds.add_relative_margin_x(margin_fraction); - } - - if auto_y { - bounds.add_relative_margin_y(margin_fraction); - } - } - - mem.transform = PlotTransform::new(plot_rect, bounds, center_axis.x, center_axis.y); - - // Enforce aspect ratio - if let Some(data_aspect) = data_aspect { - if let Some((_, linked_axes)) = &linked_axes { - let change_x = linked_axes.y && !linked_axes.x; - mem.transform.set_aspect_by_changing_axis( - data_aspect as f64, - if change_x { Axis::X } else { Axis::Y }, - ); - } else if default_auto_bounds.any() { - mem.transform.set_aspect_by_expanding(data_aspect as f64); - } else { - mem.transform - .set_aspect_by_changing_axis(data_aspect as f64, Axis::Y); - } - } - - // Dragging - if allow_drag.any() && response.dragged_by(PointerButton::Primary) { - response = response.on_hover_cursor(CursorIcon::Grabbing); - let mut delta = -response.drag_delta(); - if !allow_drag.x { - delta.x = 0.0; - } - if !allow_drag.y { - delta.y = 0.0; - } - mem.transform - .translate_bounds((delta.x as f64, delta.y as f64)); - mem.auto_bounds = mem.auto_bounds.and(!allow_drag); - } - - // Zooming - let mut boxed_zoom_rect = None; - if allow_boxed_zoom { - // Save last click to allow boxed zooming - if response.drag_started() && response.dragged_by(boxed_zoom_pointer_button) { - // it would be best for egui that input has a memory of the last click pos because it's a common pattern - mem.last_click_pos_for_zoom = response.hover_pos(); - } - let box_start_pos = mem.last_click_pos_for_zoom; - let box_end_pos = response.hover_pos(); - if let (Some(box_start_pos), Some(box_end_pos)) = (box_start_pos, box_end_pos) { - // while dragging prepare a Shape and draw it later on top of the plot - if response.dragged_by(boxed_zoom_pointer_button) { - response = response.on_hover_cursor(CursorIcon::ZoomIn); - let rect = epaint::Rect::from_two_pos(box_start_pos, box_end_pos); - boxed_zoom_rect = Some(( - epaint::RectShape::stroke( - rect, - 0.0, - epaint::Stroke::new(4., Color32::DARK_BLUE), - ), // Outer stroke - epaint::RectShape::stroke( - rect, - 0.0, - epaint::Stroke::new(2., Color32::WHITE), - ), // Inner stroke - )); - } - // when the click is release perform the zoom - if response.drag_stopped() { - let box_start_pos = mem.transform.value_from_position(box_start_pos); - let box_end_pos = mem.transform.value_from_position(box_end_pos); - let new_bounds = PlotBounds { - min: [ - box_start_pos.x.min(box_end_pos.x), - box_start_pos.y.min(box_end_pos.y), - ], - max: [ - box_start_pos.x.max(box_end_pos.x), - box_start_pos.y.max(box_end_pos.y), - ], - }; - if new_bounds.is_valid() { - mem.transform.set_bounds(new_bounds); - mem.auto_bounds = false.into(); - } - // reset the boxed zoom state - mem.last_click_pos_for_zoom = None; - } - } - } - - // Note: we catch zoom/pan if the response contains the pointer, even if it isn't hovered. - // For instance: The user is painting another interactive widget on top of the plot - // but they still want to be able to pan/zoom the plot. - if let (true, Some(hover_pos)) = ( - response.contains_pointer, - ui.input(|i| i.pointer.hover_pos()), - ) { - if allow_zoom.any() { - let mut zoom_factor = if data_aspect.is_some() { - Vec2::splat(ui.input(|i| i.zoom_delta())) - } else { - ui.input(|i| i.zoom_delta_2d()) - }; - if !allow_zoom.x { - zoom_factor.x = 1.0; - } - if !allow_zoom.y { - zoom_factor.y = 1.0; - } - if zoom_factor != Vec2::splat(1.0) { - mem.transform.zoom(zoom_factor, hover_pos); - mem.auto_bounds = mem.auto_bounds.and(!allow_zoom); - } - } - if allow_scroll.any() { - let mut scroll_delta = ui.input(|i| i.smooth_scroll_delta); - if !allow_scroll.x { - scroll_delta.x = 0.0; - } - if !allow_scroll.y { - scroll_delta.y = 0.0; - } - if scroll_delta != Vec2::ZERO { - mem.transform - .translate_bounds((-scroll_delta.x as f64, -scroll_delta.y as f64)); - mem.auto_bounds = false.into(); - } - } - } - - // --- transform initialized - - // Add legend widgets to plot - let bounds = mem.transform.bounds(); - let x_axis_range = bounds.range_x(); - let x_steps = Arc::new({ - let input = GridInput { - bounds: (bounds.min[0], bounds.max[0]), - base_step_size: mem.transform.dvalue_dpos()[0].abs() * grid_spacing.min as f64, - }; - (grid_spacers[0])(input) - }); - let y_axis_range = bounds.range_y(); - let y_steps = Arc::new({ - let input = GridInput { - bounds: (bounds.min[1], bounds.max[1]), - base_step_size: mem.transform.dvalue_dpos()[1].abs() * grid_spacing.min as f64, - }; - (grid_spacers[1])(input) - }); - for (i, mut widget) in x_axis_widgets.into_iter().enumerate() { - widget.range = x_axis_range.clone(); - widget.transform = Some(mem.transform); - widget.steps = x_steps.clone(); - let (_response, thickness) = widget.ui(ui, Axis::X); - mem.x_axis_thickness.insert(i, thickness); - } - for (i, mut widget) in y_axis_widgets.into_iter().enumerate() { - widget.range = y_axis_range.clone(); - widget.transform = Some(mem.transform); - widget.steps = y_steps.clone(); - let (_response, thickness) = widget.ui(ui, Axis::Y); - mem.y_axis_thickness.insert(i, thickness); - } - - // Initialize values from functions. - for item in &mut items { - item.initialize(mem.transform.bounds().range_x()); - } - - let prepared = PreparedPlot { - items, - show_x, - show_y, - label_formatter, - coordinates_formatter, - show_grid, - grid_spacing, - transform: mem.transform, - draw_cursor_x: linked_cursors.as_ref().map_or(false, |group| group.1.x), - draw_cursor_y: linked_cursors.as_ref().map_or(false, |group| group.1.y), - draw_cursors, - grid_spacers, - sharp_grid_lines, - clamp_grid, - }; - - let (plot_cursors, hovered_plot_item) = prepared.ui(ui, &response); - - if let Some(boxed_zoom_rect) = boxed_zoom_rect { - ui.painter() - .with_clip_rect(plot_rect) - .add(boxed_zoom_rect.0); - ui.painter() - .with_clip_rect(plot_rect) - .add(boxed_zoom_rect.1); - } - - if let Some(mut legend) = legend { - ui.add(&mut legend); - mem.hidden_items = legend.hidden_items(); - mem.hovered_legend_item = legend.hovered_item_name(); - } - - if let Some((id, _)) = linked_cursors.as_ref() { - // Push the frame we just drew to the list of frames - ui.data_mut(|data| { - let frames: &mut CursorLinkGroups = data.get_temp_mut_or_default(Id::NULL); - let cursors = frames.0.entry(*id).or_default(); - cursors.push(PlotFrameCursors { - id: plot_id, - cursors: plot_cursors, - }); - }); - } - - if let Some((id, _)) = linked_axes.as_ref() { - // Save the linked bounds. - ui.data_mut(|data| { - let link_groups: &mut BoundsLinkGroups = data.get_temp_mut_or_default(Id::NULL); - link_groups.0.insert( - *id, - LinkedBounds { - bounds: *mem.transform.bounds(), - auto_bounds: mem.auto_bounds, - }, - ); - }); - } - - let transform = mem.transform; - mem.store(ui.ctx(), plot_id); - - let response = if show_x || show_y { - response.on_hover_cursor(CursorIcon::Crosshair) - } else { - response - }; - - ui.advance_cursor_after_rect(complete_rect); - - PlotResponse { - inner, - response, - transform, - hovered_plot_item, - } - } -} - -/// Returns the rect left after adding axes. -fn axis_widgets<'a>( - mem: Option<&PlotMemory>, - show_axes: Vec2b, - complete_rect: Rect, - [x_axes, y_axes]: [&'a [AxisHints<'a>]; 2], -) -> ([Vec>; 2], Rect) { - // Next we want to create this layout. - // Indices are only examples. - // - // left right - // +---+---------x----------+ + - // | | X-axis 3 | - // | +--------------------+ top - // | | X-axis 2 | - // +-+-+--------------------+-+-+ - // |y|y| |y|y| - // |-|-| |-|-| - // |A|A| |A|A| - // y|x|x| Plot Window |x|x| - // |i|i| |i|i| - // |s|s| |s|s| - // |1|0| |2|3| - // +-+-+--------------------+-+-+ - // | X-axis 0 | | - // +--------------------+ | bottom - // | X-axis 1 | | - // + +--------------------+---+ - // - - let mut x_axis_widgets = Vec::>::new(); - let mut y_axis_widgets = Vec::>::new(); - - // Will shrink as we add more axes. - let mut rect_left = complete_rect; - - if show_axes.x { - // We will fix this later, once we know how much space the y axes take up. - let initial_x_range = complete_rect.x_range(); - - for (i, cfg) in x_axes.iter().enumerate().rev() { - let mut height = cfg.thickness(Axis::X); - if let Some(mem) = mem { - // If the labels took up too much space the previous frame, give them more space now: - height = height.max(mem.x_axis_thickness.get(&i).copied().unwrap_or_default()); - } - - let rect = match VPlacement::from(cfg.placement) { - VPlacement::Bottom => { - let bottom = rect_left.bottom(); - *rect_left.bottom_mut() -= height; - let top = rect_left.bottom(); - Rect::from_x_y_ranges(initial_x_range, top..=bottom) - } - VPlacement::Top => { - let top = rect_left.top(); - *rect_left.top_mut() += height; - let bottom = rect_left.top(); - Rect::from_x_y_ranges(initial_x_range, top..=bottom) - } - }; - x_axis_widgets.push(AxisWidget::new(cfg.clone(), rect)); - } - } - if show_axes.y { - // We know this, since we've already allocated space for the x axes. - let plot_y_range = rect_left.y_range(); - - for (i, cfg) in y_axes.iter().enumerate().rev() { - let mut width = cfg.thickness(Axis::Y); - if let Some(mem) = mem { - // If the labels took up too much space the previous frame, give them more space now: - width = width.max(mem.y_axis_thickness.get(&i).copied().unwrap_or_default()); - } - - let rect = match HPlacement::from(cfg.placement) { - HPlacement::Left => { - let left = rect_left.left(); - *rect_left.left_mut() += width; - let right = rect_left.left(); - Rect::from_x_y_ranges(left..=right, plot_y_range) - } - HPlacement::Right => { - let right = rect_left.right(); - *rect_left.right_mut() -= width; - let left = rect_left.right(); - Rect::from_x_y_ranges(left..=right, plot_y_range) - } - }; - y_axis_widgets.push(AxisWidget::new(cfg.clone(), rect)); - } - } - - let mut plot_rect = rect_left; - - // If too little space, remove axis widgets - if plot_rect.width() <= 0.0 || plot_rect.height() <= 0.0 { - y_axis_widgets.clear(); - x_axis_widgets.clear(); - plot_rect = complete_rect; - } - - // Now that we know the final x_range of the plot_rect, - // assign it to the x_axis_widgets (they are currently too wide): - for widget in &mut x_axis_widgets { - widget.rect = Rect::from_x_y_ranges(plot_rect.x_range(), widget.rect.y_range()); - } - - ([x_axis_widgets, y_axis_widgets], plot_rect) -} - -/// User-requested modifications to the plot bounds. We collect them in the plot build function to later apply -/// them at the right time, as other modifications need to happen first. -enum BoundsModification { - Set(PlotBounds), - Translate(Vec2), - AutoBounds(Vec2b), - Zoom(Vec2, PlotPoint), -} - -// ---------------------------------------------------------------------------- -// Grid - -/// Input for "grid spacer" functions. -/// -/// See [`Plot::x_grid_spacer()`] and [`Plot::y_grid_spacer()`]. -pub struct GridInput { - /// Min/max of the visible data range (the values at the two edges of the plot, - /// for the current axis). - pub bounds: (f64, f64), - - /// Recommended (but not required) lower-bound on the step size returned by custom grid spacers. - /// - /// Computed as the ratio between the diagram's bounds (in plot coordinates) and the viewport - /// (in frame/window coordinates), scaled up to represent the minimal possible step. - /// - /// Always positive. - pub base_step_size: f64, -} - -/// One mark (horizontal or vertical line) in the background grid of a plot. -#[derive(Debug, Clone, Copy, 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) -} - -// ---------------------------------------------------------------------------- - -struct PreparedPlot<'a> { - items: Vec>, - show_x: bool, - show_y: bool, - label_formatter: LabelFormatter<'a>, - coordinates_formatter: Option<(Corner, CoordinatesFormatter<'a>)>, - // axis_formatters: [AxisFormatter; 2], - transform: PlotTransform, - show_grid: Vec2b, - grid_spacing: Rangef, - grid_spacers: [GridSpacer<'a>; 2], - draw_cursor_x: bool, - draw_cursor_y: bool, - draw_cursors: Vec, - - sharp_grid_lines: bool, - clamp_grid: bool, -} - -impl<'a> PreparedPlot<'a> { - fn ui(self, ui: &mut Ui, response: &Response) -> (Vec, Option) { - let mut axes_shapes = Vec::new(); - - if self.show_grid.x { - self.paint_grid(ui, &mut axes_shapes, Axis::X, self.grid_spacing); - } - if self.show_grid.y { - self.paint_grid(ui, &mut axes_shapes, Axis::Y, self.grid_spacing); - } - - // Sort the axes by strength so that those with higher strength are drawn in front. - axes_shapes.sort_by(|(_, strength1), (_, strength2)| strength1.total_cmp(strength2)); - - let mut shapes = axes_shapes.into_iter().map(|(shape, _)| shape).collect(); - - let transform = &self.transform; - - let mut plot_ui = ui.child_ui(*transform.frame(), Layout::default(), None); - plot_ui.set_clip_rect(transform.frame().intersect(ui.clip_rect())); - for item in &self.items { - item.shapes(&plot_ui, transform, &mut shapes); - } - - let hover_pos = response.hover_pos(); - let (cursors, hovered_item_id) = if let Some(pointer) = hover_pos { - self.hover(ui, pointer, &mut shapes) - } else { - (Vec::new(), None) - }; - - // Draw cursors - let line_color = rulers_color(ui); - - let mut draw_cursor = |cursors: &Vec, always| { - for &cursor in cursors { - match cursor { - Cursor::Horizontal { y } => { - if self.draw_cursor_y || always { - shapes.push(horizontal_line( - transform.position_from_point(&PlotPoint::new(0.0, y)), - &self.transform, - line_color, - )); - } - } - Cursor::Vertical { x } => { - if self.draw_cursor_x || always { - shapes.push(vertical_line( - transform.position_from_point(&PlotPoint::new(x, 0.0)), - &self.transform, - line_color, - )); - } - } - } - } - }; - - draw_cursor(&self.draw_cursors, false); - draw_cursor(&cursors, true); - - let painter = ui.painter().with_clip_rect(*transform.frame()); - painter.extend(shapes); - - if let Some((corner, formatter)) = self.coordinates_formatter.as_ref() { - let hover_pos = response.hover_pos(); - if let Some(pointer) = hover_pos { - let font_id = TextStyle::Monospace.resolve(ui.style()); - let coordinate = transform.value_from_position(pointer); - let text = formatter.format(&coordinate, transform.bounds()); - let padded_frame = transform.frame().shrink(4.0); - let (anchor, position) = match corner { - Corner::LeftTop => (Align2::LEFT_TOP, padded_frame.left_top()), - Corner::RightTop => (Align2::RIGHT_TOP, padded_frame.right_top()), - Corner::LeftBottom => (Align2::LEFT_BOTTOM, padded_frame.left_bottom()), - Corner::RightBottom => (Align2::RIGHT_BOTTOM, padded_frame.right_bottom()), - }; - painter.text(position, anchor, text, font_id, ui.visuals().text_color()); - } - } - - (cursors, hovered_item_id) - } - - fn paint_grid(&self, ui: &Ui, shapes: &mut Vec<(Shape, f32)>, axis: Axis, fade_range: Rangef) { - #![allow(clippy::collapsible_else_if)] - let Self { - transform, - // axis_formatters, - grid_spacers, - clamp_grid, - .. - } = self; - - let iaxis = usize::from(axis); - - // Where on the cross-dimension to show the label values - let bounds = transform.bounds(); - let value_cross = 0.0_f64.clamp(bounds.min[1 - iaxis], bounds.max[1 - iaxis]); - - let input = GridInput { - bounds: (bounds.min[iaxis], bounds.max[iaxis]), - base_step_size: transform.dvalue_dpos()[iaxis].abs() * fade_range.min as f64, - }; - let steps = (grid_spacers[iaxis])(input); - - let clamp_range = clamp_grid.then(|| { - let mut tight_bounds = PlotBounds::NOTHING; - for item in &self.items { - let item_bounds = item.bounds(); - tight_bounds.merge_x(&item_bounds); - tight_bounds.merge_y(&item_bounds); - } - tight_bounds - }); - - for step in steps { - let value_main = step.value; - - if let Some(clamp_range) = clamp_range { - match axis { - Axis::X => { - if !clamp_range.range_x().contains(&value_main) { - continue; - }; - } - Axis::Y => { - if !clamp_range.range_y().contains(&value_main) { - continue; - }; - } - } - } - - let value = match axis { - Axis::X => PlotPoint::new(value_main, value_cross), - Axis::Y => PlotPoint::new(value_cross, value_main), - }; - - let pos_in_gui = transform.position_from_point(&value); - let spacing_in_points = (transform.dpos_dvalue()[iaxis] * step.step_size).abs() as f32; - - if spacing_in_points <= fade_range.min { - continue; // Too close together - } - - let line_strength = remap_clamp(spacing_in_points, fade_range, 0.0..=1.0); - - let line_color = color_from_strength(ui, line_strength); - - let mut p0 = pos_in_gui; - let mut p1 = pos_in_gui; - p0[1 - iaxis] = transform.frame().min[1 - iaxis]; - p1[1 - iaxis] = transform.frame().max[1 - iaxis]; - - if let Some(clamp_range) = clamp_range { - match axis { - Axis::X => { - p0.y = transform.position_from_point_y(clamp_range.min[1]); - p1.y = transform.position_from_point_y(clamp_range.max[1]); - } - Axis::Y => { - p0.x = transform.position_from_point_x(clamp_range.min[0]); - p1.x = transform.position_from_point_x(clamp_range.max[0]); - } - } - } - - if self.sharp_grid_lines { - // Round to avoid aliasing - p0 = ui.painter().round_pos_to_pixels(p0); - p1 = ui.painter().round_pos_to_pixels(p1); - } - - shapes.push(( - Shape::line_segment([p0, p1], Stroke::new(1.0, line_color)), - line_strength, - )); - } - } - - fn hover(&self, ui: &Ui, pointer: Pos2, shapes: &mut Vec) -> (Vec, Option) { - let Self { - transform, - show_x, - show_y, - label_formatter, - items, - .. - } = self; - - if !show_x && !show_y { - return (Vec::new(), None); - } - - let interact_radius_sq = ui.style().interaction.interact_radius.powi(2); - - let candidates = items - .iter() - .filter(|entry| entry.allow_hover()) - .filter_map(|item| { - let item = &**item; - let closest = item.find_closest(pointer, transform); - - Some(item).zip(closest) - }); - - let closest = candidates - .min_by_key(|(_, elem)| elem.dist_sq.ord()) - .filter(|(_, elem)| elem.dist_sq <= interact_radius_sq); - - let plot = items::PlotConfig { - ui, - transform, - show_x: *show_x, - show_y: *show_y, - }; - - let mut cursors = Vec::new(); - - let hovered_plot_item_id = if let Some((item, elem)) = closest { - item.on_hover(elem, shapes, &mut cursors, &plot, label_formatter); - item.id() - } else { - let value = transform.value_from_position(pointer); - items::rulers_at_value( - pointer, - value, - "", - &plot, - shapes, - &mut cursors, - label_formatter, - ); - None - }; - - (cursors, hovered_plot_item_id) - } -} - -/// Returns next bigger power in given base -/// e.g. -/// ```ignore -/// use egui_plot::next_power; -/// assert_eq!(next_power(0.01, 10.0), 0.01); -/// assert_eq!(next_power(0.02, 10.0), 0.1); -/// assert_eq!(next_power(0.2, 10.0), 1); -/// ``` -fn next_power(value: f64, base: f64) -> f64 { - debug_assert_ne!(value, 0.0); // can be negative (typical for Y axis) - base.powi(value.abs().log(base).ceil() as i32) -} - -/// Fill in all values between [min, max] which are a multiple of `step_size` -fn generate_marks(step_sizes: [f64; 3], bounds: (f64, f64)) -> Vec { - 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 = None; - if marks.len() != expected.len() { - problem = 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()) -} diff --git a/crates/egui_plot/src/memory.rs b/crates/egui_plot/src/memory.rs deleted file mode 100644 index 5e671865144..00000000000 --- a/crates/egui_plot/src/memory.rs +++ /dev/null @@ -1,81 +0,0 @@ -use std::collections::BTreeMap; - -use egui::{Context, Id, Pos2, Vec2b}; - -use crate::{PlotBounds, PlotTransform}; - -/// Information about the plot that has to persist between frames. -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[derive(Clone)] -pub struct PlotMemory { - /// Indicates if the plot uses automatic bounds. - /// - /// This is set to `false` whenever the user modifies - /// the bounds, for example by moving or zooming. - pub auto_bounds: Vec2b, - - /// Display string of the hovered legend item if any. - pub hovered_legend_item: Option, - - /// Which items _not_ to show? - pub hidden_items: ahash::HashSet, - - /// The transform from last frame. - pub(crate) transform: PlotTransform, - - /// Allows to remember the first click position when performing a boxed zoom - pub(crate) last_click_pos_for_zoom: Option, - - /// The thickness of each of the axes the previous frame. - /// - /// This is used in the next frame to make the axes thicker - /// in order to fit the labels, if necessary. - pub(crate) x_axis_thickness: BTreeMap, - pub(crate) y_axis_thickness: BTreeMap, -} - -impl PlotMemory { - #[inline] - pub fn transform(&self) -> PlotTransform { - self.transform - } - - #[inline] - pub fn set_transform(&mut self, t: PlotTransform) { - self.transform = t; - } - - /// Plot-space bounds. - #[inline] - pub fn bounds(&self) -> &PlotBounds { - self.transform.bounds() - } - - /// Plot-space bounds. - #[inline] - pub fn set_bounds(&mut self, bounds: PlotBounds) { - self.transform.set_bounds(bounds); - } -} - -#[cfg(feature = "serde")] -impl PlotMemory { - pub fn load(ctx: &Context, id: Id) -> Option { - ctx.data_mut(|d| d.get_persisted(id)) - } - - pub fn store(self, ctx: &Context, id: Id) { - ctx.data_mut(|d| d.insert_persisted(id, self)); - } -} - -#[cfg(not(feature = "serde"))] -impl PlotMemory { - pub fn load(ctx: &Context, id: Id) -> Option { - ctx.data_mut(|d| d.get_temp(id)) - } - - pub fn store(self, ctx: &Context, id: Id) { - ctx.data_mut(|d| d.insert_temp(id, self)); - } -} diff --git a/crates/egui_plot/src/plot_ui.rs b/crates/egui_plot/src/plot_ui.rs deleted file mode 100644 index 83c4c367de0..00000000000 --- a/crates/egui_plot/src/plot_ui.rs +++ /dev/null @@ -1,235 +0,0 @@ -use crate::*; - -/// 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 { - pub(crate) ctx: 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 PlotUi { - fn auto_color(&mut self) -> Color32 { - let i = self.next_auto_color_idx; - self.next_auto_color_idx += 1; - let golden_ratio = (5.0_f32.sqrt() - 1.0) / 2.0; // 0.61803398875 - let h = i as f32 * golden_ratio; - Hsva::new(h, 0.85, 0.5, 1.0).into() // TODO(emilk): OkLab or some other perspective color space - } - - pub fn ctx(&self) -> &Context { - &self.ctx - } - - /// The plot bounds as they were in the last frame. If called on the first frame and the bounds were not - /// further specified in the plot builder, this will return bounds centered on the origin. The bounds do - /// not change until the plot is drawn. - pub fn plot_bounds(&self) -> PlotBounds { - *self.last_plot_transform.bounds() - } - - /// Set the plot bounds. Can be useful for implementing alternative plot navigation methods. - pub fn set_plot_bounds(&mut self, plot_bounds: PlotBounds) { - self.bounds_modifications - .push(BoundsModification::Set(plot_bounds)); - } - - /// Move the plot bounds. Can be useful for implementing alternative plot navigation methods. - pub fn translate_bounds(&mut self, delta_pos: Vec2) { - self.bounds_modifications - .push(BoundsModification::Translate(delta_pos)); - } - - /// 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: Vec2b) { - self.bounds_modifications - .push(BoundsModification::AutoBounds(auto_bounds)); - } - - /// 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 + 'static) { - self.items.push(Box::new(item)); - } - - /// Add a data line. - pub fn line(&mut self, mut line: Line) { - if line.series.is_empty() { - return; - }; - - // Give the stroke an automatic color if no color has been assigned. - if line.stroke.color == Color32::TRANSPARENT { - line.stroke.color = self.auto_color(); - } - self.items.push(Box::new(line)); - } - - /// Add a polygon. The polygon has to be convex. - pub fn polygon(&mut self, mut polygon: Polygon) { - if polygon.series.is_empty() { - return; - }; - - // Give the stroke an automatic color if no color has been assigned. - if polygon.stroke.color == Color32::TRANSPARENT { - polygon.stroke.color = self.auto_color(); - } - self.items.push(Box::new(polygon)); - } - - /// Add a text. - pub fn text(&mut self, text: Text) { - if text.text.is_empty() { - return; - }; - - self.items.push(Box::new(text)); - } - - /// Add data points. - pub fn points(&mut self, mut points: Points) { - if points.series.is_empty() { - return; - }; - - // Give the points an automatic color if no color has been assigned. - if points.color == Color32::TRANSPARENT { - points.color = self.auto_color(); - } - self.items.push(Box::new(points)); - } - - /// Add arrows. - pub fn arrows(&mut self, mut arrows: Arrows) { - if arrows.origins.is_empty() || arrows.tips.is_empty() { - return; - }; - - // Give the arrows an automatic color if no color has been assigned. - if arrows.color == Color32::TRANSPARENT { - arrows.color = self.auto_color(); - } - self.items.push(Box::new(arrows)); - } - - /// Add an image. - pub fn image(&mut self, image: PlotImage) { - self.items.push(Box::new(image)); - } - - /// Add a horizontal line. - /// Can be useful e.g. to show min/max bounds or similar. - /// Always fills the full width of the plot. - pub fn hline(&mut self, mut hline: HLine) { - if hline.stroke.color == Color32::TRANSPARENT { - hline.stroke.color = self.auto_color(); - } - self.items.push(Box::new(hline)); - } - - /// Add a vertical line. - /// Can be useful e.g. to show min/max bounds or similar. - /// Always fills the full height of the plot. - pub fn vline(&mut self, mut vline: VLine) { - if vline.stroke.color == Color32::TRANSPARENT { - vline.stroke.color = self.auto_color(); - } - self.items.push(Box::new(vline)); - } - - /// Add a box plot diagram. - pub fn box_plot(&mut self, mut box_plot: BoxPlot) { - if box_plot.boxes.is_empty() { - return; - } - - // Give the elements an automatic color if no color has been assigned. - if box_plot.default_color == Color32::TRANSPARENT { - box_plot = box_plot.color(self.auto_color()); - } - self.items.push(Box::new(box_plot)); - } - - /// Add a bar chart. - pub fn bar_chart(&mut self, mut chart: BarChart) { - if chart.bars.is_empty() { - return; - } - - // Give the elements an automatic color if no color has been assigned. - if chart.default_color == Color32::TRANSPARENT { - chart = chart.color(self.auto_color()); - } - self.items.push(Box::new(chart)); - } -} diff --git a/crates/egui_plot/src/transform.rs b/crates/egui_plot/src/transform.rs deleted file mode 100644 index 405b6e44172..00000000000 --- a/crates/egui_plot/src/transform.rs +++ /dev/null @@ -1,509 +0,0 @@ -use std::ops::RangeInclusive; - -use super::PlotPoint; -use crate::*; - -/// 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 { - [ - (self.min[0] + self.max[0]) / 2.0, - (self.min[1] + self.max[1]) / 2.0, - ] - .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; - } -} - -/// 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 of the bounds. - x_centered: bool, - - /// Whether to always center the y-range of the bounds. - y_centered: bool, -} - -impl PlotTransform { - pub fn new(frame: Rect, bounds: PlotBounds, x_centered: bool, y_centered: bool) -> Self { - debug_assert!( - 0.0 <= frame.width() && 0.0 <= frame.height(), - "Bad plot frame: {frame:?}" - ); - - // 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 x_centered { - new_bounds.make_x_symmetrical(); - }; - if y_centered { - new_bounds.make_y_symmetrical(); - }; - - debug_assert!( - new_bounds.is_valid(), - "Bad final plot bounds: {new_bounds:?}" - ); - - Self { - frame, - bounds: new_bounds, - x_centered, - y_centered, - } - } - - /// 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.x_centered { - delta_pos.0 = 0.; - } - if self.y_centered { - 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], - (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], - (self.frame.bottom() as f64)..=(self.frame.top() as f64), // negated y axis! - ) 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, - (self.frame.left() as f64)..=(self.frame.right() as f64), - self.bounds.range_x(), - ); - let y = remap( - pos.y as f64, - (self.frame.bottom() as f64)..=(self.frame.top() as f64), // negated y axis! - 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 { - 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 { - -self.frame.height() as f64 / self.bounds.height() // negated y axis! - } - - /// 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/examples/custom_plot_manipulation/Cargo.toml b/examples/custom_plot_manipulation/Cargo.toml deleted file mode 100644 index fc7b66c5ccc..00000000000 --- a/examples/custom_plot_manipulation/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "custom_plot_manipulation" -version = "0.1.0" -authors = ["Ygor Souza "] -license = "MIT OR Apache-2.0" -edition = "2021" -rust-version = "1.76" -publish = false - -[lints] -workspace = true - - -[dependencies] -eframe = { workspace = true, features = [ - "default", - "__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO -] } -egui_plot.workspace = true -env_logger = { version = "0.10", default-features = false, features = [ - "auto-color", - "humantime", -] } diff --git a/examples/custom_plot_manipulation/README.md b/examples/custom_plot_manipulation/README.md deleted file mode 100644 index fedd08e574a..00000000000 --- a/examples/custom_plot_manipulation/README.md +++ /dev/null @@ -1,7 +0,0 @@ -Example how to use raw input events to implement alternative controls to pan and zoom the plot - -```sh -cargo run -p custom_plot_manipulation -``` - -![](screenshot.png) diff --git a/examples/custom_plot_manipulation/screenshot.png b/examples/custom_plot_manipulation/screenshot.png deleted file mode 100644 index cc49659227d9852299f6dafd8eb45511d38f067c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 39733 zcmbUJ2T&Ai(*_I=S(2g%NJdEl5+sO(1ym#l36g^dND>g1xJyzIK|nG{mYkzx7Eq8Z zEE!3QQ+E`Co58NBiR{kYy_=of2m9L{aQ-$hvqN zzcx|H$>EcA(ON@aLuo_>?%-+sd`Nf=zl-+aH7n(SNVc0)qz~lsotspoX8VO4gdvG* z-u(+ZFplBjnNib?6GKdCAr5nbZea|pG;BHD4IMt(TagOB5=iy%QpWTc?$$Lb;0<_v zGZ3J<__??Q2rqsvE{TZQwzjsG$Ibo4#GO%UDw6u+RP~pD2jHJmJ>(%!;{84MKJqiG z&+?B%-)k0MY5uwuK4GNpT$@f6Ts=vY$MMSfkgA}ybmjW``j6T3gW6B%_kTSo_A#*A zN-nr48lXgSg@t1QVEc39+X;BtSy^qxhF?0uBk!2h4VWOjDmH3o{Z9IdjjDUj?)e_O z>6n?FGX?lyM)hB8jFrr6*ZZD$>`mCDyZg>oSiYY?c<(_i%FJ6(YAN>z)lwx#PY)L& zNXyPvvmzQkZZJU-O3&urvvxfJE?q30s)q{ORlj+c@inWLNwsO;_{zQ9#DYK6-YR z2?7({t~RT~PrLaj=+jK2XY%~~^Dj0?7Gd3Zf-VwPAF4g^*G>YRS_sdL(33^`Lks=V zl|7%(DHDM!4&$X_*4Ea@c6D|2@C{qt#%361{Zr>|0g7^=(OS~UR?4+34GjgNECela z=VvFTouZk)cRE><+umPkoRyT4%4&E%3R$3nBAdCE__8rg___{mnSCCdNzuWz|IIh-~Nx5pYWqrR>;eZu{gjU~t= z(`|H84i*hz$EOe49nBLuQQ!M;N3yt1_wC0WeQ

jm+7;j*h9Rqeja8t9u3lBLv;K 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 Ar2qf` diff --git a/examples/custom_plot_manipulation/src/main.rs b/examples/custom_plot_manipulation/src/main.rs deleted file mode 100644 index 4e218dd233f..00000000000 --- a/examples/custom_plot_manipulation/src/main.rs +++ /dev/null @@ -1,129 +0,0 @@ -//! This example shows how to implement custom gestures to pan and zoom in the plot -#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release -#![allow(rustdoc::missing_crate_level_docs)] // it's an example - -use eframe::egui::{self, DragValue, Event, Vec2}; -use egui_plot::{Legend, Line, PlotPoints}; - -fn main() -> eframe::Result { - env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). - let options = eframe::NativeOptions::default(); - eframe::run_native( - "Plot", - options, - Box::new(|_cc| Ok(Box::::default())), - ) -} - -struct PlotExample { - lock_x: bool, - lock_y: bool, - ctrl_to_zoom: bool, - shift_to_horizontal: bool, - zoom_speed: f32, - scroll_speed: f32, -} - -impl Default for PlotExample { - fn default() -> Self { - Self { - lock_x: false, - lock_y: false, - ctrl_to_zoom: false, - shift_to_horizontal: false, - zoom_speed: 1.0, - scroll_speed: 1.0, - } - } -} - -impl eframe::App for PlotExample { - fn update(&mut self, ctx: &egui::Context, _: &mut eframe::Frame) { - egui::SidePanel::left("options").show(ctx, |ui| { - ui.checkbox(&mut self.lock_x, "Lock x axis").on_hover_text("Check to keep the X axis fixed, i.e., pan and zoom will only affect the Y axis"); - ui.checkbox(&mut self.lock_y, "Lock y axis").on_hover_text("Check to keep the Y axis fixed, i.e., pan and zoom will only affect the X axis"); - ui.checkbox(&mut self.ctrl_to_zoom, "Ctrl to zoom").on_hover_text("If unchecked, the behavior of the Ctrl key is inverted compared to the default controls\ni.e., scrolling the mouse without pressing any keys zooms the plot"); - ui.checkbox(&mut self.shift_to_horizontal, "Shift for horizontal scroll").on_hover_text("If unchecked, the behavior of the shift key is inverted compared to the default controls\ni.e., hold to scroll vertically, release to scroll horizontally"); - ui.horizontal(|ui| { - ui.add( - DragValue::new(&mut self.zoom_speed) - .range(0.1..=2.0) - .speed(0.1), - ); - ui.label("Zoom speed").on_hover_text("How fast to zoom in and out with the mouse wheel"); - }); - ui.horizontal(|ui| { - ui.add( - DragValue::new(&mut self.scroll_speed) - .range(0.1..=100.0) - .speed(0.1), - ); - ui.label("Scroll speed").on_hover_text("How fast to pan with the mouse wheel"); - }); - }); - egui::CentralPanel::default().show(ctx, |ui| { - let (scroll, pointer_down, modifiers) = ui.input(|i| { - let scroll = i.events.iter().find_map(|e| match e { - Event::MouseWheel { - unit: _, - delta, - modifiers: _, - } => Some(*delta), - _ => None, - }); - (scroll, i.pointer.primary_down(), i.modifiers) - }); - - ui.label("This example shows how to use raw input events to implement different plot controls than the ones egui provides by default, e.g., default to zooming instead of panning when the Ctrl key is not pressed, or controlling much it zooms with each mouse wheel step."); - - egui_plot::Plot::new("plot") - .allow_zoom(false) - .allow_drag(false) - .allow_scroll(false) - .legend(Legend::default()) - .show(ui, |plot_ui| { - if let Some(mut scroll) = scroll { - if modifiers.ctrl == self.ctrl_to_zoom { - scroll = Vec2::splat(scroll.x + scroll.y); - let mut zoom_factor = Vec2::from([ - (scroll.x * self.zoom_speed / 10.0).exp(), - (scroll.y * self.zoom_speed / 10.0).exp(), - ]); - if self.lock_x { - zoom_factor.x = 1.0; - } - if self.lock_y { - zoom_factor.y = 1.0; - } - plot_ui.zoom_bounds_around_hovered(zoom_factor); - } else { - if modifiers.shift == self.shift_to_horizontal { - scroll = Vec2::new(scroll.y, scroll.x); - } - if self.lock_x { - scroll.x = 0.0; - } - if self.lock_y { - scroll.y = 0.0; - } - let delta_pos = self.scroll_speed * scroll; - plot_ui.translate_bounds(delta_pos); - } - } - if plot_ui.response().hovered() && pointer_down { - let mut pointer_translate = -plot_ui.pointer_coordinate_drag_delta(); - if self.lock_x { - pointer_translate.x = 0.0; - } - if self.lock_y { - pointer_translate.y = 0.0; - } - plot_ui.translate_bounds(pointer_translate); - } - - let sine_points = PlotPoints::from_explicit_callback(|x| x.sin(), .., 5000); - plot_ui.line(Line::new(sine_points).name("Sine")); - }); - }); - } -} diff --git a/examples/save_plot/Cargo.toml b/examples/save_plot/Cargo.toml deleted file mode 100644 index d20c927b058..00000000000 --- a/examples/save_plot/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[package] -name = "save_plot" -version = "0.1.0" -authors = ["hacknus "] -license = "MIT OR Apache-2.0" -edition = "2021" -rust-version = "1.76" -publish = false - -[lints] -workspace = true - -[dependencies] -eframe = { workspace = true, features = [ - "default", - "__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO -] } -egui_plot.workspace = true -image = { workspace = true, features = ["png"] } -rfd = "0.13.0" -env_logger = { version = "0.10", default-features = false, features = [ - "auto-color", - "humantime", -] } diff --git a/examples/save_plot/README.md b/examples/save_plot/README.md deleted file mode 100644 index 4a6c2e0380e..00000000000 --- a/examples/save_plot/README.md +++ /dev/null @@ -1,7 +0,0 @@ -This example shows that you can save a plot in egui as a png. - -```sh -cargo run -p save_plot -``` - -![](screenshot.png) diff --git a/examples/save_plot/screenshot.png b/examples/save_plot/screenshot.png deleted file mode 100644 index 121f2ac0c8db7a7dc4f6b3c2da97a02a8a8e7d45..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6467 zcmZ`;X*iU9)Sn5HFtW#xT}Ff$d$NrsWDk+Om~3UqF3T{+-XdfjBiXk~h%iXDG^1?E zo+bO9z4ylRJnxtH{cz89U;pi#^E>DK&i_6WZJ?(KrGim`Kp^N1Eu;|$1hxj=+bPcg zzhYp{d=Q9@>IPE9#P7#aX0(#Y0@vq|zat}6vB{R&s&khM-)U)ydI-5&%_qmq&p2z| z;2BC$b?9Jtv!d`~H@HXcr2?BI#H%esF8ga&*Fv|X(@Uow|Iv+-v9Et$!(;F?n@W!< znAGW^|MxS5fMKGd0s{i%U9CTU{OI7|Q2&(ao&W+KPW^0ef8U=^s8bs?XOQrruI~7~ zhxBl~35*ZJGodkqgM+1()$Tvubqo#3$;v}yL#uAi* z!WHzXv~YzSRQj%r`cp78Ze(R;UB7-^#{FkgS9`mbfm(pDnApnCTCYKrv$MR{Om{=@ zzFziqD>E}EEM&w-PcLE9$ZTi$#}8g!-iM3PjDp(Q+OJwy8uphf;KA1bII84P$}cr* z6+@1K0t4$r>5)z4!K$j^XtbJ!Mq7?rl&YFqUUlfX`c?p+sPgg$o}TLj^w~ z`km<%n@!UA(h^h@1XfW|LA9Hhndw9w2vC{v!{E+a`>^Gay4lIeH;)hZR##U6v*sEj z?Gv;Aq{}>5{#k3RJ@nS;y!Gq+{0Bd447CY7@ZFWM#_T8v!ie*Fkl)&*x}aeGb3y%i zVyMGFspY_rA4M$@-@BS16zW4PF#ep@<-tlaimj>7i3PK{It-aurN0M~^VC{kS#2F1 z7iZ^HzQoL_$;rn%6H#tndq?{#kFL<(@VnqN{4wy)(c!+BO+(38dZ@!zy14Rr3sEGQ z`Vbt3D=NB-*qNG|B6*lwTE-^vX(njhJm(G0Pz+?5x}ZhSN%Ap4==!R`7Etv8TSZ5e z1Onmc=;(Me3v9IBU$T4*tTa`ipd}=axa;VMw*uYOa$uWNK=Pf5?ow7$SErE9y5`SI zpzNc2XHXH(I3L0a#E~f@6u!$Y?Wzpr%A`a|;gsGK6lmc=Mg|xas{8t8W@fMEXzRUy zGi+-Xg9AAge7ZRhwh3tROkuOKaeRToxKaRbp05W(Ju+WISs6r9HY^^)!G(qEg7>B6 z`Nlp53g+RwROXQ-B_#;O5FP~u1sR!v?haa6Hx-qzck%98o9=UE@N+BpneY)TB<4*{ z1Eeem2gip25sRO&^e~hLGKU(Qa|a#+jKuK5P9Gx$*!srC)p!PB!dR@cgaNkTb>!W} zCwZ@jX9^|M3&p1{t@p&`R^ZwT&OqCXOusuBT96&`XxBZd_?seTL-VZ>Yat#?&`I@k zDNHeM>x;LVEQQ||tE`V>-=|I4vwFE>(k^>xAU&kr=v&HKVVq)iasc`VY@g+%QuLsHCJM>vb+R zHa00bM-Zz{l*R>yZJX6A4*4+ps_{Yu49dc0OU4C6x2KPf=@k_5oN!I$Q4FxRBo1=T zGdMUn^|nJv#xS-iAuKGctV~o~RUOszK@9o&@#Y*oNf{yHSG3-(D@PQ+dNr&WW8+Y& zy7~K?Ajlqt(qcw6tv?hYYV6!-^={G;S^JJHqR+cZylTMspp2)L3=r3?BU0;kbs@zjYk3I$_mhVQ zOFj8~enEi?GyMBSkW>{89Nq_onYbEMsN_7qbe$7fkPxHVW|Ju&x#U|}ne1#}TUlPI z?|yS)W4uz+Ni!erI-XZqmuus}%Ee`-F2nCCMi-bI3`L^fDJmjNJl6TmiK!VS3mDgi zV0NKWs# zEy!$ukd{VqRxD~^-g;$3I83b;Md$fr=p1>FKsbSGw3)We{rO=o!2pR)BN0=dg&P?@ zKG-n{X^i%VO188xhlLGp9BwUm;=nB|4-E;S^1P)jz06Tab@ks$VZrvX5K|h;;J6Wh zy2xTLjc|nM8mU&?E3^i!Xf1-oWDj<}xX|M!xW8YB!+{^zowX_s0|MxKEM|TV#dPk= z?uMhDf6?KF{-O;tTVZB8A4&*e3m^IV_54HtgKF6oiFiknSL)~-#`699Ks z7%Nv@aM%CvnISbao?Uu;n7qdJEYk4rQuWxTSlF_LxA4J0g7|^g!zS9n_)}pWSy{cg znueTd%Rg_gnzT8pN$01E@pp1Yh!-U(;{LdfBufeote$4L?zR?U{!!mxM8}% z7CSf`4U?ZAjSNC05c&yaY@Aaq)AKDWtHBe2iw6nj`*X22?!G30C?F7f=Lh!oyryIm z)5qMta?RC%4p8Uz7_&URA>=YT+_Cj`wD4`~Er0vTn<#is|LUJHVI*0MV)swtenIx! z++6DO=MFIF>Ql_U%`&UH&aSStH+gv!vtd%NDx=RWV)y~tK&v@A7V+{LfaY7X4%C%i zyc8B&U}W^@skXC|WMmZnjj4aMz*2v~u$@G=V|n+cwlM|E$%Hs~PuGdxZ3XWLeGL>+JM=?;c$1@1<{-vzzUF zV-2>at>g9H)&KXe@0KQaT|@Mej=7IrRc(0cbF==3#Khm(o6L9>xE$AYCN5dq5BAN| zw}7K;8Bqm|K||dMp)Yb({Je>oYx^szzHK%kS89}A3tsj$l;$OlD*@aBXNP7B3XWyS z@zHSIsl`{hgKGY;a<_7~|NfRu15q(}w<=spG!QMgT_7OMPqHmXvAVfyP^+eTul|bu ztuJis+McMNO=T>2O;K~S(Fr6AU{P@n;obQ7u$g3-S_G%vDyM?In~dnH&s4ZN@kUZ6 z#m*rbg137?A|9+XHoMv=HAW}UdR;Fqq0bYntJ6v3Ak5xBn&oOaWOL0rX52ce@U^b7J@VCxg&oVHfkoI-*>ilxyy9oy%ppcbq;j%ypHv5J6l+;| z$kMWCgl%W%o7=HzJr~5pe~X8F&EolZBL=`o8POhzDf@Yc=iyYD75dgKi(#K~STAb* z)9Ulgp`p_SSd1fU2lr1e<)-gHCK>k7j3`9t=LIcF+vDCje+j2Nmv=ZR1{bpw_{HG^WN&U>md8B@4OBaxaY?+L*l_J;WGVr|qD(}kP#mh`kgN2r; zc8ALH6?04W6P02)dM!;cQc2>$b3$6Rv(>U`YR_AZJ%rUfD zh#;oG0lQZ5yL5H&D068T&Mt1<&yOqvBiqlQL95484iCIC0^ zg#;s=-yVsiHhtJ>DhPXJeBax>Y32)D0C49mwHBnA+3$&#=7YcAO@w9d6>~s!tfEu_ z6N}LhyT0*+i$ej$kZ4FPaBawHZca&;nROrTH#5}$R!B!dScS)DsV`CQRYI^3VH_Cd zc3!ok_^rzksvgKJr%V@6=p{s@Dk(F{5z`Z>I(&2v&LRDe- zWLPL2W!~m1OM6yasnMU-7J!yp3^uRc*jByjlJSxRMC46`$o|C3!}UCQ38Pi$N5A8! z{iWWrIR}qpSbZmT=i`S(9Xe>g3i?$Wy9{~tnsNTPv>pJ+8=Q)H-_&&EY{}v9pc@i> zB^NyZ1m9j;cOHg3_7LU;9?ZyLm+)kNp*(fHrWrWIRD%p*erwBlplo)b|Izf!acqAg zg4DwBJx%K83%9#}V$O!Z1q_O)oia~MjC-OM-9$L?bPQvF~m zEBT^=bl*U}GvZx!wWfwf|G0-K?QHqT&sCa7cbzP^8IUdWc<_8@ zhkt|5Qt?RL+2Z0M99>d!^33e4CPE|zV$RvL1KK`|mXcy=BqvqOqv+bw=jX*8d2mX? zv8Yse!5EZyJSIGqmB(jzZK^`)$rNDT6&yH?jkAoBhAFvMQ@s!d{j)r>vAX(X5BBEm zTPeIV@;;5XMUo>Ao5J0~-|zj4bUl3BRJ5=^Bxn^G^4`Z?$dE%T9R2DQ_!fn09~6Wc z85@(Jw+#(>KJvq8z7oJOnY8kEknYEoMUC91Y$khQA4vzLg5}G!0=9LmqE~MCr4X5?pF6Y@B4(4ONBC` zfELPES}HCEx?s^5KXRN*NOS-R)DoUiYH4Rpv^04)o+Jq52f&t55OZcqJM}*+T)WaG z@DrMzS64fn?owTRUs3UL&7=0%o4{HF5X@E=6nF+eb}YnY+PCwhtEgSDua=WL>1MW9bv?w3S;HUAJnz##NWHLoD9^1ab+wC58M7hg8D>I5cC>TefD#MWwzt(+L{F42QdV2a(=l3nfBWObe_-lefCkoP zVHq17Owoef|1;h2&weuKM*#u3o}Qfrq04WYPfY;Q1qEDSup=`Iu`%e=L57)Z}J8c8mf6TKEWpsC~7 z`w6LwSfRujrT!BG1|?w?+-F`yGKBnHOGpq<{ActEtRmD1JHof>9!fgB$12P`of+=i4PJcPfb)1_N$> zpA+@B{*=G^b&~G3N~q9VH&qEApcm%0Ve%4hlIp?e@6={!ZgAHm3=)GE5dhkV@JG4e zrLv{rYM`I$G5p@`)2C0)b>H@MOm5*LBFM$W#C(1K;BdI)WR|VF*MMHNnh6Spa&i(R zRbZ7P6a%-v4-8BRdjTCV{WfSyL6OSR($dqXPisb?P$X9rR=>03EsFq9QCt{jm(k#fy)`!p`YF|Q<*~-ZBZ`qOD7!S!rl5yzmzI=>$jDT8c6JIeQ;&S*Th|* eframe::Result { - env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). - - let options = eframe::NativeOptions { - viewport: egui::ViewportBuilder::default().with_inner_size([350.0, 200.0]), - ..Default::default() - }; - eframe::run_native( - "My egui App with a plot", - options, - Box::new(|_cc| Ok(Box::::default())), - ) -} - -#[derive(Default)] -struct MyApp {} - -impl eframe::App for MyApp { - fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { - let mut plot_rect = None; - egui::CentralPanel::default().show(ctx, |ui| { - if ui.button("Save Plot").clicked() { - ctx.send_viewport_cmd(egui::ViewportCommand::Screenshot); - } - - let my_plot = Plot::new("My Plot").legend(Legend::default()); - - // let's create a dummy line in the plot - let graph: Vec<[f64; 2]> = vec![[0.0, 1.0], [2.0, 3.0], [3.0, 2.0]]; - let inner = my_plot.show(ui, |plot_ui| { - plot_ui.line(Line::new(PlotPoints::from(graph)).name("curve")); - }); - // Remember the position of the plot - plot_rect = Some(inner.response.rect); - }); - - // Check for returned screenshot: - let screenshot = ctx.input(|i| { - for event in &i.raw.events { - if let egui::Event::Screenshot { image, .. } = event { - return Some(image.clone()); - } - } - None - }); - - if let (Some(screenshot), Some(plot_location)) = (screenshot, plot_rect) { - if let Some(mut path) = rfd::FileDialog::new().save_file() { - path.set_extension("png"); - - // for a full size application, we should put this in a different thread, - // so that the GUI doesn't lag during saving - - let pixels_per_point = ctx.pixels_per_point(); - let plot = screenshot.region(&plot_location, Some(pixels_per_point)); - // save the plot to png - image::save_buffer( - &path, - plot.as_raw(), - plot.width() as u32, - plot.height() as u32, - image::ColorType::Rgba8, - ) - .unwrap(); - eprintln!("Image saved to {path:?}."); - } - } - } -} diff --git a/scripts/generate_changelog.py b/scripts/generate_changelog.py index 303593a581d..eaa5d2ee4dc 100755 --- a/scripts/generate_changelog.py +++ b/scripts/generate_changelog.py @@ -223,7 +223,6 @@ def main() -> None: "ecolor", "eframe", "egui_extras", - "egui_plot", "egui_glow", "egui-wgpu", "egui-winit",