Skip to main content

ggplot_rs/geom/
bar.rs

1use crate::aes::Aesthetic;
2use crate::coord::Coord;
3use crate::data::DataFrame;
4use crate::position::stack::PositionStack;
5use crate::position::Position;
6use crate::render::backend::{DrawBackend, RectStyle};
7use crate::render::RenderError;
8use crate::scale::ScaleSet;
9use crate::stat::count::StatCount;
10use crate::stat::Stat;
11use crate::theme::Theme;
12
13use super::{Geom, GeomParams};
14
15/// Bar geometry for bar charts.
16pub struct GeomBar {
17    pub fill: (u8, u8, u8),
18    pub color: (u8, u8, u8),
19    pub alpha: f64,
20    pub width: f64,
21}
22
23impl Default for GeomBar {
24    fn default() -> Self {
25        GeomBar {
26            fill: (97, 156, 255),
27            color: (50, 50, 50),
28            alpha: 1.0,
29            width: 0.9,
30        }
31    }
32}
33
34impl Geom for GeomBar {
35    fn key_fill(&self) -> Option<((u8, u8, u8), f64)> {
36        Some((self.fill, self.alpha))
37    }
38
39    fn draw(
40        &self,
41        data: &DataFrame,
42        coord: &dyn Coord,
43        scales: &ScaleSet,
44        _theme: &Theme,
45        backend: &mut dyn DrawBackend,
46    ) -> Result<(), RenderError> {
47        let x_col = data
48            .column("x")
49            .ok_or(RenderError::MissingAesthetic("x".into()))?;
50        let y_col = data
51            .column("y")
52            .ok_or(RenderError::MissingAesthetic("y".into()))?;
53        let fill_col = data.column("fill");
54        let ymin_col = data.column("ymin");
55
56        let plot_area = backend.plot_area();
57        let x_scale = scales.get(&Aesthetic::X);
58        let y_scale = scales.get(&Aesthetic::Y);
59
60        let x_is_discrete = x_scale.map(|s| s.is_discrete()).unwrap_or(false);
61
62        // Bar width in normalized coords — computed once, not per row (breaks()
63        // allocates every level label, which made many-category charts O(n²)).
64        let half_width = if x_is_discrete {
65            // Band-based: each category occupies 1/n of the axis
66            let n_breaks = x_scale.map(|s| s.breaks().len()).unwrap_or(1);
67            self.width / n_breaks.max(1) as f64 / 2.0
68        } else {
69            super::continuous_bar_half_width(
70                x_col.iter().filter_map(|v| x_scale.map(|s| s.map(v))),
71                self.width,
72                0.02,
73            )
74        };
75
76        // A discrete-axis dodge stores per-row offsets (in category units) and
77        // the number of groups sharing the slot; bars shift and narrow by them.
78        let x_mapper = super::support::XMapper::new(data, scales);
79        let dodge_n = data.column(crate::position::DODGE_N_COL);
80
81        for i in 0..data.nrows() {
82            let nx = x_mapper.map(&x_col[i], i);
83            let half_width = match dodge_n.and_then(|c| c[i].as_f64()) {
84                Some(n) if n > 1.0 && x_is_discrete => half_width / n,
85                _ => half_width,
86            };
87            let ny = y_scale.map(|s| s.map(&y_col[i])).unwrap_or(0.0);
88
89            let ny_base = ymin_col
90                .and_then(|c| c[i].as_f64())
91                .and_then(|v| y_scale.map(|s| s.map(&crate::data::Value::Float(v))))
92                .unwrap_or_else(|| {
93                    y_scale
94                        .map(|s| s.map(&crate::data::Value::Float(0.0)))
95                        .unwrap_or(0.0)
96                });
97
98            let (fr, fg, fb) = if let Some(fc) = fill_col {
99                scales
100                    .map_color(&Aesthetic::Fill, &fc[i])
101                    .unwrap_or(self.fill)
102            } else {
103                self.fill
104            };
105            let style = RectStyle {
106                fill: Some((fr, fg, fb)),
107                stroke: Some(self.color),
108                stroke_width: 0.5,
109                alpha: self.alpha,
110                clip: !coord.is_polar(),
111                corner_radius: 0.0,
112            };
113
114            // Hover: "<series>: <count>" with data-x / data-series / data-value.
115            let raw = super::measured_value(data, i);
116            let series = super::series_key(data, i);
117            let tip = raw.as_ref().map(|v| match &series {
118                Some(s) => format!("{s}: {v}"),
119                None => v.clone(),
120            });
121            super::set_mark(backend, tip, Some(super::tip_value(&x_col[i])), series, raw);
122            super::set_mark_formatted(
123                backend,
124                scales,
125                Some(&x_col[i]),
126                &Aesthetic::Y,
127                super::measured_raw(data, i),
128            );
129            super::set_row_extras(backend, data, i);
130
131            if coord.is_polar() {
132                let points = super::col::polar_sector(
133                    coord,
134                    &plot_area,
135                    nx - half_width,
136                    nx + half_width,
137                    ny_base,
138                    ny,
139                );
140                backend.draw_polygon(&points, &style)?;
141            } else {
142                let (left_px, top_px) = coord.transform((nx - half_width, ny), &plot_area);
143                let (right_px, bottom_px) = coord.transform((nx + half_width, ny_base), &plot_area);
144                backend.draw_rect(
145                    (left_px, top_px.min(bottom_px)),
146                    (right_px, top_px.max(bottom_px)),
147                    &style,
148                )?;
149            }
150        }
151        super::clear_mark(backend);
152        super::clear_row_extras(backend);
153
154        Ok(())
155    }
156
157    fn required_aes(&self) -> Vec<Aesthetic> {
158        vec![Aesthetic::X]
159    }
160
161    fn default_stat(&self) -> Box<dyn Stat> {
162        Box::new(StatCount)
163    }
164
165    fn default_position(&self) -> Box<dyn Position> {
166        Box::new(PositionStack)
167    }
168
169    fn default_params(&self) -> GeomParams {
170        GeomParams::default()
171    }
172
173    fn name(&self) -> &str {
174        "bar"
175    }
176
177    fn apply_element_geom(&mut self, el: &crate::theme::elements::ElementGeom) {
178        if let (true, Some(ink)) = (self.color == (50, 50, 50), el.ink) {
179            self.color = ink;
180        }
181    }
182
183    fn consumes_tooltips(&self) -> bool {
184        true
185    }
186
187    fn include_zero_baseline(&self) -> bool {
188        true
189    }
190
191    fn set_series_color(&mut self, color: (u8, u8, u8)) {
192        self.fill = color;
193    }
194}