Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Customize grid spacing in plots #1180

Merged
merged 19 commits into from
Apr 19, 2022
Merged

Customize grid spacing in plots #1180

merged 19 commits into from
Apr 19, 2022

Conversation

Bromeon
Copy link
Contributor

@Bromeon Bromeon commented Jan 29, 2022

Description

Adds two Plot methods x_grid_spacer() and y_grid_spacer(), which customize the distance between the plot's background grid. The default is the log-10 grid, but occasionally, it can be useful to have different subdivision than 10. Example use cases:

  • date and time
  • non-decimal unit systems (imperial)
  • other bases (binary, hexadecimal)
  • ...

I added a small example just to visualize it. It's definitely not the best, but maybe we can improve this in the future, in a separate PR.
grafik

Implementation

While providing a customization API, this PR also refactors the hardcoded log-10 logic a bit, making it slightly more modular and easier to understand.

The customization function look as follows:

fn get_step_sizes(
    bounds: (f64, f64),
    bounds_frame_ratio: f64
) -> [f64; 3]

where bounds are the boundaries of the visible diagram (plot coordinates), and bounds_frame_ratio is a bounds/frame ratio, i.e how much one plot unit amounts to in the visible viewport (scaled with min step size).

The built-in logarithmic spacer is implemented as follows:

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
        // to the next-bigger power of 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)
}

Now, why an array of exactly 3 elements? In the current implementation, 3 is the number of different thicknesses, with which the grid lines are rendered. The thick line for the largest unit (e.g. 10s), the medium for the middle one (e.g. 1s), and the thin line for the smallest unit (e.g. 0.1s).

@Bromeon Bromeon marked this pull request as ready for review January 30, 2022 09:54
@Bromeon Bromeon marked this pull request as draft January 30, 2022 10:02
@Bromeon
Copy link
Contributor Author

Bromeon commented Jan 30, 2022

I realized that having a fixed step size may not be flexible enough. Consider e.g. using months as input -- it would be nice if there could be a grid line at every beginning of a month. Now months are 28, 29, 30 or 31 days long, so equidistant grid lines won't cut it.

Instead, grid spacers have the following function signature:

fn get_step_sizes(input: GridInput) -> Vec<GridMark>;

with:

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),

    /// 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,
}

/// One mark (horizontal or vertical line) in the background grid of a plat.
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 at that point.
    pub step_size: f64,
}

So for any given range (called bounds), the user can decide where exactly the grid lines appear.

Since this may be a bit tedious to implement for more simple cases, I provided two convenience functions:

  • Logarithmic spacing, e.g. 2, 4, 8, 16...

     pub fn log_grid_spacer(base: i64) -> Box<dyn Fn(GridInput) -> Vec<GridMark>>
  • Uniform custom spacing, e.g. 1, 25, 100...
    This lets the user return 3 step sizes (dependent on bounds) and automatically adds grid lines at multiples of those steps. I used a fixed-size array because 3 is the number of different thicknesses used for grid lines, we could think about allowing to provide only 1 or 2 as well (e.g. by specifying 0.0 or None).

     pub fn uniform_grid_spacer(
        spacer: impl Fn(GridInput) -> [f64; 3] + 'static
     ) -> Box<dyn Fn(GridInput) -> Vec<GridMark>>

The results of those two functions can be passed to Plot::{x/y}_grid_spacer().

@Bromeon Bromeon marked this pull request as ready for review January 30, 2022 18:25
# Conflicts:
#	egui/src/widgets/plot/mod.rs
@Bromeon
Copy link
Contributor Author

Bromeon commented Jan 31, 2022

Fixed conflicts, merged latest master.

# Conflicts:
#	CHANGELOG.md
#	egui/src/widgets/plot/mod.rs
#	egui_demo_lib/src/apps/demo/plot_demo.rs
@Bromeon
Copy link
Contributor Author

Bromeon commented Mar 4, 2022

Any feedback on this? 🙂

Copy link
Owner

@emilk emilk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a nice feature to have, but it could do with a more motivated demo

Comment on lines 1218 to 1221
/// ```ignore
/// next_power(0.01, 10) == 0.01
/// next_power(0.02, 10) == 0.1
/// next_power(0.2, 10) == 1
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// ```ignore
/// next_power(0.01, 10) == 0.01
/// next_power(0.02, 10) == 0.1
/// next_power(0.2, 10) == 1
/// ```
/// 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.0);

use the power of the doc-tests!

(and if precision is a problem, use a power-of-two as base)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, that would require making this function public -- and I'm not sure if that's worth it just for doc tests (and where it would belong in that case).

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh right, you cannot doctest private functions 🤦

But perhaps this could belong in emath anyway

egui/src/widgets/plot/mod.rs Show resolved Hide resolved
egui/src/widgets/plot/mod.rs Outdated Show resolved Hide resolved
egui/src/widgets/plot/mod.rs Outdated Show resolved Hide resolved
egui/src/widgets/plot/mod.rs Outdated Show resolved Hide resolved
egui_demo_lib/src/apps/demo/plot_demo.rs Outdated Show resolved Hide resolved
@Bromeon
Copy link
Contributor Author

Bromeon commented Mar 13, 2022

Added a new plot category called Custom Axes, which showcases multiple features at once:

  • Custom axis value formatting
  • Custom label formatting (when hovering)
  • Custom grid spacing

For this reason, I removed the custom axis formatting from the bar chart I added back in the day, as it seemed too artificial there.

The new demo uses time on the X axis (days, hours, minutes) and percent on the Y axis:

egui-grid-spacer.mp4

Copy link
Owner

@emilk emilk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the slow (re)review! It looks great, but the changelog line needs to move up!

CHANGELOG.md Outdated Show resolved Hide resolved
egui_demo_lib/src/apps/demo/plot_demo.rs Show resolved Hide resolved
egui_demo_lib/src/apps/demo/plot_demo.rs Outdated Show resolved Hide resolved
@Bromeon
Copy link
Contributor Author

Bromeon commented Apr 16, 2022

Implemented feedback (besides get_percent(), see above).

One issue with the new minute base unit is that the default view is very much zoomed in.

  1. I was under the impression egui would adjust the plot bounds according to the data, but maybe I remember this wrong?
  2. Is there a way to explicitly set the visible bounds?

@Bromeon
Copy link
Contributor Author

Bromeon commented Apr 16, 2022

That was a sneaky merge conflict, a few minutes after my push 😁

Also removed the colon here in the changelog:

`ComboBox`:es   -->   `ComboBox`es

I assume that was a typo? Because further below you also used

`DragValue`s

without any colon. If not, I'll revert.

@emilk
Copy link
Owner

emilk commented Apr 19, 2022

One issue with the new minute base unit is that the default view is very much zoomed in.

1. I was under the impression egui would adjust the plot bounds according to the data, but maybe I remember this wrong?

Not for "generator" functions (Values::from_explicit_callback) - but I'll push a fix for that after merging this PR.

2. Is there a way to explicitly set the visible bounds?

I don't think so, but we should add it

@emilk emilk merged commit e22f6d9 into emilk:master Apr 19, 2022
@Bromeon Bromeon deleted the feature/grid-spacer branch April 19, 2022 09:49
@nrot
Copy link

nrot commented May 31, 2023

Hello.

fn next_power()

have this

assert_ne!(value, 0.0); // can be negative (typical for Y axis)

This create random panic on runtime, when used plot with boxed zoom. I think this happing when boxed zoom create to small zone and plot cant calculate new grid. I don`t shure, because cant stable repeating this bug. Perhaps need some simple protected of zero, but i dont sure where exactly need this protection

[2023-05-31][22:30:12][wave_gen::app::waves][DEBUG] Mouse position: Some([355.0 242.0])
[2023-05-31][22:30:12][wave_gen::app::waves][DEBUG] Clicked by plot: 22; Data size: 128
[2023-05-31][22:30:12][wave_gen::app::waves][DEBUG] Mouse position: Some([355.0 242.0])
thread 'main' panicked at 'assertion failed: `(left != right)`
  left: `-0.0`,
 right: `0.0`', github.com-1ecc6299db9ec823\egui-0.22.0\src\widgets\plot\mod.rs:1637:5
stack backtrace:
   0: std::panicking::begin_panic_handler
             at /rustc/84c898d65adf2f39a5a98507f1fe0ce10a2b8dbc/library\std\src\panicking.rs:579
   1: core::panicking::panic_fmt
             at /rustc/84c898d65adf2f39a5a98507f1fe0ce10a2b8dbc/library\core\src\panicking.rs:64
   2: core::fmt::Arguments::new_v1
             at /rustc/84c898d65adf2f39a5a98507f1fe0ce10a2b8dbc/library\core\src\fmt\mod.rs:405
   3: core::panicking::assert_failed_inner
             at /rustc/84c898d65adf2f39a5a98507f1fe0ce10a2b8dbc/library\core\src\panicking.rs:257
   4: core::panicking::assert_failed<f64,f64>
             at /rustc/84c898d65adf2f39a5a98507f1fe0ce10a2b8dbc\library\core\src\panicking.rs:211
   5: egui::widgets::plot::next_power
             at github.com-1ecc6299db9ec823\egui-0.22.0\src\widgets\plot\mod.rs:1637
   6: egui::widgets::plot::log_grid_spacer::closure$0
             at github.com-1ecc6299db9ec823\egui-0.22.0\src\widgets\plot\mod.rs:1283
   7: alloc::boxed::impl$47::call<tuple$<egui::widgets::plot::GridInput>,dyn$<core::ops::function::Fn<tuple$<egui::widgets::plot::GridInput>,assoc$<Output,alloc::vec::Vec<egui::widgets::plot::GridMark,alloc::alloc::Global> > > >,alloc::alloc::Global>
             at /rustc/84c898d65adf2f39a5a98507f1fe0ce10a2b8dbc\library\alloc\src\boxed.rs:2001
   8: egui::widgets::plot::PreparedPlot::paint_axis
             at github.com-1ecc6299db9ec823\egui-0.22.0\src\widgets\plot\mod.rs:1458
   9: egui::widgets::plot::PreparedPlot::ui
             at github.com-1ecc6299db9ec823\egui-0.22.0\src\widgets\plot\mod.rs:1340
  10: egui::widgets::plot::Plot::show_dyn<tuple$<> >
             at github.com-1ecc6299db9ec823\egui-0.22.0\src\widgets\plot\mod.rs:971
  11: egui::widgets::plot::Plot::show<tuple$<>,wave_gen::app::waves::impl$2::display::closure$0::closure$0::closure_env$1>
             at github.com-1ecc6299db9ec823\egui-0.22.0\src\widgets\plot\mod.rs:588
  12: wave_gen::app::waves::impl$2::display::closure$0::closure$0
             at .\src\app\waves\mod.rs:115
  13: core::ops::function::FnOnce::call_once<wave_gen::app::waves::impl$2::display::closure$0::closure_env$0,tuple$<ref_mut$<egui::ui::Ui> > >
             at /rustc/84c898d65adf2f39a5a98507f1fe0ce10a2b8dbc\library\core\src\ops\function.rs:250
  14: alloc::boxed::impl$45::call_once<tuple$<ref_mut$<egui::ui::Ui> >,dyn$<core::ops::function::FnOnce<tuple$<ref_mut$<egui::ui::Ui> >,assoc$<Output,tuple$<> > > >,alloc::alloc::Global>
             at /rustc/84c898d65adf2f39a5a98507f1fe0ce10a2b8dbc\library\alloc\src\boxed.rs:1987
  15: egui::ui::Ui::allocate_ui_with_layout_dyn<tuple$<> >
             at github.com-1ecc6299db9ec823\egui-0.22.0\src\ui.rs:880
  16: egui::ui::Ui::allocate_ui_with_layout<tuple$<>,wave_gen::app::waves::impl$2::display::closure$0::closure_env$0>
             at github.com-1ecc6299db9ec823\egui-0.22.0\src\ui.rs:865
  17: egui::ui::Ui::allocate_ui<tuple$<>,wave_gen::app::waves::impl$2::display::closure$0::closure_env$0>
             at github.com-1ecc6299db9ec823\egui-0.22.0\src\ui.rs:851
  18: wave_gen::app::waves::impl$2::display::closure$0
  ...

@nrot nrot mentioned this pull request Jun 6, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants