maelstrom_plot/
lib.rs

1//! Plotting library for egui for use with maelstrom-web
2//!
3//! This code is originally forked from egui-plot
4
5pub use axis::{Axis, AxisHints, HPlacement, Placement, VPlacement};
6pub use items::{
7    Line, LineStyle, MarkerShape, Orientation, PlotPoint, PlotPoints, Points, Polygon, StackedLine,
8    Text,
9};
10pub use legend::{Corner, Legend};
11pub use transform::{PlotBounds, PlotTransform};
12
13use axis::AxisWidget;
14use egui::{
15    ahash::{self, HashMap},
16    epaint::{self, util::FloatOrd, Hsva},
17    lerp, remap_clamp, vec2, Align2, Color32, Context, CursorIcon, Id, Layout, Margin, NumExt as _,
18    PointerButton, Pos2, Rect, Response, Rounding, Sense, Shape, Stroke, TextStyle, Ui, Vec2,
19    WidgetText,
20};
21use items::{horizontal_line, rulers_color, vertical_line, PlotItem};
22use legend::LegendWidget;
23use std::{ops::RangeInclusive, sync::Arc};
24
25mod axis;
26mod items;
27mod legend;
28mod transform;
29
30type LabelFormatterFn = dyn Fn(&str, &PlotPoint) -> String;
31type LabelFormatter = Option<Box<LabelFormatterFn>>;
32
33type GridSpacerFn = dyn Fn(GridInput) -> Vec<GridMark>;
34type GridSpacer = Box<GridSpacerFn>;
35
36type CoordinatesFormatterFn = dyn Fn(&PlotPoint, &PlotBounds) -> String;
37
38/// Specifies the coordinates formatting when passed to [`Plot::coordinates_formatter`].
39pub struct CoordinatesFormatter {
40    function: Box<CoordinatesFormatterFn>,
41}
42
43impl CoordinatesFormatter {
44    /// Create a new formatter based on the pointer coordinate and the plot bounds.
45    pub fn new(function: impl Fn(&PlotPoint, &PlotBounds) -> String + 'static) -> Self {
46        Self {
47            function: Box::new(function),
48        }
49    }
50
51    /// Show a fixed number of decimal places.
52    pub fn with_decimals(num_decimals: usize) -> Self {
53        Self {
54            function: Box::new(move |value, _| {
55                format!("x: {:.d$}\ny: {:.d$}", value.x, value.y, d = num_decimals)
56            }),
57        }
58    }
59
60    fn format(&self, value: &PlotPoint, bounds: &PlotBounds) -> String {
61        (self.function)(value, bounds)
62    }
63}
64
65impl Default for CoordinatesFormatter {
66    fn default() -> Self {
67        Self::with_decimals(3)
68    }
69}
70
71const MIN_LINE_SPACING_IN_POINTS: f64 = 6.0;
72
73/// Two bools, one for each axis (X and Y).
74#[derive(Copy, Clone, Debug, PartialEq, Eq)]
75pub struct AxisBools {
76    pub x: bool,
77    pub y: bool,
78}
79
80impl AxisBools {
81    #[inline]
82    pub fn new(x: bool, y: bool) -> Self {
83        Self { x, y }
84    }
85
86    #[inline]
87    pub fn any(&self) -> bool {
88        self.x || self.y
89    }
90}
91
92impl From<bool> for AxisBools {
93    #[inline]
94    fn from(val: bool) -> Self {
95        AxisBools { x: val, y: val }
96    }
97}
98
99impl From<[bool; 2]> for AxisBools {
100    #[inline]
101    fn from([x, y]: [bool; 2]) -> Self {
102        AxisBools { x, y }
103    }
104}
105
106/// Information about the plot that has to persist between frames.
107#[derive(Clone)]
108struct PlotMemory {
109    /// Indicates if the user has modified the bounds, for example by moving or zooming,
110    /// or if the bounds should be calculated based by included point or auto bounds.
111    bounds_modified: AxisBools,
112
113    hovered_entry: Option<String>,
114    hidden_items: ahash::HashSet<String>,
115    last_plot_transform: PlotTransform,
116
117    /// Allows to remember the first click position when performing a boxed zoom
118    last_click_pos_for_zoom: Option<Pos2>,
119}
120
121impl PlotMemory {
122    pub fn load(ctx: &Context, id: Id) -> Option<Self> {
123        ctx.data_mut(|d| d.get_temp(id))
124    }
125
126    pub fn store(self, ctx: &Context, id: Id) {
127        ctx.data_mut(|d| d.insert_temp(id, self));
128    }
129}
130
131// ----------------------------------------------------------------------------
132
133/// Indicates a vertical or horizontal cursor line in plot coordinates.
134#[derive(Copy, Clone, PartialEq)]
135enum Cursor {
136    Horizontal { y: f64 },
137    Vertical { x: f64 },
138}
139
140/// Contains the cursors drawn for a plot widget in a single frame.
141#[derive(PartialEq, Clone)]
142struct PlotFrameCursors {
143    id: Id,
144    cursors: Vec<Cursor>,
145}
146
147#[derive(Default, Clone)]
148struct CursorLinkGroups(HashMap<Id, Vec<PlotFrameCursors>>);
149
150#[derive(Clone)]
151struct LinkedBounds {
152    bounds: PlotBounds,
153    bounds_modified: AxisBools,
154}
155
156#[derive(Default, Clone)]
157struct BoundsLinkGroups(HashMap<Id, LinkedBounds>);
158
159// ----------------------------------------------------------------------------
160
161/// What [`Plot::show`] returns.
162pub struct PlotResponse<R> {
163    /// What the user closure returned.
164    pub inner: R,
165
166    /// The response of the plot.
167    pub response: Response,
168
169    /// The transform between screen coordinates and plot coordinates.
170    pub transform: PlotTransform,
171}
172
173// ----------------------------------------------------------------------------
174
175/// A 2D plot, e.g. a graph of a function.
176///
177/// [`Plot`] supports multiple lines and points.
178pub struct Plot {
179    id_source: Id,
180
181    center_axis: AxisBools,
182    allow_zoom: AxisBools,
183    allow_drag: AxisBools,
184    allow_scroll: bool,
185    allow_double_click_reset: bool,
186    allow_boxed_zoom: bool,
187    auto_bounds: AxisBools,
188    min_auto_bounds: PlotBounds,
189    margin_fraction: Vec2,
190    boxed_zoom_pointer_button: PointerButton,
191    linked_axes: Option<(Id, AxisBools)>,
192    linked_cursors: Option<(Id, AxisBools)>,
193
194    min_size: Vec2,
195    width: Option<f32>,
196    height: Option<f32>,
197    data_aspect: Option<f32>,
198    view_aspect: Option<f32>,
199
200    reset: bool,
201
202    show_x: bool,
203    show_y: bool,
204    label_formatter: LabelFormatter,
205    coordinates_formatter: Option<(Corner, CoordinatesFormatter)>,
206    x_axes: Vec<AxisHints>, // default x axes
207    y_axes: Vec<AxisHints>, // default y axes
208    legend_config: Option<Legend>,
209    show_background: bool,
210    show_axes: AxisBools,
211    show_grid: AxisBools,
212    grid_spacers: [GridSpacer; 2],
213    sharp_grid_lines: bool,
214    clamp_grid: bool,
215}
216
217impl Plot {
218    /// Give a unique id for each plot within the same [`Ui`].
219    pub fn new(id_source: impl std::hash::Hash) -> Self {
220        Self {
221            id_source: Id::new(id_source),
222
223            center_axis: false.into(),
224            allow_zoom: true.into(),
225            allow_drag: true.into(),
226            allow_scroll: true,
227            allow_double_click_reset: true,
228            allow_boxed_zoom: true,
229            auto_bounds: false.into(),
230            min_auto_bounds: PlotBounds::NOTHING,
231            margin_fraction: Vec2::splat(0.05),
232            boxed_zoom_pointer_button: PointerButton::Secondary,
233            linked_axes: None,
234            linked_cursors: None,
235
236            min_size: Vec2::splat(64.0),
237            width: None,
238            height: None,
239            data_aspect: None,
240            view_aspect: None,
241
242            reset: false,
243
244            show_x: true,
245            show_y: true,
246            label_formatter: None,
247            coordinates_formatter: None,
248            x_axes: vec![Default::default()],
249            y_axes: vec![Default::default()],
250            legend_config: None,
251            show_background: true,
252            show_axes: true.into(),
253            show_grid: true.into(),
254            grid_spacers: [log_grid_spacer(10), log_grid_spacer(10)],
255            sharp_grid_lines: true,
256            clamp_grid: false,
257        }
258    }
259
260    /// width / height ratio of the data.
261    /// For instance, it can be useful to set this to `1.0` for when the two axes show the same
262    /// unit.
263    /// By default the plot window's aspect ratio is used.
264    pub fn data_aspect(mut self, data_aspect: f32) -> Self {
265        self.data_aspect = Some(data_aspect);
266        self
267    }
268
269    /// width / height ratio of the plot region.
270    /// By default no fixed aspect ratio is set (and width/height will fill the ui it is in).
271    pub fn view_aspect(mut self, view_aspect: f32) -> Self {
272        self.view_aspect = Some(view_aspect);
273        self
274    }
275
276    /// Width of plot. By default a plot will fill the ui it is in.
277    /// If you set [`Self::view_aspect`], the width can be calculated from the height.
278    pub fn width(mut self, width: f32) -> Self {
279        self.min_size.x = width;
280        self.width = Some(width);
281        self
282    }
283
284    /// Height of plot. By default a plot will fill the ui it is in.
285    /// If you set [`Self::view_aspect`], the height can be calculated from the width.
286    pub fn height(mut self, height: f32) -> Self {
287        self.min_size.y = height;
288        self.height = Some(height);
289        self
290    }
291
292    /// Minimum size of the plot view.
293    pub fn min_size(mut self, min_size: Vec2) -> Self {
294        self.min_size = min_size;
295        self
296    }
297
298    /// Show the x-value (e.g. when hovering). Default: `true`.
299    pub fn show_x(mut self, show_x: bool) -> Self {
300        self.show_x = show_x;
301        self
302    }
303
304    /// Show the y-value (e.g. when hovering). Default: `true`.
305    pub fn show_y(mut self, show_y: bool) -> Self {
306        self.show_y = show_y;
307        self
308    }
309
310    /// Always keep the X-axis centered. Default: `false`.
311    pub fn center_x_axis(mut self, on: bool) -> Self {
312        self.center_axis.x = on;
313        self
314    }
315
316    /// Always keep the Y-axis centered. Default: `false`.
317    pub fn center_y_axis(mut self, on: bool) -> Self {
318        self.center_axis.y = on;
319        self
320    }
321
322    /// Whether to allow zooming in the plot. Default: `true`.
323    ///
324    /// Note: Allowing zoom in one axis but not the other may lead to unexpected results if used in combination with `data_aspect`.
325    pub fn allow_zoom<T>(mut self, on: T) -> Self
326    where
327        T: Into<AxisBools>,
328    {
329        self.allow_zoom = on.into();
330        self
331    }
332
333    /// Whether to allow scrolling in the plot. Default: `true`.
334    pub fn allow_scroll(mut self, on: bool) -> Self {
335        self.allow_scroll = on;
336        self
337    }
338
339    /// Whether to allow double clicking to reset the view.
340    /// Default: `true`.
341    pub fn allow_double_click_reset(mut self, on: bool) -> Self {
342        self.allow_double_click_reset = on;
343        self
344    }
345
346    /// Set the side margin as a fraction of the plot size. Only used for auto bounds.
347    ///
348    /// For instance, a value of `0.1` will add 10% space on both sides.
349    pub fn set_margin_fraction(mut self, margin_fraction: Vec2) -> Self {
350        self.margin_fraction = margin_fraction;
351        self
352    }
353
354    /// Whether to allow zooming in the plot by dragging out a box with the secondary mouse button.
355    ///
356    /// Default: `true`.
357    pub fn allow_boxed_zoom(mut self, on: bool) -> Self {
358        self.allow_boxed_zoom = on;
359        self
360    }
361
362    /// Config the button pointer to use for boxed zooming. Default: [`Secondary`](PointerButton::Secondary)
363    pub fn boxed_zoom_pointer_button(mut self, boxed_zoom_pointer_button: PointerButton) -> Self {
364        self.boxed_zoom_pointer_button = boxed_zoom_pointer_button;
365        self
366    }
367
368    /// Whether to allow dragging in the plot to move the bounds. Default: `true`.
369    pub fn allow_drag<T>(mut self, on: T) -> Self
370    where
371        T: Into<AxisBools>,
372    {
373        self.allow_drag = on.into();
374        self
375    }
376
377    /// Provide a function to customize the on-hover label for the x and y axis
378    pub fn label_formatter(
379        mut self,
380        label_formatter: impl Fn(&str, &PlotPoint) -> String + 'static,
381    ) -> Self {
382        self.label_formatter = Some(Box::new(label_formatter));
383        self
384    }
385
386    /// Show the pointer coordinates in the plot.
387    pub fn coordinates_formatter(
388        mut self,
389        position: Corner,
390        formatter: CoordinatesFormatter,
391    ) -> Self {
392        self.coordinates_formatter = Some((position, formatter));
393        self
394    }
395
396    /// Configure how the grid in the background is spaced apart along the X axis.
397    ///
398    /// Default is a log-10 grid, i.e. every plot unit is divided into 10 other units.
399    ///
400    /// The function has this signature:
401    /// ```ignore
402    /// fn step_sizes(input: GridInput) -> Vec<GridMark>;
403    /// ```
404    ///
405    /// This function should return all marks along the visible range of the X axis.
406    /// `step_size` also determines how thick/faint each line is drawn.
407    ///
408    /// There are helpers for common cases, see [`log_grid_spacer`] and [`uniform_grid_spacer`].
409    pub fn x_grid_spacer(mut self, spacer: impl Fn(GridInput) -> Vec<GridMark> + 'static) -> Self {
410        self.grid_spacers[0] = Box::new(spacer);
411        self
412    }
413
414    /// Default is a log-10 grid, i.e. every plot unit is divided into 10 other units.
415    ///
416    /// See [`Self::x_grid_spacer`] for explanation.
417    pub fn y_grid_spacer(mut self, spacer: impl Fn(GridInput) -> Vec<GridMark> + 'static) -> Self {
418        self.grid_spacers[1] = Box::new(spacer);
419        self
420    }
421
422    /// Clamp the grid to only be visible at the range of data where we have values.
423    ///
424    /// Default: `false`.
425    pub fn clamp_grid(mut self, clamp_grid: bool) -> Self {
426        self.clamp_grid = clamp_grid;
427        self
428    }
429
430    /// Expand bounds to include the given x value.
431    /// For instance, to always show the y axis, call `plot.include_x(0.0)`.
432    pub fn include_x(mut self, x: impl Into<f64>) -> Self {
433        self.min_auto_bounds.extend_with_x(x.into());
434        self
435    }
436
437    /// Expand bounds to include the given y value.
438    /// For instance, to always show the x axis, call `plot.include_y(0.0)`.
439    pub fn include_y(mut self, y: impl Into<f64>) -> Self {
440        self.min_auto_bounds.extend_with_y(y.into());
441        self
442    }
443
444    /// Expand bounds to fit all items across the x axis, including values given by `include_x`.
445    pub fn auto_bounds_x(mut self) -> Self {
446        self.auto_bounds.x = true;
447        self
448    }
449
450    /// Expand bounds to fit all items across the y axis, including values given by `include_y`.
451    pub fn auto_bounds_y(mut self) -> Self {
452        self.auto_bounds.y = true;
453        self
454    }
455
456    /// Show a legend including all named items.
457    pub fn legend(mut self, legend: Legend) -> Self {
458        self.legend_config = Some(legend);
459        self
460    }
461
462    /// Whether or not to show the background [`Rect`].
463    /// Can be useful to disable if the plot is overlaid over existing content.
464    /// Default: `true`.
465    pub fn show_background(mut self, show: bool) -> Self {
466        self.show_background = show;
467        self
468    }
469
470    /// Show axis labels and grid tick values on the side of the plot.
471    ///
472    /// Default: `[true; 2]`.
473    pub fn show_axes(mut self, show: impl Into<AxisBools>) -> Self {
474        self.show_axes = show.into();
475        self
476    }
477
478    /// Show a grid overlay on the plot.
479    ///
480    /// Default: `[true; 2]`.
481    pub fn show_grid(mut self, show: impl Into<AxisBools>) -> Self {
482        self.show_grid = show.into();
483        self
484    }
485
486    /// Add this plot to an axis link group so that this plot will share the bounds with other plots in the
487    /// same group. A plot cannot belong to more than one axis group.
488    pub fn link_axis(mut self, group_id: impl Into<Id>, link_x: bool, link_y: bool) -> Self {
489        self.linked_axes = Some((
490            group_id.into(),
491            AxisBools {
492                x: link_x,
493                y: link_y,
494            },
495        ));
496        self
497    }
498
499    /// Add this plot to a cursor link group so that this plot will share the cursor position with other plots
500    /// in the same group. A plot cannot belong to more than one cursor group.
501    pub fn link_cursor(mut self, group_id: impl Into<Id>, link_x: bool, link_y: bool) -> Self {
502        self.linked_cursors = Some((
503            group_id.into(),
504            AxisBools {
505                x: link_x,
506                y: link_y,
507            },
508        ));
509        self
510    }
511
512    /// Round grid positions to full pixels to avoid aliasing. Improves plot appearance but might have an
513    /// undesired effect when shifting the plot bounds. Enabled by default.
514    pub fn sharp_grid_lines(mut self, enabled: bool) -> Self {
515        self.sharp_grid_lines = enabled;
516        self
517    }
518
519    /// Resets the plot.
520    pub fn reset(mut self) -> Self {
521        self.reset = true;
522        self
523    }
524
525    /// Set the x axis label of the main X-axis.
526    ///
527    /// Default: no label.
528    pub fn x_axis_label(mut self, label: impl Into<WidgetText>) -> Self {
529        if let Some(main) = self.x_axes.first_mut() {
530            main.label = label.into();
531        }
532        self
533    }
534
535    /// Set the y axis label of the main Y-axis.
536    ///
537    /// Default: no label.
538    pub fn y_axis_label(mut self, label: impl Into<WidgetText>) -> Self {
539        if let Some(main) = self.y_axes.first_mut() {
540            main.label = label.into();
541        }
542        self
543    }
544
545    /// Set the position of the main X-axis.
546    pub fn x_axis_position(mut self, placement: axis::VPlacement) -> Self {
547        if let Some(main) = self.x_axes.first_mut() {
548            main.placement = placement.into();
549        }
550        self
551    }
552
553    /// Set the position of the main Y-axis.
554    pub fn y_axis_position(mut self, placement: axis::HPlacement) -> Self {
555        if let Some(main) = self.y_axes.first_mut() {
556            main.placement = placement.into();
557        }
558        self
559    }
560
561    /// Specify custom formatter for ticks on the main X-axis.
562    ///
563    /// Arguments of `fmt`:
564    /// * raw tick value as `f64`.
565    /// * maximum requested number of characters per tick label.
566    /// * currently shown range on this axis.
567    pub fn x_axis_formatter(
568        mut self,
569        fmt: impl Fn(f64, usize, &RangeInclusive<f64>) -> String + 'static,
570    ) -> Self {
571        if let Some(main) = self.x_axes.first_mut() {
572            main.formatter = Arc::new(fmt);
573        }
574        self
575    }
576
577    /// Specify custom formatter for ticks on the main Y-axis.
578    ///
579    /// Arguments of `fmt`:
580    /// * raw tick value as `f64`.
581    /// * maximum requested number of characters per tick label.
582    /// * currently shown range on this axis.
583    pub fn y_axis_formatter(
584        mut self,
585        fmt: impl Fn(f64, usize, &RangeInclusive<f64>) -> String + 'static,
586    ) -> Self {
587        if let Some(main) = self.y_axes.first_mut() {
588            main.formatter = Arc::new(fmt);
589        }
590        self
591    }
592
593    /// Set the main Y-axis-width by number of digits
594    ///
595    /// The default is 5 digits.
596    ///
597    /// > Todo: This is experimental. Changing the font size might break this.
598    pub fn y_axis_width(mut self, digits: usize) -> Self {
599        if let Some(main) = self.y_axes.first_mut() {
600            main.digits = digits;
601        }
602        self
603    }
604
605    /// Set custom configuration for X-axis
606    ///
607    /// More than one axis may be specified. The first specified axis is considered the main axis.
608    pub fn custom_x_axes(mut self, hints: Vec<AxisHints>) -> Self {
609        self.x_axes = hints;
610        self
611    }
612
613    /// Set custom configuration for left Y-axis
614    ///
615    /// More than one axis may be specified. The first specified axis is considered the main axis.
616    pub fn custom_y_axes(mut self, hints: Vec<AxisHints>) -> Self {
617        self.y_axes = hints;
618        self
619    }
620
621    /// Interact with and add items to the plot and finally draw it.
622    pub fn show<R>(self, ui: &mut Ui, build_fn: impl FnOnce(&mut PlotUi) -> R) -> PlotResponse<R> {
623        self.show_dyn(ui, Box::new(build_fn))
624    }
625
626    fn show_dyn<'a, R>(
627        self,
628        ui: &mut Ui,
629        build_fn: Box<dyn FnOnce(&mut PlotUi) -> R + 'a>,
630    ) -> PlotResponse<R> {
631        let Self {
632            id_source,
633            center_axis,
634            allow_zoom,
635            allow_drag,
636            allow_scroll,
637            allow_double_click_reset,
638            allow_boxed_zoom,
639            boxed_zoom_pointer_button: boxed_zoom_pointer,
640            auto_bounds,
641            min_auto_bounds,
642            margin_fraction,
643            width,
644            height,
645            min_size,
646            data_aspect,
647            view_aspect,
648            mut show_x,
649            mut show_y,
650            label_formatter,
651            coordinates_formatter,
652            x_axes,
653            y_axes,
654            legend_config,
655            reset,
656            show_background,
657            show_axes,
658            show_grid,
659            linked_axes,
660            linked_cursors,
661
662            clamp_grid,
663            grid_spacers,
664            sharp_grid_lines,
665        } = self;
666
667        // Determine position of widget.
668        let pos = ui.available_rect_before_wrap().min;
669        // Determine size of widget.
670        let size = {
671            let width = width
672                .unwrap_or_else(|| {
673                    if let (Some(height), Some(aspect)) = (height, view_aspect) {
674                        height * aspect
675                    } else {
676                        ui.available_size_before_wrap().x
677                    }
678                })
679                .at_least(min_size.x);
680
681            let height = height
682                .unwrap_or_else(|| {
683                    if let Some(aspect) = view_aspect {
684                        width / aspect
685                    } else {
686                        ui.available_size_before_wrap().y
687                    }
688                })
689                .at_least(min_size.y);
690            vec2(width, height)
691        };
692        // Determine complete rect of widget.
693        let complete_rect = Rect {
694            min: pos,
695            max: pos + size,
696        };
697        // Next we want to create this layout.
698        // Incides are only examples.
699        //
700        //  left                     right
701        //  +---+---------x----------+   +
702        //  |   |      X-axis 3      |
703        //  |   +--------------------+    top
704        //  |   |      X-axis 2      |
705        //  +-+-+--------------------+-+-+
706        //  |y|y|                    |y|y|
707        //  |-|-|                    |-|-|
708        //  |A|A|                    |A|A|
709        // y|x|x|    Plot Window     |x|x|
710        //  |i|i|                    |i|i|
711        //  |s|s|                    |s|s|
712        //  |1|0|                    |2|3|
713        //  +-+-+--------------------+-+-+
714        //      |      X-axis 0      |   |
715        //      +--------------------+   | bottom
716        //      |      X-axis 1      |   |
717        //  +   +--------------------+---+
718        //
719
720        let mut plot_rect: Rect = {
721            // Calcuclate the space needed for each axis labels.
722            let mut margin = Margin::ZERO;
723            if show_axes.x {
724                for cfg in &x_axes {
725                    match cfg.placement {
726                        axis::Placement::LeftBottom => {
727                            margin.bottom += cfg.thickness(Axis::X);
728                        }
729                        axis::Placement::RightTop => {
730                            margin.top += cfg.thickness(Axis::X);
731                        }
732                    }
733                }
734            }
735            if show_axes.y {
736                for cfg in &y_axes {
737                    match cfg.placement {
738                        axis::Placement::LeftBottom => {
739                            margin.left += cfg.thickness(Axis::Y);
740                        }
741                        axis::Placement::RightTop => {
742                            margin.right += cfg.thickness(Axis::Y);
743                        }
744                    }
745                }
746            }
747
748            // determine plot rectangle
749            margin.shrink_rect(complete_rect)
750        };
751
752        let [mut x_axis_widgets, mut y_axis_widgets] =
753            axis_widgets(show_axes, plot_rect, [&x_axes, &y_axes]);
754
755        // If too little space, remove axis widgets
756        if plot_rect.width() <= 0.0 || plot_rect.height() <= 0.0 {
757            y_axis_widgets.clear();
758            x_axis_widgets.clear();
759            plot_rect = complete_rect;
760        }
761
762        // Allocate the plot window.
763        let response = ui.allocate_rect(plot_rect, Sense::drag());
764        let rect = plot_rect;
765
766        // Load or initialize the memory.
767        let plot_id = ui.make_persistent_id(id_source);
768        ui.ctx().check_for_id_clash(plot_id, rect, "Plot");
769        let memory = if reset {
770            if let Some((name, _)) = linked_axes.as_ref() {
771                ui.memory_mut(|memory| {
772                    let link_groups: &mut BoundsLinkGroups =
773                        memory.data.get_temp_mut_or_default(Id::NULL);
774                    link_groups.0.remove(name);
775                });
776            };
777            None
778        } else {
779            PlotMemory::load(ui.ctx(), plot_id)
780        }
781        .unwrap_or_else(|| PlotMemory {
782            bounds_modified: false.into(),
783            hovered_entry: None,
784            hidden_items: Default::default(),
785            last_plot_transform: PlotTransform::new(
786                rect,
787                min_auto_bounds,
788                center_axis.x,
789                center_axis.y,
790            ),
791            last_click_pos_for_zoom: None,
792        });
793
794        let PlotMemory {
795            mut bounds_modified,
796            mut hovered_entry,
797            mut hidden_items,
798            last_plot_transform,
799            mut last_click_pos_for_zoom,
800        } = memory;
801
802        // Call the plot build function.
803        let mut plot_ui = PlotUi {
804            items: Vec::new(),
805            next_auto_color_idx: 0,
806            last_plot_transform,
807            response,
808            bounds_modifications: Vec::new(),
809            ctx: ui.ctx().clone(),
810        };
811        let inner = build_fn(&mut plot_ui);
812        let PlotUi {
813            mut items,
814            mut response,
815            last_plot_transform,
816            bounds_modifications,
817            ..
818        } = plot_ui;
819
820        // Background
821        if show_background {
822            ui.painter()
823                .with_clip_rect(rect)
824                .add(epaint::RectShape::new(
825                    rect,
826                    Rounding::same(2.0),
827                    ui.visuals().extreme_bg_color,
828                    ui.visuals().widgets.noninteractive.bg_stroke,
829                ));
830        }
831
832        // --- Legend ---
833        let legend = legend_config
834            .and_then(|config| LegendWidget::try_new(rect, config, &items, &hidden_items));
835        // Don't show hover cursor when hovering over legend.
836        if hovered_entry.is_some() {
837            show_x = false;
838            show_y = false;
839        }
840        // Remove the deselected items.
841        items.retain(|item| !hidden_items.contains(item.name()));
842        // Highlight the hovered items.
843        if let Some(hovered_name) = &hovered_entry {
844            items
845                .iter_mut()
846                .filter(|entry| entry.name() == hovered_name)
847                .for_each(|entry| entry.highlight());
848        }
849        // Move highlighted items to front.
850        items.sort_by_key(|item| item.highlighted());
851
852        // --- Bound computation ---
853        let mut bounds = *last_plot_transform.bounds();
854
855        // Find the cursors from other plots we need to draw
856        let draw_cursors: Vec<Cursor> = if let Some((id, _)) = linked_cursors.as_ref() {
857            ui.memory_mut(|memory| {
858                let frames: &mut CursorLinkGroups = memory.data.get_temp_mut_or_default(Id::NULL);
859                let cursors = frames.0.entry(*id).or_default();
860
861                // Look for our previous frame
862                let index = cursors
863                    .iter()
864                    .enumerate()
865                    .find(|(_, frame)| frame.id == plot_id)
866                    .map(|(i, _)| i);
867
868                // Remove our previous frame and all older frames as these are no longer displayed. This avoids
869                // unbounded growth, as we add an entry each time we draw a plot.
870                index.map(|index| cursors.drain(0..=index));
871
872                // Gather all cursors of the remaining frames. This will be all the cursors of the
873                // other plots in the group. We want to draw these in the current plot too.
874                cursors
875                    .iter()
876                    .flat_map(|frame| frame.cursors.iter().copied())
877                    .collect()
878            })
879        } else {
880            Vec::new()
881        };
882
883        // Transfer the bounds from a link group.
884        if let Some((id, axes)) = linked_axes.as_ref() {
885            ui.memory_mut(|memory| {
886                let link_groups: &mut BoundsLinkGroups =
887                    memory.data.get_temp_mut_or_default(Id::NULL);
888                if let Some(linked_bounds) = link_groups.0.get(id) {
889                    if axes.x {
890                        bounds.set_x(&linked_bounds.bounds);
891                        bounds_modified.x = linked_bounds.bounds_modified.x;
892                    }
893                    if axes.y {
894                        bounds.set_y(&linked_bounds.bounds);
895                        bounds_modified.y = linked_bounds.bounds_modified.y;
896                    }
897                };
898            });
899        };
900
901        // Allow double clicking to reset to the initial bounds.
902        if allow_double_click_reset && response.double_clicked() {
903            bounds_modified = false.into();
904        }
905
906        // Apply bounds modifications.
907        for modification in bounds_modifications {
908            match modification {
909                BoundsModification::Set(new_bounds) => {
910                    bounds = new_bounds;
911                    bounds_modified = true.into();
912                }
913                BoundsModification::Translate(delta) => {
914                    bounds.translate(delta);
915                    bounds_modified = true.into();
916                }
917            }
918        }
919
920        // Reset bounds to initial bounds if they haven't been modified.
921        if !bounds_modified.x {
922            bounds.set_x(&min_auto_bounds);
923        }
924        if !bounds_modified.y {
925            bounds.set_y(&min_auto_bounds);
926        }
927
928        let auto_x = !bounds_modified.x && (!min_auto_bounds.is_valid_x() || auto_bounds.x);
929        let auto_y = !bounds_modified.y && (!min_auto_bounds.is_valid_y() || auto_bounds.y);
930
931        // Set bounds automatically based on content.
932        if auto_x || auto_y {
933            for item in &items {
934                let item_bounds = item.bounds();
935                if auto_x {
936                    bounds.merge_x(&item_bounds);
937                }
938                if auto_y {
939                    bounds.merge_y(&item_bounds);
940                }
941            }
942
943            if auto_x {
944                bounds.add_relative_margin_x(margin_fraction);
945            }
946
947            if auto_y {
948                bounds.add_relative_margin_y(margin_fraction);
949            }
950        }
951
952        let mut transform = PlotTransform::new(rect, bounds, center_axis.x, center_axis.y);
953
954        // Enforce aspect ratio
955        if let Some(data_aspect) = data_aspect {
956            if let Some((_, linked_axes)) = &linked_axes {
957                let change_x = linked_axes.y && !linked_axes.x;
958                transform.set_aspect_by_changing_axis(data_aspect as f64, change_x);
959            } else if auto_bounds.any() {
960                transform.set_aspect_by_expanding(data_aspect as f64);
961            } else {
962                transform.set_aspect_by_changing_axis(data_aspect as f64, false);
963            }
964        }
965
966        // Dragging
967        if allow_drag.any() && response.dragged_by(PointerButton::Primary) {
968            response = response.on_hover_cursor(CursorIcon::Grabbing);
969            let mut delta = -response.drag_delta();
970            if !allow_drag.x {
971                delta.x = 0.0;
972            }
973            if !allow_drag.y {
974                delta.y = 0.0;
975            }
976            transform.translate_bounds(delta);
977            bounds_modified = allow_drag;
978        }
979
980        // Zooming
981        let mut boxed_zoom_rect = None;
982        if allow_boxed_zoom {
983            // Save last click to allow boxed zooming
984            if response.drag_started() && response.dragged_by(boxed_zoom_pointer) {
985                // it would be best for egui that input has a memory of the last click pos because it's a common pattern
986                last_click_pos_for_zoom = response.hover_pos();
987            }
988            let box_start_pos = last_click_pos_for_zoom;
989            let box_end_pos = response.hover_pos();
990            if let (Some(box_start_pos), Some(box_end_pos)) = (box_start_pos, box_end_pos) {
991                // while dragging prepare a Shape and draw it later on top of the plot
992                if response.dragged_by(boxed_zoom_pointer) {
993                    response = response.on_hover_cursor(CursorIcon::ZoomIn);
994                    let rect = epaint::Rect::from_two_pos(box_start_pos, box_end_pos);
995                    boxed_zoom_rect = Some((
996                        epaint::RectShape::stroke(
997                            rect,
998                            0.0,
999                            epaint::Stroke::new(4., Color32::DARK_BLUE),
1000                        ), // Outer stroke
1001                        epaint::RectShape::stroke(
1002                            rect,
1003                            0.0,
1004                            epaint::Stroke::new(2., Color32::WHITE),
1005                        ), // Inner stroke
1006                    ));
1007                }
1008                // when the click is release perform the zoom
1009                if response.drag_released() {
1010                    let box_start_pos = transform.value_from_position(box_start_pos);
1011                    let box_end_pos = transform.value_from_position(box_end_pos);
1012                    let new_bounds = PlotBounds {
1013                        min: [
1014                            box_start_pos.x.min(box_end_pos.x),
1015                            box_start_pos.y.min(box_end_pos.y),
1016                        ],
1017                        max: [
1018                            box_start_pos.x.max(box_end_pos.x),
1019                            box_start_pos.y.max(box_end_pos.y),
1020                        ],
1021                    };
1022                    if new_bounds.is_valid() {
1023                        transform.set_bounds(new_bounds);
1024                        bounds_modified = true.into();
1025                    }
1026                    // reset the boxed zoom state
1027                    last_click_pos_for_zoom = None;
1028                }
1029            }
1030        }
1031
1032        let hover_pos = response.hover_pos();
1033        if let Some(hover_pos) = hover_pos {
1034            if allow_zoom.any() {
1035                let mut zoom_factor = if data_aspect.is_some() {
1036                    Vec2::splat(ui.input(|i| i.zoom_delta()))
1037                } else {
1038                    ui.input(|i| i.zoom_delta_2d())
1039                };
1040                if !allow_zoom.x {
1041                    zoom_factor.x = 1.0;
1042                }
1043                if !allow_zoom.y {
1044                    zoom_factor.y = 1.0;
1045                }
1046                if zoom_factor != Vec2::splat(1.0) {
1047                    transform.zoom(zoom_factor, hover_pos);
1048                    bounds_modified = allow_zoom;
1049                }
1050            }
1051            if allow_scroll {
1052                let scroll_delta = ui.input(|i| i.raw_scroll_delta);
1053                if scroll_delta != Vec2::ZERO {
1054                    transform.translate_bounds(-scroll_delta);
1055                    bounds_modified = true.into();
1056                }
1057            }
1058        }
1059
1060        // --- transform initialized
1061
1062        // Add legend widgets to plot
1063        let bounds = transform.bounds();
1064        let x_axis_range = bounds.range_x();
1065        let x_steps = Arc::new({
1066            let input = GridInput {
1067                bounds: (bounds.min[0], bounds.max[0]),
1068                base_step_size: transform.dvalue_dpos()[0] * MIN_LINE_SPACING_IN_POINTS * 2.0,
1069            };
1070            (grid_spacers[0])(input)
1071        });
1072        let y_axis_range = bounds.range_y();
1073        let y_steps = Arc::new({
1074            let input = GridInput {
1075                bounds: (bounds.min[1], bounds.max[1]),
1076                base_step_size: transform.dvalue_dpos()[1] * MIN_LINE_SPACING_IN_POINTS * 2.0,
1077            };
1078            (grid_spacers[1])(input)
1079        });
1080        for mut widget in x_axis_widgets {
1081            widget.range = x_axis_range.clone();
1082            widget.transform = Some(transform);
1083            widget.steps = x_steps.clone();
1084            widget.ui(ui, Axis::X);
1085        }
1086        for mut widget in y_axis_widgets {
1087            widget.range = y_axis_range.clone();
1088            widget.transform = Some(transform);
1089            widget.steps = y_steps.clone();
1090            widget.ui(ui, Axis::Y);
1091        }
1092
1093        // Initialize values from functions.
1094        for item in &mut items {
1095            item.initialize(transform.bounds().range_x());
1096        }
1097
1098        let prepared = PreparedPlot {
1099            items,
1100            show_x,
1101            show_y,
1102            label_formatter,
1103            coordinates_formatter,
1104            show_grid,
1105            transform,
1106            draw_cursor_x: linked_cursors.as_ref().is_some_and(|group| group.1.x),
1107            draw_cursor_y: linked_cursors.as_ref().is_some_and(|group| group.1.y),
1108            draw_cursors,
1109            grid_spacers,
1110            sharp_grid_lines,
1111            clamp_grid,
1112        };
1113
1114        let plot_cursors = prepared.ui(ui, &response);
1115
1116        if let Some(boxed_zoom_rect) = boxed_zoom_rect {
1117            ui.painter().with_clip_rect(rect).add(boxed_zoom_rect.0);
1118            ui.painter().with_clip_rect(rect).add(boxed_zoom_rect.1);
1119        }
1120
1121        if let Some(mut legend) = legend {
1122            ui.add(&mut legend);
1123            hidden_items = legend.hidden_items();
1124            hovered_entry = legend.hovered_entry_name();
1125        }
1126
1127        if let Some((id, _)) = linked_cursors.as_ref() {
1128            // Push the frame we just drew to the list of frames
1129            ui.memory_mut(|memory| {
1130                let frames: &mut CursorLinkGroups = memory.data.get_temp_mut_or_default(Id::NULL);
1131                let cursors = frames.0.entry(*id).or_default();
1132                cursors.push(PlotFrameCursors {
1133                    id: plot_id,
1134                    cursors: plot_cursors,
1135                });
1136            });
1137        }
1138
1139        if let Some((id, _)) = linked_axes.as_ref() {
1140            // Save the linked bounds.
1141            ui.memory_mut(|memory| {
1142                let link_groups: &mut BoundsLinkGroups =
1143                    memory.data.get_temp_mut_or_default(Id::NULL);
1144                link_groups.0.insert(
1145                    *id,
1146                    LinkedBounds {
1147                        bounds: *transform.bounds(),
1148                        bounds_modified,
1149                    },
1150                );
1151            });
1152        }
1153
1154        let memory = PlotMemory {
1155            bounds_modified,
1156            hovered_entry,
1157            hidden_items,
1158            last_plot_transform: transform,
1159            last_click_pos_for_zoom,
1160        };
1161        memory.store(ui.ctx(), plot_id);
1162
1163        let response = if show_x || show_y {
1164            response.on_hover_cursor(CursorIcon::Crosshair)
1165        } else {
1166            response
1167        };
1168        ui.advance_cursor_after_rect(complete_rect);
1169        PlotResponse {
1170            inner,
1171            response,
1172            transform,
1173        }
1174    }
1175}
1176
1177fn axis_widgets(
1178    show_axes: AxisBools,
1179    plot_rect: Rect,
1180    [x_axes, y_axes]: [&[AxisHints]; 2],
1181) -> [Vec<AxisWidget>; 2] {
1182    let mut x_axis_widgets = Vec::<AxisWidget>::new();
1183    let mut y_axis_widgets = Vec::<AxisWidget>::new();
1184
1185    // Widget count per border of plot in order left, top, right, bottom
1186    struct NumWidgets {
1187        left: usize,
1188        top: usize,
1189        right: usize,
1190        bottom: usize,
1191    }
1192    let mut num_widgets = NumWidgets {
1193        left: 0,
1194        top: 0,
1195        right: 0,
1196        bottom: 0,
1197    };
1198    if show_axes.x {
1199        for cfg in x_axes {
1200            let size_y = Vec2::new(0.0, cfg.thickness(Axis::X));
1201            let rect = match cfg.placement {
1202                axis::Placement::LeftBottom => {
1203                    let off = num_widgets.bottom as f32;
1204                    num_widgets.bottom += 1;
1205                    Rect {
1206                        min: plot_rect.left_bottom() + size_y * off,
1207                        max: plot_rect.right_bottom() + size_y * (off + 1.0),
1208                    }
1209                }
1210                axis::Placement::RightTop => {
1211                    let off = num_widgets.top as f32;
1212                    num_widgets.top += 1;
1213                    Rect {
1214                        min: plot_rect.left_top() - size_y * (off + 1.0),
1215                        max: plot_rect.right_top() - size_y * off,
1216                    }
1217                }
1218            };
1219            x_axis_widgets.push(AxisWidget::new(cfg.clone(), rect));
1220        }
1221    }
1222    if show_axes.y {
1223        for cfg in y_axes {
1224            let size_x = Vec2::new(cfg.thickness(Axis::Y), 0.0);
1225            let rect = match cfg.placement {
1226                axis::Placement::LeftBottom => {
1227                    let off = num_widgets.left as f32;
1228                    num_widgets.left += 1;
1229                    Rect {
1230                        min: plot_rect.left_top() - size_x * (off + 1.0),
1231                        max: plot_rect.left_bottom() - size_x * off,
1232                    }
1233                }
1234                axis::Placement::RightTop => {
1235                    let off = num_widgets.right as f32;
1236                    num_widgets.right += 1;
1237                    Rect {
1238                        min: plot_rect.right_top() + size_x * off,
1239                        max: plot_rect.right_bottom() + size_x * (off + 1.0),
1240                    }
1241                }
1242            };
1243            y_axis_widgets.push(AxisWidget::new(cfg.clone(), rect));
1244        }
1245    }
1246
1247    [x_axis_widgets, y_axis_widgets]
1248}
1249
1250/// User-requested modifications to the plot bounds. We collect them in the plot build function to
1251/// later apply them at the right time, as other modifications need to happen first.
1252enum BoundsModification {
1253    Set(PlotBounds),
1254    Translate(Vec2),
1255}
1256
1257/// Provides methods to interact with a plot while building it. It is the single argument of the
1258/// closure provided to [`Plot::show`]. See [`Plot`] for an example of how to use it.
1259pub struct PlotUi {
1260    items: Vec<Box<dyn PlotItem>>,
1261    next_auto_color_idx: usize,
1262    last_plot_transform: PlotTransform,
1263    response: Response,
1264    bounds_modifications: Vec<BoundsModification>,
1265    ctx: Context,
1266}
1267
1268impl PlotUi {
1269    fn auto_color(&mut self) -> Color32 {
1270        let i = self.next_auto_color_idx;
1271        self.next_auto_color_idx += 1;
1272        let golden_ratio = (5.0_f32.sqrt() - 1.0) / 2.0; // 0.61803398875
1273        let h = i as f32 * golden_ratio;
1274        Hsva::new(h, 0.85, 0.5, 1.0).into()
1275    }
1276
1277    pub fn ctx(&self) -> &Context {
1278        &self.ctx
1279    }
1280
1281    /// The plot bounds as they were in the last frame. If called on the first frame and the bounds
1282    /// were not further specified in the plot builder, this will return bounds centered on the
1283    /// origin. The bounds do not change until the plot is drawn.
1284    pub fn plot_bounds(&self) -> PlotBounds {
1285        *self.last_plot_transform.bounds()
1286    }
1287
1288    /// Set the plot bounds. Can be useful for implementing alternative plot navigation methods.
1289    pub fn set_plot_bounds(&mut self, plot_bounds: PlotBounds) {
1290        self.bounds_modifications
1291            .push(BoundsModification::Set(plot_bounds));
1292    }
1293
1294    /// Move the plot bounds. Can be useful for implementing alternative plot navigation methods.
1295    pub fn translate_bounds(&mut self, delta_pos: Vec2) {
1296        self.bounds_modifications
1297            .push(BoundsModification::Translate(delta_pos));
1298    }
1299
1300    /// Can be used to check if the plot was hovered or clicked.
1301    pub fn response(&self) -> &Response {
1302        &self.response
1303    }
1304
1305    /// Returns `true` if the plot area is currently hovered.
1306    #[deprecated = "Use plot_ui.response().hovered()"]
1307    pub fn plot_hovered(&self) -> bool {
1308        self.response.hovered()
1309    }
1310
1311    /// Returns `true` if the plot was clicked by the primary button.
1312    #[deprecated = "Use plot_ui.response().clicked()"]
1313    pub fn plot_clicked(&self) -> bool {
1314        self.response.clicked()
1315    }
1316
1317    /// Returns `true` if the plot was clicked by the secondary button.
1318    #[deprecated = "Use plot_ui.response().secondary_clicked()"]
1319    pub fn plot_secondary_clicked(&self) -> bool {
1320        self.response.secondary_clicked()
1321    }
1322
1323    /// The pointer position in plot coordinates. Independent of whether the pointer is in the plot
1324    /// area.
1325    pub fn pointer_coordinate(&self) -> Option<PlotPoint> {
1326        // We need to subtract the drag delta to keep in sync with the frame-delayed screen
1327        // transform:
1328        let last_pos = self.ctx().input(|i| i.pointer.latest_pos())? - self.response.drag_delta();
1329        let value = self.plot_from_screen(last_pos);
1330        Some(value)
1331    }
1332
1333    /// The pointer drag delta in plot coordinates.
1334    pub fn pointer_coordinate_drag_delta(&self) -> Vec2 {
1335        let delta = self.response.drag_delta();
1336        let dp_dv = self.last_plot_transform.dpos_dvalue();
1337        Vec2::new(delta.x / dp_dv[0] as f32, delta.y / dp_dv[1] as f32)
1338    }
1339
1340    /// Read the transform netween plot coordinates and screen coordinates.
1341    pub fn transform(&self) -> &PlotTransform {
1342        &self.last_plot_transform
1343    }
1344
1345    /// Transform the plot coordinates to screen coordinates.
1346    pub fn screen_from_plot(&self, position: PlotPoint) -> Pos2 {
1347        self.last_plot_transform.position_from_point(&position)
1348    }
1349
1350    /// Transform the screen coordinates to plot coordinates.
1351    pub fn plot_from_screen(&self, position: Pos2) -> PlotPoint {
1352        self.last_plot_transform.value_from_position(position)
1353    }
1354
1355    /// Add a data line.
1356    pub fn line(&mut self, mut line: Line) {
1357        if line.series.is_empty() {
1358            return;
1359        };
1360
1361        // Give the stroke an automatic color if no color has been assigned.
1362        if line.stroke.color == Color32::TRANSPARENT {
1363            line.stroke.color = self.auto_color();
1364        }
1365        self.items.push(Box::new(line));
1366    }
1367
1368    /// Add a stacked data line.
1369    pub fn stacked_line(&mut self, mut line: StackedLine) {
1370        if line.series.is_empty() {
1371            return;
1372        };
1373
1374        // Give the stroke an automatic color if no color has been assigned.
1375        if line.stroke.color == Color32::TRANSPARENT {
1376            line.stroke.color = self.auto_color();
1377        }
1378        self.items.push(Box::new(line));
1379    }
1380
1381    /// Add a text.
1382    pub fn text(&mut self, text: Text) {
1383        if text.text.is_empty() {
1384            return;
1385        };
1386
1387        self.items.push(Box::new(text));
1388    }
1389
1390    /// Add data points.
1391    pub fn points(&mut self, mut points: Points) {
1392        if points.series.is_empty() {
1393            return;
1394        };
1395
1396        // Give the points an automatic color if no color has been assigned.
1397        if points.color == Color32::TRANSPARENT {
1398            points.color = self.auto_color();
1399        }
1400        self.items.push(Box::new(points));
1401    }
1402}
1403
1404/// Input for "grid spacer" functions.
1405///
1406/// See [`Plot::x_grid_spacer()`] and [`Plot::y_grid_spacer()`].
1407pub struct GridInput {
1408    /// Min/max of the visible data range (the values at the two edges of the plot,
1409    /// for the current axis).
1410    pub bounds: (f64, f64),
1411
1412    /// Recommended (but not required) lower-bound on the step size returned by custom grid spacers.
1413    ///
1414    /// Computed as the ratio between the diagram's bounds (in plot coordinates) and the viewport
1415    /// (in frame/window coordinates), scaled up to represent the minimal possible step.
1416    pub base_step_size: f64,
1417}
1418
1419/// One mark (horizontal or vertical line) in the background grid of a plot.
1420#[derive(Debug, Clone, Copy)]
1421pub struct GridMark {
1422    /// X or Y value in the plot.
1423    pub value: f64,
1424
1425    /// The (approximate) distance to the next value of same thickness.
1426    ///
1427    /// Determines how thick the grid line is painted. It's not important that `step_size`
1428    /// matches the difference between two `value`s precisely, but rather that grid marks of
1429    /// same thickness have same `step_size`. For example, months can have a different number
1430    /// of days, but consistently using a `step_size` of 30 days is a valid approximation.
1431    pub step_size: f64,
1432}
1433
1434/// Recursively splits the grid into `base` subdivisions (e.g. 100, 10, 1).
1435///
1436/// The logarithmic base, expressing how many times each grid unit is subdivided.
1437/// 10 is a typical value, others are possible though.
1438pub fn log_grid_spacer(log_base: i64) -> GridSpacer {
1439    let log_base = log_base as f64;
1440    let step_sizes = move |input: GridInput| -> Vec<GridMark> {
1441        // The distance between two of the thinnest grid lines is "rounded" up
1442        // to the next-bigger power of base
1443        let smallest_visible_unit = next_power(input.base_step_size, log_base);
1444
1445        let step_sizes = [
1446            smallest_visible_unit,
1447            smallest_visible_unit * log_base,
1448            smallest_visible_unit * log_base * log_base,
1449        ];
1450
1451        generate_marks(step_sizes, input.bounds)
1452    };
1453
1454    Box::new(step_sizes)
1455}
1456
1457/// Splits the grid into uniform-sized spacings (e.g. 100, 25, 1).
1458///
1459/// This function should return 3 positive step sizes, designating where the lines in the grid are
1460/// drawn.  Lines are thicker for larger step sizes. Ordering of returned value is irrelevant.
1461///
1462/// Why only 3 step sizes? Three is the number of different line thicknesses that egui typically
1463/// uses in the grid.  Ideally, those 3 are not hardcoded values, but depend on the visible range
1464/// (accessible through `GridInput`).
1465pub fn uniform_grid_spacer(spacer: impl Fn(GridInput) -> [f64; 3] + 'static) -> GridSpacer {
1466    let get_marks = move |input: GridInput| -> Vec<GridMark> {
1467        let bounds = input.bounds;
1468        let step_sizes = spacer(input);
1469        generate_marks(step_sizes, bounds)
1470    };
1471
1472    Box::new(get_marks)
1473}
1474
1475struct PreparedPlot {
1476    items: Vec<Box<dyn PlotItem>>,
1477    show_x: bool,
1478    show_y: bool,
1479    label_formatter: LabelFormatter,
1480    coordinates_formatter: Option<(Corner, CoordinatesFormatter)>,
1481    transform: PlotTransform,
1482    show_grid: AxisBools,
1483    grid_spacers: [GridSpacer; 2],
1484    draw_cursor_x: bool,
1485    draw_cursor_y: bool,
1486    draw_cursors: Vec<Cursor>,
1487
1488    sharp_grid_lines: bool,
1489    clamp_grid: bool,
1490}
1491
1492impl PreparedPlot {
1493    fn ui(self, ui: &mut Ui, response: &Response) -> Vec<Cursor> {
1494        let mut axes_shapes = Vec::new();
1495
1496        if self.show_grid.x {
1497            self.paint_grid(ui, &mut axes_shapes, Axis::X);
1498        }
1499        if self.show_grid.y {
1500            self.paint_grid(ui, &mut axes_shapes, Axis::Y);
1501        }
1502
1503        // Sort the axes by strength so that those with higher strength are drawn in front.
1504        axes_shapes.sort_by(|(_, strength1), (_, strength2)| strength1.total_cmp(strength2));
1505
1506        let mut shapes = axes_shapes.into_iter().map(|(shape, _)| shape).collect();
1507
1508        let transform = &self.transform;
1509
1510        let mut plot_ui = ui.child_ui(*transform.frame(), Layout::default());
1511        plot_ui.set_clip_rect(*transform.frame());
1512        for item in &self.items {
1513            item.shapes(&mut plot_ui, transform, &mut shapes);
1514        }
1515
1516        let hover_pos = response.hover_pos();
1517        let cursors = if let Some(pointer) = hover_pos {
1518            self.hover(ui, pointer, &mut shapes)
1519        } else {
1520            Vec::new()
1521        };
1522
1523        // Draw cursors
1524        let line_color = rulers_color(ui);
1525
1526        let mut draw_cursor = |cursors: &Vec<Cursor>, always| {
1527            for &cursor in cursors {
1528                match cursor {
1529                    Cursor::Horizontal { y } => {
1530                        if self.draw_cursor_y || always {
1531                            shapes.push(horizontal_line(
1532                                transform.position_from_point(&PlotPoint::new(0.0, y)),
1533                                &self.transform,
1534                                line_color,
1535                            ));
1536                        }
1537                    }
1538                    Cursor::Vertical { x } => {
1539                        if self.draw_cursor_x || always {
1540                            shapes.push(vertical_line(
1541                                transform.position_from_point(&PlotPoint::new(x, 0.0)),
1542                                &self.transform,
1543                                line_color,
1544                            ));
1545                        }
1546                    }
1547                }
1548            }
1549        };
1550
1551        draw_cursor(&self.draw_cursors, false);
1552        draw_cursor(&cursors, true);
1553
1554        let painter = ui.painter().with_clip_rect(*transform.frame());
1555        painter.extend(shapes);
1556
1557        if let Some((corner, formatter)) = self.coordinates_formatter.as_ref() {
1558            let hover_pos = response.hover_pos();
1559            if let Some(pointer) = hover_pos {
1560                let font_id = TextStyle::Monospace.resolve(ui.style());
1561                let coordinate = transform.value_from_position(pointer);
1562                let text = formatter.format(&coordinate, transform.bounds());
1563                let padded_frame = transform.frame().shrink(4.0);
1564                let (anchor, position) = match corner {
1565                    Corner::LeftTop => (Align2::LEFT_TOP, padded_frame.left_top()),
1566                    Corner::RightTop => (Align2::RIGHT_TOP, padded_frame.right_top()),
1567                    Corner::LeftBottom => (Align2::LEFT_BOTTOM, padded_frame.left_bottom()),
1568                    Corner::RightBottom => (Align2::RIGHT_BOTTOM, padded_frame.right_bottom()),
1569                };
1570                painter.text(position, anchor, text, font_id, ui.visuals().text_color());
1571            }
1572        }
1573
1574        cursors
1575    }
1576
1577    fn paint_grid(&self, ui: &Ui, shapes: &mut Vec<(Shape, f32)>, axis: Axis) {
1578        #![allow(clippy::collapsible_else_if)]
1579        let Self {
1580            transform,
1581            // axis_formatters,
1582            grid_spacers,
1583            clamp_grid,
1584            ..
1585        } = self;
1586
1587        let iaxis = usize::from(axis);
1588
1589        // Where on the cross-dimension to show the label values
1590        let bounds = transform.bounds();
1591        let value_cross = 0.0_f64.clamp(bounds.min[1 - iaxis], bounds.max[1 - iaxis]);
1592
1593        let input = GridInput {
1594            bounds: (bounds.min[iaxis], bounds.max[iaxis]),
1595            base_step_size: transform.dvalue_dpos()[iaxis] * MIN_LINE_SPACING_IN_POINTS,
1596        };
1597        let steps = (grid_spacers[iaxis])(input);
1598
1599        let clamp_range = clamp_grid.then(|| {
1600            let mut tight_bounds = PlotBounds::NOTHING;
1601            for item in &self.items {
1602                let item_bounds = item.bounds();
1603                tight_bounds.merge_x(&item_bounds);
1604                tight_bounds.merge_y(&item_bounds);
1605            }
1606            tight_bounds
1607        });
1608
1609        for step in steps {
1610            let value_main = step.value;
1611
1612            if let Some(clamp_range) = clamp_range {
1613                match axis {
1614                    Axis::X => {
1615                        if !clamp_range.range_x().contains(&value_main) {
1616                            continue;
1617                        };
1618                    }
1619                    Axis::Y => {
1620                        if !clamp_range.range_y().contains(&value_main) {
1621                            continue;
1622                        };
1623                    }
1624                }
1625            }
1626
1627            let value = match axis {
1628                Axis::X => PlotPoint::new(value_main, value_cross),
1629                Axis::Y => PlotPoint::new(value_cross, value_main),
1630            };
1631
1632            let pos_in_gui = transform.position_from_point(&value);
1633            let spacing_in_points = (transform.dpos_dvalue()[iaxis] * step.step_size).abs() as f32;
1634
1635            if spacing_in_points > MIN_LINE_SPACING_IN_POINTS as f32 {
1636                let line_strength = remap_clamp(
1637                    spacing_in_points,
1638                    MIN_LINE_SPACING_IN_POINTS as f32..=300.0,
1639                    0.0..=1.0,
1640                );
1641
1642                let line_color = color_from_strength(ui, line_strength);
1643
1644                let mut p0 = pos_in_gui;
1645                let mut p1 = pos_in_gui;
1646                p0[1 - iaxis] = transform.frame().min[1 - iaxis];
1647                p1[1 - iaxis] = transform.frame().max[1 - iaxis];
1648
1649                if let Some(clamp_range) = clamp_range {
1650                    match axis {
1651                        Axis::X => {
1652                            p0.y = transform.position_from_point_y(clamp_range.min[1]);
1653                            p1.y = transform.position_from_point_y(clamp_range.max[1]);
1654                        }
1655                        Axis::Y => {
1656                            p0.x = transform.position_from_point_x(clamp_range.min[0]);
1657                            p1.x = transform.position_from_point_x(clamp_range.max[0]);
1658                        }
1659                    }
1660                }
1661
1662                if self.sharp_grid_lines {
1663                    // Round to avoid aliasing
1664                    p0 = ui.painter().round_pos_to_pixels(p0);
1665                    p1 = ui.painter().round_pos_to_pixels(p1);
1666                }
1667
1668                shapes.push((
1669                    Shape::line_segment([p0, p1], Stroke::new(1.0, line_color)),
1670                    line_strength,
1671                ));
1672            }
1673        }
1674    }
1675
1676    fn hover(&self, ui: &Ui, pointer: Pos2, shapes: &mut Vec<Shape>) -> Vec<Cursor> {
1677        let Self {
1678            transform,
1679            show_x,
1680            show_y,
1681            label_formatter,
1682            items,
1683            ..
1684        } = self;
1685
1686        if !show_x && !show_y {
1687            return Vec::new();
1688        }
1689
1690        let interact_radius_sq: f32 = (16.0f32).powi(2);
1691
1692        let candidates = items.iter().filter_map(|item| {
1693            let item = &**item;
1694            let closest = item.find_closest(pointer, transform);
1695
1696            Some(item).zip(closest)
1697        });
1698
1699        let closest = candidates
1700            .min_by_key(|(_, elem)| elem.dist_sq.ord())
1701            .filter(|(_, elem)| elem.dist_sq <= interact_radius_sq);
1702
1703        let mut cursors = Vec::new();
1704
1705        let plot = items::PlotConfig {
1706            ui,
1707            transform,
1708            show_x: *show_x,
1709            show_y: *show_y,
1710        };
1711
1712        if let Some((item, elem)) = closest {
1713            item.on_hover(elem, shapes, &mut cursors, &plot, label_formatter);
1714        } else {
1715            let value = transform.value_from_position(pointer);
1716            items::rulers_at_value(
1717                pointer,
1718                value,
1719                "",
1720                &plot,
1721                shapes,
1722                &mut cursors,
1723                label_formatter,
1724            );
1725        }
1726
1727        cursors
1728    }
1729}
1730
1731/// Returns next bigger power in given base
1732/// e.g.
1733/// ```ignore
1734/// use egui_plot::next_power;
1735/// assert_eq!(next_power(0.01, 10.0), 0.01);
1736/// assert_eq!(next_power(0.02, 10.0), 0.1);
1737/// assert_eq!(next_power(0.2,  10.0), 1);
1738/// ```
1739fn next_power(value: f64, base: f64) -> f64 {
1740    assert_ne!(value, 0.0); // can be negative (typical for Y axis)
1741    base.powi(value.abs().log(base).ceil() as i32)
1742}
1743
1744/// Fill in all values between [min, max] which are a multiple of `step_size`
1745fn generate_marks(step_sizes: [f64; 3], bounds: (f64, f64)) -> Vec<GridMark> {
1746    let mut steps = vec![];
1747    fill_marks_between(&mut steps, step_sizes[0], bounds);
1748    fill_marks_between(&mut steps, step_sizes[1], bounds);
1749    fill_marks_between(&mut steps, step_sizes[2], bounds);
1750    steps
1751}
1752
1753/// Fill in all values between [min, max] which are a multiple of `step_size`
1754fn fill_marks_between(out: &mut Vec<GridMark>, step_size: f64, (min, max): (f64, f64)) {
1755    assert!(max > min);
1756    let first = (min / step_size).ceil() as i64;
1757    let last = (max / step_size).ceil() as i64;
1758
1759    let marks_iter = (first..last).map(|i| {
1760        let value = (i as f64) * step_size;
1761        GridMark { value, step_size }
1762    });
1763    out.extend(marks_iter);
1764}
1765
1766/// Helper for formatting a number so that we always show at least a few decimals,
1767/// unless it is an integer, in which case we never show any decimals.
1768pub fn format_number(number: f64, num_decimals: usize) -> String {
1769    let is_integral = number as i64 as f64 == number;
1770    if is_integral {
1771        // perfect integer - show it as such:
1772        format!("{number:.0}")
1773    } else {
1774        // make sure we tell the user it is not an integer by always showing a decimal or two:
1775        format!("{:.*}", num_decimals.at_least(1), number)
1776    }
1777}
1778
1779/// Determine a color from a 0-1 strength value.
1780pub fn color_from_strength(ui: &Ui, strength: f32) -> Color32 {
1781    let bg = ui.visuals().extreme_bg_color;
1782    let fg = ui.visuals().widgets.open.fg_stroke.color;
1783    let mix = 0.5 * strength.sqrt();
1784    Color32::from_rgb(
1785        lerp((bg.r() as f32)..=(fg.r() as f32), mix) as u8,
1786        lerp((bg.g() as f32)..=(fg.g() as f32), mix) as u8,
1787        lerp((bg.b() as f32)..=(fg.b() as f32), mix) as u8,
1788    )
1789}