egui_plot 0.29.0

Immediate mode plotting for the egui GUI library
Documentation
use egui::emath::NumExt as _;
use egui::epaint::{Color32, RectShape, Rounding, Shape, Stroke};

use crate::{BoxPlot, Cursor, PlotPoint, PlotTransform};

use super::{add_rulers_and_text, highlighted_color, Orientation, PlotConfig, RectElement};

/// Contains the values of a single box in a box plot.
#[derive(Clone, Debug, PartialEq)]
pub struct BoxSpread {
    /// Value of lower whisker (typically minimum).
    ///
    /// The whisker is not drawn if `lower_whisker >= quartile1`.
    pub lower_whisker: f64,

    /// Value of lower box threshold (typically 25% quartile)
    pub quartile1: f64,

    /// Value of middle line in box (typically median)
    pub median: f64,

    /// Value of upper box threshold (typically 75% quartile)
    pub quartile3: f64,

    /// Value of upper whisker (typically maximum)
    ///
    /// The whisker is not drawn if `upper_whisker <= quartile3`.
    pub upper_whisker: f64,
}

impl BoxSpread {
    pub fn new(
        lower_whisker: f64,
        quartile1: f64,
        median: f64,
        quartile3: f64,
        upper_whisker: f64,
    ) -> Self {
        Self {
            lower_whisker,
            quartile1,
            median,
            quartile3,
            upper_whisker,
        }
    }
}

/// A box in a [`BoxPlot`] diagram. This is a low level graphical element; it will not compute quartiles and whiskers,
/// letting one use their preferred formula. Use [`Points`][`super::Points`] to draw the outliers.
#[derive(Clone, Debug, PartialEq)]
pub struct BoxElem {
    /// Name of plot element in the diagram (annotated by default formatter).
    pub name: String,

    /// Which direction the box faces in the diagram.
    pub orientation: Orientation,

    /// Position on the argument (input) axis -- X if vertical, Y if horizontal.
    pub argument: f64,

    /// Values of the box
    pub spread: BoxSpread,

    /// Thickness of the box
    pub box_width: f64,

    /// Width of the whisker at minimum/maximum
    pub whisker_width: f64,

    /// Line width and color
    pub stroke: Stroke,

    /// Fill color
    pub fill: Color32,
}

impl BoxElem {
    /// Create a box element. Its `orientation` is set by its [`BoxPlot`] parent.
    ///
    /// Check [`BoxElem`] fields for detailed description.
    pub fn new(argument: f64, spread: BoxSpread) -> Self {
        Self {
            argument,
            orientation: Orientation::default(),
            name: String::default(),
            spread,
            box_width: 0.25,
            whisker_width: 0.15,
            stroke: Stroke::new(1.0, Color32::TRANSPARENT),
            fill: Color32::TRANSPARENT,
        }
    }

    /// Name of this box element.
    #[allow(clippy::needless_pass_by_value)]
    #[inline]
    pub fn name(mut self, name: impl ToString) -> Self {
        self.name = name.to_string();
        self
    }

    /// Add a custom stroke.
    #[inline]
    pub fn stroke(mut self, stroke: impl Into<Stroke>) -> Self {
        self.stroke = stroke.into();
        self
    }

    /// Add a custom fill color.
    #[inline]
    pub fn fill(mut self, color: impl Into<Color32>) -> Self {
        self.fill = color.into();
        self
    }

    /// Set the box width.
    #[inline]
    pub fn box_width(mut self, width: f64) -> Self {
        self.box_width = width;
        self
    }

    /// Set the whisker width.
    #[inline]
    pub fn whisker_width(mut self, width: f64) -> Self {
        self.whisker_width = width;
        self
    }

    /// Set orientation of the element as vertical. Argument axis is X.
    #[inline]
    pub fn vertical(mut self) -> Self {
        self.orientation = Orientation::Vertical;
        self
    }

    /// Set orientation of the element as horizontal. Argument axis is Y.
    #[inline]
    pub fn horizontal(mut self) -> Self {
        self.orientation = Orientation::Horizontal;
        self
    }

    pub(super) fn add_shapes(
        &self,
        transform: &PlotTransform,
        highlighted: bool,
        shapes: &mut Vec<Shape>,
    ) {
        let (stroke, fill) = if highlighted {
            highlighted_color(self.stroke, self.fill)
        } else {
            (self.stroke, self.fill)
        };

        let rect = transform.rect_from_values(
            &self.point_at(self.argument - self.box_width / 2.0, self.spread.quartile1),
            &self.point_at(self.argument + self.box_width / 2.0, self.spread.quartile3),
        );
        let rect = Shape::Rect(RectShape::new(rect, Rounding::ZERO, fill, stroke));
        shapes.push(rect);

        let line_between = |v1, v2| {
            Shape::line_segment(
                [
                    transform.position_from_point(&v1),
                    transform.position_from_point(&v2),
                ],
                stroke,
            )
        };
        let median = line_between(
            self.point_at(self.argument - self.box_width / 2.0, self.spread.median),
            self.point_at(self.argument + self.box_width / 2.0, self.spread.median),
        );
        shapes.push(median);

        if self.spread.upper_whisker > self.spread.quartile3 {
            let high_whisker = line_between(
                self.point_at(self.argument, self.spread.quartile3),
                self.point_at(self.argument, self.spread.upper_whisker),
            );
            shapes.push(high_whisker);
            if self.box_width > 0.0 {
                let high_whisker_end = line_between(
                    self.point_at(
                        self.argument - self.whisker_width / 2.0,
                        self.spread.upper_whisker,
                    ),
                    self.point_at(
                        self.argument + self.whisker_width / 2.0,
                        self.spread.upper_whisker,
                    ),
                );
                shapes.push(high_whisker_end);
            }
        }

        if self.spread.lower_whisker < self.spread.quartile1 {
            let low_whisker = line_between(
                self.point_at(self.argument, self.spread.quartile1),
                self.point_at(self.argument, self.spread.lower_whisker),
            );
            shapes.push(low_whisker);
            if self.box_width > 0.0 {
                let low_whisker_end = line_between(
                    self.point_at(
                        self.argument - self.whisker_width / 2.0,
                        self.spread.lower_whisker,
                    ),
                    self.point_at(
                        self.argument + self.whisker_width / 2.0,
                        self.spread.lower_whisker,
                    ),
                );
                shapes.push(low_whisker_end);
            }
        }
    }

    pub(super) fn add_rulers_and_text(
        &self,
        parent: &BoxPlot,
        plot: &PlotConfig<'_>,
        shapes: &mut Vec<Shape>,
        cursors: &mut Vec<Cursor>,
    ) {
        let text: Option<String> = parent
            .element_formatter
            .as_ref()
            .map(|fmt| fmt(self, parent));

        add_rulers_and_text(self, plot, text, shapes, cursors);
    }
}

impl RectElement for BoxElem {
    fn name(&self) -> &str {
        self.name.as_str()
    }

    fn bounds_min(&self) -> PlotPoint {
        let argument = self.argument - self.box_width.max(self.whisker_width) / 2.0;
        let value = self.spread.lower_whisker;
        self.point_at(argument, value)
    }

    fn bounds_max(&self) -> PlotPoint {
        let argument = self.argument + self.box_width.max(self.whisker_width) / 2.0;
        let value = self.spread.upper_whisker;
        self.point_at(argument, value)
    }

    fn values_with_ruler(&self) -> Vec<PlotPoint> {
        let median = self.point_at(self.argument, self.spread.median);
        let q1 = self.point_at(self.argument, self.spread.quartile1);
        let q3 = self.point_at(self.argument, self.spread.quartile3);
        let upper = self.point_at(self.argument, self.spread.upper_whisker);
        let lower = self.point_at(self.argument, self.spread.lower_whisker);

        vec![median, q1, q3, upper, lower]
    }

    fn orientation(&self) -> Orientation {
        self.orientation
    }

    fn corner_value(&self) -> PlotPoint {
        self.point_at(self.argument, self.spread.upper_whisker)
    }

    fn default_values_format(&self, transform: &PlotTransform) -> String {
        let scale = transform.dvalue_dpos();
        let scale = match self.orientation {
            Orientation::Horizontal => scale[0],
            Orientation::Vertical => scale[1],
        };
        let y_decimals = ((-scale.abs().log10()).ceil().at_least(0.0) as usize)
            .at_most(6)
            .at_least(1);
        format!(
            "Max = {max:.decimals$}\
             \nQuartile 3 = {q3:.decimals$}\
             \nMedian = {med:.decimals$}\
             \nQuartile 1 = {q1:.decimals$}\
             \nMin = {min:.decimals$}",
            max = self.spread.upper_whisker,
            q3 = self.spread.quartile3,
            med = self.spread.median,
            q1 = self.spread.quartile1,
            min = self.spread.lower_whisker,
            decimals = y_decimals
        )
    }
}