Skip to main content

egui_plot_bintrade/items/
box_elem.rs

1use egui::emath::NumExt as _;
2use egui::epaint::{Color32, CornerRadius, RectShape, Shape, Stroke};
3
4use crate::{BoxPlot, Cursor, PlotPoint, PlotTransform};
5
6use super::{Orientation, PlotConfig, RectElement, add_rulers_and_text, highlighted_color};
7
8/// Contains the values of a single box in a box plot.
9#[derive(Clone, Debug, PartialEq)]
10pub struct BoxSpread {
11    /// Value of lower whisker (typically minimum).
12    ///
13    /// The whisker is not drawn if `lower_whisker >= quartile1`.
14    pub lower_whisker: f64,
15
16    /// Value of lower box threshold (typically 25% quartile)
17    pub quartile1: f64,
18
19    /// Value of middle line in box (typically median)
20    pub median: f64,
21
22    /// Value of upper box threshold (typically 75% quartile)
23    pub quartile3: f64,
24
25    /// Value of upper whisker (typically maximum)
26    ///
27    /// The whisker is not drawn if `upper_whisker <= quartile3`.
28    pub upper_whisker: f64,
29}
30
31impl BoxSpread {
32    pub fn new(
33        lower_whisker: f64,
34        quartile1: f64,
35        median: f64,
36        quartile3: f64,
37        upper_whisker: f64,
38    ) -> Self {
39        Self {
40            lower_whisker,
41            quartile1,
42            median,
43            quartile3,
44            upper_whisker,
45        }
46    }
47}
48
49/// A box in a [`BoxPlot`] diagram.
50///
51/// This is a low-level graphical element; it will not compute quartiles and whiskers, letting one
52/// use their preferred formula. Use [`Points`][`super::Points`] to draw the outliers.
53#[derive(Clone, Debug, PartialEq)]
54pub struct BoxElem {
55    /// Name of plot element in the diagram (annotated by default formatter).
56    pub name: String,
57
58    /// Which direction the box faces in the diagram.
59    pub orientation: Orientation,
60
61    /// Position on the argument (input) axis -- X if vertical, Y if horizontal.
62    pub argument: f64,
63
64    /// Values of the box
65    pub spread: BoxSpread,
66
67    /// Thickness of the box
68    pub box_width: f64,
69
70    /// Width of the whisker at minimum/maximum
71    pub whisker_width: f64,
72
73    /// Line width and color
74    pub stroke: Stroke,
75
76    /// Fill color
77    pub fill: Color32,
78}
79
80impl BoxElem {
81    /// Create a box element. Its `orientation` is set by its [`BoxPlot`] parent.
82    ///
83    /// Check [`BoxElem`] fields for detailed description.
84    pub fn new(argument: f64, spread: BoxSpread) -> Self {
85        Self {
86            argument,
87            orientation: Orientation::default(),
88            name: String::default(),
89            spread,
90            box_width: 0.25,
91            whisker_width: 0.15,
92            stroke: Stroke::new(1.0, Color32::TRANSPARENT),
93            fill: Color32::TRANSPARENT,
94        }
95    }
96
97    /// Name of this box element.
98    #[allow(clippy::needless_pass_by_value)]
99    #[inline]
100    pub fn name(mut self, name: impl ToString) -> Self {
101        self.name = name.to_string();
102        self
103    }
104
105    /// Add a custom stroke.
106    #[inline]
107    pub fn stroke(mut self, stroke: impl Into<Stroke>) -> Self {
108        self.stroke = stroke.into();
109        self
110    }
111
112    /// Add a custom fill color.
113    #[inline]
114    pub fn fill(mut self, color: impl Into<Color32>) -> Self {
115        self.fill = color.into();
116        self
117    }
118
119    /// Set the box width.
120    #[inline]
121    pub fn box_width(mut self, width: f64) -> Self {
122        self.box_width = width;
123        self
124    }
125
126    /// Set the whisker width.
127    #[inline]
128    pub fn whisker_width(mut self, width: f64) -> Self {
129        self.whisker_width = width;
130        self
131    }
132
133    /// Set orientation of the element as vertical. Argument axis is X.
134    #[inline]
135    pub fn vertical(mut self) -> Self {
136        self.orientation = Orientation::Vertical;
137        self
138    }
139
140    /// Set orientation of the element as horizontal. Argument axis is Y.
141    #[inline]
142    pub fn horizontal(mut self) -> Self {
143        self.orientation = Orientation::Horizontal;
144        self
145    }
146
147    pub(super) fn add_shapes(
148        &self,
149        transform: &PlotTransform,
150        highlighted: bool,
151        shapes: &mut Vec<Shape>,
152    ) {
153        let (stroke, fill) = if highlighted {
154            highlighted_color(self.stroke, self.fill)
155        } else {
156            (self.stroke, self.fill)
157        };
158
159        let rect = transform.rect_from_values(
160            &self.point_at(self.argument - self.box_width / 2.0, self.spread.quartile1),
161            &self.point_at(self.argument + self.box_width / 2.0, self.spread.quartile3),
162        );
163        let rect = Shape::Rect(RectShape::new(
164            rect,
165            CornerRadius::ZERO,
166            fill,
167            stroke,
168            egui::StrokeKind::Inside,
169        ));
170        shapes.push(rect);
171
172        let line_between = |v1, v2| {
173            Shape::line_segment(
174                [
175                    transform.position_from_point(&v1),
176                    transform.position_from_point(&v2),
177                ],
178                stroke,
179            )
180        };
181        /* NOTE get rid of the median line
182        let median = line_between(
183            self.point_at(self.argument - self.box_width / 2.0, self.spread.median),
184            self.point_at(self.argument + self.box_width / 2.0, self.spread.median),
185        );
186        shapes.push(median);
187        */
188        if self.spread.upper_whisker > self.spread.quartile3 {
189            let high_whisker = line_between(
190                self.point_at(self.argument, self.spread.quartile3),
191                self.point_at(self.argument, self.spread.upper_whisker),
192            );
193            shapes.push(high_whisker);
194            if self.box_width > 0.0 {
195                let high_whisker_end = line_between(
196                    self.point_at(
197                        self.argument - self.whisker_width / 2.0,
198                        self.spread.upper_whisker,
199                    ),
200                    self.point_at(
201                        self.argument + self.whisker_width / 2.0,
202                        self.spread.upper_whisker,
203                    ),
204                );
205                shapes.push(high_whisker_end);
206            }
207        }
208
209        if self.spread.lower_whisker < self.spread.quartile1 {
210            let low_whisker = line_between(
211                self.point_at(self.argument, self.spread.quartile1),
212                self.point_at(self.argument, self.spread.lower_whisker),
213            );
214            shapes.push(low_whisker);
215            if self.box_width > 0.0 {
216                let low_whisker_end = line_between(
217                    self.point_at(
218                        self.argument - self.whisker_width / 2.0,
219                        self.spread.lower_whisker,
220                    ),
221                    self.point_at(
222                        self.argument + self.whisker_width / 2.0,
223                        self.spread.lower_whisker,
224                    ),
225                );
226                shapes.push(low_whisker_end);
227            }
228        }
229    }
230
231    pub(super) fn add_rulers_and_text(
232        &self,
233        parent: &BoxPlot,
234        plot: &PlotConfig<'_>,
235        shapes: &mut Vec<Shape>,
236        cursors: &mut Vec<Cursor>,
237    ) {
238        let text: Option<String> = parent
239            .element_formatter
240            .as_ref()
241            .map(|fmt| fmt(self, parent));
242
243        add_rulers_and_text(self, plot, text, shapes, cursors);
244    }
245}
246
247impl RectElement for BoxElem {
248    fn name(&self) -> &str {
249        self.name.as_str()
250    }
251
252    fn bounds_min(&self) -> PlotPoint {
253        let argument = self.argument - self.box_width.max(self.whisker_width) / 2.0;
254        let value = self.spread.lower_whisker;
255        self.point_at(argument, value)
256    }
257
258    fn bounds_max(&self) -> PlotPoint {
259        let argument = self.argument + self.box_width.max(self.whisker_width) / 2.0;
260        let value = self.spread.upper_whisker;
261        self.point_at(argument, value)
262    }
263
264    fn values_with_ruler(&self) -> Vec<PlotPoint> {
265        let median = self.point_at(self.argument, self.spread.median);
266        let q1 = self.point_at(self.argument, self.spread.quartile1);
267        let q3 = self.point_at(self.argument, self.spread.quartile3);
268        let upper = self.point_at(self.argument, self.spread.upper_whisker);
269        let lower = self.point_at(self.argument, self.spread.lower_whisker);
270
271        vec![median, q1, q3, upper, lower]
272    }
273
274    fn orientation(&self) -> Orientation {
275        self.orientation
276    }
277
278    fn corner_value(&self) -> PlotPoint {
279        self.point_at(self.argument, self.spread.upper_whisker)
280    }
281
282    fn default_values_format(&self, transform: &PlotTransform) -> String {
283        let scale = transform.dvalue_dpos();
284        let scale = match self.orientation {
285            Orientation::Horizontal => scale[0],
286            Orientation::Vertical => scale[1],
287        };
288        let y_decimals = ((-scale.abs().log10()).ceil().at_least(0.0) as usize)
289            .at_most(6)
290            .at_least(1);
291        format!(
292            "Max = {max:.decimals$}\
293             \nQuartile 3 = {q3:.decimals$}\
294             \nMedian = {med:.decimals$}\
295             \nQuartile 1 = {q1:.decimals$}\
296             \nMin = {min:.decimals$}",
297            max = self.spread.upper_whisker,
298            q3 = self.spread.quartile3,
299            med = self.spread.median,
300            q1 = self.spread.quartile1,
301            min = self.spread.lower_whisker,
302            decimals = y_decimals
303        )
304    }
305}