1pub 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
38pub struct CoordinatesFormatter {
40 function: Box<CoordinatesFormatterFn>,
41}
42
43impl CoordinatesFormatter {
44 pub fn new(function: impl Fn(&PlotPoint, &PlotBounds) -> String + 'static) -> Self {
46 Self {
47 function: Box::new(function),
48 }
49 }
50
51 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#[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#[derive(Clone)]
108struct PlotMemory {
109 bounds_modified: AxisBools,
112
113 hovered_entry: Option<String>,
114 hidden_items: ahash::HashSet<String>,
115 last_plot_transform: PlotTransform,
116
117 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#[derive(Copy, Clone, PartialEq)]
135enum Cursor {
136 Horizontal { y: f64 },
137 Vertical { x: f64 },
138}
139
140#[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
159pub struct PlotResponse<R> {
163 pub inner: R,
165
166 pub response: Response,
168
169 pub transform: PlotTransform,
171}
172
173pub 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>, y_axes: Vec<AxisHints>, 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 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 pub fn data_aspect(mut self, data_aspect: f32) -> Self {
265 self.data_aspect = Some(data_aspect);
266 self
267 }
268
269 pub fn view_aspect(mut self, view_aspect: f32) -> Self {
272 self.view_aspect = Some(view_aspect);
273 self
274 }
275
276 pub fn width(mut self, width: f32) -> Self {
279 self.min_size.x = width;
280 self.width = Some(width);
281 self
282 }
283
284 pub fn height(mut self, height: f32) -> Self {
287 self.min_size.y = height;
288 self.height = Some(height);
289 self
290 }
291
292 pub fn min_size(mut self, min_size: Vec2) -> Self {
294 self.min_size = min_size;
295 self
296 }
297
298 pub fn show_x(mut self, show_x: bool) -> Self {
300 self.show_x = show_x;
301 self
302 }
303
304 pub fn show_y(mut self, show_y: bool) -> Self {
306 self.show_y = show_y;
307 self
308 }
309
310 pub fn center_x_axis(mut self, on: bool) -> Self {
312 self.center_axis.x = on;
313 self
314 }
315
316 pub fn center_y_axis(mut self, on: bool) -> Self {
318 self.center_axis.y = on;
319 self
320 }
321
322 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 pub fn allow_scroll(mut self, on: bool) -> Self {
335 self.allow_scroll = on;
336 self
337 }
338
339 pub fn allow_double_click_reset(mut self, on: bool) -> Self {
342 self.allow_double_click_reset = on;
343 self
344 }
345
346 pub fn set_margin_fraction(mut self, margin_fraction: Vec2) -> Self {
350 self.margin_fraction = margin_fraction;
351 self
352 }
353
354 pub fn allow_boxed_zoom(mut self, on: bool) -> Self {
358 self.allow_boxed_zoom = on;
359 self
360 }
361
362 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 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 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 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 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 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 pub fn clamp_grid(mut self, clamp_grid: bool) -> Self {
426 self.clamp_grid = clamp_grid;
427 self
428 }
429
430 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 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 pub fn auto_bounds_x(mut self) -> Self {
446 self.auto_bounds.x = true;
447 self
448 }
449
450 pub fn auto_bounds_y(mut self) -> Self {
452 self.auto_bounds.y = true;
453 self
454 }
455
456 pub fn legend(mut self, legend: Legend) -> Self {
458 self.legend_config = Some(legend);
459 self
460 }
461
462 pub fn show_background(mut self, show: bool) -> Self {
466 self.show_background = show;
467 self
468 }
469
470 pub fn show_axes(mut self, show: impl Into<AxisBools>) -> Self {
474 self.show_axes = show.into();
475 self
476 }
477
478 pub fn show_grid(mut self, show: impl Into<AxisBools>) -> Self {
482 self.show_grid = show.into();
483 self
484 }
485
486 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 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 pub fn sharp_grid_lines(mut self, enabled: bool) -> Self {
515 self.sharp_grid_lines = enabled;
516 self
517 }
518
519 pub fn reset(mut self) -> Self {
521 self.reset = true;
522 self
523 }
524
525 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 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 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 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 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 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 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 pub fn custom_x_axes(mut self, hints: Vec<AxisHints>) -> Self {
609 self.x_axes = hints;
610 self
611 }
612
613 pub fn custom_y_axes(mut self, hints: Vec<AxisHints>) -> Self {
617 self.y_axes = hints;
618 self
619 }
620
621 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 let pos = ui.available_rect_before_wrap().min;
669 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 let complete_rect = Rect {
694 min: pos,
695 max: pos + size,
696 };
697 let mut plot_rect: Rect = {
721 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 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 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 let response = ui.allocate_rect(plot_rect, Sense::drag());
764 let rect = plot_rect;
765
766 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 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 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 let legend = legend_config
834 .and_then(|config| LegendWidget::try_new(rect, config, &items, &hidden_items));
835 if hovered_entry.is_some() {
837 show_x = false;
838 show_y = false;
839 }
840 items.retain(|item| !hidden_items.contains(item.name()));
842 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 items.sort_by_key(|item| item.highlighted());
851
852 let mut bounds = *last_plot_transform.bounds();
854
855 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 let index = cursors
863 .iter()
864 .enumerate()
865 .find(|(_, frame)| frame.id == plot_id)
866 .map(|(i, _)| i);
867
868 index.map(|index| cursors.drain(0..=index));
871
872 cursors
875 .iter()
876 .flat_map(|frame| frame.cursors.iter().copied())
877 .collect()
878 })
879 } else {
880 Vec::new()
881 };
882
883 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 if allow_double_click_reset && response.double_clicked() {
903 bounds_modified = false.into();
904 }
905
906 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 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 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 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 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 let mut boxed_zoom_rect = None;
982 if allow_boxed_zoom {
983 if response.drag_started() && response.dragged_by(boxed_zoom_pointer) {
985 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 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 ), epaint::RectShape::stroke(
1002 rect,
1003 0.0,
1004 epaint::Stroke::new(2., Color32::WHITE),
1005 ), ));
1007 }
1008 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 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 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 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 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 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 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
1250enum BoundsModification {
1253 Set(PlotBounds),
1254 Translate(Vec2),
1255}
1256
1257pub 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; 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 pub fn plot_bounds(&self) -> PlotBounds {
1285 *self.last_plot_transform.bounds()
1286 }
1287
1288 pub fn set_plot_bounds(&mut self, plot_bounds: PlotBounds) {
1290 self.bounds_modifications
1291 .push(BoundsModification::Set(plot_bounds));
1292 }
1293
1294 pub fn translate_bounds(&mut self, delta_pos: Vec2) {
1296 self.bounds_modifications
1297 .push(BoundsModification::Translate(delta_pos));
1298 }
1299
1300 pub fn response(&self) -> &Response {
1302 &self.response
1303 }
1304
1305 #[deprecated = "Use plot_ui.response().hovered()"]
1307 pub fn plot_hovered(&self) -> bool {
1308 self.response.hovered()
1309 }
1310
1311 #[deprecated = "Use plot_ui.response().clicked()"]
1313 pub fn plot_clicked(&self) -> bool {
1314 self.response.clicked()
1315 }
1316
1317 #[deprecated = "Use plot_ui.response().secondary_clicked()"]
1319 pub fn plot_secondary_clicked(&self) -> bool {
1320 self.response.secondary_clicked()
1321 }
1322
1323 pub fn pointer_coordinate(&self) -> Option<PlotPoint> {
1326 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 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 pub fn transform(&self) -> &PlotTransform {
1342 &self.last_plot_transform
1343 }
1344
1345 pub fn screen_from_plot(&self, position: PlotPoint) -> Pos2 {
1347 self.last_plot_transform.position_from_point(&position)
1348 }
1349
1350 pub fn plot_from_screen(&self, position: Pos2) -> PlotPoint {
1352 self.last_plot_transform.value_from_position(position)
1353 }
1354
1355 pub fn line(&mut self, mut line: Line) {
1357 if line.series.is_empty() {
1358 return;
1359 };
1360
1361 if line.stroke.color == Color32::TRANSPARENT {
1363 line.stroke.color = self.auto_color();
1364 }
1365 self.items.push(Box::new(line));
1366 }
1367
1368 pub fn stacked_line(&mut self, mut line: StackedLine) {
1370 if line.series.is_empty() {
1371 return;
1372 };
1373
1374 if line.stroke.color == Color32::TRANSPARENT {
1376 line.stroke.color = self.auto_color();
1377 }
1378 self.items.push(Box::new(line));
1379 }
1380
1381 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 pub fn points(&mut self, mut points: Points) {
1392 if points.series.is_empty() {
1393 return;
1394 };
1395
1396 if points.color == Color32::TRANSPARENT {
1398 points.color = self.auto_color();
1399 }
1400 self.items.push(Box::new(points));
1401 }
1402}
1403
1404pub struct GridInput {
1408 pub bounds: (f64, f64),
1411
1412 pub base_step_size: f64,
1417}
1418
1419#[derive(Debug, Clone, Copy)]
1421pub struct GridMark {
1422 pub value: f64,
1424
1425 pub step_size: f64,
1432}
1433
1434pub 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 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
1457pub 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 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 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 grid_spacers,
1583 clamp_grid,
1584 ..
1585 } = self;
1586
1587 let iaxis = usize::from(axis);
1588
1589 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 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
1731fn next_power(value: f64, base: f64) -> f64 {
1740 assert_ne!(value, 0.0); base.powi(value.abs().log(base).ceil() as i32)
1742}
1743
1744fn 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
1753fn 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
1766pub fn format_number(number: f64, num_decimals: usize) -> String {
1769 let is_integral = number as i64 as f64 == number;
1770 if is_integral {
1771 format!("{number:.0}")
1773 } else {
1774 format!("{:.*}", num_decimals.at_least(1), number)
1776 }
1777}
1778
1779pub 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}