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, Size};
17
18use super::{bar_heights, bar_slot, series_color};
19
20/// A vertical bar chart. Bars scale against the largest value with the
21/// baseline at zero; negative values clamp to zero (no downward bars in v1).
22/// Bar colors rotate through the theme palette by default.
23#[derive(IntoElement)]
24pub struct BarChart {
25    values: Vec<f32>,
26    labels: Vec<SharedString>,
27    colors: Vec<ColorValue>,
28    gap: f32,
29    width: Option<f32>,
30    height: f32,
31}
32
33impl BarChart {
34    pub fn new(values: impl IntoIterator<Item = f32>) -> Self {
35        BarChart {
36            values: values.into_iter().collect(),
37            labels: Vec::new(),
38            colors: Vec::new(),
39            gap: 0.2,
40            width: None,
41            height: 140.0,
42        }
43    }
44
45    /// Build from `(label, value)` pairs; labels render below the bars.
46    pub fn entries(entries: impl IntoIterator<Item = (impl Into<SharedString>, f32)>) -> Self {
47        let (labels, values): (Vec<SharedString>, Vec<f32>) = entries
48            .into_iter()
49            .map(|(label, value)| (label.into(), value))
50            .unzip();
51        BarChart {
52            labels,
53            ..BarChart::new(values)
54        }
55    }
56
57    /// One color for every bar (disables the palette rotation).
58    pub fn color(mut self, color: impl Into<ColorValue>) -> Self {
59        self.colors = vec![color.into()];
60        self
61    }
62
63    /// Per-bar colors, cycled when shorter than the series.
64    pub fn colors(mut self, colors: impl IntoIterator<Item = impl Into<ColorValue>>) -> Self {
65        self.colors = colors.into_iter().map(Into::into).collect();
66        self
67    }
68
69    /// Fraction of each bar slot left empty, `0.0..=0.9` (default 0.2).
70    pub fn gap(mut self, gap: f32) -> Self {
71        self.gap = gap.clamp(0.0, 0.9);
72        self
73    }
74
75    /// Fixed width in px. Defaults to the parent's full width.
76    pub fn width(mut self, width: f32) -> Self {
77        self.width = Some(width);
78        self
79    }
80
81    /// Plot height in px (default 140), excluding the label row.
82    pub fn height(mut self, height: f32) -> Self {
83        self.height = height;
84        self
85    }
86}
87
88impl RenderOnce for BarChart {
89    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
90        let t = theme(cx);
91        let n = self.values.len();
92        let bar_colors: Vec<Hsla> = (0..n).map(|i| series_color(t, &self.colors, i)).collect();
93        let dimmed = t.dimmed().hsla();
94        let font_xs = t.font_size(Size::Xs);
95        let fracs = bar_heights(&self.values);
96        let gap = self.gap;
97
98        let plot = canvas(
99            |_, _, _| (),
100            move |bounds, _, window, _cx| {
101                let w = f32::from(bounds.size.width);
102                let h = f32::from(bounds.size.height);
103                if w <= 0.0 || h <= 0.0 {
104                    return;
105                }
106                for (i, frac) in fracs.iter().enumerate() {
107                    let bh = h * frac;
108                    if bh <= 0.0 {
109                        continue;
110                    }
111                    let (x, bw) = bar_slot(i, fracs.len(), w, gap);
112                    window.paint_quad(fill(
113                        Bounds::new(
114                            bounds.origin + point(px(x), px(h - bh)),
115                            size(px(bw), px(bh)),
116                        ),
117                        bar_colors[i],
118                    ));
119                }
120            },
121        )
122        .w_full()
123        .h(px(self.height));
124
125        let mut root = div().flex().flex_col().gap(px(4.0));
126        root = match self.width {
127            Some(w) => root.w(px(w)),
128            None => root.w_full(),
129        };
130        root = root.child(plot);
131
132        // Category labels: one equal-width cell per bar slot, so the row lines
133        // up with the bars painted above. Plain divs — no canvas text in v1.
134        if !self.labels.is_empty() && n > 0 {
135            let cells = (0..n).map(|i| {
136                div()
137                    .flex_1()
138                    .flex()
139                    .justify_center()
140                    .overflow_hidden()
141                    .text_size(px(font_xs))
142                    .text_color(dimmed)
143                    .child(self.labels.get(i).cloned().unwrap_or_default())
144            });
145            root = root.child(div().flex().flex_row().w_full().children(cells));
146        }
147        root
148    }
149}