From c450f9b0adee02298de5c38ae9e34c7c57a2a3cc Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Mon, 24 Jan 2022 22:42:20 +0100 Subject: [PATCH 01/13] Customize grid spacing in plots --- egui/src/widgets/plot/mod.rs | 149 +++++++++++++++++++++++++++++------ 1 file changed, 123 insertions(+), 26 deletions(-) diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index 9550d98f0aa..7176a3fac43 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -4,6 +4,7 @@ use crate::*; use epaint::ahash::AHashSet; use epaint::color::Hsva; use epaint::util::FloatOrd; + use items::PlotItem; use legend::LegendWidget; use transform::{PlotBounds, ScreenTransform}; @@ -26,6 +27,8 @@ type AxisFormatter = Option>; // ---------------------------------------------------------------------------- +const MIN_LINE_SPACING_IN_POINTS: f64 = 6.0; // TODO: large enough for a wide label + /// Information about the plot that has to persist between frames. #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[derive(Clone)] @@ -87,6 +90,7 @@ pub struct Plot { legend_config: Option, show_background: bool, show_axes: [bool; 2], + grid_spacers: [Box; 2], } impl Plot { @@ -115,6 +119,7 @@ impl Plot { legend_config: None, show_background: true, show_axes: [true; 2], + grid_spacers: [LogGridSpacer::new_boxed(10), LogGridSpacer::new_boxed(10)], } } @@ -281,6 +286,22 @@ impl Plot { 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. This is represented by [`LogGridSpacer`]. + pub fn x_grid_spacer(mut self, spacer: Box) -> Self { + self.grid_spacers[0] = spacer; + self + } + + /// Configure how the grid in the background is spaced apart along the Y axis. + /// + /// Default is a log-10 grid, i.e. every plot unit is divided into 10 other units. This is represented by [`LogGridSpacer`]. + pub fn y_grid_spacer(mut self, spacer: Box) -> Self { + self.grid_spacers[1] = spacer; + 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) -> InnerResponse { let Self { @@ -303,6 +324,7 @@ impl Plot { legend_config, show_background, show_axes, + grid_spacers, } = self; // Determine the size of the plot in the UI @@ -475,6 +497,7 @@ impl Plot { axis_formatters, show_axes, transform: transform.clone(), + grid_spacers, }; prepared.ui(ui, &response); @@ -676,6 +699,65 @@ impl PlotUi { } } +// ---------------------------------------------------------------------------- +// Grid + +/// One step (horizontal or vertical line) in the background grid +struct GridStep { + /// X or Y value in the plot + value: f64, + + /// The step size for that value, determines how thick the grid line is painted at that point + step_size: f64, +} + +/// Determines how the background grid in a plot is spaced apart. +pub trait GridSpacer { + /// Generate steps on the grid. + /// + /// `bounds` are min/max of the visible data range (the values at the two edges of the plot, + /// for the current axis). + /// + /// `bounds_frame_ratio` is the ratio between the diagram's bounds (in plot coordinates) and + /// the viewport (in frame/window coordinates). + /// + /// This function should return 3 "units", designating where + /// + fn get_step_sizes(&self, bounds: (f64, f64), bounds_frame_ratio: f64) -> [f64; 3]; +} + +/// Built in configuration for grid spacing, using a logarithmic spacing which automatically adjusts +/// according to zoom level. +pub struct LogGridSpacer { + base: f64, +} + +impl LogGridSpacer { + /// The logarithmic base, expressing how many times each grid unit is subdivided. + /// 10 is a typical value, others are possible though. + pub fn new_boxed(base: i64) -> Box { + Box::new(Self { base: base as f64 }) + } +} + +impl GridSpacer for LogGridSpacer { + fn get_step_sizes(&self, _bounds: (f64, f64), dvalue_dpos: f64) -> [f64; 3] { + let Self { base } = *self; + + // 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(dvalue_dpos, base); + + [ + smallest_visible_unit, + smallest_visible_unit * base, + smallest_visible_unit * base * base, + ] + } +} + +// ---------------------------------------------------------------------------- + struct PreparedPlot { items: Vec>, show_x: bool, @@ -684,6 +766,7 @@ struct PreparedPlot { axis_formatters: [AxisFormatter; 2], show_axes: [bool; 2], transform: ScreenTransform, + grid_spacers: [Box; 2], } impl PreparedPlot { @@ -715,50 +798,40 @@ impl PreparedPlot { let Self { transform, axis_formatters, + grid_spacers, .. } = self; - let bounds = transform.bounds(); - let font_id = TextStyle::Body.resolve(ui.style()); - let base: i64 = 10; - let basef = base as f64; - - let min_line_spacing_in_points = 6.0; // TODO: large enough for a wide label - let step_size = transform.dvalue_dpos()[axis] * min_line_spacing_in_points; - let step_size = basef.powi(step_size.abs().log(basef).ceil() as i32); - - let step_size_in_points = (transform.dpos_dvalue()[axis] * step_size).abs() as f32; - // Where on the cross-dimension to show the label values + let bounds = transform.bounds(); let value_cross = 0.0_f64.clamp(bounds.min[1 - axis], bounds.max[1 - axis]); - for i in 0.. { - let value_main = step_size * (bounds.min[axis] / step_size + i as f64).floor(); - if value_main > bounds.max[axis] { - break; - } + let bounds = (bounds.min[axis], bounds.max[axis]); + let bounds_frame_ratio = transform.dvalue_dpos()[axis] * MIN_LINE_SPACING_IN_POINTS; + let step_sizes = grid_spacers[axis].get_step_sizes(bounds, bounds_frame_ratio); + + let mut steps = vec![]; + fill_steps_between(&mut steps, step_sizes[0], bounds); + fill_steps_between(&mut steps, step_sizes[1], bounds); + fill_steps_between(&mut steps, step_sizes[2], bounds); + + for step in steps { + let value_main = step.value; let value = if axis == 0 { Value::new(value_main, value_cross) } else { Value::new(value_cross, value_main) }; - let pos_in_gui = transform.position_from_value(&value); - let n = (value_main / step_size).round() as i64; - let spacing_in_points = if n % (base * base) == 0 { - step_size_in_points * (basef * basef) as f32 // think line (multiple of 100) - } else if n % base == 0 { - step_size_in_points * basef as f32 // medium line (multiple of 10) - } else { - step_size_in_points // thin line - }; + let pos_in_gui = transform.position_from_value(&value); + let spacing_in_points = (transform.dpos_dvalue()[axis] * step.step_size).abs() as f32; let line_alpha = remap_clamp( spacing_in_points, - (min_line_spacing_in_points as f32)..=300.0, + (MIN_LINE_SPACING_IN_POINTS as f32)..=300.0, 0.0..=0.15, ); @@ -850,3 +923,27 @@ impl PreparedPlot { } } } + +/// Returns next bigger power in given base +/// e.g. +/// ```ignore +/// next_power(0.01, 10) == 0.01 +/// next_power(0.02, 10) == 0.1 +/// next_power(0.2, 10) == 1 +/// ``` +fn next_power(value: f64, base: f64) -> f64 { + assert_ne!(value, 0.0); // can be negative (typical for Y axis) + base.powi(value.abs().log(base).ceil() as i32) +} + +/// Fill in all values between [min, max] which are a multiple of `step_size` +fn fill_steps_between(out: &mut Vec, step_size: f64, (min, max): (f64, f64)) { + assert!(max > min); + let first = (min / step_size).ceil() as i64; + let last = (max / step_size).ceil() as i64; + + for i in first..last { + let value = (i as f64) * step_size; + out.push(GridStep { value, step_size }); + } +} From 9152a29578cde9286f930ae895304577f63b914b Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sat, 29 Jan 2022 21:45:21 +0100 Subject: [PATCH 02/13] Change trait to function --- egui/src/widgets/plot/mod.rs | 85 +++++++++++++++++++----------------- 1 file changed, 44 insertions(+), 41 deletions(-) diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index 7176a3fac43..a5457047e49 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -90,7 +90,7 @@ pub struct Plot { legend_config: Option, show_background: bool, show_axes: [bool; 2], - grid_spacers: [Box; 2], + grid_spacers: [Box; 2], } impl Plot { @@ -119,7 +119,7 @@ impl Plot { legend_config: None, show_background: true, show_axes: [true; 2], - grid_spacers: [LogGridSpacer::new_boxed(10), LogGridSpacer::new_boxed(10)], + grid_spacers: [log_grid_spacer(10), log_grid_spacer(10)], } } @@ -288,16 +288,18 @@ impl Plot { /// 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. This is represented by [`LogGridSpacer`]. - pub fn x_grid_spacer(mut self, spacer: Box) -> Self { + /// Default is a log-10 grid, i.e. every plot unit is divided into 10 other units. + /// See [`GridSpacer`] for explanation of the function signature. + pub fn x_grid_spacer(mut self, spacer: Box) -> Self { self.grid_spacers[0] = spacer; self } /// Configure how the grid in the background is spaced apart along the Y axis. /// - /// Default is a log-10 grid, i.e. every plot unit is divided into 10 other units. This is represented by [`LogGridSpacer`]. - pub fn y_grid_spacer(mut self, spacer: Box) -> Self { + /// Default is a log-10 grid, i.e. every plot unit is divided into 10 other units. + /// See [`GridSpacer`] for explanation of the function signature. + pub fn y_grid_spacer(mut self, spacer: Box) -> Self { self.grid_spacers[1] = spacer; self } @@ -712,48 +714,49 @@ struct GridStep { } /// Determines how the background grid in a plot is spaced apart. -pub trait GridSpacer { - /// Generate steps on the grid. - /// - /// `bounds` are min/max of the visible data range (the values at the two edges of the plot, - /// for the current axis). - /// - /// `bounds_frame_ratio` is the ratio between the diagram's bounds (in plot coordinates) and - /// the viewport (in frame/window coordinates). - /// - /// This function should return 3 "units", designating where - /// - fn get_step_sizes(&self, bounds: (f64, f64), bounds_frame_ratio: f64) -> [f64; 3]; -} - -/// Built in configuration for grid spacing, using a logarithmic spacing which automatically adjusts -/// according to zoom level. -pub struct LogGridSpacer { - base: f64, -} - -impl LogGridSpacer { - /// The logarithmic base, expressing how many times each grid unit is subdivided. - /// 10 is a typical value, others are possible though. - pub fn new_boxed(base: i64) -> Box { - Box::new(Self { base: base as f64 }) - } -} - -impl GridSpacer for LogGridSpacer { - fn get_step_sizes(&self, _bounds: (f64, f64), dvalue_dpos: f64) -> [f64; 3] { - let Self { base } = *self; +/// +/// The function has this signature: +/// ```no_run +/// fn get_step_sizes( +/// bounds: (f64, f64), +/// bounds_frame_ratio: f64 +/// ) -> [f64; 3] +/// # { todo!() } +/// ``` +/// +/// `bounds` are min/max of the visible data range (the values at the two edges of the plot, +/// for the current axis). +/// +/// `bounds_frame_ratio` is 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. +/// This is a good value to be used as the smallest of the returned steps. +/// +/// This function should return 3 positive step sizes, designating where the lines in the grid are drawn. +/// Lines are thicker for larger values. +/// An example return values is `[bounds_frame_ratio, 10 * bounds_frame_ratio, 100 * bounds_frame_ratio]`. +/// +/// See also [`log_grid_spacer`]. +pub type GridSpacer = dyn Fn((f64, f64), f64) -> [f64; 3]; +/// A function splitting the grid into `base` subdivisions. +/// +/// 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(base: i64) -> Box { + let base = base as f64; + let get_step_sizes = move |_bounds: (f64, f64), bounds_frame_ratio: f64| -> [f64; 3] { // 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(dvalue_dpos, base); + let smallest_visible_unit = next_power(bounds_frame_ratio, base); [ smallest_visible_unit, smallest_visible_unit * base, smallest_visible_unit * base * base, ] - } + }; + + Box::new(get_step_sizes) } // ---------------------------------------------------------------------------- @@ -766,7 +769,7 @@ struct PreparedPlot { axis_formatters: [AxisFormatter; 2], show_axes: [bool; 2], transform: ScreenTransform, - grid_spacers: [Box; 2], + grid_spacers: [Box; 2], } impl PreparedPlot { @@ -810,7 +813,7 @@ impl PreparedPlot { let bounds = (bounds.min[axis], bounds.max[axis]); let bounds_frame_ratio = transform.dvalue_dpos()[axis] * MIN_LINE_SPACING_IN_POINTS; - let step_sizes = grid_spacers[axis].get_step_sizes(bounds, bounds_frame_ratio); + let step_sizes = (grid_spacers[axis])(bounds, bounds_frame_ratio); let mut steps = vec![]; fill_steps_between(&mut steps, step_sizes[0], bounds); From 6a17d4f9039fe57ae7437809b8a3ef2a3c9da699 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sat, 29 Jan 2022 22:21:37 +0100 Subject: [PATCH 03/13] Simple example (static grid spacing...) --- egui/src/widgets/plot/mod.rs | 94 ++++++++++++------------ egui_demo_lib/src/apps/demo/plot_demo.rs | 44 +++++++---- 2 files changed, 77 insertions(+), 61 deletions(-) diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index a5457047e49..f3786dd4487 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -25,6 +25,9 @@ type CustomLabelFuncRef = Option>; type AxisFormatterFn = dyn Fn(f64) -> String; type AxisFormatter = Option>; +type GridSpacerFn = dyn Fn((f64, f64), f64) -> [f64; 3]; +type GridSpacer = Box; + // ---------------------------------------------------------------------------- const MIN_LINE_SPACING_IN_POINTS: f64 = 6.0; // TODO: large enough for a wide label @@ -90,7 +93,7 @@ pub struct Plot { legend_config: Option, show_background: bool, show_axes: [bool; 2], - grid_spacers: [Box; 2], + grid_spacers: [GridSpacer; 2], } impl Plot { @@ -250,6 +253,48 @@ impl Plot { 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: + /// ```no_run + /// fn get_step_sizes( + /// bounds: (f64, f64), + /// bounds_frame_ratio: f64 + /// ) -> [f64; 3] + /// # { todo!() } + /// ``` + /// + /// `bounds` are min/max of the visible data range (the values at the two edges of the plot, + /// for the current axis). + /// + /// `bounds_frame_ratio` is 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. + /// This is a good value to be used as the smallest of the returned steps. + /// + /// This function should return 3 positive step sizes, designating where the lines in the grid are drawn. + /// Lines are thicker for larger values. + /// An example return values is `[bounds_frame_ratio, 10 * bounds_frame_ratio, 100 * bounds_frame_ratio]`. + /// + /// Why only 3 step sizes? The idea is that you compute the units dynamically, depending on the currently + /// displayed value range. + /// + /// See also [`log_grid_spacer`]. + pub fn x_grid_spacer(mut self, spacer: impl Fn((f64, f64), f64) -> [f64; 3] + 'static) -> Self { + self.grid_spacers[0] = Box::new(spacer); + self + } + + /// Configure how the grid in the background is spaced apart along the Y axis. + /// + /// Default is a log-10 grid, i.e. every plot unit is divided into 10 other units. + /// See [`x_grid_spacer()`] for explanation of the function signature. + pub fn y_grid_spacer(mut self, spacer: impl Fn((f64, f64), f64) -> [f64; 3] + 'static) -> Self { + self.grid_spacers[1] = Box::new(spacer); + self + } + /// Expand bounds to include the given x value. /// For instance, to always show the y axis, call `plot.include_x(0.0)`. pub fn include_x(mut self, x: impl Into) -> Self { @@ -286,24 +331,6 @@ impl Plot { 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. - /// See [`GridSpacer`] for explanation of the function signature. - pub fn x_grid_spacer(mut self, spacer: Box) -> Self { - self.grid_spacers[0] = spacer; - self - } - - /// Configure how the grid in the background is spaced apart along the Y axis. - /// - /// Default is a log-10 grid, i.e. every plot unit is divided into 10 other units. - /// See [`GridSpacer`] for explanation of the function signature. - pub fn y_grid_spacer(mut self, spacer: Box) -> Self { - self.grid_spacers[1] = spacer; - 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) -> InnerResponse { let Self { @@ -713,36 +740,11 @@ struct GridStep { step_size: f64, } -/// Determines how the background grid in a plot is spaced apart. -/// -/// The function has this signature: -/// ```no_run -/// fn get_step_sizes( -/// bounds: (f64, f64), -/// bounds_frame_ratio: f64 -/// ) -> [f64; 3] -/// # { todo!() } -/// ``` -/// -/// `bounds` are min/max of the visible data range (the values at the two edges of the plot, -/// for the current axis). -/// -/// `bounds_frame_ratio` is 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. -/// This is a good value to be used as the smallest of the returned steps. -/// -/// This function should return 3 positive step sizes, designating where the lines in the grid are drawn. -/// Lines are thicker for larger values. -/// An example return values is `[bounds_frame_ratio, 10 * bounds_frame_ratio, 100 * bounds_frame_ratio]`. -/// -/// See also [`log_grid_spacer`]. -pub type GridSpacer = dyn Fn((f64, f64), f64) -> [f64; 3]; - /// A function splitting the grid into `base` subdivisions. /// /// 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(base: i64) -> Box { +pub fn log_grid_spacer(base: i64) -> GridSpacer { let base = base as f64; let get_step_sizes = move |_bounds: (f64, f64), bounds_frame_ratio: f64| -> [f64; 3] { // The distance between two of the thinnest grid lines is "rounded" up @@ -769,7 +771,7 @@ struct PreparedPlot { axis_formatters: [AxisFormatter; 2], show_axes: [bool; 2], transform: ScreenTransform, - grid_spacers: [Box; 2], + grid_spacers: [GridSpacer; 2], } impl PreparedPlot { diff --git a/egui_demo_lib/src/apps/demo/plot_demo.rs b/egui_demo_lib/src/apps/demo/plot_demo.rs index 931bc40a779..f47edd76591 100644 --- a/egui_demo_lib/src/apps/demo/plot_demo.rs +++ b/egui_demo_lib/src/apps/demo/plot_demo.rs @@ -523,7 +523,7 @@ impl ChartsDemo { .name("Set 4") .stack_on(&[&chart1, &chart2, &chart3]); - let mut x_fmt: fn(f64) -> String = |val| { + let x_fmt: fn(f64) -> String = |val| { if val >= 0.0 && val <= 4.0 && is_approx_integer(val) { // Only label full days from 0 to 4 format!("Day {}", val) @@ -533,7 +533,7 @@ impl ChartsDemo { } }; - let mut y_fmt: fn(f64) -> String = |val| { + let y_fmt: fn(f64) -> String = |val| { let percent = 100.0 * val; if is_approx_integer(percent) && !is_approx_zero(percent) { @@ -545,26 +545,40 @@ impl ChartsDemo { } }; + let grid: fn((f64, f64), f64) -> [f64; 3] = + // statically return 1, 5 and 30 days (just when zooming out) + |_bounds, _bounds_frame_ratio| { + [1.0, 5.0, 30.0] + }; + if !self.vertical { chart1 = chart1.horizontal(); chart2 = chart2.horizontal(); chart3 = chart3.horizontal(); chart4 = chart4.horizontal(); - std::mem::swap(&mut x_fmt, &mut y_fmt); - } + }; - Plot::new("Stacked Bar Chart Demo") + let plot = Plot::new("Stacked Bar Chart Demo") .legend(Legend::default()) - .x_axis_formatter(x_fmt) - .y_axis_formatter(y_fmt) - .data_aspect(1.0) - .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 + .data_aspect(1.0); + + let plot = if self.vertical { + plot.x_axis_formatter(x_fmt) + .y_axis_formatter(y_fmt) + .x_grid_spacer(grid) + } else { + plot.x_axis_formatter(y_fmt) + .y_axis_formatter(x_fmt) + .y_grid_spacer(grid) + }; + + plot.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 { From d9731c41d575351d335a8aaad3c61a4c19eafb23 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sat, 29 Jan 2022 23:14:06 +0100 Subject: [PATCH 04/13] Changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6454b4aa85..e27e5fc5b6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui_w * Added `Ui::add_visible` and `Ui::add_visible_ui`. * Added `CollapsingHeader::icon` to override the default open/close icon using a custom function. ([1147](https://github.com/emilk/egui/pull/1147)). * Added `Plot::x_axis_formatter` and `Plot::y_axis_formatter` for custom axis labels ([#1130](https://github.com/emilk/egui/pull/1130)). +* Added `Plot::x_grid_spacer` and `Plot::y_grid_spacer` for custom grid spacing ([#1180](https://github.com/emilk/egui/pull/1180)). * Added `ui.data()`, `ctx.data()`, `ctx.options()` and `ctx.tessellation_options()` ([#1175](https://github.com/emilk/egui/pull/1175)). ### Changed 🔧 From d13ea8cef6ba7417a82fda2838b9258441454cbb Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sun, 30 Jan 2022 11:13:11 +0100 Subject: [PATCH 05/13] More flexible API, allow non-uniform grid marks --- egui/src/widgets/plot/mod.rs | 133 +++++++++++++++-------- egui_demo_lib/src/apps/demo/plot_demo.rs | 6 +- 2 files changed, 91 insertions(+), 48 deletions(-) diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index f3786dd4487..73bfa2b8cb6 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -25,7 +25,7 @@ type CustomLabelFuncRef = Option>; type AxisFormatterFn = dyn Fn(f64) -> String; type AxisFormatter = Option>; -type GridSpacerFn = dyn Fn((f64, f64), f64) -> [f64; 3]; +type GridSpacerFn = dyn Fn(GridInput) -> Vec; type GridSpacer = Box; // ---------------------------------------------------------------------------- @@ -258,39 +258,39 @@ impl Plot { /// Default is a log-10 grid, i.e. every plot unit is divided into 10 other units. /// /// The function has this signature: - /// ```no_run - /// fn get_step_sizes( - /// bounds: (f64, f64), - /// bounds_frame_ratio: f64 - /// ) -> [f64; 3] - /// # { todo!() } + /// ```ignore + /// fn get_step_sizes(input: GridInput) -> Vec; /// ``` /// - /// `bounds` are min/max of the visible data range (the values at the two edges of the plot, - /// for the current axis). - /// - /// `bounds_frame_ratio` is 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. - /// This is a good value to be used as the smallest of the returned steps. - /// - /// This function should return 3 positive step sizes, designating where the lines in the grid are drawn. - /// Lines are thicker for larger values. - /// An example return values is `[bounds_frame_ratio, 10 * bounds_frame_ratio, 100 * bounds_frame_ratio]`. + /// This function should return all marks along the visible range of the X axis. + /// 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 }, /// - /// Why only 3 step sizes? The idea is that you compute the units dynamically, depending on the currently - /// displayed value range. + /// // 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 }, + /// ]; + /// # () + /// ``` /// - /// See also [`log_grid_spacer`]. - pub fn x_grid_spacer(mut self, spacer: impl Fn((f64, f64), f64) -> [f64; 3] + 'static) -> Self { + /// There are helpers for common cases, see [`log_grid_spacer`] and [`uniform_grid_spacer`]. + pub fn x_grid_spacer(mut self, spacer: impl Fn(GridInput) -> Vec + 'static) -> Self { self.grid_spacers[0] = Box::new(spacer); self } - /// Configure how the grid in the background is spaced apart along the Y axis. - /// /// Default is a log-10 grid, i.e. every plot unit is divided into 10 other units. - /// See [`x_grid_spacer()`] for explanation of the function signature. - pub fn y_grid_spacer(mut self, spacer: impl Fn((f64, f64), f64) -> [f64; 3] + 'static) -> Self { + /// + /// See [`Self::x_grid_spacer`] for explanation. + pub fn y_grid_spacer(mut self, spacer: impl Fn(GridInput) -> Vec + 'static) -> Self { self.grid_spacers[1] = Box::new(spacer); self } @@ -731,36 +731,74 @@ impl PlotUi { // ---------------------------------------------------------------------------- // Grid -/// One step (horizontal or vertical line) in the background grid -struct GridStep { - /// X or Y value in the plot - value: f64, +/// 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), - /// The step size for that value, determines how thick the grid line is painted at that point - step_size: f64, + /// Ratio between the diagram's bounds (in plot coordinates) and the viewport + /// (in frame/window coordinates), scaled up to represent the minimal possible step. + /// This is a good value to be used as the smallest of the returned steps. + pub bounds_frame_ratio: f64, } -/// A function splitting the grid into `base` subdivisions. +/// One mark (horizontal or vertical line) in the background grid of a plot. +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(base: i64) -> GridSpacer { let base = base as f64; - let get_step_sizes = move |_bounds: (f64, f64), bounds_frame_ratio: f64| -> [f64; 3] { + let get_step_sizes = move |input: GridInput| -> Vec { // The distance between two of the thinnest grid lines is "rounded" up // to the next-bigger power of base - let smallest_visible_unit = next_power(bounds_frame_ratio, base); + let smallest_visible_unit = next_power(input.bounds_frame_ratio, base); - [ + let step_sizes = [ smallest_visible_unit, smallest_visible_unit * base, smallest_visible_unit * base * base, - ] + ]; + + generate_marks(step_sizes, input.bounds) }; Box::new(get_step_sizes) } +/// Splits the grid into uniform-sized spacings (e.g. 100, 25, 1). +/// +/// This function should return 3 positive step sizes, designating where the lines in the grid are drawn. +/// Lines are thicker for larger step sizes. Ordering of returned value is irrelevant. +/// +/// Why only 3 step sizes? Three is the number of different line thicknesses that egui typically uses in the grid. +/// Ideally, those 3 are not hardcoded values, but depend on the visible range (accessible through `GridInput`). +pub fn uniform_grid_spacer(spacer: impl Fn(GridInput) -> [f64; 3] + 'static) -> GridSpacer { + let get_marks = move |input: GridInput| -> Vec { + let bounds = input.bounds; + let step_sizes = spacer(input); + generate_marks(step_sizes, bounds) + }; + + Box::new(get_marks) +} + // ---------------------------------------------------------------------------- struct PreparedPlot { @@ -815,12 +853,12 @@ impl PreparedPlot { let bounds = (bounds.min[axis], bounds.max[axis]); let bounds_frame_ratio = transform.dvalue_dpos()[axis] * MIN_LINE_SPACING_IN_POINTS; - let step_sizes = (grid_spacers[axis])(bounds, bounds_frame_ratio); - let mut steps = vec![]; - fill_steps_between(&mut steps, step_sizes[0], bounds); - fill_steps_between(&mut steps, step_sizes[1], bounds); - fill_steps_between(&mut steps, step_sizes[2], bounds); + let input = GridInput { + bounds, + bounds_frame_ratio, + }; + let steps = (grid_spacers[axis])(input); for step in steps { let value_main = step.value; @@ -942,13 +980,22 @@ fn next_power(value: f64, base: f64) -> f64 { } /// Fill in all values between [min, max] which are a multiple of `step_size` -fn fill_steps_between(out: &mut Vec, step_size: f64, (min, max): (f64, f64)) { +fn generate_marks(step_sizes: [f64; 3], bounds: (f64, f64)) -> Vec { + let mut steps = vec![]; + fill_marks_between(&mut steps, step_sizes[0], bounds); + fill_marks_between(&mut steps, step_sizes[1], bounds); + fill_marks_between(&mut steps, step_sizes[2], bounds); + steps +} + +/// Fill in all values between [min, max] which are a multiple of `step_size` +fn fill_marks_between(out: &mut Vec, step_size: f64, (min, max): (f64, f64)) { assert!(max > min); let first = (min / step_size).ceil() as i64; let last = (max / step_size).ceil() as i64; for i in first..last { let value = (i as f64) * step_size; - out.push(GridStep { value, step_size }); + out.push(GridMark { value, step_size }); } } diff --git a/egui_demo_lib/src/apps/demo/plot_demo.rs b/egui_demo_lib/src/apps/demo/plot_demo.rs index f47edd76591..93bda22238b 100644 --- a/egui_demo_lib/src/apps/demo/plot_demo.rs +++ b/egui_demo_lib/src/apps/demo/plot_demo.rs @@ -545,11 +545,7 @@ impl ChartsDemo { } }; - let grid: fn((f64, f64), f64) -> [f64; 3] = - // statically return 1, 5 and 30 days (just when zooming out) - |_bounds, _bounds_frame_ratio| { - [1.0, 5.0, 30.0] - }; + let grid = plot::uniform_grid_spacer(|_input| -> [f64; 3] { [1.0, 5.0, 30.0] }); if !self.vertical { chart1 = chart1.horizontal(); From a3cd7cb81115ba730bc0975e1cbd1242db982308 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Tue, 8 Mar 2022 00:16:57 +0100 Subject: [PATCH 06/13] Incorporate review feedback --- egui/src/widgets/plot/mod.rs | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index 0877908acdf..3d2c203016e 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -402,6 +402,7 @@ impl Plot { /// ``` /// /// 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 @@ -968,10 +969,11 @@ pub struct GridInput { /// for the current axis). pub bounds: (f64, f64), - /// Ratio between the diagram's bounds (in plot coordinates) and the viewport + /// 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. - /// This is a good value to be used as the smallest of the returned steps. - pub bounds_frame_ratio: f64, + pub min_step_size: f64, } /// One mark (horizontal or vertical line) in the background grid of a plot. @@ -997,7 +999,7 @@ pub fn log_grid_spacer(base: i64) -> GridSpacer { let get_step_sizes = move |input: GridInput| -> Vec { // The distance between two of the thinnest grid lines is "rounded" up // to the next-bigger power of base - let smallest_visible_unit = next_power(input.bounds_frame_ratio, base); + let smallest_visible_unit = next_power(input.min_step_size, base); let step_sizes = [ smallest_visible_unit, @@ -1105,12 +1107,9 @@ impl PreparedPlot { let bounds = transform.bounds(); let value_cross = 0.0_f64.clamp(bounds.min[1 - axis], bounds.max[1 - axis]); - let bounds = (bounds.min[axis], bounds.max[axis]); - let bounds_frame_ratio = transform.dvalue_dpos()[axis] * MIN_LINE_SPACING_IN_POINTS; - let input = GridInput { - bounds, - bounds_frame_ratio, + bounds: (bounds.min[axis], bounds.max[axis]), + min_step_size: transform.dvalue_dpos()[axis] * MIN_LINE_SPACING_IN_POINTS, }; let steps = (grid_spacers[axis])(input); @@ -1223,12 +1222,13 @@ impl PreparedPlot { /// Returns next bigger power in given base /// e.g. -/// ```ignore -/// next_power(0.01, 10) == 0.01 -/// next_power(0.02, 10) == 0.1 -/// next_power(0.2, 10) == 1 /// ``` -fn next_power(value: f64, base: f64) -> f64 { +/// 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); +/// ``` +pub(crate) fn next_power(value: f64, base: f64) -> f64 { assert_ne!(value, 0.0); // can be negative (typical for Y axis) base.powi(value.abs().log(base).ceil() as i32) } @@ -1248,8 +1248,9 @@ fn fill_marks_between(out: &mut Vec, step_size: f64, (min, max): (f64, let first = (min / step_size).ceil() as i64; let last = (max / step_size).ceil() as i64; - for i in first..last { + let marks_iter = (first..last).map(|i| { let value = (i as f64) * step_size; - out.push(GridMark { value, step_size }); - } + GridMark { value, step_size } + }); + out.extend(marks_iter); } From e896aa8eb02714533f4d16e6b6b235df659815c8 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Tue, 8 Mar 2022 23:07:03 +0100 Subject: [PATCH 07/13] Revert plot demo --- egui_demo_lib/src/apps/demo/plot_demo.rs | 33 ++++++++---------------- 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/egui_demo_lib/src/apps/demo/plot_demo.rs b/egui_demo_lib/src/apps/demo/plot_demo.rs index 18ce7ba8f35..bf845f094ec 100644 --- a/egui_demo_lib/src/apps/demo/plot_demo.rs +++ b/egui_demo_lib/src/apps/demo/plot_demo.rs @@ -626,8 +626,6 @@ impl ChartsDemo { } }; - let grid = plot::uniform_grid_spacer(|_input| -> [f64; 3] { [1.0, 5.0, 30.0] }); - if !self.vertical { chart1 = chart1.horizontal(); chart2 = chart2.horizontal(); @@ -636,27 +634,18 @@ impl ChartsDemo { std::mem::swap(&mut x_fmt, &mut y_fmt); } - let plot = Plot::new("Stacked Bar Chart Demo") + Plot::new("Stacked Bar Chart Demo") .legend(Legend::default()) - .data_aspect(1.0); - - let plot = if self.vertical { - plot.x_axis_formatter(x_fmt) - .y_axis_formatter(y_fmt) - .x_grid_spacer(grid) - } else { - plot.x_axis_formatter(y_fmt) - .y_axis_formatter(x_fmt) - .y_grid_spacer(grid) - }; - - plot.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 + .x_axis_formatter(x_fmt) + .y_axis_formatter(y_fmt) + .data_aspect(1.0) + .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 { From 7b37617df9ba0119537d8797d351100dcddde6f2 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Fri, 11 Mar 2022 00:12:39 +0100 Subject: [PATCH 08/13] New plot with time axis --- egui_demo_lib/src/apps/demo/plot_demo.rs | 110 ++++++++++++++++++++++- 1 file changed, 109 insertions(+), 1 deletion(-) diff --git a/egui_demo_lib/src/apps/demo/plot_demo.rs b/egui_demo_lib/src/apps/demo/plot_demo.rs index bf845f094ec..55972503423 100644 --- a/egui_demo_lib/src/apps/demo/plot_demo.rs +++ b/egui_demo_lib/src/apps/demo/plot_demo.rs @@ -1,5 +1,7 @@ use std::f64::consts::TAU; +use std::ops::RangeInclusive; +use egui::plot::{GridInput, GridMark}; use egui::*; use plot::{ Arrows, Bar, BarChart, BoxElem, BoxPlot, BoxSpread, CoordinatesFormatter, Corner, HLine, @@ -309,6 +311,106 @@ impl Widget for &mut LegendDemo { } } +#[derive(PartialEq)] +struct CustomAxisDemo {} + +impl Default for CustomAxisDemo { + fn default() -> Self { + Self {} + } +} + +impl CustomAxisDemo { + fn logistic_fn() -> Line { + let values = Values::from_explicit_callback( + move |x| 1.0 / (1.0 + (-2.5 * (x - 2.0)).exp()), + 0.0..5.0, + 100, + ); + Line::new(values) + } + + fn configure_plot(plot_ui: &mut plot::PlotUi) { + plot_ui.line(Self::logistic_fn()); + } + + fn x_grid(input: GridInput) -> Vec { + let mut marks = vec![]; + + let (min, max) = input.bounds; + + //let step = 1.0 / (24.0 * 12.0); // 5min intervals + let steps_per_day = 24 * 12; + let fsteps_per_day = steps_per_day as f64; + + let min = (min * fsteps_per_day).floor() as i32; + let max = (max * fsteps_per_day).ceil() as i32; + + for i in min..=max { + let step_size = if i % steps_per_day == 0 { + // 1 day + 1.0 + } else if i % 12 == 0 { + // 1 hour + 1.0 / 24.0 + } else { + // 5min + 1.0 / fsteps_per_day + }; + + let value = i as f64 / fsteps_per_day; + + marks.push(GridMark { value, step_size }); + } + + marks + } +} + +impl Widget for &mut CustomAxisDemo { + fn ui(self, ui: &mut Ui) -> Response { + ui.horizontal(|ui| { + ui.label("Custom axes:"); + //ui.checkbox(&mut self.link_x, "X"); + //ui.checkbox(&mut self.link_y, "Y"); + }); + + let x_fmt = |x, _range: &RangeInclusive| { + if x < 0.0 || x >= 5.0 { + // No labels outside value bounds + String::new() + } else if is_approx_integer(x) { + // Days + format!("Day {}", x.round()) + } else { + // Hours and minutes + format!( + "{h}:{m:02}", + h = (24.0 * x.fract()).round(), + m = ((24.0 * x).fract() * 60.0).round() + ) + } + }; + + let y_fmt = |y, _range: &RangeInclusive| { + // Display only integer percentages + if !is_approx_zero(y) && is_approx_integer(100.0 * y) { + format!("{}%", (100.0 * y).round()) + } else { + String::new() + } + }; + + Plot::new("custom_axes") + .data_aspect(2.0) + .x_axis_formatter(x_fmt) + .y_axis_formatter(y_fmt) + .x_grid_spacer(CustomAxisDemo::x_grid) + .show(ui, CustomAxisDemo::configure_plot) + .response + } +} + #[derive(PartialEq)] struct LinkedAxisDemo { link_x: bool, @@ -720,12 +822,13 @@ enum Panel { Charts, Items, Interaction, + CustomAxes, LinkedAxes, } impl Default for Panel { fn default() -> Self { - Self::Lines + Self::CustomAxes } } @@ -737,6 +840,7 @@ pub struct PlotDemo { charts_demo: ChartsDemo, items_demo: ItemsDemo, interaction_demo: InteractionDemo, + custom_axes_demo: CustomAxisDemo, linked_axes_demo: LinkedAxisDemo, open_panel: Panel, } @@ -782,6 +886,7 @@ impl super::View for PlotDemo { 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(); @@ -805,6 +910,9 @@ impl super::View for PlotDemo { Panel::Interaction => { ui.add(&mut self.interaction_demo); } + Panel::CustomAxes => { + ui.add(&mut self.custom_axes_demo); + } Panel::LinkedAxes => { ui.add(&mut self.linked_axes_demo); } From a4da3dcaa524e6724ce1b66ee7b2979c96294343 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Fri, 11 Mar 2022 00:17:45 +0100 Subject: [PATCH 09/13] Remove custom formatting for bar chart (too many features at once) --- egui_demo_lib/src/apps/demo/plot_demo.rs | 25 ------------------------ 1 file changed, 25 deletions(-) diff --git a/egui_demo_lib/src/apps/demo/plot_demo.rs b/egui_demo_lib/src/apps/demo/plot_demo.rs index 55972503423..c9db316dc75 100644 --- a/egui_demo_lib/src/apps/demo/plot_demo.rs +++ b/egui_demo_lib/src/apps/demo/plot_demo.rs @@ -706,40 +706,15 @@ impl ChartsDemo { .name("Set 4") .stack_on(&[&chart1, &chart2, &chart3]); - let mut x_fmt: fn(f64, &std::ops::RangeInclusive) -> String = |val, _range| { - if val >= 0.0 && val <= 4.0 && is_approx_integer(val) { - // Only label full days from 0 to 4 - format!("Day {}", val) - } else { - // Otherwise return empty string (i.e. no label) - String::new() - } - }; - - let mut y_fmt: fn(f64, &std::ops::RangeInclusive) -> String = |val, _range| { - let percent = 100.0 * val; - - if is_approx_integer(percent) && !is_approx_zero(percent) { - // Only show integer percentages, - // and don't show at Y=0 (label overlaps with X axis label) - format!("{}%", percent) - } else { - String::new() - } - }; - if !self.vertical { chart1 = chart1.horizontal(); chart2 = chart2.horizontal(); chart3 = chart3.horizontal(); chart4 = chart4.horizontal(); - std::mem::swap(&mut x_fmt, &mut y_fmt); } Plot::new("Stacked Bar Chart Demo") .legend(Legend::default()) - .x_axis_formatter(x_fmt) - .y_axis_formatter(y_fmt) .data_aspect(1.0) .show(ui, |plot_ui| { plot_ui.bar_chart(chart1); From d94dee79bd21f3099e7e53be13e33360645c1f4b Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Fri, 11 Mar 2022 00:33:58 +0100 Subject: [PATCH 10/13] Label formatter --- egui/src/widgets/plot/mod.rs | 18 +++---- egui_demo_lib/src/apps/demo/plot_demo.rs | 67 ++++++++++++++---------- 2 files changed, 48 insertions(+), 37 deletions(-) diff --git a/egui/src/widgets/plot/mod.rs b/egui/src/widgets/plot/mod.rs index f929f1b9f4a..d17305b1fe1 100644 --- a/egui/src/widgets/plot/mod.rs +++ b/egui/src/widgets/plot/mod.rs @@ -973,7 +973,7 @@ pub struct GridInput { /// /// Computed as the ratio between the diagram's bounds (in plot coordinates) and the viewport /// (in frame/window coordinates), scaled up to represent the minimal possible step. - pub min_step_size: f64, + pub base_step_size: f64, } /// One mark (horizontal or vertical line) in the background grid of a plot. @@ -994,17 +994,17 @@ pub struct GridMark { /// /// 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(base: i64) -> GridSpacer { - let base = base as f64; +pub fn log_grid_spacer(log_base: i64) -> GridSpacer { + let log_base = log_base as f64; let get_step_sizes = move |input: GridInput| -> Vec { // The distance between two of the thinnest grid lines is "rounded" up // to the next-bigger power of base - let smallest_visible_unit = next_power(input.min_step_size, base); + let smallest_visible_unit = next_power(input.base_step_size, log_base); let step_sizes = [ smallest_visible_unit, - smallest_visible_unit * base, - smallest_visible_unit * base * base, + smallest_visible_unit * log_base, + smallest_visible_unit * log_base * log_base, ]; generate_marks(step_sizes, input.bounds) @@ -1109,7 +1109,7 @@ impl PreparedPlot { let input = GridInput { bounds: (bounds.min[axis], bounds.max[axis]), - min_step_size: transform.dvalue_dpos()[axis] * MIN_LINE_SPACING_IN_POINTS, + base_step_size: transform.dvalue_dpos()[axis] * MIN_LINE_SPACING_IN_POINTS, }; let steps = (grid_spacers[axis])(input); @@ -1222,13 +1222,13 @@ impl PreparedPlot { /// 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); /// ``` -pub(crate) fn next_power(value: f64, base: f64) -> f64 { +fn next_power(value: f64, base: f64) -> f64 { assert_ne!(value, 0.0); // can be negative (typical for Y axis) base.powi(value.abs().log(base).ceil() as i32) } diff --git a/egui_demo_lib/src/apps/demo/plot_demo.rs b/egui_demo_lib/src/apps/demo/plot_demo.rs index c9db316dc75..cb1580268e5 100644 --- a/egui_demo_lib/src/apps/demo/plot_demo.rs +++ b/egui_demo_lib/src/apps/demo/plot_demo.rs @@ -311,15 +311,9 @@ impl Widget for &mut LegendDemo { } } -#[derive(PartialEq)] +#[derive(PartialEq, Default)] struct CustomAxisDemo {} -impl Default for CustomAxisDemo { - fn default() -> Self { - Self {} - } -} - impl CustomAxisDemo { fn logistic_fn() -> Line { let values = Values::from_explicit_callback( @@ -330,19 +324,17 @@ impl CustomAxisDemo { Line::new(values) } - fn configure_plot(plot_ui: &mut plot::PlotUi) { - plot_ui.line(Self::logistic_fn()); - } - + #[allow(clippy::needless_pass_by_value)] fn x_grid(input: GridInput) -> Vec { - let mut marks = 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 (min, max) = input.bounds; + let mut marks = vec![]; - //let step = 1.0 / (24.0 * 12.0); // 5min intervals let steps_per_day = 24 * 12; let fsteps_per_day = steps_per_day as f64; + let (min, max) = input.bounds; let min = (min * fsteps_per_day).floor() as i32; let max = (max * fsteps_per_day).ceil() as i32; @@ -369,11 +361,21 @@ impl CustomAxisDemo { impl Widget for &mut CustomAxisDemo { fn ui(self, ui: &mut Ui) -> Response { - ui.horizontal(|ui| { - ui.label("Custom axes:"); - //ui.checkbox(&mut self.link_x, "X"); - //ui.checkbox(&mut self.link_y, "Y"); - }); + // Make sure floor() still rounds down, even in presence of arithmetic imprecision + const EPS: f64 = 1e-6; + + fn get_day(x: f64) -> f64 { + x.floor() + } + fn get_hour(x: f64) -> f64 { + (24.0 * x.fract() + EPS).floor() + } + fn get_minute(x: f64) -> f64 { + ((24.0 * x).fract() * 60.0 + EPS).floor() + } + fn get_percent(y: f64) -> f64 { + (100.0 * y + EPS).floor() + } let x_fmt = |x, _range: &RangeInclusive| { if x < 0.0 || x >= 5.0 { @@ -381,32 +383,41 @@ impl Widget for &mut CustomAxisDemo { String::new() } else if is_approx_integer(x) { // Days - format!("Day {}", x.round()) + format!("Day {}", get_day(x)) } else { // Hours and minutes - format!( - "{h}:{m:02}", - h = (24.0 * x.fract()).round(), - m = ((24.0 * x).fract() * 60.0).round() - ) + format!("{h}:{m:02}", h = get_hour(x), m = get_minute(x)) } }; let y_fmt = |y, _range: &RangeInclusive| { // Display only integer percentages if !is_approx_zero(y) && is_approx_integer(100.0 * y) { - format!("{}%", (100.0 * y).round()) + format!("{}%", get_percent(y)) } else { String::new() } }; + let label_fmt = |_s: &str, val: &Value| { + format!( + "Day {d}, {h}:{m:02}\n{p}%", + d = get_day(val.x), + h = get_hour(val.x), + m = get_minute(val.x), + p = get_percent(val.y) + ) + }; + Plot::new("custom_axes") .data_aspect(2.0) .x_axis_formatter(x_fmt) .y_axis_formatter(y_fmt) .x_grid_spacer(CustomAxisDemo::x_grid) - .show(ui, CustomAxisDemo::configure_plot) + .label_formatter(label_fmt) + .show(ui, |plot_ui| { + plot_ui.line(CustomAxisDemo::logistic_fn()); + }) .response } } @@ -803,7 +814,7 @@ enum Panel { impl Default for Panel { fn default() -> Self { - Self::CustomAxes + Self::Lines } } From 9662d917243ff0a65115d92dfdf6948d398b4b85 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sat, 16 Apr 2022 19:04:50 +0200 Subject: [PATCH 11/13] Implement feedback --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5dc1ca548d..2a0f22b79f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui_w * Added `Ui::push_id` to resolve id clashes ([#1374](https://github.com/emilk/egui/pull/1374)). * Added `Frame::outer_margin`. * Added `Painter::hline` and `Painter::vline`. +* Added `Plot::x_grid_spacer` and `Plot::y_grid_spacer` for custom grid spacing ([#1180](https://github.com/emilk/egui/pull/1180)). ### Changed 🔧 * `ClippedMesh` has been replaced with `ClippedPrimitive` ([#1351](https://github.com/emilk/egui/pull/1351)). @@ -54,7 +55,6 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui_w * `Ui::input_mut` to modify how subsequent widgets see the `InputState` and a convenience method `InputState::consume_key` for shortcuts or hotkeys ([#1212](https://github.com/emilk/egui/pull/1212)). * Added `Ui::add_visible` and `Ui::add_visible_ui`. * Added `CollapsingHeader::icon` to override the default open/close icon using a custom function. ([1147](https://github.com/emilk/egui/pull/1147)). -* Added `Plot::x_grid_spacer` and `Plot::y_grid_spacer` for custom grid spacing ([#1180](https://github.com/emilk/egui/pull/1180)). * Added `ui.data()`, `ctx.data()`, `ctx.options()` and `ctx.tessellation_options()` ([#1175](https://github.com/emilk/egui/pull/1175)). * Added `Response::on_hover_text_at_pointer` as a convenience akin to `Response::on_hover_text` ([1179](https://github.com/emilk/egui/pull/1179)). * Opt-in dependency on `tracing` crate for logging warnings ([#1192](https://github.com/emilk/egui/pull/1192)). From 4a37a6c5397a40356548221614f36a267706b333 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sat, 16 Apr 2022 19:52:04 +0200 Subject: [PATCH 12/13] Use minutes as base unit in plot demo (instead of days) --- egui_demo_lib/src/apps/demo/plot_demo.rs | 55 ++++++++++++++---------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/egui_demo_lib/src/apps/demo/plot_demo.rs b/egui_demo_lib/src/apps/demo/plot_demo.rs index cb1580268e5..344104d6f83 100644 --- a/egui_demo_lib/src/apps/demo/plot_demo.rs +++ b/egui_demo_lib/src/apps/demo/plot_demo.rs @@ -315,10 +315,17 @@ impl Widget for &mut LegendDemo { struct CustomAxisDemo {} impl CustomAxisDemo { + 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 { + CustomAxisDemo::MINS_PER_DAY * min + } + let values = Values::from_explicit_callback( - move |x| 1.0 / (1.0 + (-2.5 * (x - 2.0)).exp()), - 0.0..5.0, + move |x| 1.0 / (1.0 + (-2.5 * (x / CustomAxisDemo::MINS_PER_DAY - 2.0)).exp()), + days(0.0)..days(5.0), 100, ); Line::new(values) @@ -331,28 +338,29 @@ impl CustomAxisDemo { let mut marks = vec![]; - let steps_per_day = 24 * 12; - let fsteps_per_day = steps_per_day as f64; - let (min, max) = input.bounds; - let min = (min * fsteps_per_day).floor() as i32; - let max = (max * fsteps_per_day).ceil() as i32; + let min = min.floor() as i32; + let max = max.ceil() as i32; for i in min..=max { - let step_size = if i % steps_per_day == 0 { + let step_size = if i % Self::MINS_PER_DAY as i32 == 0 { // 1 day - 1.0 - } else if i % 12 == 0 { + Self::MINS_PER_DAY + } else if i % Self::MINS_PER_H as i32 == 0 { // 1 hour - 1.0 / 24.0 - } else { + Self::MINS_PER_H + } else if i % 5 == 0 { // 5min - 1.0 / fsteps_per_day + 5.0 + } else { + // skip grids below 5min + continue; }; - let value = i as f64 / fsteps_per_day; - - marks.push(GridMark { value, step_size }); + marks.push(GridMark { + value: i as f64, + step_size, + }); } marks @@ -361,27 +369,30 @@ impl CustomAxisDemo { impl Widget for &mut CustomAxisDemo { fn ui(self, ui: &mut Ui) -> Response { + const MINS_PER_DAY: f64 = CustomAxisDemo::MINS_PER_DAY; + const MINS_PER_H: f64 = CustomAxisDemo::MINS_PER_H; + // Make sure floor() still rounds down, even in presence of arithmetic imprecision const EPS: f64 = 1e-6; fn get_day(x: f64) -> f64 { - x.floor() + (x / MINS_PER_DAY).floor() } fn get_hour(x: f64) -> f64 { - (24.0 * x.fract() + EPS).floor() + (x.rem_euclid(MINS_PER_DAY) / MINS_PER_H).floor() } fn get_minute(x: f64) -> f64 { - ((24.0 * x).fract() * 60.0 + EPS).floor() + x.rem_euclid(MINS_PER_H).floor() } fn get_percent(y: f64) -> f64 { (100.0 * y + EPS).floor() } let x_fmt = |x, _range: &RangeInclusive| { - if x < 0.0 || x >= 5.0 { + if x < 0.0 * MINS_PER_DAY || x >= 5.0 * MINS_PER_DAY { // No labels outside value bounds String::new() - } else if is_approx_integer(x) { + } else if is_approx_integer(x / MINS_PER_DAY) { // Days format!("Day {}", get_day(x)) } else { @@ -410,7 +421,7 @@ impl Widget for &mut CustomAxisDemo { }; Plot::new("custom_axes") - .data_aspect(2.0) + .data_aspect(2.0 * MINS_PER_DAY as f32) .x_axis_formatter(x_fmt) .y_axis_formatter(y_fmt) .x_grid_spacer(CustomAxisDemo::x_grid) From fa5077bc643b8665ab2f2c8cf15ba77cb05cf435 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Sat, 16 Apr 2022 22:49:34 +0200 Subject: [PATCH 13/13] Simplify get_percent() --- egui_demo_lib/src/apps/demo/plot_demo.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/egui_demo_lib/src/apps/demo/plot_demo.rs b/egui_demo_lib/src/apps/demo/plot_demo.rs index 344104d6f83..d1c2dc6c80f 100644 --- a/egui_demo_lib/src/apps/demo/plot_demo.rs +++ b/egui_demo_lib/src/apps/demo/plot_demo.rs @@ -372,9 +372,6 @@ impl Widget for &mut CustomAxisDemo { const MINS_PER_DAY: f64 = CustomAxisDemo::MINS_PER_DAY; const MINS_PER_H: f64 = CustomAxisDemo::MINS_PER_H; - // Make sure floor() still rounds down, even in presence of arithmetic imprecision - const EPS: f64 = 1e-6; - fn get_day(x: f64) -> f64 { (x / MINS_PER_DAY).floor() } @@ -385,7 +382,7 @@ impl Widget for &mut CustomAxisDemo { x.rem_euclid(MINS_PER_H).floor() } fn get_percent(y: f64) -> f64 { - (100.0 * y + EPS).floor() + (100.0 * y).round() } let x_fmt = |x, _range: &RangeInclusive| {