1#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
9mod axis;
12mod items;
13mod legend;
14mod memory;
15mod plot_ui;
16mod transform;
17
18use std::{cmp::Ordering, ops::RangeInclusive, sync::Arc};
19
20use ahash::HashMap;
21use egui::{
22 epaint, remap_clamp, vec2, Align2, Color32, CursorIcon, Id, Layout, NumExt, PointerButton,
23 Pos2, Rangef, Rect, Response, Sense, Shape, Stroke, TextStyle, Ui, Vec2, Vec2b, WidgetText,
24};
25use emath::Float as _;
26
27pub use crate::{
28 axis::{Axis, AxisHints, HPlacement, Placement, VPlacement},
29 items::{
30 Arrows, Bar, BarChart, BoxElem, BoxPlot, BoxSpread, ClosestElem, HLine, Line, LineStyle,
31 MarkerShape, Orientation, PlotConfig, PlotGeometry, PlotImage, PlotItem, PlotPoint,
32 PlotPoints, Points, Polygon, Text, VLine,
33 },
34 legend::{ColorConflictHandling, Corner, Legend},
35 memory::PlotMemory,
36 plot_ui::PlotUi,
37 transform::{PlotBounds, PlotTransform},
38};
39
40use axis::AxisWidget;
41use items::{horizontal_line, rulers_color, vertical_line};
42use legend::LegendWidget;
43
44type LabelFormatterFn<'a> = dyn Fn(&str, &PlotPoint) -> String + 'a;
45pub type LabelFormatter<'a> = Option<Box<LabelFormatterFn<'a>>>;
46
47type GridSpacerFn<'a> = dyn Fn(GridInput) -> Vec<GridMark> + 'a;
48type GridSpacer<'a> = Box<GridSpacerFn<'a>>;
49
50type CoordinatesFormatterFn<'a> = dyn Fn(&PlotPoint, &PlotBounds) -> String + 'a;
51
52pub struct CoordinatesFormatter<'a> {
54 function: Box<CoordinatesFormatterFn<'a>>,
55}
56
57impl<'a> CoordinatesFormatter<'a> {
58 pub fn new(function: impl Fn(&PlotPoint, &PlotBounds) -> String + 'a) -> Self {
60 Self {
61 function: Box::new(function),
62 }
63 }
64
65 pub fn with_decimals(num_decimals: usize) -> Self {
67 Self {
68 function: Box::new(move |value, _| {
69 format!("x: {:.d$}\ny: {:.d$}", value.x, value.y, d = num_decimals)
70 }),
71 }
72 }
73
74 fn format(&self, value: &PlotPoint, bounds: &PlotBounds) -> String {
75 (self.function)(value, bounds)
76 }
77}
78
79impl Default for CoordinatesFormatter<'_> {
80 fn default() -> Self {
81 Self::with_decimals(3)
82 }
83}
84
85#[derive(Copy, Clone, PartialEq)]
89pub enum Cursor {
90 Horizontal { y: f64 },
91 Vertical { x: f64 },
92}
93
94#[derive(PartialEq, Clone)]
96struct PlotFrameCursors {
97 id: Id,
98 cursors: Vec<Cursor>,
99}
100
101#[derive(Default, Clone)]
102struct CursorLinkGroups(HashMap<Id, Vec<PlotFrameCursors>>);
103
104#[derive(Clone)]
105struct LinkedBounds {
106 bounds: PlotBounds,
107 auto_bounds: Vec2b,
108}
109
110#[derive(Default, Clone)]
111struct BoundsLinkGroups(HashMap<Id, LinkedBounds>);
112
113pub struct PlotResponse<R> {
117 pub inner: R,
119
120 pub response: Response,
122
123 pub transform: PlotTransform,
125
126 pub hovered_plot_item: Option<Id>,
132}
133
134pub struct Plot<'a> {
153 id_source: Id,
154 id: Option<Id>,
155
156 center_axis: Vec2b,
157 allow_zoom: Vec2b,
158 allow_drag: Vec2b,
159 allow_scroll: Vec2b,
160 allow_double_click_reset: bool,
161 allow_boxed_zoom: bool,
162 default_auto_bounds: Vec2b,
163 min_auto_bounds: PlotBounds,
164 margin_fraction: Vec2,
165 boxed_zoom_pointer_button: PointerButton,
166 linked_axes: Option<(Id, Vec2b)>,
167 linked_cursors: Option<(Id, Vec2b)>,
168
169 min_size: Vec2,
170 width: Option<f32>,
171 height: Option<f32>,
172 data_aspect: Option<f32>,
173 view_aspect: Option<f32>,
174
175 reset: bool,
176
177 show_x: bool,
178 show_y: bool,
179 label_formatter: LabelFormatter<'a>,
180 coordinates_formatter: Option<(Corner, CoordinatesFormatter<'a>)>,
181 x_axes: Vec<AxisHints<'a>>, y_axes: Vec<AxisHints<'a>>, legend_config: Option<Legend>,
184 cursor_color: Option<Color32>,
185 show_background: bool,
186 show_axes: Vec2b,
187
188 show_grid: Vec2b,
189 grid_spacing: Rangef,
190 grid_spacers: [GridSpacer<'a>; 2],
191 clamp_grid: bool,
192
193 sense: Sense,
194}
195
196impl<'a> Plot<'a> {
197 pub fn new(id_source: impl std::hash::Hash) -> Self {
199 Self {
200 id_source: Id::new(id_source),
201 id: None,
202
203 center_axis: false.into(),
204 allow_zoom: true.into(),
205 allow_drag: true.into(),
206 allow_scroll: true.into(),
207 allow_double_click_reset: true,
208 allow_boxed_zoom: true,
209 default_auto_bounds: true.into(),
210 min_auto_bounds: PlotBounds::NOTHING,
211 margin_fraction: Vec2::splat(0.05),
212 boxed_zoom_pointer_button: PointerButton::Secondary,
213 linked_axes: None,
214 linked_cursors: None,
215
216 min_size: Vec2::splat(64.0),
217 width: None,
218 height: None,
219 data_aspect: None,
220 view_aspect: None,
221
222 reset: false,
223
224 show_x: true,
225 show_y: true,
226 label_formatter: None,
227 coordinates_formatter: None,
228 x_axes: vec![AxisHints::new(Axis::X)],
229 y_axes: vec![AxisHints::new(Axis::Y)],
230 legend_config: None,
231 cursor_color: None,
232 show_background: true,
233 show_axes: true.into(),
234
235 show_grid: true.into(),
236 grid_spacing: Rangef::new(8.0, 300.0),
237 grid_spacers: [log_grid_spacer(10), log_grid_spacer(10)],
238 clamp_grid: false,
239
240 sense: egui::Sense::click_and_drag(),
241 }
242 }
243
244 #[inline]
250 pub fn id(mut self, id: Id) -> Self {
251 self.id = Some(id);
252 self
253 }
254
255 #[inline]
260 pub fn data_aspect(mut self, data_aspect: f32) -> Self {
261 self.data_aspect = Some(data_aspect);
262 self
263 }
264
265 #[inline]
268 pub fn view_aspect(mut self, view_aspect: f32) -> Self {
269 self.view_aspect = Some(view_aspect);
270 self
271 }
272
273 #[inline]
276 pub fn width(mut self, width: f32) -> Self {
277 self.min_size.x = width;
278 self.width = Some(width);
279 self
280 }
281
282 #[inline]
285 pub fn height(mut self, height: f32) -> Self {
286 self.min_size.y = height;
287 self.height = Some(height);
288 self
289 }
290
291 #[inline]
293 pub fn min_size(mut self, min_size: Vec2) -> Self {
294 self.min_size = min_size;
295 self
296 }
297
298 #[inline]
300 pub fn show_x(mut self, show_x: bool) -> Self {
301 self.show_x = show_x;
302 self
303 }
304
305 #[inline]
307 pub fn show_y(mut self, show_y: bool) -> Self {
308 self.show_y = show_y;
309 self
310 }
311
312 #[inline]
314 pub fn center_x_axis(mut self, on: bool) -> Self {
315 self.center_axis.x = on;
316 self
317 }
318
319 #[inline]
321 pub fn center_y_axis(mut self, on: bool) -> Self {
322 self.center_axis.y = on;
323 self
324 }
325
326 #[inline]
330 pub fn allow_zoom<T>(mut self, on: T) -> Self
331 where
332 T: Into<Vec2b>,
333 {
334 self.allow_zoom = on.into();
335 self
336 }
337
338 #[inline]
340 pub fn allow_scroll<T>(mut self, on: T) -> Self
341 where
342 T: Into<Vec2b>,
343 {
344 self.allow_scroll = on.into();
345 self
346 }
347
348 #[inline]
351 pub fn allow_double_click_reset(mut self, on: bool) -> Self {
352 self.allow_double_click_reset = on;
353 self
354 }
355
356 #[inline]
360 pub fn set_margin_fraction(mut self, margin_fraction: Vec2) -> Self {
361 self.margin_fraction = margin_fraction;
362 self
363 }
364
365 #[inline]
369 pub fn allow_boxed_zoom(mut self, on: bool) -> Self {
370 self.allow_boxed_zoom = on;
371 self
372 }
373
374 #[inline]
376 pub fn boxed_zoom_pointer_button(mut self, boxed_zoom_pointer_button: PointerButton) -> Self {
377 self.boxed_zoom_pointer_button = boxed_zoom_pointer_button;
378 self
379 }
380
381 #[inline]
383 pub fn allow_drag<T>(mut self, on: T) -> Self
384 where
385 T: Into<Vec2b>,
386 {
387 self.allow_drag = on.into();
388 self
389 }
390
391 pub fn label_formatter(
413 mut self,
414 label_formatter: impl Fn(&str, &PlotPoint) -> String + 'a,
415 ) -> Self {
416 self.label_formatter = Some(Box::new(label_formatter));
417 self
418 }
419
420 pub fn coordinates_formatter(
422 mut self,
423 position: Corner,
424 formatter: CoordinatesFormatter<'a>,
425 ) -> Self {
426 self.coordinates_formatter = Some((position, formatter));
427 self
428 }
429
430 #[inline]
461 pub fn x_grid_spacer(mut self, spacer: impl Fn(GridInput) -> Vec<GridMark> + 'a) -> Self {
462 self.grid_spacers[0] = Box::new(spacer);
463 self
464 }
465
466 #[inline]
470 pub fn y_grid_spacer(mut self, spacer: impl Fn(GridInput) -> Vec<GridMark> + 'a) -> Self {
471 self.grid_spacers[1] = Box::new(spacer);
472 self
473 }
474
475 #[inline]
481 pub fn grid_spacing(mut self, grid_spacing: impl Into<Rangef>) -> Self {
482 self.grid_spacing = grid_spacing.into();
483 self
484 }
485
486 #[inline]
490 pub fn clamp_grid(mut self, clamp_grid: bool) -> Self {
491 self.clamp_grid = clamp_grid;
492 self
493 }
494
495 #[inline]
499 pub fn sense(mut self, sense: Sense) -> Self {
500 self.sense = sense;
501 self
502 }
503
504 #[inline]
509 pub fn default_x_bounds(mut self, min: f64, max: f64) -> Self {
510 debug_assert!(
511 min < max,
512 "`min` must be less than `max` in `default_x_bounds`"
513 );
514 self.default_auto_bounds.x = false;
515 self.min_auto_bounds.min[0] = min;
516 self.min_auto_bounds.max[0] = max;
517 self
518 }
519
520 #[inline]
525 pub fn default_y_bounds(mut self, min: f64, max: f64) -> Self {
526 debug_assert!(
527 min < max,
528 "`min` must be less than `max` in `default_y_bounds`"
529 );
530 self.default_auto_bounds.y = false;
531 self.min_auto_bounds.min[1] = min;
532 self.min_auto_bounds.max[1] = max;
533 self
534 }
535
536 #[inline]
539 pub fn include_x(mut self, x: impl Into<f64>) -> Self {
540 self.min_auto_bounds.extend_with_x(x.into());
541 self
542 }
543
544 #[inline]
547 pub fn include_y(mut self, y: impl Into<f64>) -> Self {
548 self.min_auto_bounds.extend_with_y(y.into());
549 self
550 }
551
552 #[inline]
556 pub fn auto_bounds(mut self, auto_bounds: impl Into<Vec2b>) -> Self {
557 self.default_auto_bounds = auto_bounds.into();
558 self
559 }
560
561 #[deprecated = "Use `auto_bounds` instead"]
563 #[inline]
564 pub fn auto_bounds_x(mut self) -> Self {
565 self.default_auto_bounds.x = true;
566 self
567 }
568
569 #[deprecated = "Use `auto_bounds` instead"]
571 #[inline]
572 pub fn auto_bounds_y(mut self) -> Self {
573 self.default_auto_bounds.y = true;
574 self
575 }
576
577 #[inline]
579 pub fn legend(mut self, legend: Legend) -> Self {
580 self.legend_config = Some(legend);
581 self
582 }
583
584 #[inline]
589 pub fn show_background(mut self, show: bool) -> Self {
590 self.show_background = show;
591 self
592 }
593
594 #[inline]
598 pub fn show_axes(mut self, show: impl Into<Vec2b>) -> Self {
599 self.show_axes = show.into();
600 self
601 }
602
603 #[inline]
607 pub fn show_grid(mut self, show: impl Into<Vec2b>) -> Self {
608 self.show_grid = show.into();
609 self
610 }
611
612 #[inline]
615 pub fn link_axis(mut self, group_id: impl Into<Id>, link: impl Into<Vec2b>) -> Self {
616 self.linked_axes = Some((group_id.into(), link.into()));
617 self
618 }
619
620 #[inline]
623 pub fn link_cursor(mut self, group_id: impl Into<Id>, link: impl Into<Vec2b>) -> Self {
624 self.linked_cursors = Some((group_id.into(), link.into()));
625 self
626 }
627
628 #[inline]
631 #[deprecated = "This no longer has any effect and is always enabled."]
632 pub fn sharp_grid_lines(self, _enabled: bool) -> Self {
633 self
634 }
635
636 #[inline]
638 pub fn reset(mut self) -> Self {
639 self.reset = true;
640 self
641 }
642
643 #[inline]
647 pub fn x_axis_label(mut self, label: impl Into<WidgetText>) -> Self {
648 if let Some(main) = self.x_axes.first_mut() {
649 main.label = label.into();
650 }
651 self
652 }
653
654 #[inline]
658 pub fn y_axis_label(mut self, label: impl Into<WidgetText>) -> Self {
659 if let Some(main) = self.y_axes.first_mut() {
660 main.label = label.into();
661 }
662 self
663 }
664
665 #[inline]
667 pub fn x_axis_position(mut self, placement: axis::VPlacement) -> Self {
668 if let Some(main) = self.x_axes.first_mut() {
669 main.placement = placement.into();
670 }
671 self
672 }
673
674 #[inline]
676 pub fn y_axis_position(mut self, placement: axis::HPlacement) -> Self {
677 if let Some(main) = self.y_axes.first_mut() {
678 main.placement = placement.into();
679 }
680 self
681 }
682
683 pub fn x_axis_formatter(
689 mut self,
690 fmt: impl Fn(GridMark, &RangeInclusive<f64>) -> String + 'a,
691 ) -> Self {
692 if let Some(main) = self.x_axes.first_mut() {
693 main.formatter = Arc::new(fmt);
694 }
695 self
696 }
697
698 pub fn y_axis_formatter(
704 mut self,
705 fmt: impl Fn(GridMark, &RangeInclusive<f64>) -> String + 'a,
706 ) -> Self {
707 if let Some(main) = self.y_axes.first_mut() {
708 main.formatter = Arc::new(fmt);
709 }
710 self
711 }
712
713 #[inline]
717 pub fn y_axis_min_width(mut self, min_width: f32) -> Self {
718 if let Some(main) = self.y_axes.first_mut() {
719 main.min_thickness = min_width;
720 }
721 self
722 }
723
724 #[inline]
726 #[deprecated = "Use `y_axis_min_width` instead"]
727 pub fn y_axis_width(self, digits: usize) -> Self {
728 self.y_axis_min_width(12.0 * digits as f32)
729 }
730
731 #[inline]
735 pub fn custom_x_axes(mut self, hints: Vec<AxisHints<'a>>) -> Self {
736 self.x_axes = hints;
737 self
738 }
739
740 #[inline]
744 pub fn custom_y_axes(mut self, hints: Vec<AxisHints<'a>>) -> Self {
745 self.y_axes = hints;
746 self
747 }
748
749 #[inline]
753 pub fn cursor_color(mut self, color: Color32) -> Self {
754 self.cursor_color = Some(color);
755 self
756 }
757
758 pub fn show<'b, R>(
760 self,
761 ui: &mut Ui,
762 build_fn: impl FnOnce(&mut PlotUi<'b>) -> R + 'a,
763 ) -> PlotResponse<R> {
764 self.show_dyn(ui, Box::new(build_fn))
765 }
766
767 #[allow(clippy::too_many_lines)] #[allow(clippy::type_complexity)] fn show_dyn<'b, R>(
770 self,
771 ui: &mut Ui,
772 build_fn: Box<dyn FnOnce(&mut PlotUi<'b>) -> R + 'a>,
773 ) -> PlotResponse<R> {
774 let Self {
775 id_source,
776 id,
777 center_axis,
778 allow_zoom,
779 allow_drag,
780 allow_scroll,
781 allow_double_click_reset,
782 allow_boxed_zoom,
783 boxed_zoom_pointer_button,
784 default_auto_bounds,
785 min_auto_bounds,
786 margin_fraction,
787 width,
788 height,
789 mut min_size,
790 data_aspect,
791 view_aspect,
792 mut show_x,
793 mut show_y,
794 label_formatter,
795 coordinates_formatter,
796 x_axes,
797 y_axes,
798 legend_config,
799 cursor_color,
800 reset,
801 show_background,
802 show_axes,
803 show_grid,
804 grid_spacing,
805 linked_axes,
806 linked_cursors,
807
808 clamp_grid,
809 grid_spacers,
810 sense,
811 } = self;
812
813 let allow_zoom = allow_zoom.and(ui.is_enabled());
815 let allow_drag = allow_drag.and(ui.is_enabled());
816 let allow_scroll = allow_scroll.and(ui.is_enabled());
817
818 let pos = ui.available_rect_before_wrap().min;
820 min_size.x = min_size.x.at_least(1.0);
822 min_size.y = min_size.y.at_least(1.0);
823
824 let size = {
826 let width = width
827 .unwrap_or_else(|| {
828 if let (Some(height), Some(aspect)) = (height, view_aspect) {
829 height * aspect
830 } else {
831 ui.available_size_before_wrap().x
832 }
833 })
834 .at_least(min_size.x);
835
836 let height = height
837 .unwrap_or_else(|| {
838 if let Some(aspect) = view_aspect {
839 width / aspect
840 } else {
841 ui.available_size_before_wrap().y
842 }
843 })
844 .at_least(min_size.y);
845 vec2(width, height)
846 };
847
848 let complete_rect = Rect {
850 min: pos,
851 max: pos + size,
852 };
853
854 let plot_id = id.unwrap_or_else(|| ui.make_persistent_id(id_source));
855
856 let ([x_axis_widgets, y_axis_widgets], plot_rect) = axis_widgets(
857 PlotMemory::load(ui.ctx(), plot_id).as_ref(), show_axes,
859 complete_rect,
860 [&x_axes, &y_axes],
861 );
862
863 let response = ui.allocate_rect(plot_rect, sense);
865
866 ui.ctx().check_for_id_clash(plot_id, plot_rect, "Plot");
868
869 let mut mem = if reset {
870 if let Some((name, _)) = linked_axes.as_ref() {
871 ui.data_mut(|data| {
872 let link_groups: &mut BoundsLinkGroups = data.get_temp_mut_or_default(Id::NULL);
873 link_groups.0.remove(name);
874 });
875 };
876 None
877 } else {
878 PlotMemory::load(ui.ctx(), plot_id)
879 }
880 .unwrap_or_else(|| PlotMemory {
881 auto_bounds: default_auto_bounds,
882 hovered_legend_item: None,
883 hidden_items: Default::default(),
884 transform: PlotTransform::new(plot_rect, min_auto_bounds, center_axis),
885 last_click_pos_for_zoom: None,
886 x_axis_thickness: Default::default(),
887 y_axis_thickness: Default::default(),
888 });
889
890 let last_plot_transform = mem.transform;
891
892 let mut plot_ui = PlotUi {
894 ctx: ui.ctx().clone(),
895 items: Vec::new(),
896 next_auto_color_idx: 0,
897 last_plot_transform,
898 last_auto_bounds: mem.auto_bounds,
899 response,
900 bounds_modifications: Vec::new(),
901 };
902 let inner = build_fn(&mut plot_ui);
903 let PlotUi {
904 mut items,
905 mut response,
906 last_plot_transform,
907 bounds_modifications,
908 ..
909 } = plot_ui;
910
911 if show_background {
913 ui.painter()
914 .with_clip_rect(plot_rect)
915 .add(epaint::RectShape::new(
916 plot_rect,
917 2,
918 ui.visuals().extreme_bg_color,
919 ui.visuals().widgets.noninteractive.bg_stroke,
920 egui::StrokeKind::Inside,
921 ));
922 }
923
924 let legend = legend_config
926 .and_then(|config| LegendWidget::try_new(plot_rect, config, &items, &mem.hidden_items));
927 if mem.hovered_legend_item.is_some() {
929 show_x = false;
930 show_y = false;
931 }
932 items.retain(|item| !mem.hidden_items.contains(&item.id()));
934 if let Some(item_id) = &mem.hovered_legend_item {
936 items
937 .iter_mut()
938 .filter(|entry| &entry.id() == item_id)
939 .for_each(|entry| entry.highlight());
940 }
941 items.sort_by_key(|item| item.highlighted());
943
944 let mut bounds = *last_plot_transform.bounds();
946
947 let draw_cursors: Vec<Cursor> = if let Some((id, _)) = linked_cursors.as_ref() {
949 ui.data_mut(|data| {
950 let frames: &mut CursorLinkGroups = data.get_temp_mut_or_default(Id::NULL);
951 let cursors = frames.0.entry(*id).or_default();
952
953 let index = cursors
955 .iter()
956 .enumerate()
957 .find(|(_, frame)| frame.id == plot_id)
958 .map(|(i, _)| i);
959
960 index.map(|index| cursors.drain(0..=index));
963
964 cursors
967 .iter()
968 .flat_map(|frame| frame.cursors.iter().copied())
969 .collect()
970 })
971 } else {
972 Vec::new()
973 };
974
975 if let Some((id, axes)) = linked_axes.as_ref() {
977 ui.data_mut(|data| {
978 let link_groups: &mut BoundsLinkGroups = data.get_temp_mut_or_default(Id::NULL);
979 if let Some(linked_bounds) = link_groups.0.get(id) {
980 if axes.x {
981 bounds.set_x(&linked_bounds.bounds);
982 mem.auto_bounds.x = linked_bounds.auto_bounds.x;
983 }
984 if axes.y {
985 bounds.set_y(&linked_bounds.bounds);
986 mem.auto_bounds.y = linked_bounds.auto_bounds.y;
987 }
988 };
989 });
990 };
991
992 if allow_double_click_reset && response.double_clicked() {
994 mem.auto_bounds = true.into();
995 }
996
997 let any_dynamic_modifications = !bounds_modifications.is_empty();
998 for modification in bounds_modifications {
1000 match modification {
1001 BoundsModification::Set(new_bounds) => {
1002 bounds = new_bounds;
1003 mem.auto_bounds = false.into();
1004 }
1005 BoundsModification::Translate(delta) => {
1006 let delta = (delta.x as f64, delta.y as f64);
1007 bounds.translate(delta);
1008 mem.auto_bounds = false.into();
1009 }
1010 BoundsModification::AutoBounds(new_auto_bounds) => {
1011 mem.auto_bounds = new_auto_bounds;
1012 }
1013 BoundsModification::Zoom(zoom_factor, center) => {
1014 bounds.zoom(zoom_factor, center);
1015 mem.auto_bounds = false.into();
1016 }
1017 }
1018 }
1019
1020 if (!default_auto_bounds.x && !any_dynamic_modifications) || mem.auto_bounds.x {
1022 bounds.set_x(&min_auto_bounds);
1023 }
1024 if (!default_auto_bounds.y && !any_dynamic_modifications) || mem.auto_bounds.y {
1025 bounds.set_y(&min_auto_bounds);
1026 }
1027
1028 let auto_x = mem.auto_bounds.x && (!min_auto_bounds.is_valid_x() || default_auto_bounds.x);
1029 let auto_y = mem.auto_bounds.y && (!min_auto_bounds.is_valid_y() || default_auto_bounds.y);
1030
1031 if auto_x || auto_y {
1033 for item in &items {
1034 let item_bounds = item.bounds();
1035 if auto_x {
1036 bounds.merge_x(&item_bounds);
1037 }
1038 if auto_y {
1039 bounds.merge_y(&item_bounds);
1040 }
1041 }
1042
1043 if auto_x {
1044 bounds.add_relative_margin_x(margin_fraction);
1045 }
1046
1047 if auto_y {
1048 bounds.add_relative_margin_y(margin_fraction);
1049 }
1050 }
1051
1052 mem.transform = PlotTransform::new(plot_rect, bounds, center_axis);
1053
1054 if let Some(data_aspect) = data_aspect {
1056 if let Some((_, linked_axes)) = &linked_axes {
1057 let change_x = linked_axes.y && !linked_axes.x;
1058 mem.transform.set_aspect_by_changing_axis(
1059 data_aspect as f64,
1060 if change_x { Axis::X } else { Axis::Y },
1061 );
1062 } else if default_auto_bounds.any() {
1063 mem.transform.set_aspect_by_expanding(data_aspect as f64);
1064 } else {
1065 mem.transform
1066 .set_aspect_by_changing_axis(data_aspect as f64, Axis::Y);
1067 }
1068 }
1069
1070 if allow_drag.any() && response.dragged_by(PointerButton::Primary) {
1072 response = response.on_hover_cursor(CursorIcon::Grabbing);
1073 let mut delta = -response.drag_delta();
1074 if !allow_drag.x {
1075 delta.x = 0.0;
1076 }
1077 if !allow_drag.y {
1078 delta.y = 0.0;
1079 }
1080 mem.transform
1081 .translate_bounds((delta.x as f64, delta.y as f64));
1082 mem.auto_bounds = mem.auto_bounds.and(!allow_drag);
1083 }
1084
1085 let mut boxed_zoom_rect = None;
1087 if allow_boxed_zoom {
1088 if response.drag_started() && response.dragged_by(boxed_zoom_pointer_button) {
1090 mem.last_click_pos_for_zoom = response.hover_pos();
1092 }
1093 let box_start_pos = mem.last_click_pos_for_zoom;
1094 let box_end_pos = response.hover_pos();
1095 if let (Some(box_start_pos), Some(box_end_pos)) = (box_start_pos, box_end_pos) {
1096 if response.dragged_by(boxed_zoom_pointer_button) {
1098 response = response.on_hover_cursor(CursorIcon::ZoomIn);
1099 let rect = epaint::Rect::from_two_pos(box_start_pos, box_end_pos);
1100 boxed_zoom_rect = Some((
1101 epaint::RectShape::stroke(
1102 rect,
1103 0.0,
1104 epaint::Stroke::new(4., Color32::DARK_BLUE),
1105 egui::StrokeKind::Middle,
1106 ), epaint::RectShape::stroke(
1108 rect,
1109 0.0,
1110 epaint::Stroke::new(2., Color32::WHITE),
1111 egui::StrokeKind::Middle,
1112 ), ));
1114 }
1115 if response.drag_stopped() {
1117 let box_start_pos = mem.transform.value_from_position(box_start_pos);
1118 let box_end_pos = mem.transform.value_from_position(box_end_pos);
1119 let new_bounds = PlotBounds {
1120 min: [
1121 box_start_pos.x.min(box_end_pos.x),
1122 box_start_pos.y.min(box_end_pos.y),
1123 ],
1124 max: [
1125 box_start_pos.x.max(box_end_pos.x),
1126 box_start_pos.y.max(box_end_pos.y),
1127 ],
1128 };
1129 if new_bounds.is_valid() {
1130 mem.transform.set_bounds(new_bounds);
1131 mem.auto_bounds = false.into();
1132 }
1133 mem.last_click_pos_for_zoom = None;
1135 }
1136 }
1137 }
1138
1139 if let (true, Some(hover_pos)) = (
1143 response.contains_pointer(),
1144 ui.input(|i| i.pointer.hover_pos()),
1145 ) {
1146 if allow_zoom.any() {
1147 let mut zoom_factor = if data_aspect.is_some() {
1148 Vec2::splat(ui.input(|i| i.zoom_delta()))
1149 } else {
1150 ui.input(|i| i.zoom_delta_2d())
1151 };
1152 if !allow_zoom.x {
1153 zoom_factor.x = 1.0;
1154 }
1155 if !allow_zoom.y {
1156 zoom_factor.y = 1.0;
1157 }
1158 if zoom_factor != Vec2::splat(1.0) {
1159 mem.transform.zoom(zoom_factor, hover_pos);
1160 mem.auto_bounds = mem.auto_bounds.and(!allow_zoom);
1161 }
1162 }
1163 if allow_scroll.any() {
1164 let mut scroll_delta = ui.input(|i| i.smooth_scroll_delta);
1165 if !allow_scroll.x {
1166 scroll_delta.x = 0.0;
1167 }
1168 if !allow_scroll.y {
1169 scroll_delta.y = 0.0;
1170 }
1171 if scroll_delta != Vec2::ZERO {
1172 mem.transform
1173 .translate_bounds((-scroll_delta.x as f64, -scroll_delta.y as f64));
1174 mem.auto_bounds = false.into();
1175 }
1176 }
1177 }
1178
1179 let bounds = mem.transform.bounds();
1183 let x_axis_range = bounds.range_x();
1184 let x_steps = Arc::new({
1185 let input = GridInput {
1186 bounds: (bounds.min[0], bounds.max[0]),
1187 base_step_size: mem.transform.dvalue_dpos()[0].abs() * grid_spacing.min as f64,
1188 };
1189 (grid_spacers[0])(input)
1190 });
1191 let y_axis_range = bounds.range_y();
1192 let y_steps = Arc::new({
1193 let input = GridInput {
1194 bounds: (bounds.min[1], bounds.max[1]),
1195 base_step_size: mem.transform.dvalue_dpos()[1].abs() * grid_spacing.min as f64,
1196 };
1197 (grid_spacers[1])(input)
1198 });
1199 for (i, mut widget) in x_axis_widgets.into_iter().enumerate() {
1200 widget.range = x_axis_range.clone();
1201 widget.transform = Some(mem.transform);
1202 widget.steps = x_steps.clone();
1203 let (_response, thickness) = widget.ui(ui, Axis::X);
1204 mem.x_axis_thickness.insert(i, thickness);
1205 }
1206 for (i, mut widget) in y_axis_widgets.into_iter().enumerate() {
1207 widget.range = y_axis_range.clone();
1208 widget.transform = Some(mem.transform);
1209 widget.steps = y_steps.clone();
1210 let (_response, thickness) = widget.ui(ui, Axis::Y);
1211 mem.y_axis_thickness.insert(i, thickness);
1212 }
1213
1214 for item in &mut items {
1216 item.initialize(mem.transform.bounds().range_x());
1217 }
1218
1219 let prepared = PreparedPlot {
1220 items,
1221 show_x,
1222 show_y,
1223 label_formatter,
1224 coordinates_formatter,
1225 show_grid,
1226 grid_spacing,
1227 transform: mem.transform,
1228 draw_cursor_x: linked_cursors.as_ref().map_or(false, |group| group.1.x),
1229 draw_cursor_y: linked_cursors.as_ref().map_or(false, |group| group.1.y),
1230 draw_cursors,
1231 cursor_color,
1232 grid_spacers,
1233 clamp_grid,
1234 };
1235
1236 let (plot_cursors, mut hovered_plot_item) = prepared.ui(ui, &response);
1237
1238 if let Some(boxed_zoom_rect) = boxed_zoom_rect {
1239 ui.painter()
1240 .with_clip_rect(plot_rect)
1241 .add(boxed_zoom_rect.0);
1242 ui.painter()
1243 .with_clip_rect(plot_rect)
1244 .add(boxed_zoom_rect.1);
1245 }
1246
1247 if let Some(mut legend) = legend {
1248 ui.add(&mut legend);
1249 mem.hidden_items = legend.hidden_items();
1250 mem.hovered_legend_item = legend.hovered_item();
1251
1252 if let Some(item_id) = &mem.hovered_legend_item {
1253 hovered_plot_item.get_or_insert(*item_id);
1254 }
1255 }
1256
1257 if let Some((id, _)) = linked_cursors.as_ref() {
1258 ui.data_mut(|data| {
1260 let frames: &mut CursorLinkGroups = data.get_temp_mut_or_default(Id::NULL);
1261 let cursors = frames.0.entry(*id).or_default();
1262 cursors.push(PlotFrameCursors {
1263 id: plot_id,
1264 cursors: plot_cursors,
1265 });
1266 });
1267 }
1268
1269 if let Some((id, _)) = linked_axes.as_ref() {
1270 ui.data_mut(|data| {
1272 let link_groups: &mut BoundsLinkGroups = data.get_temp_mut_or_default(Id::NULL);
1273 link_groups.0.insert(
1274 *id,
1275 LinkedBounds {
1276 bounds: *mem.transform.bounds(),
1277 auto_bounds: mem.auto_bounds,
1278 },
1279 );
1280 });
1281 }
1282
1283 let transform = mem.transform;
1284 mem.store(ui.ctx(), plot_id);
1285
1286 let response = if show_x || show_y {
1287 response.on_hover_cursor(CursorIcon::Crosshair)
1288 } else {
1289 response
1290 };
1291
1292 ui.advance_cursor_after_rect(complete_rect);
1293
1294 PlotResponse {
1295 inner,
1296 response,
1297 transform,
1298 hovered_plot_item,
1299 }
1300 }
1301}
1302
1303fn axis_widgets<'a>(
1305 mem: Option<&PlotMemory>,
1306 show_axes: impl Into<Vec2b>,
1307 complete_rect: Rect,
1308 [x_axes, y_axes]: [&'a [AxisHints<'a>]; 2],
1309) -> ([Vec<AxisWidget<'a>>; 2], Rect) {
1310 let show_axes = show_axes.into();
1333
1334 let mut x_axis_widgets = Vec::<AxisWidget<'_>>::new();
1335 let mut y_axis_widgets = Vec::<AxisWidget<'_>>::new();
1336
1337 let mut rect_left = complete_rect;
1339
1340 if show_axes.x {
1341 let initial_x_range = complete_rect.x_range();
1343
1344 for (i, cfg) in x_axes.iter().enumerate().rev() {
1345 let mut height = cfg.thickness(Axis::X);
1346 if let Some(mem) = mem {
1347 height = height.max(mem.x_axis_thickness.get(&i).copied().unwrap_or_default());
1349 }
1350
1351 let rect = match VPlacement::from(cfg.placement) {
1352 VPlacement::Bottom => {
1353 let bottom = rect_left.bottom();
1354 *rect_left.bottom_mut() -= height;
1355 let top = rect_left.bottom();
1356 Rect::from_x_y_ranges(initial_x_range, top..=bottom)
1357 }
1358 VPlacement::Top => {
1359 let top = rect_left.top();
1360 *rect_left.top_mut() += height;
1361 let bottom = rect_left.top();
1362 Rect::from_x_y_ranges(initial_x_range, top..=bottom)
1363 }
1364 };
1365 x_axis_widgets.push(AxisWidget::new(cfg.clone(), rect));
1366 }
1367 }
1368 if show_axes.y {
1369 let plot_y_range = rect_left.y_range();
1371
1372 for (i, cfg) in y_axes.iter().enumerate().rev() {
1373 let mut width = cfg.thickness(Axis::Y);
1374 if let Some(mem) = mem {
1375 width = width.max(mem.y_axis_thickness.get(&i).copied().unwrap_or_default());
1377 }
1378
1379 let rect = match HPlacement::from(cfg.placement) {
1380 HPlacement::Left => {
1381 let left = rect_left.left();
1382 *rect_left.left_mut() += width;
1383 let right = rect_left.left();
1384 Rect::from_x_y_ranges(left..=right, plot_y_range)
1385 }
1386 HPlacement::Right => {
1387 let right = rect_left.right();
1388 *rect_left.right_mut() -= width;
1389 let left = rect_left.right();
1390 Rect::from_x_y_ranges(left..=right, plot_y_range)
1391 }
1392 };
1393 y_axis_widgets.push(AxisWidget::new(cfg.clone(), rect));
1394 }
1395 }
1396
1397 x_axis_widgets.reverse();
1401 y_axis_widgets.reverse();
1402
1403 let mut plot_rect = rect_left;
1404
1405 if plot_rect.width() <= 0.0 || plot_rect.height() <= 0.0 {
1407 y_axis_widgets.clear();
1408 x_axis_widgets.clear();
1409 plot_rect = complete_rect;
1410 }
1411
1412 for widget in &mut x_axis_widgets {
1415 widget.rect = Rect::from_x_y_ranges(plot_rect.x_range(), widget.rect.y_range());
1416 }
1417
1418 ([x_axis_widgets, y_axis_widgets], plot_rect)
1419}
1420
1421enum BoundsModification {
1424 Set(PlotBounds),
1425 Translate(Vec2),
1426 AutoBounds(Vec2b),
1427 Zoom(Vec2, PlotPoint),
1428}
1429
1430pub struct GridInput {
1437 pub bounds: (f64, f64),
1440
1441 pub base_step_size: f64,
1448}
1449
1450#[derive(Debug, Clone, Copy, PartialEq)]
1452pub struct GridMark {
1453 pub value: f64,
1455
1456 pub step_size: f64,
1463}
1464
1465pub fn log_grid_spacer(log_base: i64) -> GridSpacer<'static> {
1470 let log_base = log_base as f64;
1471 let step_sizes = move |input: GridInput| -> Vec<GridMark> {
1472 if input.base_step_size.abs() < f64::EPSILON {
1474 return Vec::new();
1475 }
1476
1477 let smallest_visible_unit = next_power(input.base_step_size, log_base);
1480
1481 let step_sizes = [
1482 smallest_visible_unit,
1483 smallest_visible_unit * log_base,
1484 smallest_visible_unit * log_base * log_base,
1485 ];
1486
1487 generate_marks(step_sizes, input.bounds)
1488 };
1489
1490 Box::new(step_sizes)
1491}
1492
1493pub fn uniform_grid_spacer<'a>(spacer: impl Fn(GridInput) -> [f64; 3] + 'a) -> GridSpacer<'a> {
1501 let get_marks = move |input: GridInput| -> Vec<GridMark> {
1502 let bounds = input.bounds;
1503 let step_sizes = spacer(input);
1504 generate_marks(step_sizes, bounds)
1505 };
1506
1507 Box::new(get_marks)
1508}
1509
1510struct PreparedPlot<'a> {
1513 items: Vec<Box<dyn PlotItem + 'a>>,
1514 show_x: bool,
1515 show_y: bool,
1516 label_formatter: LabelFormatter<'a>,
1517 coordinates_formatter: Option<(Corner, CoordinatesFormatter<'a>)>,
1518 transform: PlotTransform,
1520 show_grid: Vec2b,
1521 grid_spacing: Rangef,
1522 grid_spacers: [GridSpacer<'a>; 2],
1523 draw_cursor_x: bool,
1524 draw_cursor_y: bool,
1525 draw_cursors: Vec<Cursor>,
1526 cursor_color: Option<Color32>,
1527
1528 clamp_grid: bool,
1529}
1530
1531impl<'a> PreparedPlot<'a> {
1532 fn ui(self, ui: &mut Ui, response: &Response) -> (Vec<Cursor>, Option<Id>) {
1533 let mut axes_shapes = Vec::new();
1534
1535 if self.show_grid.x {
1536 self.paint_grid(ui, &mut axes_shapes, Axis::X, self.grid_spacing);
1537 }
1538 if self.show_grid.y {
1539 self.paint_grid(ui, &mut axes_shapes, Axis::Y, self.grid_spacing);
1540 }
1541
1542 axes_shapes.sort_by(|(_, strength1), (_, strength2)| strength1.total_cmp(strength2));
1544
1545 let mut shapes = axes_shapes.into_iter().map(|(shape, _)| shape).collect();
1546
1547 let transform = &self.transform;
1548
1549 let mut plot_ui = ui.new_child(
1550 egui::UiBuilder::new()
1551 .max_rect(*transform.frame())
1552 .layout(Layout::default()),
1553 );
1554 plot_ui.set_clip_rect(transform.frame().intersect(ui.clip_rect()));
1555 for item in &self.items {
1556 item.shapes(&plot_ui, transform, &mut shapes);
1557 }
1558
1559 let hover_pos = response.hover_pos();
1560 let (cursors, hovered_item_id) = if let Some(pointer) = hover_pos {
1561 self.hover(ui, pointer, &mut shapes)
1562 } else {
1563 (Vec::new(), None)
1564 };
1565
1566 let line_color = self.cursor_color.unwrap_or_else(|| rulers_color(ui));
1568
1569 let mut draw_cursor = |cursors: &Vec<Cursor>, always| {
1570 for &cursor in cursors {
1571 match cursor {
1572 Cursor::Horizontal { y } => {
1573 if self.draw_cursor_y || always {
1574 shapes.push(horizontal_line(
1575 transform.position_from_point(&PlotPoint::new(0.0, y)),
1576 &self.transform,
1577 line_color,
1578 ));
1579 }
1580 }
1581 Cursor::Vertical { x } => {
1582 if self.draw_cursor_x || always {
1583 shapes.push(vertical_line(
1584 transform.position_from_point(&PlotPoint::new(x, 0.0)),
1585 &self.transform,
1586 line_color,
1587 ));
1588 }
1589 }
1590 }
1591 }
1592 };
1593
1594 draw_cursor(&self.draw_cursors, false);
1595 draw_cursor(&cursors, true);
1596
1597 let painter = ui.painter().with_clip_rect(*transform.frame());
1598 painter.extend(shapes);
1599
1600 if let Some((corner, formatter)) = self.coordinates_formatter.as_ref() {
1601 let hover_pos = response.hover_pos();
1602 if let Some(pointer) = hover_pos {
1603 let font_id = TextStyle::Monospace.resolve(ui.style());
1604 let coordinate = transform.value_from_position(pointer);
1605 let text = formatter.format(&coordinate, transform.bounds());
1606 let padded_frame = transform.frame().shrink(4.0);
1607 let (anchor, position) = match corner {
1608 Corner::LeftTop => (Align2::LEFT_TOP, padded_frame.left_top()),
1609 Corner::RightTop => (Align2::RIGHT_TOP, padded_frame.right_top()),
1610 Corner::LeftBottom => (Align2::LEFT_BOTTOM, padded_frame.left_bottom()),
1611 Corner::RightBottom => (Align2::RIGHT_BOTTOM, padded_frame.right_bottom()),
1612 };
1613 painter.text(position, anchor, text, font_id, ui.visuals().text_color());
1614 }
1615 }
1616
1617 (cursors, hovered_item_id)
1618 }
1619
1620 fn paint_grid(&self, ui: &Ui, shapes: &mut Vec<(Shape, f32)>, axis: Axis, fade_range: Rangef) {
1621 #![allow(clippy::collapsible_else_if)]
1622 let Self {
1623 transform,
1624 grid_spacers,
1626 clamp_grid,
1627 ..
1628 } = self;
1629
1630 let iaxis = usize::from(axis);
1631
1632 let bounds = transform.bounds();
1634 let value_cross = 0.0_f64.clamp(bounds.min[1 - iaxis], bounds.max[1 - iaxis]);
1635
1636 let input = GridInput {
1637 bounds: (bounds.min[iaxis], bounds.max[iaxis]),
1638 base_step_size: transform.dvalue_dpos()[iaxis].abs() * fade_range.min as f64,
1639 };
1640 let steps = (grid_spacers[iaxis])(input);
1641
1642 let clamp_range = clamp_grid.then(|| {
1643 let mut tight_bounds = PlotBounds::NOTHING;
1644 for item in &self.items {
1645 let item_bounds = item.bounds();
1646 tight_bounds.merge_x(&item_bounds);
1647 tight_bounds.merge_y(&item_bounds);
1648 }
1649 tight_bounds
1650 });
1651
1652 for step in steps {
1653 let value_main = step.value;
1654
1655 if let Some(clamp_range) = clamp_range {
1656 match axis {
1657 Axis::X => {
1658 if !clamp_range.range_x().contains(&value_main) {
1659 continue;
1660 };
1661 }
1662 Axis::Y => {
1663 if !clamp_range.range_y().contains(&value_main) {
1664 continue;
1665 };
1666 }
1667 }
1668 }
1669
1670 let value = match axis {
1671 Axis::X => PlotPoint::new(value_main, value_cross),
1672 Axis::Y => PlotPoint::new(value_cross, value_main),
1673 };
1674
1675 let pos_in_gui = transform.position_from_point(&value);
1676 let spacing_in_points = (transform.dpos_dvalue()[iaxis] * step.step_size).abs() as f32;
1677
1678 if spacing_in_points <= fade_range.min {
1679 continue; }
1681
1682 let line_strength = remap_clamp(spacing_in_points, fade_range, 0.0..=1.0);
1683
1684 let line_color = color_from_strength(ui, line_strength);
1685
1686 let mut p0 = pos_in_gui;
1687 let mut p1 = pos_in_gui;
1688 p0[1 - iaxis] = transform.frame().min[1 - iaxis];
1689 p1[1 - iaxis] = transform.frame().max[1 - iaxis];
1690
1691 if let Some(clamp_range) = clamp_range {
1692 match axis {
1693 Axis::X => {
1694 p0.y = transform.position_from_point_y(clamp_range.min[1]);
1695 p1.y = transform.position_from_point_y(clamp_range.max[1]);
1696 }
1697 Axis::Y => {
1698 p0.x = transform.position_from_point_x(clamp_range.min[0]);
1699 p1.x = transform.position_from_point_x(clamp_range.max[0]);
1700 }
1701 }
1702 }
1703
1704 shapes.push((
1705 Shape::line_segment([p0, p1], Stroke::new(1.0, line_color)),
1706 line_strength,
1707 ));
1708 }
1709 }
1710
1711 fn hover(&self, ui: &Ui, pointer: Pos2, shapes: &mut Vec<Shape>) -> (Vec<Cursor>, Option<Id>) {
1712 let Self {
1713 transform,
1714 show_x,
1715 show_y,
1716 label_formatter,
1717 items,
1718 ..
1719 } = self;
1720
1721 if !show_x && !show_y {
1722 return (Vec::new(), None);
1723 }
1724
1725 let interact_radius_sq = ui.style().interaction.interact_radius.powi(2);
1726
1727 let candidates = items
1728 .iter()
1729 .filter(|entry| entry.allow_hover())
1730 .filter_map(|item| {
1731 let item = &**item;
1732 let closest = item.find_closest(pointer, transform);
1733
1734 Some(item).zip(closest)
1735 });
1736
1737 let closest = candidates
1738 .min_by_key(|(_, elem)| elem.dist_sq.ord())
1739 .filter(|(_, elem)| elem.dist_sq <= interact_radius_sq);
1740
1741 let plot = items::PlotConfig {
1742 ui,
1743 transform,
1744 show_x: *show_x,
1745 show_y: *show_y,
1746 };
1747
1748 let mut cursors = Vec::new();
1749
1750 let hovered_plot_item_id = if let Some((item, elem)) = closest {
1751 item.on_hover(elem, shapes, &mut cursors, &plot, label_formatter);
1752 Some(item.id())
1753 } else {
1754 let value = transform.value_from_position(pointer);
1755 items::rulers_at_value(
1756 pointer,
1757 value,
1758 "",
1759 &plot,
1760 shapes,
1761 &mut cursors,
1762 label_formatter,
1763 );
1764 None
1765 };
1766
1767 (cursors, hovered_plot_item_id)
1768 }
1769}
1770
1771fn next_power(value: f64, base: f64) -> f64 {
1780 debug_assert_ne!(value, 0.0, "Bad input"); base.powi(value.abs().log(base).ceil() as i32)
1782}
1783
1784fn generate_marks(step_sizes: [f64; 3], bounds: (f64, f64)) -> Vec<GridMark> {
1786 let mut steps = vec![];
1787 fill_marks_between(&mut steps, step_sizes[0], bounds);
1788 fill_marks_between(&mut steps, step_sizes[1], bounds);
1789 fill_marks_between(&mut steps, step_sizes[2], bounds);
1790
1791 steps.sort_by(|a, b| cmp_f64(a.value, b.value));
1798
1799 let min_step = step_sizes.iter().fold(f64::INFINITY, |a, &b| a.min(b));
1800 let eps = 0.1 * min_step; let mut deduplicated: Vec<GridMark> = Vec::with_capacity(steps.len());
1803 for step in steps {
1804 if let Some(last) = deduplicated.last_mut() {
1805 if (last.value - step.value).abs() < eps {
1806 if last.step_size < step.step_size {
1808 *last = step;
1809 }
1810 continue;
1811 }
1812 }
1813 deduplicated.push(step);
1814 }
1815
1816 deduplicated
1817}
1818
1819#[test]
1820fn test_generate_marks() {
1821 fn approx_eq(a: &GridMark, b: &GridMark) -> bool {
1822 (a.value - b.value).abs() < 1e-10 && a.step_size == b.step_size
1823 }
1824
1825 let gm = |value, step_size| GridMark { value, step_size };
1826
1827 let marks = generate_marks([0.01, 0.1, 1.0], (2.855, 3.015));
1828 let expected = vec![
1829 gm(2.86, 0.01),
1830 gm(2.87, 0.01),
1831 gm(2.88, 0.01),
1832 gm(2.89, 0.01),
1833 gm(2.90, 0.1),
1834 gm(2.91, 0.01),
1835 gm(2.92, 0.01),
1836 gm(2.93, 0.01),
1837 gm(2.94, 0.01),
1838 gm(2.95, 0.01),
1839 gm(2.96, 0.01),
1840 gm(2.97, 0.01),
1841 gm(2.98, 0.01),
1842 gm(2.99, 0.01),
1843 gm(3.00, 1.),
1844 gm(3.01, 0.01),
1845 ];
1846
1847 let mut problem = None;
1848 if marks.len() != expected.len() {
1849 problem = Some(format!(
1850 "Different lengths: got {}, expected {}",
1851 marks.len(),
1852 expected.len()
1853 ));
1854 }
1855
1856 for (i, (a, b)) in marks.iter().zip(&expected).enumerate() {
1857 if !approx_eq(a, b) {
1858 problem = Some(format!("Mismatch at index {i}: {a:?} != {b:?}"));
1859 break;
1860 }
1861 }
1862
1863 if let Some(problem) = problem {
1864 panic!("Test failed: {problem}. Got: {marks:#?}, expected: {expected:#?}");
1865 }
1866}
1867
1868fn cmp_f64(a: f64, b: f64) -> Ordering {
1869 match a.partial_cmp(&b) {
1870 Some(ord) => ord,
1871 None => a.is_nan().cmp(&b.is_nan()),
1872 }
1873}
1874
1875fn fill_marks_between(out: &mut Vec<GridMark>, step_size: f64, (min, max): (f64, f64)) {
1877 debug_assert!(min <= max, "Bad plot bounds: min: {min}, max: {max}");
1878 let first = (min / step_size).ceil() as i64;
1879 let last = (max / step_size).ceil() as i64;
1880
1881 let marks_iter = (first..last).map(|i| {
1882 let value = (i as f64) * step_size;
1883 GridMark { value, step_size }
1884 });
1885 out.extend(marks_iter);
1886}
1887
1888pub fn format_number(number: f64, num_decimals: usize) -> String {
1891 let is_integral = number as i64 as f64 == number;
1892 if is_integral {
1893 format!("{number:.0}")
1895 } else {
1896 format!("{:.*}", num_decimals.at_least(1), number)
1898 }
1899}
1900
1901pub fn color_from_strength(ui: &Ui, strength: f32) -> Color32 {
1903 let base_color = ui.visuals().text_color();
1904 base_color.gamma_multiply(strength.sqrt())
1905}