Skip to main content

egui_plot/items/
bar_chart.rs

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
30/// A bar chart.
31pub struct BarChart {
32    base: PlotItemBase,
33
34    pub(crate) bars: Vec<Bar>,
35    default_color: Color32,
36
37    /// A custom element formatter
38    pub(crate) element_formatter: Option<Box<dyn Fn(&Bar, &Self) -> String>>,
39}
40
41impl BarChart {
42    /// Create a bar chart. It defaults to vertically oriented elements.
43    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    /// Set the default color. It is set on all elements that do not already
53    /// have a specific color. This is the color that shows up in the
54    /// legend. It can be overridden at the bar level (see [[`Bar`]]).
55    /// Default is `Color32::TRANSPARENT` which means a color will be
56    /// auto-assigned.
57    #[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    /// Set all elements to be in a vertical orientation.
71    /// Argument axis will be X and bar values will be on the Y axis.
72    #[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    /// Set all elements to be in a horizontal orientation.
81    /// Argument axis will be Y and bar values will be on the X axis.
82    #[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    /// Set the width (thickness) of all its elements.
91    #[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    /// Add a custom way to format an element.
100    /// Can be used to display a set number of decimals or custom labels.
101    #[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    /// Stacks the bars on top of another chart.
108    /// Positive values are stacked on top of other positive values.
109    /// Negative values are stacked below other negative values.
110    #[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    /// Name of this plot item.
133    ///
134    /// This name will show up in the plot legend, if legends are turned on.
135    ///
136    /// Setting the name via this method does not change the item's id, so you
137    /// can use it to change the name dynamically between frames without
138    /// losing the item's state. You should make sure the name passed to
139    /// [`Self::new`] is unique and stable for each item, or set unique and
140    /// stable ids explicitly via [`Self::id`].
141    #[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    /// Highlight this plot item, typically by scaling it up.
149    ///
150    /// If false, the item may still be highlighted via user interaction.
151    #[inline]
152    pub fn highlight(mut self, highlight: bool) -> Self {
153        self.base_mut().highlight = highlight;
154        self
155    }
156
157    /// Allowed hovering this item in the plot. Default: `true`.
158    #[inline]
159    pub fn allow_hover(mut self, hovering: bool) -> Self {
160        self.base_mut().allow_hover = hovering;
161        self
162    }
163
164    /// Sets the id of this plot item.
165    ///
166    /// By default the id is determined from the name passed to [`Self::new`],
167    /// but it can be explicitly set to a different value.
168    #[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        // nothing to do
184    }
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/// One bar in a [`BarChart`]. Potentially floating, allowing stacked bar
231/// charts. Width can be changed to allow variable-width histograms.
232#[derive(Clone, Debug, PartialEq)]
233pub struct Bar {
234    /// Name of plot element in the diagram (annotated by default formatter)
235    pub name: String,
236
237    /// Which direction the bar faces in the diagram
238    pub orientation: Orientation,
239
240    /// Position on the argument (input) axis -- X if vertical, Y if horizontal
241    pub argument: f64,
242
243    /// Position on the value (output) axis -- Y if vertical, X if horizontal
244    pub value: f64,
245
246    /// For stacked bars, this denotes where the bar starts. None if base axis
247    pub base_offset: Option<f64>,
248
249    /// Thickness of the bar
250    pub bar_width: f64,
251
252    /// Line width and color
253    pub stroke: Stroke,
254
255    /// Fill color
256    pub fill: Color32,
257}
258
259impl Bar {
260    /// Create a bar. Its `orientation` is set by its [`BarChart`] parent.
261    ///
262    /// - `argument`: Position on the argument axis (X if vertical, Y if
263    ///   horizontal).
264    /// - `value`: Height of the bar (if vertical).
265    ///
266    /// By default the bar is vertical and its base is at zero.
267    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    /// Name of this bar chart element.
281    #[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    /// Add a custom stroke.
289    #[inline]
290    pub fn stroke(mut self, stroke: impl Into<Stroke>) -> Self {
291        self.stroke = stroke.into();
292        self
293    }
294
295    /// Add a custom fill color.
296    #[inline]
297    pub fn fill(mut self, color: impl Into<Color32>) -> Self {
298        self.fill = color.into();
299        self
300    }
301
302    /// Offset the base of the bar.
303    /// This offset is on the Y axis for a vertical bar
304    /// and on the X axis for a horizontal bar.
305    #[inline]
306    pub fn base_offset(mut self, offset: f64) -> Self {
307        self.base_offset = Some(offset);
308        self
309    }
310
311    /// Set the bar width.
312    #[inline]
313    pub fn width(mut self, width: f64) -> Self {
314        self.bar_width = width;
315        self
316    }
317
318    /// Set orientation of the element as vertical. Argument axis is X.
319    #[inline]
320    pub fn vertical(mut self) -> Self {
321        self.orientation = Orientation::Vertical;
322        self
323    }
324
325    /// Set orientation of the element as horizontal. Argument axis is Y.
326    #[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}