maelstrom_plot/items/
mod.rs

1pub use values::{LineStyle, MarkerShape, Orientation, PlotPoint, PlotPoints};
2
3use super::{Cursor, LabelFormatter, PlotBounds, PlotTransform};
4use egui::{
5    epaint,
6    epaint::{util::FloatOrd, Mesh},
7    pos2, vec2, Align2, Color32, NumExt as _, Pos2, Rect, Rgba, Shape, Stroke, TextStyle, Ui,
8    WidgetText,
9};
10use std::ops::RangeInclusive;
11use values::{ClosestElem, PlotGeometry};
12
13mod values;
14
15const DEFAULT_FILL_ALPHA: f32 = 0.05;
16
17/// Container to pass-through several parameters related to plot visualization
18pub(super) struct PlotConfig<'a> {
19    pub ui: &'a Ui,
20    pub transform: &'a PlotTransform,
21    pub show_x: bool,
22    pub show_y: bool,
23}
24
25/// Trait shared by things that can be drawn in the plot.
26pub(super) trait PlotItem {
27    fn shapes(&self, ui: &mut Ui, transform: &PlotTransform, shapes: &mut Vec<Shape>);
28
29    /// For plot-items which are generated based on x values (plotting functions).
30    fn initialize(&mut self, x_range: RangeInclusive<f64>);
31
32    fn name(&self) -> &str;
33
34    fn color(&self) -> Color32;
35
36    fn highlight(&mut self);
37
38    fn highlighted(&self) -> bool;
39
40    fn geometry(&self) -> PlotGeometry<'_>;
41
42    fn bounds(&self) -> PlotBounds;
43
44    fn find_closest(&self, point: Pos2, transform: &PlotTransform) -> Option<ClosestElem> {
45        match self.geometry() {
46            PlotGeometry::None => None,
47
48            PlotGeometry::Points(points) => points
49                .iter()
50                .enumerate()
51                .map(|(index, value)| {
52                    let pos = transform.position_from_point(value);
53                    let dist_sq = point.distance_sq(pos);
54                    ClosestElem { index, dist_sq }
55                })
56                .min_by_key(|e| e.dist_sq.ord()),
57        }
58    }
59
60    fn on_hover(
61        &self,
62        elem: ClosestElem,
63        shapes: &mut Vec<Shape>,
64        cursors: &mut Vec<Cursor>,
65        plot: &PlotConfig<'_>,
66        label_formatter: &LabelFormatter,
67    ) {
68        let points = match self.geometry() {
69            PlotGeometry::Points(points) => points,
70            PlotGeometry::None => {
71                panic!("If the PlotItem has no geometry, on_hover() must not be called")
72            }
73        };
74
75        let line_color = if plot.ui.visuals().dark_mode {
76            Color32::from_gray(100).additive()
77        } else {
78            Color32::from_black_alpha(180)
79        };
80
81        // this method is only called, if the value is in the result set of find_closest()
82        let value = points[elem.index];
83        let pointer = plot.transform.position_from_point(&value);
84        shapes.push(Shape::circle_filled(pointer, 3.0, line_color));
85
86        rulers_at_value(
87            pointer,
88            value,
89            self.name(),
90            plot,
91            shapes,
92            cursors,
93            label_formatter,
94        );
95    }
96}
97
98/// A series of values forming a path.
99pub struct Line {
100    pub(super) series: PlotPoints,
101    pub(super) stroke: Stroke,
102    pub(super) name: String,
103    pub(super) highlight: bool,
104    pub(super) fill: Option<f32>,
105    pub(super) style: LineStyle,
106}
107
108impl Line {
109    pub fn new(series: impl Into<PlotPoints>) -> Self {
110        Self {
111            series: series.into(),
112            stroke: Stroke::new(1.0, Color32::TRANSPARENT),
113            name: Default::default(),
114            highlight: false,
115            fill: None,
116            style: LineStyle::Solid,
117        }
118    }
119
120    /// Highlight this line in the plot by scaling up the line.
121    pub fn highlight(mut self, highlight: bool) -> Self {
122        self.highlight = highlight;
123        self
124    }
125
126    /// Add a stroke.
127    pub fn stroke(mut self, stroke: impl Into<Stroke>) -> Self {
128        self.stroke = stroke.into();
129        self
130    }
131
132    /// Stroke width. A high value means the plot thickens.
133    pub fn width(mut self, width: impl Into<f32>) -> Self {
134        self.stroke.width = width.into();
135        self
136    }
137
138    /// Stroke color. Default is `Color32::TRANSPARENT` which means a color will be auto-assigned.
139    pub fn color(mut self, color: impl Into<Color32>) -> Self {
140        self.stroke.color = color.into();
141        self
142    }
143
144    /// Fill the area between this line and a given horizontal reference line.
145    pub fn fill(mut self, y_reference: impl Into<f32>) -> Self {
146        self.fill = Some(y_reference.into());
147        self
148    }
149
150    /// Set the line's style. Default is `LineStyle::Solid`.
151    pub fn style(mut self, style: LineStyle) -> Self {
152        self.style = style;
153        self
154    }
155
156    /// Name of this line.
157    ///
158    /// This name will show up in the plot legend, if legends are turned on.
159    ///
160    /// Multiple plot items may share the same name, in which case they will also share an entry in
161    /// the legend.
162    #[allow(clippy::needless_pass_by_value)]
163    pub fn name(mut self, name: impl ToString) -> Self {
164        self.name = name.to_string();
165        self
166    }
167}
168
169/// Returns the x-coordinate of a possible intersection between a line segment from `p1` to `p2` and
170/// a horizontal line at the given y-coordinate.
171fn y_intersection(p1: &Pos2, p2: &Pos2, y: f32) -> Option<f32> {
172    ((p1.y > y && p2.y < y) || (p1.y < y && p2.y > y))
173        .then_some(((y * (p1.x - p2.x)) - (p1.x * p2.y - p1.y * p2.x)) / (p1.y - p2.y))
174}
175
176impl PlotItem for Line {
177    fn shapes(&self, _ui: &mut Ui, transform: &PlotTransform, shapes: &mut Vec<Shape>) {
178        let Self {
179            series,
180            stroke,
181            highlight,
182            mut fill,
183            style,
184            ..
185        } = self;
186
187        let values_tf: Vec<_> = series
188            .points()
189            .iter()
190            .map(|v| transform.position_from_point(v))
191            .collect();
192        let n_values = values_tf.len();
193
194        // Fill the area between the line and a reference line, if required.
195        if n_values < 2 {
196            fill = None;
197        }
198        if let Some(y_reference) = fill {
199            let mut fill_alpha = DEFAULT_FILL_ALPHA;
200            if *highlight {
201                fill_alpha = (2.0 * fill_alpha).at_most(1.0);
202            }
203            let y = transform
204                .position_from_point(&PlotPoint::new(0.0, y_reference))
205                .y;
206            let fill_color = Rgba::from(stroke.color)
207                .to_opaque()
208                .multiply(fill_alpha)
209                .into();
210            let mut mesh = Mesh::default();
211            let expected_intersections = 20;
212            mesh.reserve_triangles((n_values - 1) * 2);
213            mesh.reserve_vertices(n_values * 2 + expected_intersections);
214            values_tf.windows(2).for_each(|w| {
215                let i = mesh.vertices.len() as u32;
216                mesh.colored_vertex(w[0], fill_color);
217                mesh.colored_vertex(pos2(w[0].x, y), fill_color);
218                if let Some(x) = y_intersection(&w[0], &w[1], y) {
219                    let point = pos2(x, y);
220                    mesh.colored_vertex(point, fill_color);
221                    mesh.add_triangle(i, i + 1, i + 2);
222                    mesh.add_triangle(i + 2, i + 3, i + 4);
223                } else {
224                    mesh.add_triangle(i, i + 1, i + 2);
225                    mesh.add_triangle(i + 1, i + 2, i + 3);
226                }
227            });
228            let last = values_tf[n_values - 1];
229            mesh.colored_vertex(last, fill_color);
230            mesh.colored_vertex(pos2(last.x, y), fill_color);
231            shapes.push(Shape::Mesh(mesh));
232        }
233        style.style_line(values_tf, *stroke, *highlight, shapes);
234    }
235
236    fn initialize(&mut self, x_range: RangeInclusive<f64>) {
237        self.series.generate_points(x_range);
238    }
239
240    fn name(&self) -> &str {
241        self.name.as_str()
242    }
243
244    fn color(&self) -> Color32 {
245        self.stroke.color
246    }
247
248    fn highlight(&mut self) {
249        self.highlight = true;
250    }
251
252    fn highlighted(&self) -> bool {
253        self.highlight
254    }
255
256    fn geometry(&self) -> PlotGeometry<'_> {
257        PlotGeometry::Points(self.series.points())
258    }
259
260    fn bounds(&self) -> PlotBounds {
261        self.series.bounds()
262    }
263}
264
265/// A series of values forming a path stacked on top of another line
266pub struct StackedLine {
267    pub(super) series: PlotPoints,
268    pub(super) stroke: Stroke,
269    pub(super) name: String,
270    pub(super) highlight: bool,
271    pub(super) stacked_on: Option<PlotPoints>,
272    pub(super) style: LineStyle,
273}
274
275impl StackedLine {
276    pub fn new(series: impl Into<PlotPoints>) -> Self {
277        Self {
278            series: series.into(),
279            stroke: Stroke::new(1.0, Color32::TRANSPARENT),
280            name: Default::default(),
281            highlight: false,
282            stacked_on: None,
283            style: LineStyle::Solid,
284        }
285    }
286
287    /// Highlight this line in the plot by scaling up the line.
288    pub fn highlight(mut self, highlight: bool) -> Self {
289        self.highlight = highlight;
290        self
291    }
292
293    /// Add a stroke.
294    pub fn stroke(mut self, stroke: impl Into<Stroke>) -> Self {
295        self.stroke = stroke.into();
296        self
297    }
298
299    /// Stroke width. A high value means the plot thickens.
300    pub fn width(mut self, width: impl Into<f32>) -> Self {
301        self.stroke.width = width.into();
302        self
303    }
304
305    /// Stroke color. Default is `Color32::TRANSPARENT` which means a color will be auto-assigned.
306    pub fn color(mut self, color: impl Into<Color32>) -> Self {
307        self.stroke.color = color.into();
308        self
309    }
310
311    /// This is the other line this line in stacked on top of
312    pub fn stacked_on(mut self, stacked_on: impl Into<PlotPoints>) -> Self {
313        self.stacked_on = Some(stacked_on.into());
314        self
315    }
316
317    /// Set the line's style. Default is `LineStyle::Solid`.
318    pub fn style(mut self, style: LineStyle) -> Self {
319        self.style = style;
320        self
321    }
322
323    /// Name of this line.
324    ///
325    /// This name will show up in the plot legend, if legends are turned on.
326    ///
327    /// Multiple plot items may share the same name, in which case they will also share an entry in
328    /// the legend.
329    #[allow(clippy::needless_pass_by_value)]
330    pub fn name(mut self, name: impl ToString) -> Self {
331        self.name = name.to_string();
332        self
333    }
334}
335
336impl PlotItem for StackedLine {
337    fn shapes(&self, _ui: &mut Ui, transform: &PlotTransform, shapes: &mut Vec<Shape>) {
338        let Self {
339            series,
340            stroke,
341            highlight,
342            stacked_on,
343            style,
344            ..
345        } = self;
346
347        if series.points().len() <= 1 {
348            let values_tf: Vec<_> = series
349                .points()
350                .iter()
351                .map(|v| transform.position_from_point(v))
352                .collect();
353            style.style_line(values_tf, *stroke, *highlight, shapes);
354            return;
355        }
356
357        let bottom = PlotPoints::new(
358            (0..series.points().len())
359                .map(|i| [i as f64, 0.0])
360                .collect(),
361        );
362        let stacked_on = stacked_on.as_ref().unwrap_or(&bottom);
363        let values_tf: Vec<_> = series
364            .points()
365            .iter()
366            .zip(stacked_on.points().iter())
367            .map(|(v, y)| {
368                (
369                    transform.position_from_point(v),
370                    transform.position_from_point(y),
371                )
372            })
373            .collect();
374
375        let n_values = values_tf.len();
376        let mut fill_alpha = DEFAULT_FILL_ALPHA;
377        if *highlight {
378            fill_alpha = (2.0 * fill_alpha).at_most(1.0);
379        }
380        let fill_color = Rgba::from(stroke.color)
381            .to_opaque()
382            .multiply(fill_alpha)
383            .into();
384        let mut mesh = Mesh::default();
385        let expected_intersections = 20;
386        mesh.reserve_triangles((n_values - 1) * 2);
387        mesh.reserve_vertices(n_values * 2 + expected_intersections);
388        values_tf.windows(2).for_each(|w| {
389            let primary = [w[0].0, w[1].0];
390            let stacked = [w[0].1, w[1].1];
391            let i = mesh.vertices.len() as u32;
392            mesh.colored_vertex(primary[0], fill_color);
393            mesh.colored_vertex(pos2(primary[0].x, stacked[0].y), fill_color);
394            if let Some(x) = y_intersection(&primary[0], &primary[1], stacked[0].y) {
395                let point = pos2(x, stacked[0].y);
396                mesh.colored_vertex(point, fill_color);
397                mesh.add_triangle(i, i + 1, i + 2);
398                mesh.add_triangle(i + 2, i + 3, i + 4);
399            } else {
400                mesh.add_triangle(i, i + 1, i + 2);
401                mesh.add_triangle(i + 1, i + 2, i + 3);
402            }
403        });
404        let last = values_tf[n_values - 1];
405        mesh.colored_vertex(last.0, fill_color);
406        mesh.colored_vertex(pos2(last.0.x, last.1.y), fill_color);
407        shapes.push(Shape::Mesh(mesh));
408
409        let values_tf = values_tf.iter().map(|e| e.0).collect();
410        style.style_line(values_tf, *stroke, *highlight, shapes);
411    }
412
413    fn initialize(&mut self, x_range: RangeInclusive<f64>) {
414        self.series.generate_points(x_range);
415    }
416
417    fn name(&self) -> &str {
418        self.name.as_str()
419    }
420
421    fn color(&self) -> Color32 {
422        self.stroke.color
423    }
424
425    fn highlight(&mut self) {
426        self.highlight = true;
427    }
428
429    fn highlighted(&self) -> bool {
430        self.highlight
431    }
432
433    fn geometry(&self) -> PlotGeometry<'_> {
434        PlotGeometry::Points(self.series.points())
435    }
436
437    fn bounds(&self) -> PlotBounds {
438        self.series.bounds()
439    }
440}
441
442/// A convex polygon.
443pub struct Polygon {
444    pub(super) series: PlotPoints,
445    pub(super) stroke: Stroke,
446    pub(super) name: String,
447    pub(super) highlight: bool,
448    pub(super) fill_color: Option<Color32>,
449    pub(super) style: LineStyle,
450}
451
452impl Polygon {
453    pub fn new(series: impl Into<PlotPoints>) -> Self {
454        Self {
455            series: series.into(),
456            stroke: Stroke::new(1.0, Color32::TRANSPARENT),
457            name: Default::default(),
458            highlight: false,
459            fill_color: None,
460            style: LineStyle::Solid,
461        }
462    }
463
464    /// Highlight this polygon in the plot by scaling up the stroke and reducing the fill
465    /// transparency.
466    pub fn highlight(mut self, highlight: bool) -> Self {
467        self.highlight = highlight;
468        self
469    }
470
471    /// Add a custom stroke.
472    pub fn stroke(mut self, stroke: impl Into<Stroke>) -> Self {
473        self.stroke = stroke.into();
474        self
475    }
476
477    /// Set the stroke width.
478    pub fn width(mut self, width: impl Into<f32>) -> Self {
479        self.stroke.width = width.into();
480        self
481    }
482
483    #[deprecated = "Use `fill_color`."]
484    #[allow(unused, clippy::needless_pass_by_value)]
485    pub fn color(mut self, color: impl Into<Color32>) -> Self {
486        self
487    }
488
489    #[deprecated = "Use `fill_color`."]
490    #[allow(unused, clippy::needless_pass_by_value)]
491    pub fn fill_alpha(mut self, _alpha: impl Into<f32>) -> Self {
492        self
493    }
494
495    /// Fill color. Defaults to the stroke color with added transparency.
496    pub fn fill_color(mut self, color: impl Into<Color32>) -> Self {
497        self.fill_color = Some(color.into());
498        self
499    }
500
501    /// Set the outline's style. Default is `LineStyle::Solid`.
502    pub fn style(mut self, style: LineStyle) -> Self {
503        self.style = style;
504        self
505    }
506
507    /// Name of this polygon.
508    ///
509    /// This name will show up in the plot legend, if legends are turned on.
510    ///
511    /// Multiple plot items may share the same name, in which case they will also share an entry in
512    /// the legend.
513    #[allow(clippy::needless_pass_by_value)]
514    pub fn name(mut self, name: impl ToString) -> Self {
515        self.name = name.to_string();
516        self
517    }
518}
519
520impl PlotItem for Polygon {
521    fn shapes(&self, _ui: &mut Ui, transform: &PlotTransform, shapes: &mut Vec<Shape>) {
522        let Self {
523            series,
524            stroke,
525            highlight,
526            fill_color,
527            style,
528            ..
529        } = self;
530
531        let mut values_tf: Vec<_> = series
532            .points()
533            .iter()
534            .map(|v| transform.position_from_point(v))
535            .collect();
536
537        let fill_color = fill_color.unwrap_or(stroke.color.linear_multiply(DEFAULT_FILL_ALPHA));
538
539        let shape = Shape::convex_polygon(values_tf.clone(), fill_color, Stroke::NONE);
540        shapes.push(shape);
541        values_tf.push(*values_tf.first().unwrap());
542        style.style_line(values_tf, *stroke, *highlight, shapes);
543    }
544
545    fn initialize(&mut self, x_range: RangeInclusive<f64>) {
546        self.series.generate_points(x_range);
547    }
548
549    fn name(&self) -> &str {
550        self.name.as_str()
551    }
552
553    fn color(&self) -> Color32 {
554        self.stroke.color
555    }
556
557    fn highlight(&mut self) {
558        self.highlight = true;
559    }
560
561    fn highlighted(&self) -> bool {
562        self.highlight
563    }
564
565    fn geometry(&self) -> PlotGeometry<'_> {
566        PlotGeometry::Points(self.series.points())
567    }
568
569    fn bounds(&self) -> PlotBounds {
570        self.series.bounds()
571    }
572}
573
574/// Text inside the plot.
575#[derive(Clone)]
576pub struct Text {
577    pub(super) text: WidgetText,
578    pub(super) position: PlotPoint,
579    pub(super) name: String,
580    pub(super) highlight: bool,
581    pub(super) color: Color32,
582    pub(super) anchor: Align2,
583}
584
585impl Text {
586    pub fn new(position: PlotPoint, text: impl Into<WidgetText>) -> Self {
587        Self {
588            text: text.into(),
589            position,
590            name: Default::default(),
591            highlight: false,
592            color: Color32::TRANSPARENT,
593            anchor: Align2::CENTER_CENTER,
594        }
595    }
596
597    /// Highlight this text in the plot by drawing a rectangle around it.
598    pub fn highlight(mut self, highlight: bool) -> Self {
599        self.highlight = highlight;
600        self
601    }
602
603    /// Text color.
604    pub fn color(mut self, color: impl Into<Color32>) -> Self {
605        self.color = color.into();
606        self
607    }
608
609    /// Anchor position of the text. Default is `Align2::CENTER_CENTER`.
610    pub fn anchor(mut self, anchor: Align2) -> Self {
611        self.anchor = anchor;
612        self
613    }
614
615    /// Name of this text.
616    ///
617    /// This name will show up in the plot legend, if legends are turned on.
618    ///
619    /// Multiple plot items may share the same name, in which case they will also share an entry in
620    /// the legend.
621    #[allow(clippy::needless_pass_by_value)]
622    pub fn name(mut self, name: impl ToString) -> Self {
623        self.name = name.to_string();
624        self
625    }
626}
627
628impl PlotItem for Text {
629    fn shapes(&self, ui: &mut Ui, transform: &PlotTransform, shapes: &mut Vec<Shape>) {
630        let color = if self.color == Color32::TRANSPARENT {
631            ui.style().visuals.text_color()
632        } else {
633            self.color
634        };
635
636        let galley =
637            self.text
638                .clone()
639                .into_galley(ui, Some(false), f32::INFINITY, TextStyle::Small);
640
641        let pos = transform.position_from_point(&self.position);
642        let rect = self
643            .anchor
644            .anchor_rect(Rect::from_min_size(pos, galley.size()));
645
646        shapes.push(egui::epaint::TextShape::new(rect.min, galley, color).into());
647
648        if self.highlight {
649            shapes.push(Shape::rect_stroke(
650                rect.expand(2.0),
651                1.0,
652                Stroke::new(0.5, color),
653            ));
654        }
655    }
656
657    fn initialize(&mut self, _x_range: RangeInclusive<f64>) {}
658
659    fn name(&self) -> &str {
660        self.name.as_str()
661    }
662
663    fn color(&self) -> Color32 {
664        self.color
665    }
666
667    fn highlight(&mut self) {
668        self.highlight = true;
669    }
670
671    fn highlighted(&self) -> bool {
672        self.highlight
673    }
674
675    fn geometry(&self) -> PlotGeometry<'_> {
676        PlotGeometry::None
677    }
678
679    fn bounds(&self) -> PlotBounds {
680        let mut bounds = PlotBounds::NOTHING;
681        bounds.extend_with(&self.position);
682        bounds
683    }
684}
685
686/// A set of points.
687pub struct Points {
688    pub(super) series: PlotPoints,
689
690    pub(super) shape: MarkerShape,
691
692    /// Color of the marker. `Color32::TRANSPARENT` means that it will be picked automatically.
693    pub(super) color: Color32,
694
695    /// Whether to fill the marker. Does not apply to all types.
696    pub(super) filled: bool,
697
698    /// The maximum extent of the marker from its center.
699    pub(super) radius: f32,
700
701    pub(super) name: String,
702
703    pub(super) highlight: bool,
704
705    pub(super) stems: Option<f32>,
706}
707
708impl Points {
709    pub fn new(series: impl Into<PlotPoints>) -> Self {
710        Self {
711            series: series.into(),
712            shape: MarkerShape::Circle,
713            color: Color32::TRANSPARENT,
714            filled: true,
715            radius: 1.0,
716            name: Default::default(),
717            highlight: false,
718            stems: None,
719        }
720    }
721
722    /// Set the shape of the markers.
723    pub fn shape(mut self, shape: MarkerShape) -> Self {
724        self.shape = shape;
725        self
726    }
727
728    /// Highlight these points in the plot by scaling up their markers.
729    pub fn highlight(mut self, highlight: bool) -> Self {
730        self.highlight = highlight;
731        self
732    }
733
734    /// Set the marker's color.
735    pub fn color(mut self, color: impl Into<Color32>) -> Self {
736        self.color = color.into();
737        self
738    }
739
740    /// Whether to fill the marker.
741    pub fn filled(mut self, filled: bool) -> Self {
742        self.filled = filled;
743        self
744    }
745
746    /// Whether to add stems between the markers and a horizontal reference line.
747    pub fn stems(mut self, y_reference: impl Into<f32>) -> Self {
748        self.stems = Some(y_reference.into());
749        self
750    }
751
752    /// Set the maximum extent of the marker around its position.
753    pub fn radius(mut self, radius: impl Into<f32>) -> Self {
754        self.radius = radius.into();
755        self
756    }
757
758    /// Name of this set of points.
759    ///
760    /// This name will show up in the plot legend, if legends are turned on.
761    ///
762    /// Multiple plot items may share the same name, in which case they will also share an entry in
763    /// the legend.
764    #[allow(clippy::needless_pass_by_value)]
765    pub fn name(mut self, name: impl ToString) -> Self {
766        self.name = name.to_string();
767        self
768    }
769}
770
771impl PlotItem for Points {
772    fn shapes(&self, _ui: &mut Ui, transform: &PlotTransform, shapes: &mut Vec<Shape>) {
773        let sqrt_3 = 3_f32.sqrt();
774        let frac_sqrt_3_2 = 3_f32.sqrt() / 2.0;
775        let frac_1_sqrt_2 = 1.0 / 2_f32.sqrt();
776
777        let Self {
778            series,
779            shape,
780            color,
781            filled,
782            mut radius,
783            highlight,
784            stems,
785            ..
786        } = self;
787
788        let stroke_size = radius / 5.0;
789
790        let default_stroke = Stroke::new(stroke_size, *color);
791        let mut stem_stroke = default_stroke;
792        let (fill, stroke) = if *filled {
793            (*color, Stroke::NONE)
794        } else {
795            (Color32::TRANSPARENT, default_stroke)
796        };
797
798        if *highlight {
799            radius *= 2f32.sqrt();
800            stem_stroke.width *= 2.0;
801        }
802
803        let y_reference = stems.map(|y| transform.position_from_point(&PlotPoint::new(0.0, y)).y);
804
805        series
806            .points()
807            .iter()
808            .map(|value| transform.position_from_point(value))
809            .for_each(|center| {
810                let tf = |dx: f32, dy: f32| -> Pos2 { center + radius * vec2(dx, dy) };
811
812                if let Some(y) = y_reference {
813                    let stem = Shape::line_segment([center, pos2(center.x, y)], stem_stroke);
814                    shapes.push(stem);
815                }
816
817                match shape {
818                    MarkerShape::Circle => {
819                        shapes.push(Shape::Circle(epaint::CircleShape {
820                            center,
821                            radius,
822                            fill,
823                            stroke,
824                        }));
825                    }
826                    MarkerShape::Diamond => {
827                        let points = vec![
828                            tf(0.0, 1.0),  // bottom
829                            tf(-1.0, 0.0), // left
830                            tf(0.0, -1.0), // top
831                            tf(1.0, 0.0),  // right
832                        ];
833                        shapes.push(Shape::convex_polygon(points, fill, stroke));
834                    }
835                    MarkerShape::Square => {
836                        let points = vec![
837                            tf(-frac_1_sqrt_2, frac_1_sqrt_2),
838                            tf(-frac_1_sqrt_2, -frac_1_sqrt_2),
839                            tf(frac_1_sqrt_2, -frac_1_sqrt_2),
840                            tf(frac_1_sqrt_2, frac_1_sqrt_2),
841                        ];
842                        shapes.push(Shape::convex_polygon(points, fill, stroke));
843                    }
844                    MarkerShape::Cross => {
845                        let diagonal1 = [
846                            tf(-frac_1_sqrt_2, -frac_1_sqrt_2),
847                            tf(frac_1_sqrt_2, frac_1_sqrt_2),
848                        ];
849                        let diagonal2 = [
850                            tf(frac_1_sqrt_2, -frac_1_sqrt_2),
851                            tf(-frac_1_sqrt_2, frac_1_sqrt_2),
852                        ];
853                        shapes.push(Shape::line_segment(diagonal1, default_stroke));
854                        shapes.push(Shape::line_segment(diagonal2, default_stroke));
855                    }
856                    MarkerShape::Plus => {
857                        let horizontal = [tf(-1.0, 0.0), tf(1.0, 0.0)];
858                        let vertical = [tf(0.0, -1.0), tf(0.0, 1.0)];
859                        shapes.push(Shape::line_segment(horizontal, default_stroke));
860                        shapes.push(Shape::line_segment(vertical, default_stroke));
861                    }
862                    MarkerShape::Up => {
863                        let points =
864                            vec![tf(0.0, -1.0), tf(0.5 * sqrt_3, 0.5), tf(-0.5 * sqrt_3, 0.5)];
865                        shapes.push(Shape::convex_polygon(points, fill, stroke));
866                    }
867                    MarkerShape::Down => {
868                        let points = vec![
869                            tf(0.0, 1.0),
870                            tf(-0.5 * sqrt_3, -0.5),
871                            tf(0.5 * sqrt_3, -0.5),
872                        ];
873                        shapes.push(Shape::convex_polygon(points, fill, stroke));
874                    }
875                    MarkerShape::Left => {
876                        let points =
877                            vec![tf(-1.0, 0.0), tf(0.5, -0.5 * sqrt_3), tf(0.5, 0.5 * sqrt_3)];
878                        shapes.push(Shape::convex_polygon(points, fill, stroke));
879                    }
880                    MarkerShape::Right => {
881                        let points = vec![
882                            tf(1.0, 0.0),
883                            tf(-0.5, 0.5 * sqrt_3),
884                            tf(-0.5, -0.5 * sqrt_3),
885                        ];
886                        shapes.push(Shape::convex_polygon(points, fill, stroke));
887                    }
888                    MarkerShape::Asterisk => {
889                        let vertical = [tf(0.0, -1.0), tf(0.0, 1.0)];
890                        let diagonal1 = [tf(-frac_sqrt_3_2, 0.5), tf(frac_sqrt_3_2, -0.5)];
891                        let diagonal2 = [tf(-frac_sqrt_3_2, -0.5), tf(frac_sqrt_3_2, 0.5)];
892                        shapes.push(Shape::line_segment(vertical, default_stroke));
893                        shapes.push(Shape::line_segment(diagonal1, default_stroke));
894                        shapes.push(Shape::line_segment(diagonal2, default_stroke));
895                    }
896                }
897            });
898    }
899
900    fn initialize(&mut self, x_range: RangeInclusive<f64>) {
901        self.series.generate_points(x_range);
902    }
903
904    fn name(&self) -> &str {
905        self.name.as_str()
906    }
907
908    fn color(&self) -> Color32 {
909        self.color
910    }
911
912    fn highlight(&mut self) {
913        self.highlight = true;
914    }
915
916    fn highlighted(&self) -> bool {
917        self.highlight
918    }
919
920    fn geometry(&self) -> PlotGeometry<'_> {
921        PlotGeometry::Points(self.series.points())
922    }
923
924    fn bounds(&self) -> PlotBounds {
925        self.series.bounds()
926    }
927}
928
929// ----------------------------------------------------------------------------
930// Helper functions
931
932pub(crate) fn rulers_color(ui: &Ui) -> Color32 {
933    if ui.visuals().dark_mode {
934        Color32::from_gray(100).additive()
935    } else {
936        Color32::from_black_alpha(180)
937    }
938}
939
940pub(crate) fn vertical_line(
941    pointer: Pos2,
942    transform: &PlotTransform,
943    line_color: Color32,
944) -> Shape {
945    let frame = transform.frame();
946    Shape::line_segment(
947        [
948            pos2(pointer.x, frame.top()),
949            pos2(pointer.x, frame.bottom()),
950        ],
951        (1.0, line_color),
952    )
953}
954
955pub(crate) fn horizontal_line(
956    pointer: Pos2,
957    transform: &PlotTransform,
958    line_color: Color32,
959) -> Shape {
960    let frame = transform.frame();
961    Shape::line_segment(
962        [
963            pos2(frame.left(), pointer.y),
964            pos2(frame.right(), pointer.y),
965        ],
966        (1.0, line_color),
967    )
968}
969
970/// Draws a cross of horizontal and vertical ruler at the `pointer` position.
971/// `value` is used to for text displaying X/Y coordinates.
972#[allow(clippy::too_many_arguments)]
973pub(super) fn rulers_at_value(
974    pointer: Pos2,
975    value: PlotPoint,
976    name: &str,
977    plot: &PlotConfig<'_>,
978    shapes: &mut Vec<Shape>,
979    cursors: &mut Vec<Cursor>,
980    label_formatter: &LabelFormatter,
981) {
982    if plot.show_x {
983        cursors.push(Cursor::Vertical { x: value.x });
984    }
985    if plot.show_y {
986        cursors.push(Cursor::Horizontal { y: value.y });
987    }
988
989    let mut prefix = String::new();
990
991    if !name.is_empty() {
992        prefix = format!("{name}\n");
993    }
994
995    let text = {
996        let scale = plot.transform.dvalue_dpos();
997        let x_decimals = ((-scale[0].abs().log10()).ceil().at_least(0.0) as usize).clamp(1, 6);
998        let y_decimals = ((-scale[1].abs().log10()).ceil().at_least(0.0) as usize).clamp(1, 6);
999        if let Some(custom_label) = label_formatter {
1000            custom_label(name, &value)
1001        } else if plot.show_x && plot.show_y {
1002            format!(
1003                "{}x = {:.*}\ny = {:.*}",
1004                prefix, x_decimals, value.x, y_decimals, value.y
1005            )
1006        } else if plot.show_x {
1007            format!("{}x = {:.*}", prefix, x_decimals, value.x)
1008        } else if plot.show_y {
1009            format!("{}y = {:.*}", prefix, y_decimals, value.y)
1010        } else {
1011            unreachable!()
1012        }
1013    };
1014
1015    let font_id = TextStyle::Body.resolve(plot.ui.style());
1016    plot.ui.fonts(|f| {
1017        shapes.push(Shape::text(
1018            f,
1019            pointer + vec2(3.0, -2.0),
1020            Align2::LEFT_BOTTOM,
1021            text,
1022            font_id,
1023            plot.ui.visuals().text_color(),
1024        ));
1025    });
1026}