1use std::ops::RangeInclusive;
2
3use egui::Color32;
4use egui::CornerRadius;
5use egui::Id;
6use egui::Shape;
7use egui::Stroke;
8use egui::Ui;
9use egui::epaint::RectShape;
10use emath::Float as _;
11use emath::NumExt as _;
12use emath::Pos2;
13
14use crate::aesthetics::Orientation;
15use crate::axis::PlotTransform;
16use crate::bounds::PlotBounds;
17use crate::bounds::PlotPoint;
18use crate::colors::highlighted_color;
19use crate::cursor::Cursor;
20use crate::items::ClosestElem;
21use crate::items::PlotConfig;
22use crate::items::PlotGeometry;
23use crate::items::PlotItem;
24use crate::items::PlotItemBase;
25use crate::items::add_rulers_and_text;
26use crate::label::LabelFormatter;
27use crate::math::find_closest_rect;
28use crate::rect_elem::RectElement;
29
30pub struct BarChart {
32 base: PlotItemBase,
33
34 pub(crate) bars: Vec<Bar>,
35 default_color: Color32,
36
37 pub(crate) element_formatter: Option<Box<dyn Fn(&Bar, &Self) -> String>>,
39}
40
41impl BarChart {
42 pub fn new(name: impl Into<String>, bars: Vec<Bar>) -> Self {
44 Self {
45 base: PlotItemBase::new(name.into()),
46 bars,
47 default_color: Color32::TRANSPARENT,
48 element_formatter: None,
49 }
50 }
51
52 #[inline]
58 pub fn color(mut self, color: impl Into<Color32>) -> Self {
59 let plot_color = color.into();
60 self.default_color = plot_color;
61 for b in &mut self.bars {
62 if b.fill == Color32::TRANSPARENT && b.stroke.color == Color32::TRANSPARENT {
63 b.fill = plot_color.linear_multiply(0.2);
64 b.stroke.color = plot_color;
65 }
66 }
67 self
68 }
69
70 #[inline]
73 pub fn vertical(mut self) -> Self {
74 for b in &mut self.bars {
75 b.orientation = Orientation::Vertical;
76 }
77 self
78 }
79
80 #[inline]
83 pub fn horizontal(mut self) -> Self {
84 for b in &mut self.bars {
85 b.orientation = Orientation::Horizontal;
86 }
87 self
88 }
89
90 #[inline]
92 pub fn width(mut self, width: f64) -> Self {
93 for b in &mut self.bars {
94 b.bar_width = width;
95 }
96 self
97 }
98
99 #[inline]
102 pub fn element_formatter(mut self, formatter: Box<dyn Fn(&Bar, &Self) -> String>) -> Self {
103 self.element_formatter = Some(formatter);
104 self
105 }
106
107 #[inline]
111 pub fn stack_on(mut self, others: &[&Self]) -> Self {
112 for (index, bar) in self.bars.iter_mut().enumerate() {
113 let new_base_offset = if bar.value.is_sign_positive() {
114 others
115 .iter()
116 .filter_map(|other_chart| other_chart.bars.get(index).map(|bar| bar.upper()))
117 .max_by_key(|value| value.ord())
118 } else {
119 others
120 .iter()
121 .filter_map(|other_chart| other_chart.bars.get(index).map(|bar| bar.lower()))
122 .min_by_key(|value| value.ord())
123 };
124
125 if let Some(value) = new_base_offset {
126 bar.base_offset = Some(value);
127 }
128 }
129 self
130 }
131
132 #[expect(clippy::needless_pass_by_value, reason = "to allow various string types")]
142 #[inline]
143 pub fn name(mut self, name: impl ToString) -> Self {
144 self.base_mut().name = name.to_string();
145 self
146 }
147
148 #[inline]
152 pub fn highlight(mut self, highlight: bool) -> Self {
153 self.base_mut().highlight = highlight;
154 self
155 }
156
157 #[inline]
159 pub fn allow_hover(mut self, hovering: bool) -> Self {
160 self.base_mut().allow_hover = hovering;
161 self
162 }
163
164 #[inline]
169 pub fn id(mut self, id: impl Into<Id>) -> Self {
170 self.base_mut().id = id.into();
171 self
172 }
173}
174
175impl PlotItem for BarChart {
176 fn shapes(&self, _ui: &Ui, transform: &PlotTransform, shapes: &mut Vec<Shape>) {
177 for b in &self.bars {
178 b.add_shapes(transform, self.base.highlight, shapes);
179 }
180 }
181
182 fn initialize(&mut self, _x_range: RangeInclusive<f64>) {
183 }
185
186 fn color(&self) -> Color32 {
187 self.default_color
188 }
189
190 fn geometry(&self) -> PlotGeometry<'_> {
191 PlotGeometry::Rects
192 }
193
194 fn bounds(&self) -> PlotBounds {
195 let mut bounds = PlotBounds::NOTHING;
196 for b in &self.bars {
197 bounds.merge(&b.bounds());
198 }
199 bounds
200 }
201
202 fn find_closest(&self, point: Pos2, transform: &PlotTransform) -> Option<ClosestElem> {
203 find_closest_rect(&self.bars, point, transform)
204 }
205
206 fn on_hover(
207 &self,
208 _plot_area_response: &egui::Response,
209 elem: ClosestElem,
210 shapes: &mut Vec<Shape>,
211 cursors: &mut Vec<Cursor>,
212 plot: &PlotConfig<'_>,
213 _: &Option<LabelFormatter<'_>>,
214 ) {
215 let bar = &self.bars[elem.index];
216
217 bar.add_shapes(plot.transform, true, shapes);
218 bar.add_rulers_and_text(self, plot, shapes, cursors);
219 }
220
221 fn base(&self) -> &PlotItemBase {
222 &self.base
223 }
224
225 fn base_mut(&mut self) -> &mut PlotItemBase {
226 &mut self.base
227 }
228}
229
230#[derive(Clone, Debug, PartialEq)]
233pub struct Bar {
234 pub name: String,
236
237 pub orientation: Orientation,
239
240 pub argument: f64,
242
243 pub value: f64,
245
246 pub base_offset: Option<f64>,
248
249 pub bar_width: f64,
251
252 pub stroke: Stroke,
254
255 pub fill: Color32,
257}
258
259impl Bar {
260 pub fn new(argument: f64, height: f64) -> Self {
268 Self {
269 argument,
270 value: height,
271 orientation: Orientation::default(),
272 name: Default::default(),
273 base_offset: None,
274 bar_width: 0.5,
275 stroke: Stroke::new(1.0, Color32::TRANSPARENT),
276 fill: Color32::TRANSPARENT,
277 }
278 }
279
280 #[expect(clippy::needless_pass_by_value, reason = "to allow various string types")]
282 #[inline]
283 pub fn name(mut self, name: impl ToString) -> Self {
284 self.name = name.to_string();
285 self
286 }
287
288 #[inline]
290 pub fn stroke(mut self, stroke: impl Into<Stroke>) -> Self {
291 self.stroke = stroke.into();
292 self
293 }
294
295 #[inline]
297 pub fn fill(mut self, color: impl Into<Color32>) -> Self {
298 self.fill = color.into();
299 self
300 }
301
302 #[inline]
306 pub fn base_offset(mut self, offset: f64) -> Self {
307 self.base_offset = Some(offset);
308 self
309 }
310
311 #[inline]
313 pub fn width(mut self, width: f64) -> Self {
314 self.bar_width = width;
315 self
316 }
317
318 #[inline]
320 pub fn vertical(mut self) -> Self {
321 self.orientation = Orientation::Vertical;
322 self
323 }
324
325 #[inline]
327 pub fn horizontal(mut self) -> Self {
328 self.orientation = Orientation::Horizontal;
329 self
330 }
331
332 pub(in crate::items) fn lower(&self) -> f64 {
333 if self.value.is_sign_positive() {
334 self.base_offset.unwrap_or(0.0)
335 } else {
336 self.base_offset.map_or(self.value, |o| o + self.value)
337 }
338 }
339
340 pub(in crate::items) fn upper(&self) -> f64 {
341 if self.value.is_sign_positive() {
342 self.base_offset.map_or(self.value, |o| o + self.value)
343 } else {
344 self.base_offset.unwrap_or(0.0)
345 }
346 }
347
348 pub(in crate::items) fn add_shapes(&self, transform: &PlotTransform, highlighted: bool, shapes: &mut Vec<Shape>) {
349 let (stroke, fill) = if highlighted {
350 highlighted_color(self.stroke, self.fill)
351 } else {
352 (self.stroke, self.fill)
353 };
354
355 let rect = transform.rect_from_values(&self.bounds_min(), &self.bounds_max());
356 let rect = Shape::Rect(RectShape::new(
357 rect,
358 CornerRadius::ZERO,
359 fill,
360 stroke,
361 egui::StrokeKind::Inside,
362 ));
363
364 shapes.push(rect);
365 }
366
367 pub(in crate::items) fn add_rulers_and_text(
368 &self,
369 parent: &BarChart,
370 plot: &PlotConfig<'_>,
371 shapes: &mut Vec<Shape>,
372 cursors: &mut Vec<Cursor>,
373 ) {
374 let text: Option<String> = parent.element_formatter.as_ref().map(|fmt| fmt(self, parent));
375
376 add_rulers_and_text(self, plot, text, shapes, cursors);
377 }
378}
379
380impl RectElement for Bar {
381 fn name(&self) -> &str {
382 self.name.as_str()
383 }
384
385 fn bounds_min(&self) -> PlotPoint {
386 self.point_at(self.argument - self.bar_width / 2.0, self.lower())
387 }
388
389 fn bounds_max(&self) -> PlotPoint {
390 self.point_at(self.argument + self.bar_width / 2.0, self.upper())
391 }
392
393 fn values_with_ruler(&self) -> Vec<PlotPoint> {
394 let base = self.base_offset.unwrap_or(0.0);
395 let value_center = self.point_at(self.argument, base + self.value);
396
397 let mut ruler_positions = vec![value_center];
398
399 if let Some(offset) = self.base_offset {
400 ruler_positions.push(self.point_at(self.argument, offset));
401 }
402
403 ruler_positions
404 }
405
406 fn orientation(&self) -> Orientation {
407 self.orientation
408 }
409
410 fn default_values_format(&self, transform: &PlotTransform) -> String {
411 let scale = transform.dvalue_dpos();
412 let scale = match self.orientation {
413 Orientation::Horizontal => scale[0],
414 Orientation::Vertical => scale[1],
415 };
416 let decimals = ((-scale.abs().log10()).ceil().at_least(0.0) as usize).at_most(6);
417 crate::label::format_number(self.value, decimals)
418 }
419}