Skip to main content

guise/chart/
bar.rs

1//! `BarChart` — vertical bars over a value series, with optional category
2//! labels below the bars.
3//!
4//! ```ignore
5//! use guise::chart::BarChart;
6//!
7//! BarChart::entries([("Mon", 12.0), ("Tue", 9.0), ("Wed", 15.0)]).gap(0.3)
8//! ```
9
10use gpui::prelude::*;
11use gpui::{
12    canvas, div, fill, point, px, size, App, Bounds, Hsla, IntoElement, SharedString, Window,
13};
14
15use crate::style::ColorValue;
16use crate::theme::theme;
17
18use super::axis::nice_ticks;
19use super::frame::{hover_slots, x_label_row, y_axis_column};
20use super::{bar_heights, bar_slot, series_color, tick_label};
21use crate::devtools::Probed;
22
23/// A vertical bar chart. Bars scale against the largest value with the
24/// baseline at zero; negative values clamp to zero (no downward bars in v1).
25/// Bar colors rotate through the theme palette by default.
26#[derive(IntoElement)]
27pub struct BarChart {
28    values: Vec<f32>,
29    labels: Vec<SharedString>,
30    colors: Vec<ColorValue>,
31    gap: f32,
32    axis: bool,
33    hover: bool,
34    width: Option<f32>,
35    height: f32,
36}
37
38impl BarChart {
39    pub fn new(values: impl IntoIterator<Item = f32>) -> Self {
40        BarChart {
41            values: values.into_iter().collect(),
42            labels: Vec::new(),
43            colors: Vec::new(),
44            gap: 0.2,
45            axis: false,
46            hover: false,
47            width: None,
48            height: 140.0,
49        }
50    }
51
52    /// Build from `(label, value)` pairs; labels render below the bars.
53    pub fn entries(entries: impl IntoIterator<Item = (impl Into<SharedString>, f32)>) -> Self {
54        let (labels, values): (Vec<SharedString>, Vec<f32>) = entries
55            .into_iter()
56            .map(|(label, value)| (label.into(), value))
57            .unzip();
58        BarChart {
59            labels,
60            ..BarChart::new(values)
61        }
62    }
63
64    /// One color for every bar (disables the palette rotation).
65    pub fn color(mut self, color: impl Into<ColorValue>) -> Self {
66        self.colors = vec![color.into()];
67        self
68    }
69
70    /// Per-bar colors, cycled when shorter than the series.
71    pub fn colors(mut self, colors: impl IntoIterator<Item = impl Into<ColorValue>>) -> Self {
72        self.colors = colors.into_iter().map(Into::into).collect();
73        self
74    }
75
76    /// Fraction of each bar slot left empty, `0.0..=0.9` (default 0.2).
77    pub fn gap(mut self, gap: f32) -> Self {
78        self.gap = gap.clamp(0.0, 0.9);
79        self
80    }
81
82    /// Show a y-axis: nice-number ticks from zero, gridlines aligned to
83    /// them, bars scaled against the top tick.
84    pub fn axis(mut self) -> Self {
85        self.axis = true;
86        self
87    }
88
89    /// Show each bar's value in a tooltip on hover.
90    pub fn hover(mut self) -> Self {
91        self.hover = true;
92        self
93    }
94
95    /// Fixed width in px. Defaults to the parent's full width.
96    pub fn width(mut self, width: f32) -> Self {
97        self.width = Some(width);
98        self
99    }
100
101    /// Plot height in px (default 140), excluding the label row.
102    pub fn height(mut self, height: f32) -> Self {
103        self.height = height;
104        self
105    }
106}
107
108impl RenderOnce for BarChart {
109    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
110        let t = theme(cx);
111        let n = self.values.len();
112        let bar_colors: Vec<Hsla> = (0..n).map(|i| series_color(t, &self.colors, i)).collect();
113        let grid = t.border().alpha(0.5);
114        let gap = self.gap;
115
116        // Axis mode scales bars against the top nice tick (so the tallest
117        // bar lines up with a labeled gridline); otherwise against the max.
118        let ticks = if self.axis {
119            let max = self.values.iter().copied().fold(0.0_f32, f32::max);
120            nice_ticks(0.0, max.max(1.0), 4)
121        } else {
122            Vec::new()
123        };
124        let fracs: Vec<f32> = match ticks.last() {
125            Some(&top) if top > 0.0 => self
126                .values
127                .iter()
128                .map(|&v| {
129                    if v.is_finite() {
130                        (v.max(0.0) / top).min(1.0)
131                    } else {
132                        0.0
133                    }
134                })
135                .collect(),
136            _ => bar_heights(&self.values),
137        };
138        let gridline_count = ticks.len();
139
140        let plot = canvas(
141            |_, _, _| (),
142            move |bounds, _, window, _cx| {
143                let w = f32::from(bounds.size.width);
144                let h = f32::from(bounds.size.height);
145                if w <= 0.0 || h <= 0.0 {
146                    return;
147                }
148                for i in 0..gridline_count {
149                    let y = (h - 1.0) * (i as f32 / (gridline_count - 1).max(1) as f32);
150                    window.paint_quad(fill(
151                        Bounds::new(bounds.origin + point(px(0.0), px(y)), size(px(w), px(1.0))),
152                        grid,
153                    ));
154                }
155                for (i, frac) in fracs.iter().enumerate() {
156                    let bh = h * frac;
157                    if bh <= 0.0 {
158                        continue;
159                    }
160                    let (x, bw) = bar_slot(i, fracs.len(), w, gap);
161                    window.paint_quad(fill(
162                        Bounds::new(
163                            bounds.origin + point(px(x), px(h - bh)),
164                            size(px(bw), px(bh)),
165                        ),
166                        bar_colors[i],
167                    ));
168                }
169            },
170        )
171        .w_full()
172        .h(px(self.height));
173
174        // Per-bar hover readouts layered over the plot.
175        let mut plot_wrap = div().relative().w_full().child(plot);
176        if self.hover && n > 0 {
177            let texts: Vec<SharedString> = (0..n)
178                .map(|i| {
179                    let value = tick_label(self.values[i]);
180                    match self.labels.get(i) {
181                        Some(label) => format!("{label}: {value}").into(),
182                        None => value.into(),
183                    }
184                })
185                .collect();
186            plot_wrap = plot_wrap.child(hover_slots("guise-barchart-hover".into(), texts));
187        }
188
189        let mut plot_column = div()
190            .flex_1()
191            .flex()
192            .flex_col()
193            .gap(px(4.0))
194            .child(plot_wrap);
195        // Category labels: one equal-width cell per bar slot, so the row lines
196        // up with the bars painted above. Plain divs — no canvas text in v1.
197        if !self.labels.is_empty() && n > 0 {
198            plot_column = plot_column.child(x_label_row(t, &self.labels));
199        }
200
201        let mut body = div().flex().flex_row();
202        body = match self.width {
203            Some(w) => body.w(px(w)),
204            None => body.w_full(),
205        };
206        if self.axis {
207            body = body.child(y_axis_column(t, &ticks, self.height));
208        }
209        body.child(plot_column).probe("BarChart")
210    }
211}