egui_plot_bintrade/items/
box_elem.rs1use 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#[derive(Clone, Debug, PartialEq)]
10pub struct BoxSpread {
11 pub lower_whisker: f64,
15
16 pub quartile1: f64,
18
19 pub median: f64,
21
22 pub quartile3: f64,
24
25 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#[derive(Clone, Debug, PartialEq)]
54pub struct BoxElem {
55 pub name: String,
57
58 pub orientation: Orientation,
60
61 pub argument: f64,
63
64 pub spread: BoxSpread,
66
67 pub box_width: f64,
69
70 pub whisker_width: f64,
72
73 pub stroke: Stroke,
75
76 pub fill: Color32,
78}
79
80impl BoxElem {
81 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 #[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 #[inline]
107 pub fn stroke(mut self, stroke: impl Into<Stroke>) -> Self {
108 self.stroke = stroke.into();
109 self
110 }
111
112 #[inline]
114 pub fn fill(mut self, color: impl Into<Color32>) -> Self {
115 self.fill = color.into();
116 self
117 }
118
119 #[inline]
121 pub fn box_width(mut self, width: f64) -> Self {
122 self.box_width = width;
123 self
124 }
125
126 #[inline]
128 pub fn whisker_width(mut self, width: f64) -> Self {
129 self.whisker_width = width;
130 self
131 }
132
133 #[inline]
135 pub fn vertical(mut self) -> Self {
136 self.orientation = Orientation::Vertical;
137 self
138 }
139
140 #[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 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}