//! Contains items that can be added to a plot. #![allow(clippy::type_complexity)] // TODO(emilk): simplify some of the callback types with type aliases use std::ops::RangeInclusive; use epaint::{emath::Rot2, util::FloatOrd, Mesh}; use crate::*; use super::{Cursor, LabelFormatter, PlotBounds, PlotTransform}; use rect_elem::*; use values::{ClosestElem, PlotGeometry}; pub use bar::Bar; pub use box_elem::{BoxElem, BoxSpread}; pub use values::{LineStyle, MarkerShape, Orientation, PlotPoint, PlotPoints}; mod bar; mod box_elem; mod rect_elem; mod values; const DEFAULT_FILL_ALPHA: f32 = 0.05; /// Container to pass-through several parameters related to plot visualization pub(super) struct PlotConfig<'a> { pub ui: &'a Ui, pub transform: &'a PlotTransform, pub show_x: bool, pub show_y: bool, } /// Trait shared by things that can be drawn in the plot. pub(super) trait PlotItem { fn shapes(&self, ui: &mut Ui, transform: &PlotTransform, shapes: &mut Vec); /// For plot-items which are generated based on x values (plotting functions). fn initialize(&mut self, x_range: RangeInclusive); fn name(&self) -> &str; fn color(&self) -> Color32; fn highlight(&mut self); fn highlighted(&self) -> bool; fn geometry(&self) -> PlotGeometry<'_>; fn bounds(&self) -> PlotBounds; fn find_closest(&self, point: Pos2, transform: &PlotTransform) -> Option { match self.geometry() { PlotGeometry::None => None, PlotGeometry::Points(points) => points .iter() .enumerate() .map(|(index, value)| { let pos = transform.position_from_point(value); let dist_sq = point.distance_sq(pos); ClosestElem { index, dist_sq } }) .min_by_key(|e| e.dist_sq.ord()), PlotGeometry::Rects => { panic!("If the PlotItem is made of rects, it should implement find_closest()") } } } fn on_hover( &self, elem: ClosestElem, shapes: &mut Vec, cursors: &mut Vec, plot: &PlotConfig<'_>, label_formatter: &LabelFormatter, ) { let points = match self.geometry() { PlotGeometry::Points(points) => points, PlotGeometry::None => { panic!("If the PlotItem has no geometry, on_hover() must not be called") } PlotGeometry::Rects => { panic!("If the PlotItem is made of rects, it should implement on_hover()") } }; let line_color = if plot.ui.visuals().dark_mode { Color32::from_gray(100).additive() } else { Color32::from_black_alpha(180) }; // this method is only called, if the value is in the result set of find_closest() let value = points[elem.index]; let pointer = plot.transform.position_from_point(&value); shapes.push(Shape::circle_filled(pointer, 3.0, line_color)); rulers_at_value( pointer, value, self.name(), plot, shapes, cursors, label_formatter, ); } } // ---------------------------------------------------------------------------- /// A horizontal line in a plot, filling the full width #[derive(Clone, Debug, PartialEq)] pub struct HLine { pub(super) y: f64, pub(super) stroke: Stroke, pub(super) name: String, pub(super) highlight: bool, pub(super) style: LineStyle, } impl HLine { pub fn new(y: impl Into) -> Self { Self { y: y.into(), stroke: Stroke::new(1.0, Color32::TRANSPARENT), name: String::default(), highlight: false, style: LineStyle::Solid, } } /// Highlight this line in the plot by scaling up the line. #[inline] pub fn highlight(mut self, highlight: bool) -> Self { self.highlight = highlight; self } /// Add a stroke. #[inline] pub fn stroke(mut self, stroke: impl Into) -> Self { self.stroke = stroke.into(); self } /// Stroke width. A high value means the plot thickens. #[inline] pub fn width(mut self, width: impl Into) -> Self { self.stroke.width = width.into(); self } /// Stroke color. Default is `Color32::TRANSPARENT` which means a color will be auto-assigned. #[inline] pub fn color(mut self, color: impl Into) -> Self { self.stroke.color = color.into(); self } /// Set the line's style. Default is `LineStyle::Solid`. #[inline] pub fn style(mut self, style: LineStyle) -> Self { self.style = style; self } /// Name of this horizontal line. /// /// This name will show up in the plot legend, if legends are turned on. /// /// Multiple plot items may share the same name, in which case they will also share an entry in /// the legend. #[allow(clippy::needless_pass_by_value)] #[inline] pub fn name(mut self, name: impl ToString) -> Self { self.name = name.to_string(); self } } impl PlotItem for HLine { fn shapes(&self, ui: &mut Ui, transform: &PlotTransform, shapes: &mut Vec) { let Self { y, stroke, highlight, style, .. } = self; // Round to minimize aliasing: let points = vec![ ui.painter().round_pos_to_pixels( transform.position_from_point(&PlotPoint::new(transform.bounds().min[0], *y)), ), ui.painter().round_pos_to_pixels( transform.position_from_point(&PlotPoint::new(transform.bounds().max[0], *y)), ), ]; style.style_line(points, *stroke, *highlight, shapes); } fn initialize(&mut self, _x_range: RangeInclusive) {} fn name(&self) -> &str { &self.name } fn color(&self) -> Color32 { self.stroke.color } fn highlight(&mut self) { self.highlight = true; } fn highlighted(&self) -> bool { self.highlight } fn geometry(&self) -> PlotGeometry<'_> { PlotGeometry::None } fn bounds(&self) -> PlotBounds { let mut bounds = PlotBounds::NOTHING; bounds.min[1] = self.y; bounds.max[1] = self.y; bounds } } /// A vertical line in a plot, filling the full width #[derive(Clone, Debug, PartialEq)] pub struct VLine { pub(super) x: f64, pub(super) stroke: Stroke, pub(super) name: String, pub(super) highlight: bool, pub(super) style: LineStyle, } impl VLine { pub fn new(x: impl Into) -> Self { Self { x: x.into(), stroke: Stroke::new(1.0, Color32::TRANSPARENT), name: String::default(), highlight: false, style: LineStyle::Solid, } } /// Highlight this line in the plot by scaling up the line. #[inline] pub fn highlight(mut self, highlight: bool) -> Self { self.highlight = highlight; self } /// Add a stroke. #[inline] pub fn stroke(mut self, stroke: impl Into) -> Self { self.stroke = stroke.into(); self } /// Stroke width. A high value means the plot thickens. #[inline] pub fn width(mut self, width: impl Into) -> Self { self.stroke.width = width.into(); self } /// Stroke color. Default is `Color32::TRANSPARENT` which means a color will be auto-assigned. #[inline] pub fn color(mut self, color: impl Into) -> Self { self.stroke.color = color.into(); self } /// Set the line's style. Default is `LineStyle::Solid`. #[inline] pub fn style(mut self, style: LineStyle) -> Self { self.style = style; self } /// Name of this vertical line. /// /// This name will show up in the plot legend, if legends are turned on. /// /// Multiple plot items may share the same name, in which case they will also share an entry in /// the legend. #[allow(clippy::needless_pass_by_value)] #[inline] pub fn name(mut self, name: impl ToString) -> Self { self.name = name.to_string(); self } } impl PlotItem for VLine { fn shapes(&self, ui: &mut Ui, transform: &PlotTransform, shapes: &mut Vec) { let Self { x, stroke, highlight, style, .. } = self; // Round to minimize aliasing: let points = vec![ ui.painter().round_pos_to_pixels( transform.position_from_point(&PlotPoint::new(*x, transform.bounds().min[1])), ), ui.painter().round_pos_to_pixels( transform.position_from_point(&PlotPoint::new(*x, transform.bounds().max[1])), ), ]; style.style_line(points, *stroke, *highlight, shapes); } fn initialize(&mut self, _x_range: RangeInclusive) {} fn name(&self) -> &str { &self.name } fn color(&self) -> Color32 { self.stroke.color } fn highlight(&mut self) { self.highlight = true; } fn highlighted(&self) -> bool { self.highlight } fn geometry(&self) -> PlotGeometry<'_> { PlotGeometry::None } fn bounds(&self) -> PlotBounds { let mut bounds = PlotBounds::NOTHING; bounds.min[0] = self.x; bounds.max[0] = self.x; bounds } } /// A series of values forming a path. pub struct Line { pub(super) series: PlotPoints, pub(super) stroke: Stroke, pub(super) name: String, pub(super) highlight: bool, pub(super) fill: Option, pub(super) style: LineStyle, } impl Line { pub fn new(series: impl Into) -> Self { Self { series: series.into(), stroke: Stroke::new(1.0, Color32::TRANSPARENT), name: Default::default(), highlight: false, fill: None, style: LineStyle::Solid, } } /// Highlight this line in the plot by scaling up the line. #[inline] pub fn highlight(mut self, highlight: bool) -> Self { self.highlight = highlight; self } /// Add a stroke. #[inline] pub fn stroke(mut self, stroke: impl Into) -> Self { self.stroke = stroke.into(); self } /// Stroke width. A high value means the plot thickens. #[inline] pub fn width(mut self, width: impl Into) -> Self { self.stroke.width = width.into(); self } /// Stroke color. Default is `Color32::TRANSPARENT` which means a color will be auto-assigned. #[inline] pub fn color(mut self, color: impl Into) -> Self { self.stroke.color = color.into(); self } /// Fill the area between this line and a given horizontal reference line. #[inline] pub fn fill(mut self, y_reference: impl Into) -> Self { self.fill = Some(y_reference.into()); self } /// Set the line's style. Default is `LineStyle::Solid`. #[inline] pub fn style(mut self, style: LineStyle) -> Self { self.style = style; self } /// Name of this line. /// /// This name will show up in the plot legend, if legends are turned on. /// /// Multiple plot items may share the same name, in which case they will also share an entry in /// the legend. #[allow(clippy::needless_pass_by_value)] #[inline] pub fn name(mut self, name: impl ToString) -> Self { self.name = name.to_string(); self } } /// Returns the x-coordinate of a possible intersection between a line segment from `p1` to `p2` and /// a horizontal line at the given y-coordinate. fn y_intersection(p1: &Pos2, p2: &Pos2, y: f32) -> Option { ((p1.y > y && p2.y < y) || (p1.y < y && p2.y > y)) .then_some(((y * (p1.x - p2.x)) - (p1.x * p2.y - p1.y * p2.x)) / (p1.y - p2.y)) } impl PlotItem for Line { fn shapes(&self, _ui: &mut Ui, transform: &PlotTransform, shapes: &mut Vec) { let Self { series, stroke, highlight, mut fill, style, .. } = self; let values_tf: Vec<_> = series .points() .iter() .map(|v| transform.position_from_point(v)) .collect(); let n_values = values_tf.len(); // Fill the area between the line and a reference line, if required. if n_values < 2 { fill = None; } if let Some(y_reference) = fill { let mut fill_alpha = DEFAULT_FILL_ALPHA; if *highlight { fill_alpha = (2.0 * fill_alpha).at_most(1.0); } let y = transform .position_from_point(&PlotPoint::new(0.0, y_reference)) .y; let fill_color = Rgba::from(stroke.color) .to_opaque() .multiply(fill_alpha) .into(); let mut mesh = Mesh::default(); let expected_intersections = 20; mesh.reserve_triangles((n_values - 1) * 2); mesh.reserve_vertices(n_values * 2 + expected_intersections); values_tf.windows(2).for_each(|w| { let i = mesh.vertices.len() as u32; mesh.colored_vertex(w[0], fill_color); mesh.colored_vertex(pos2(w[0].x, y), fill_color); if let Some(x) = y_intersection(&w[0], &w[1], y) { let point = pos2(x, y); mesh.colored_vertex(point, fill_color); mesh.add_triangle(i, i + 1, i + 2); mesh.add_triangle(i + 2, i + 3, i + 4); } else { mesh.add_triangle(i, i + 1, i + 2); mesh.add_triangle(i + 1, i + 2, i + 3); } }); let last = values_tf[n_values - 1]; mesh.colored_vertex(last, fill_color); mesh.colored_vertex(pos2(last.x, y), fill_color); shapes.push(Shape::Mesh(mesh)); } style.style_line(values_tf, *stroke, *highlight, shapes); } fn initialize(&mut self, x_range: RangeInclusive) { self.series.generate_points(x_range); } fn name(&self) -> &str { self.name.as_str() } fn color(&self) -> Color32 { self.stroke.color } fn highlight(&mut self) { self.highlight = true; } fn highlighted(&self) -> bool { self.highlight } fn geometry(&self) -> PlotGeometry<'_> { PlotGeometry::Points(self.series.points()) } fn bounds(&self) -> PlotBounds { self.series.bounds() } } /// A convex polygon. pub struct Polygon { pub(super) series: PlotPoints, pub(super) stroke: Stroke, pub(super) name: String, pub(super) highlight: bool, pub(super) fill_color: Option, pub(super) style: LineStyle, } impl Polygon { pub fn new(series: impl Into) -> Self { Self { series: series.into(), stroke: Stroke::new(1.0, Color32::TRANSPARENT), name: Default::default(), highlight: false, fill_color: None, style: LineStyle::Solid, } } /// Highlight this polygon in the plot by scaling up the stroke and reducing the fill /// transparency. #[inline] pub fn highlight(mut self, highlight: bool) -> Self { self.highlight = highlight; self } /// Add a custom stroke. #[inline] pub fn stroke(mut self, stroke: impl Into) -> Self { self.stroke = stroke.into(); self } /// Set the stroke width. #[inline] pub fn width(mut self, width: impl Into) -> Self { self.stroke.width = width.into(); self } /// Fill color. Defaults to the stroke color with added transparency. #[inline] pub fn fill_color(mut self, color: impl Into) -> Self { self.fill_color = Some(color.into()); self } /// Set the outline's style. Default is `LineStyle::Solid`. #[inline] pub fn style(mut self, style: LineStyle) -> Self { self.style = style; self } /// Name of this polygon. /// /// This name will show up in the plot legend, if legends are turned on. /// /// Multiple plot items may share the same name, in which case they will also share an entry in /// the legend. #[allow(clippy::needless_pass_by_value)] #[inline] pub fn name(mut self, name: impl ToString) -> Self { self.name = name.to_string(); self } } impl PlotItem for Polygon { fn shapes(&self, _ui: &mut Ui, transform: &PlotTransform, shapes: &mut Vec) { let Self { series, stroke, highlight, fill_color, style, .. } = self; let mut values_tf: Vec<_> = series .points() .iter() .map(|v| transform.position_from_point(v)) .collect(); let fill_color = fill_color.unwrap_or(stroke.color.linear_multiply(DEFAULT_FILL_ALPHA)); let shape = Shape::convex_polygon(values_tf.clone(), fill_color, Stroke::NONE); shapes.push(shape); values_tf.push(*values_tf.first().unwrap()); style.style_line(values_tf, *stroke, *highlight, shapes); } fn initialize(&mut self, x_range: RangeInclusive) { self.series.generate_points(x_range); } fn name(&self) -> &str { self.name.as_str() } fn color(&self) -> Color32 { self.stroke.color } fn highlight(&mut self) { self.highlight = true; } fn highlighted(&self) -> bool { self.highlight } fn geometry(&self) -> PlotGeometry<'_> { PlotGeometry::Points(self.series.points()) } fn bounds(&self) -> PlotBounds { self.series.bounds() } } /// Text inside the plot. #[derive(Clone)] pub struct Text { pub(super) text: WidgetText, pub(super) position: PlotPoint, pub(super) name: String, pub(super) highlight: bool, pub(super) color: Color32, pub(super) anchor: Align2, } impl Text { pub fn new(position: PlotPoint, text: impl Into) -> Self { Self { text: text.into(), position, name: Default::default(), highlight: false, color: Color32::TRANSPARENT, anchor: Align2::CENTER_CENTER, } } /// Highlight this text in the plot by drawing a rectangle around it. #[inline] pub fn highlight(mut self, highlight: bool) -> Self { self.highlight = highlight; self } /// Text color. #[inline] pub fn color(mut self, color: impl Into) -> Self { self.color = color.into(); self } /// Anchor position of the text. Default is `Align2::CENTER_CENTER`. #[inline] pub fn anchor(mut self, anchor: Align2) -> Self { self.anchor = anchor; self } /// Name of this text. /// /// This name will show up in the plot legend, if legends are turned on. /// /// Multiple plot items may share the same name, in which case they will also share an entry in /// the legend. #[allow(clippy::needless_pass_by_value)] #[inline] pub fn name(mut self, name: impl ToString) -> Self { self.name = name.to_string(); self } } impl PlotItem for Text { fn shapes(&self, ui: &mut Ui, transform: &PlotTransform, shapes: &mut Vec) { let color = if self.color == Color32::TRANSPARENT { ui.style().visuals.text_color() } else { self.color }; let galley = self.text .clone() .into_galley(ui, Some(false), f32::INFINITY, TextStyle::Small); let pos = transform.position_from_point(&self.position); let rect = self.anchor.anchor_size(pos, galley.size()); shapes.push(epaint::TextShape::new(rect.min, galley, color).into()); if self.highlight { shapes.push(Shape::rect_stroke( rect.expand(2.0), 1.0, Stroke::new(0.5, color), )); } } fn initialize(&mut self, _x_range: RangeInclusive) {} fn name(&self) -> &str { self.name.as_str() } fn color(&self) -> Color32 { self.color } fn highlight(&mut self) { self.highlight = true; } fn highlighted(&self) -> bool { self.highlight } fn geometry(&self) -> PlotGeometry<'_> { PlotGeometry::None } fn bounds(&self) -> PlotBounds { let mut bounds = PlotBounds::NOTHING; bounds.extend_with(&self.position); bounds } } /// A set of points. pub struct Points { pub(super) series: PlotPoints, pub(super) shape: MarkerShape, /// Color of the marker. `Color32::TRANSPARENT` means that it will be picked automatically. pub(super) color: Color32, /// Whether to fill the marker. Does not apply to all types. pub(super) filled: bool, /// The maximum extent of the marker from its center. pub(super) radius: f32, pub(super) name: String, pub(super) highlight: bool, pub(super) stems: Option, } impl Points { pub fn new(series: impl Into) -> Self { Self { series: series.into(), shape: MarkerShape::Circle, color: Color32::TRANSPARENT, filled: true, radius: 1.0, name: Default::default(), highlight: false, stems: None, } } /// Set the shape of the markers. #[inline] pub fn shape(mut self, shape: MarkerShape) -> Self { self.shape = shape; self } /// Highlight these points in the plot by scaling up their markers. #[inline] pub fn highlight(mut self, highlight: bool) -> Self { self.highlight = highlight; self } /// Set the marker's color. #[inline] pub fn color(mut self, color: impl Into) -> Self { self.color = color.into(); self } /// Whether to fill the marker. #[inline] pub fn filled(mut self, filled: bool) -> Self { self.filled = filled; self } /// Whether to add stems between the markers and a horizontal reference line. #[inline] pub fn stems(mut self, y_reference: impl Into) -> Self { self.stems = Some(y_reference.into()); self } /// Set the maximum extent of the marker around its position. #[inline] pub fn radius(mut self, radius: impl Into) -> Self { self.radius = radius.into(); self } /// Name of this set of points. /// /// This name will show up in the plot legend, if legends are turned on. /// /// Multiple plot items may share the same name, in which case they will also share an entry in /// the legend. #[allow(clippy::needless_pass_by_value)] #[inline] pub fn name(mut self, name: impl ToString) -> Self { self.name = name.to_string(); self } } impl PlotItem for Points { fn shapes(&self, _ui: &mut Ui, transform: &PlotTransform, shapes: &mut Vec) { let sqrt_3 = 3_f32.sqrt(); let frac_sqrt_3_2 = 3_f32.sqrt() / 2.0; let frac_1_sqrt_2 = 1.0 / 2_f32.sqrt(); let Self { series, shape, color, filled, mut radius, highlight, stems, .. } = self; let stroke_size = radius / 5.0; let default_stroke = Stroke::new(stroke_size, *color); let mut stem_stroke = default_stroke; let (fill, stroke) = if *filled { (*color, Stroke::NONE) } else { (Color32::TRANSPARENT, default_stroke) }; if *highlight { radius *= 2f32.sqrt(); stem_stroke.width *= 2.0; } let y_reference = stems.map(|y| transform.position_from_point(&PlotPoint::new(0.0, y)).y); series .points() .iter() .map(|value| transform.position_from_point(value)) .for_each(|center| { let tf = |dx: f32, dy: f32| -> Pos2 { center + radius * vec2(dx, dy) }; if let Some(y) = y_reference { let stem = Shape::line_segment([center, pos2(center.x, y)], stem_stroke); shapes.push(stem); } match shape { MarkerShape::Circle => { shapes.push(Shape::Circle(epaint::CircleShape { center, radius, fill, stroke, })); } MarkerShape::Diamond => { let points = vec![ tf(0.0, 1.0), // bottom tf(-1.0, 0.0), // left tf(0.0, -1.0), // top tf(1.0, 0.0), // right ]; shapes.push(Shape::convex_polygon(points, fill, stroke)); } MarkerShape::Square => { let points = vec![ tf(-frac_1_sqrt_2, frac_1_sqrt_2), tf(-frac_1_sqrt_2, -frac_1_sqrt_2), tf(frac_1_sqrt_2, -frac_1_sqrt_2), tf(frac_1_sqrt_2, frac_1_sqrt_2), ]; shapes.push(Shape::convex_polygon(points, fill, stroke)); } MarkerShape::Cross => { let diagonal1 = [ tf(-frac_1_sqrt_2, -frac_1_sqrt_2), tf(frac_1_sqrt_2, frac_1_sqrt_2), ]; let diagonal2 = [ tf(frac_1_sqrt_2, -frac_1_sqrt_2), tf(-frac_1_sqrt_2, frac_1_sqrt_2), ]; shapes.push(Shape::line_segment(diagonal1, default_stroke)); shapes.push(Shape::line_segment(diagonal2, default_stroke)); } MarkerShape::Plus => { let horizontal = [tf(-1.0, 0.0), tf(1.0, 0.0)]; let vertical = [tf(0.0, -1.0), tf(0.0, 1.0)]; shapes.push(Shape::line_segment(horizontal, default_stroke)); shapes.push(Shape::line_segment(vertical, default_stroke)); } MarkerShape::Up => { let points = vec![tf(0.0, -1.0), tf(0.5 * sqrt_3, 0.5), tf(-0.5 * sqrt_3, 0.5)]; shapes.push(Shape::convex_polygon(points, fill, stroke)); } MarkerShape::Down => { let points = vec![ tf(0.0, 1.0), tf(-0.5 * sqrt_3, -0.5), tf(0.5 * sqrt_3, -0.5), ]; shapes.push(Shape::convex_polygon(points, fill, stroke)); } MarkerShape::Left => { let points = vec![tf(-1.0, 0.0), tf(0.5, -0.5 * sqrt_3), tf(0.5, 0.5 * sqrt_3)]; shapes.push(Shape::convex_polygon(points, fill, stroke)); } MarkerShape::Right => { let points = vec![ tf(1.0, 0.0), tf(-0.5, 0.5 * sqrt_3), tf(-0.5, -0.5 * sqrt_3), ]; shapes.push(Shape::convex_polygon(points, fill, stroke)); } MarkerShape::Asterisk => { let vertical = [tf(0.0, -1.0), tf(0.0, 1.0)]; let diagonal1 = [tf(-frac_sqrt_3_2, 0.5), tf(frac_sqrt_3_2, -0.5)]; let diagonal2 = [tf(-frac_sqrt_3_2, -0.5), tf(frac_sqrt_3_2, 0.5)]; shapes.push(Shape::line_segment(vertical, default_stroke)); shapes.push(Shape::line_segment(diagonal1, default_stroke)); shapes.push(Shape::line_segment(diagonal2, default_stroke)); } } }); } fn initialize(&mut self, x_range: RangeInclusive) { self.series.generate_points(x_range); } fn name(&self) -> &str { self.name.as_str() } fn color(&self) -> Color32 { self.color } fn highlight(&mut self) { self.highlight = true; } fn highlighted(&self) -> bool { self.highlight } fn geometry(&self) -> PlotGeometry<'_> { PlotGeometry::Points(self.series.points()) } fn bounds(&self) -> PlotBounds { self.series.bounds() } } /// A set of arrows. pub struct Arrows { pub(super) origins: PlotPoints, pub(super) tips: PlotPoints, pub(super) tip_length: Option, pub(super) color: Color32, pub(super) name: String, pub(super) highlight: bool, } impl Arrows { pub fn new(origins: impl Into, tips: impl Into) -> Self { Self { origins: origins.into(), tips: tips.into(), tip_length: None, color: Color32::TRANSPARENT, name: Default::default(), highlight: false, } } /// Highlight these arrows in the plot. #[inline] pub fn highlight(mut self, highlight: bool) -> Self { self.highlight = highlight; self } /// Set the length of the arrow tips #[inline] pub fn tip_length(mut self, tip_length: f32) -> Self { self.tip_length = Some(tip_length); self } /// Set the arrows' color. #[inline] pub fn color(mut self, color: impl Into) -> Self { self.color = color.into(); self } /// Name of this set of arrows. /// /// This name will show up in the plot legend, if legends are turned on. /// /// Multiple plot items may share the same name, in which case they will also share an entry in /// the legend. #[allow(clippy::needless_pass_by_value)] #[inline] pub fn name(mut self, name: impl ToString) -> Self { self.name = name.to_string(); self } } impl PlotItem for Arrows { fn shapes(&self, _ui: &mut Ui, transform: &PlotTransform, shapes: &mut Vec) { use crate::emath::*; let Self { origins, tips, tip_length, color, highlight, .. } = self; let stroke = Stroke::new(if *highlight { 2.0 } else { 1.0 }, *color); origins .points() .iter() .zip(tips.points().iter()) .map(|(origin, tip)| { ( transform.position_from_point(origin), transform.position_from_point(tip), ) }) .for_each(|(origin, tip)| { let vector = tip - origin; let rot = Rot2::from_angle(std::f32::consts::TAU / 10.0); let tip_length = if let Some(tip_length) = tip_length { *tip_length } else { vector.length() / 4.0 }; let tip = origin + vector; let dir = vector.normalized(); shapes.push(Shape::line_segment([origin, tip], stroke)); shapes.push(Shape::line( vec![ tip - tip_length * (rot.inverse() * dir), tip, tip - tip_length * (rot * dir), ], stroke, )); }); } fn initialize(&mut self, _x_range: RangeInclusive) { self.origins .generate_points(f64::NEG_INFINITY..=f64::INFINITY); self.tips.generate_points(f64::NEG_INFINITY..=f64::INFINITY); } fn name(&self) -> &str { self.name.as_str() } fn color(&self) -> Color32 { self.color } fn highlight(&mut self) { self.highlight = true; } fn highlighted(&self) -> bool { self.highlight } fn geometry(&self) -> PlotGeometry<'_> { PlotGeometry::Points(self.origins.points()) } fn bounds(&self) -> PlotBounds { self.origins.bounds() } } /// An image in the plot. #[derive(Clone)] pub struct PlotImage { pub(super) position: PlotPoint, pub(super) texture_id: TextureId, pub(super) uv: Rect, pub(super) size: Vec2, pub(crate) rotation: f64, pub(super) bg_fill: Color32, pub(super) tint: Color32, pub(super) highlight: bool, pub(super) name: String, } impl PlotImage { /// Create a new image with position and size in plot coordinates. pub fn new( texture_id: impl Into, center_position: PlotPoint, size: impl Into, ) -> Self { Self { position: center_position, name: Default::default(), highlight: false, texture_id: texture_id.into(), uv: Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), size: size.into(), rotation: 0.0, bg_fill: Default::default(), tint: Color32::WHITE, } } /// Highlight this image in the plot. #[inline] pub fn highlight(mut self, highlight: bool) -> Self { self.highlight = highlight; self } /// Select UV range. Default is (0,0) in top-left, (1,1) bottom right. #[inline] pub fn uv(mut self, uv: impl Into) -> Self { self.uv = uv.into(); self } /// A solid color to put behind the image. Useful for transparent images. #[inline] pub fn bg_fill(mut self, bg_fill: impl Into) -> Self { self.bg_fill = bg_fill.into(); self } /// Multiply image color with this. Default is WHITE (no tint). #[inline] pub fn tint(mut self, tint: impl Into) -> Self { self.tint = tint.into(); self } /// Name of this image. /// /// This name will show up in the plot legend, if legends are turned on. /// /// Multiple plot items may share the same name, in which case they will also share an entry in /// the legend. #[allow(clippy::needless_pass_by_value)] #[inline] pub fn name(mut self, name: impl ToString) -> Self { self.name = name.to_string(); self } /// Rotate the image counter-clockwise around its center by an angle in radians. #[inline] pub fn rotate(mut self, angle: f64) -> Self { self.rotation = angle; self } } impl PlotItem for PlotImage { fn shapes(&self, ui: &mut Ui, transform: &PlotTransform, shapes: &mut Vec) { let Self { position, rotation, texture_id, uv, size, bg_fill, tint, highlight, .. } = self; let image_screen_rect = { let left_top = PlotPoint::new( position.x - 0.5 * size.x as f64, position.y - 0.5 * size.y as f64, ); let right_bottom = PlotPoint::new( position.x + 0.5 * size.x as f64, position.y + 0.5 * size.y as f64, ); let left_top_screen = transform.position_from_point(&left_top); let right_bottom_screen = transform.position_from_point(&right_bottom); Rect::from_two_pos(left_top_screen, right_bottom_screen) }; let screen_rotation = -*rotation as f32; egui::paint_texture_at( ui.painter(), image_screen_rect, &ImageOptions { uv: *uv, bg_fill: *bg_fill, tint: *tint, rotation: Some((Rot2::from_angle(screen_rotation), Vec2::splat(0.5))), rounding: Rounding::ZERO, }, &(*texture_id, image_screen_rect.size()).into(), ); if *highlight { let center = image_screen_rect.center(); let rotation = Rot2::from_angle(screen_rotation); let outline = [ image_screen_rect.right_bottom(), image_screen_rect.right_top(), image_screen_rect.left_top(), image_screen_rect.left_bottom(), ] .iter() .map(|point| center + rotation * (*point - center)) .collect(); shapes.push(Shape::closed_line( outline, Stroke::new(1.0, ui.visuals().strong_text_color()), )); } } fn initialize(&mut self, _x_range: RangeInclusive) {} fn name(&self) -> &str { self.name.as_str() } fn color(&self) -> Color32 { Color32::TRANSPARENT } fn highlight(&mut self) { self.highlight = true; } fn highlighted(&self) -> bool { self.highlight } fn geometry(&self) -> PlotGeometry<'_> { PlotGeometry::None } fn bounds(&self) -> PlotBounds { let mut bounds = PlotBounds::NOTHING; let left_top = PlotPoint::new( self.position.x as f32 - self.size.x / 2.0, self.position.y as f32 - self.size.y / 2.0, ); let right_bottom = PlotPoint::new( self.position.x as f32 + self.size.x / 2.0, self.position.y as f32 + self.size.y / 2.0, ); bounds.extend_with(&left_top); bounds.extend_with(&right_bottom); bounds } } // ---------------------------------------------------------------------------- /// A bar chart. pub struct BarChart { pub(super) bars: Vec, pub(super) default_color: Color32, pub(super) name: String, /// A custom element formatter pub(super) element_formatter: Option String>>, highlight: bool, } impl BarChart { /// Create a bar chart. It defaults to vertically oriented elements. pub fn new(bars: Vec) -> Self { Self { bars, default_color: Color32::TRANSPARENT, name: String::new(), element_formatter: None, highlight: false, } } /// Set the default color. It is set on all elements that do not already have a specific color. /// This is the color that shows up in the legend. /// It can be overridden at the bar level (see [[`Bar`]]). /// Default is `Color32::TRANSPARENT` which means a color will be auto-assigned. #[inline] pub fn color(mut self, color: impl Into) -> Self { let plot_color = color.into(); self.default_color = plot_color; for b in &mut self.bars { if b.fill == Color32::TRANSPARENT && b.stroke.color == Color32::TRANSPARENT { b.fill = plot_color.linear_multiply(0.2); b.stroke.color = plot_color; } } self } /// Name of this chart. /// /// This name will show up in the plot legend, if legends are turned on. Multiple charts may /// share the same name, in which case they will also share an entry in the legend. #[allow(clippy::needless_pass_by_value)] #[inline] pub fn name(mut self, name: impl ToString) -> Self { self.name = name.to_string(); self } /// Set all elements to be in a vertical orientation. /// Argument axis will be X and bar values will be on the Y axis. #[inline] pub fn vertical(mut self) -> Self { for b in &mut self.bars { b.orientation = Orientation::Vertical; } self } /// Set all elements to be in a horizontal orientation. /// Argument axis will be Y and bar values will be on the X axis. #[inline] pub fn horizontal(mut self) -> Self { for b in &mut self.bars { b.orientation = Orientation::Horizontal; } self } /// Set the width (thickness) of all its elements. #[inline] pub fn width(mut self, width: f64) -> Self { for b in &mut self.bars { b.bar_width = width; } self } /// Highlight all plot elements. #[inline] pub fn highlight(mut self, highlight: bool) -> Self { self.highlight = highlight; self } /// Add a custom way to format an element. /// Can be used to display a set number of decimals or custom labels. #[inline] pub fn element_formatter(mut self, formatter: Box String>) -> Self { self.element_formatter = Some(formatter); self } /// Stacks the bars on top of another chart. /// Positive values are stacked on top of other positive values. /// Negative values are stacked below other negative values. #[inline] pub fn stack_on(mut self, others: &[&Self]) -> Self { for (index, bar) in self.bars.iter_mut().enumerate() { let new_base_offset = if bar.value.is_sign_positive() { others .iter() .filter_map(|other_chart| other_chart.bars.get(index).map(|bar| bar.upper())) .max_by_key(|value| value.ord()) } else { others .iter() .filter_map(|other_chart| other_chart.bars.get(index).map(|bar| bar.lower())) .min_by_key(|value| value.ord()) }; if let Some(value) = new_base_offset { bar.base_offset = Some(value); } } self } } impl PlotItem for BarChart { fn shapes(&self, _ui: &mut Ui, transform: &PlotTransform, shapes: &mut Vec) { for b in &self.bars { b.add_shapes(transform, self.highlight, shapes); } } fn initialize(&mut self, _x_range: RangeInclusive) { // nothing to do } fn name(&self) -> &str { self.name.as_str() } fn color(&self) -> Color32 { self.default_color } fn highlight(&mut self) { self.highlight = true; } fn highlighted(&self) -> bool { self.highlight } fn geometry(&self) -> PlotGeometry<'_> { PlotGeometry::Rects } fn bounds(&self) -> PlotBounds { let mut bounds = PlotBounds::NOTHING; for b in &self.bars { bounds.merge(&b.bounds()); } bounds } fn find_closest(&self, point: Pos2, transform: &PlotTransform) -> Option { find_closest_rect(&self.bars, point, transform) } fn on_hover( &self, elem: ClosestElem, shapes: &mut Vec, cursors: &mut Vec, plot: &PlotConfig<'_>, _: &LabelFormatter, ) { let bar = &self.bars[elem.index]; bar.add_shapes(plot.transform, true, shapes); bar.add_rulers_and_text(self, plot, shapes, cursors); } } /// A diagram containing a series of [`BoxElem`] elements. pub struct BoxPlot { pub(super) boxes: Vec, pub(super) default_color: Color32, pub(super) name: String, /// A custom element formatter pub(super) element_formatter: Option String>>, highlight: bool, } impl BoxPlot { /// Create a plot containing multiple `boxes`. It defaults to vertically oriented elements. pub fn new(boxes: Vec) -> Self { Self { boxes, default_color: Color32::TRANSPARENT, name: String::new(), element_formatter: None, highlight: false, } } /// Set the default color. It is set on all elements that do not already have a specific color. /// This is the color that shows up in the legend. /// It can be overridden at the element level (see [`BoxElem`]). /// Default is `Color32::TRANSPARENT` which means a color will be auto-assigned. #[inline] pub fn color(mut self, color: impl Into) -> Self { let plot_color = color.into(); self.default_color = plot_color; for box_elem in &mut self.boxes { if box_elem.fill == Color32::TRANSPARENT && box_elem.stroke.color == Color32::TRANSPARENT { box_elem.fill = plot_color.linear_multiply(0.2); box_elem.stroke.color = plot_color; } } self } /// Name of this box plot diagram. /// /// This name will show up in the plot legend, if legends are turned on. Multiple series may /// share the same name, in which case they will also share an entry in the legend. #[allow(clippy::needless_pass_by_value)] #[inline] pub fn name(mut self, name: impl ToString) -> Self { self.name = name.to_string(); self } /// Set all elements to be in a vertical orientation. /// Argument axis will be X and values will be on the Y axis. #[inline] pub fn vertical(mut self) -> Self { for box_elem in &mut self.boxes { box_elem.orientation = Orientation::Vertical; } self } /// Set all elements to be in a horizontal orientation. /// Argument axis will be Y and values will be on the X axis. #[inline] pub fn horizontal(mut self) -> Self { for box_elem in &mut self.boxes { box_elem.orientation = Orientation::Horizontal; } self } /// Highlight all plot elements. #[inline] pub fn highlight(mut self, highlight: bool) -> Self { self.highlight = highlight; self } /// Add a custom way to format an element. /// Can be used to display a set number of decimals or custom labels. #[inline] pub fn element_formatter(mut self, formatter: Box String>) -> Self { self.element_formatter = Some(formatter); self } } impl PlotItem for BoxPlot { fn shapes(&self, _ui: &mut Ui, transform: &PlotTransform, shapes: &mut Vec) { for b in &self.boxes { b.add_shapes(transform, self.highlight, shapes); } } fn initialize(&mut self, _x_range: RangeInclusive) { // nothing to do } fn name(&self) -> &str { self.name.as_str() } fn color(&self) -> Color32 { self.default_color } fn highlight(&mut self) { self.highlight = true; } fn highlighted(&self) -> bool { self.highlight } fn geometry(&self) -> PlotGeometry<'_> { PlotGeometry::Rects } fn bounds(&self) -> PlotBounds { let mut bounds = PlotBounds::NOTHING; for b in &self.boxes { bounds.merge(&b.bounds()); } bounds } fn find_closest(&self, point: Pos2, transform: &PlotTransform) -> Option { find_closest_rect(&self.boxes, point, transform) } fn on_hover( &self, elem: ClosestElem, shapes: &mut Vec, cursors: &mut Vec, plot: &PlotConfig<'_>, _: &LabelFormatter, ) { let box_plot = &self.boxes[elem.index]; box_plot.add_shapes(plot.transform, true, shapes); box_plot.add_rulers_and_text(self, plot, shapes, cursors); } } // ---------------------------------------------------------------------------- // Helper functions pub(crate) fn rulers_color(ui: &Ui) -> Color32 { if ui.visuals().dark_mode { Color32::from_gray(100).additive() } else { Color32::from_black_alpha(180) } } pub(crate) fn vertical_line( pointer: Pos2, transform: &PlotTransform, line_color: Color32, ) -> Shape { let frame = transform.frame(); Shape::line_segment( [ pos2(pointer.x, frame.top()), pos2(pointer.x, frame.bottom()), ], (1.0, line_color), ) } pub(crate) fn horizontal_line( pointer: Pos2, transform: &PlotTransform, line_color: Color32, ) -> Shape { let frame = transform.frame(); Shape::line_segment( [ pos2(frame.left(), pointer.y), pos2(frame.right(), pointer.y), ], (1.0, line_color), ) } fn add_rulers_and_text( elem: &dyn RectElement, plot: &PlotConfig<'_>, text: Option, shapes: &mut Vec, cursors: &mut Vec, ) { let orientation = elem.orientation(); let show_argument = plot.show_x && orientation == Orientation::Vertical || plot.show_y && orientation == Orientation::Horizontal; let show_values = plot.show_y && orientation == Orientation::Vertical || plot.show_x && orientation == Orientation::Horizontal; // Rulers for argument (usually vertical) if show_argument { for pos in elem.arguments_with_ruler() { cursors.push(match orientation { Orientation::Horizontal => Cursor::Horizontal { y: pos.y }, Orientation::Vertical => Cursor::Vertical { x: pos.x }, }); } } // Rulers for values (usually horizontal) if show_values { for pos in elem.values_with_ruler() { cursors.push(match orientation { Orientation::Horizontal => Cursor::Vertical { x: pos.x }, Orientation::Vertical => Cursor::Horizontal { y: pos.y }, }); } } // Text let text = text.unwrap_or({ let mut text = elem.name().to_owned(); // could be empty if show_values { text.push('\n'); text.push_str(&elem.default_values_format(plot.transform)); } text }); let font_id = TextStyle::Body.resolve(plot.ui.style()); let corner_value = elem.corner_value(); plot.ui.fonts(|f| { shapes.push(Shape::text( f, plot.transform.position_from_point(&corner_value) + vec2(3.0, -2.0), Align2::LEFT_BOTTOM, text, font_id, plot.ui.visuals().text_color(), )); }); } /// Draws a cross of horizontal and vertical ruler at the `pointer` position. /// `value` is used to for text displaying X/Y coordinates. #[allow(clippy::too_many_arguments)] pub(super) fn rulers_at_value( pointer: Pos2, value: PlotPoint, name: &str, plot: &PlotConfig<'_>, shapes: &mut Vec, cursors: &mut Vec, label_formatter: &LabelFormatter, ) { if plot.show_x { cursors.push(Cursor::Vertical { x: value.x }); } if plot.show_y { cursors.push(Cursor::Horizontal { y: value.y }); } let prefix = if name.is_empty() { String::new() } else { format!("{name}\n") }; let text = { let scale = plot.transform.dvalue_dpos(); let x_decimals = ((-scale[0].abs().log10()).ceil().at_least(0.0) as usize).clamp(1, 6); let y_decimals = ((-scale[1].abs().log10()).ceil().at_least(0.0) as usize).clamp(1, 6); if let Some(custom_label) = label_formatter { custom_label(name, &value) } else if plot.show_x && plot.show_y { format!( "{}x = {:.*}\ny = {:.*}", prefix, x_decimals, value.x, y_decimals, value.y ) } else if plot.show_x { format!("{}x = {:.*}", prefix, x_decimals, value.x) } else if plot.show_y { format!("{}y = {:.*}", prefix, y_decimals, value.y) } else { unreachable!() } }; let font_id = TextStyle::Body.resolve(plot.ui.style()); plot.ui.fonts(|f| { shapes.push(Shape::text( f, pointer + vec2(3.0, -2.0), Align2::LEFT_BOTTOM, text, font_id, plot.ui.visuals().text_color(), )); }); } fn find_closest_rect<'a, T>( rects: impl IntoIterator, point: Pos2, transform: &PlotTransform, ) -> Option where T: 'a + RectElement, { rects .into_iter() .enumerate() .map(|(index, bar)| { let bar_rect = transform.rect_from_values(&bar.bounds_min(), &bar.bounds_max()); let dist_sq = bar_rect.distance_sq_to_pos(point); ClosestElem { index, dist_sq } }) .min_by_key(|e| e.dist_sq.ord()) }