Skip to main content

guise/chart/
area.rs

1//! `AreaChart` — filled line series, stacked by default.
2//!
3//! Stacking accumulates the layers (`stack_layers`), so each band shows its
4//! own contribution and the top edge is the total. `.overlaid()` draws the
5//! raw series over each other instead.
6//!
7//! ```ignore
8//! use guise::chart::AreaChart;
9//!
10//! AreaChart::series("Free", [40.0, 42.0, 45.0])
11//!     .add_series("Pro", [12.0, 15.0, 21.0])
12//!     .axis()
13//!     .labels(["May", "Jun", "Jul"])
14//! ```
15
16use gpui::prelude::*;
17use gpui::{
18    canvas, div, fill, point, px, size, App, Bounds, Hsla, IntoElement, SharedString, Window,
19};
20
21use crate::style::ColorValue;
22use crate::theme::theme;
23
24use super::axis::nice_ticks;
25use super::frame::{legend_row, x_label_row, y_axis_column};
26use super::{
27    min_max, normalize_between, paint_band, paint_polyline_ys, series_color, stack_layers,
28};
29use crate::devtools::Probed;
30
31/// A stacked (or overlaid) area chart over one or more series.
32#[derive(IntoElement)]
33pub struct AreaChart {
34    series: Vec<(Option<SharedString>, Vec<f32>)>,
35    colors: Vec<ColorValue>,
36    stacked: bool,
37    axis: bool,
38    labels: Vec<SharedString>,
39    stroke: f32,
40    width: Option<f32>,
41    height: f32,
42}
43
44impl AreaChart {
45    pub fn new(values: impl IntoIterator<Item = f32>) -> Self {
46        AreaChart {
47            series: vec![(None, values.into_iter().collect())],
48            colors: Vec::new(),
49            stacked: true,
50            axis: false,
51            labels: Vec::new(),
52            stroke: 1.5,
53            width: None,
54            height: 140.0,
55        }
56    }
57
58    /// Start a named multi-series chart; extend with [`add_series`](Self::add_series).
59    pub fn series(label: impl Into<SharedString>, values: impl IntoIterator<Item = f32>) -> Self {
60        let mut chart = AreaChart::new(values);
61        chart.series[0].0 = Some(label.into());
62        chart
63    }
64
65    /// Add another named series (stacked on top by default).
66    pub fn add_series(
67        mut self,
68        label: impl Into<SharedString>,
69        values: impl IntoIterator<Item = f32>,
70    ) -> Self {
71        self.series
72            .push((Some(label.into()), values.into_iter().collect()));
73        self
74    }
75
76    /// Draw the raw series over each other instead of stacking.
77    pub fn overlaid(mut self) -> Self {
78        self.stacked = false;
79        self
80    }
81
82    /// Per-series colors, cycled when shorter than the series list.
83    pub fn colors(mut self, colors: impl IntoIterator<Item = impl Into<ColorValue>>) -> Self {
84        self.colors = colors.into_iter().map(Into::into).collect();
85        self
86    }
87
88    /// Show a y-axis with nice-number ticks and aligned gridlines.
89    pub fn axis(mut self) -> Self {
90        self.axis = true;
91        self
92    }
93
94    /// Category labels under the plot, one per data point.
95    pub fn labels(mut self, labels: impl IntoIterator<Item = impl Into<SharedString>>) -> Self {
96        self.labels = labels.into_iter().map(Into::into).collect();
97        self
98    }
99
100    /// Fixed width in px. Defaults to the parent's full width.
101    pub fn width(mut self, width: f32) -> Self {
102        self.width = Some(width);
103        self
104    }
105
106    /// Plot height in px (default 140), excluding labels and legend.
107    pub fn height(mut self, height: f32) -> Self {
108        self.height = height;
109        self
110    }
111}
112
113impl RenderOnce for AreaChart {
114    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
115        let t = theme(cx);
116        let colors: Vec<Hsla> = (0..self.series.len())
117            .map(|i| series_color(t, &self.colors, i))
118            .collect();
119        let grid = t.border().alpha(0.5);
120        let stroke = self.stroke;
121        let stacked = self.stacked;
122
123        let raw: Vec<Vec<f32>> = self.series.iter().map(|(_, v)| v.clone()).collect();
124        let layers = if stacked {
125            stack_layers(&raw)
126        } else {
127            raw.clone()
128        };
129
130        // Scale: stacked charts run 0..=top-layer max; overlaid use the
131        // combined data range.
132        let flat: Vec<f32> = layers.iter().flatten().copied().collect();
133        let (lo, hi) = if stacked {
134            (0.0, min_max(&flat).map(|(_, hi)| hi).unwrap_or(1.0))
135        } else {
136            min_max(&flat).unwrap_or((0.0, 1.0))
137        };
138        let ticks = nice_ticks(lo, hi, 4);
139        let (lo, hi) = (*ticks.first().unwrap(), *ticks.last().unwrap());
140        let gridline_count = ticks.len().max(2);
141
142        let paint_colors = colors.clone();
143        let plot = canvas(
144            |_, _, _| (),
145            move |bounds, _, window, _cx| {
146                let w = f32::from(bounds.size.width);
147                let h = f32::from(bounds.size.height);
148                if w <= 0.0 || h <= 0.0 {
149                    return;
150                }
151                for i in 0..gridline_count {
152                    let y = (h - 1.0) * (i as f32 / (gridline_count - 1) as f32);
153                    window.paint_quad(fill(
154                        Bounds::new(bounds.origin + point(px(0.0), px(y)), size(px(w), px(1.0))),
155                        grid,
156                    ));
157                }
158                let zero = normalize_between(&vec![0.0; 2], lo, hi)[0];
159                let mut below: Option<Vec<f32>> = None;
160                for (i, layer) in layers.iter().enumerate() {
161                    let ys = normalize_between(layer, lo, hi);
162                    let color = paint_colors[i];
163                    let band = Hsla { a: 0.25, ..color };
164                    if stacked {
165                        let lower = below.clone().unwrap_or_else(|| vec![zero; ys.len()]);
166                        paint_band(window, bounds, &ys, &lower, band);
167                        below = Some(ys.clone());
168                    }
169                    paint_polyline_ys(
170                        window,
171                        bounds,
172                        &ys,
173                        stroke,
174                        color,
175                        (!stacked).then_some(band),
176                    );
177                }
178            },
179        )
180        .w_full()
181        .h(px(self.height));
182
183        let mut body = div().flex().flex_row().w_full();
184        if self.axis {
185            body = body.child(y_axis_column(t, &ticks, self.height));
186        }
187        let mut plot_column = div().flex_1().flex().flex_col().gap(px(4.0)).child(plot);
188        if !self.labels.is_empty() {
189            plot_column = plot_column.child(x_label_row(t, &self.labels));
190        }
191        body = body.child(plot_column);
192
193        let legend: Vec<(SharedString, Hsla)> = self
194            .series
195            .iter()
196            .enumerate()
197            .filter_map(|(i, (label, _))| label.clone().map(|l| (l, colors[i])))
198            .collect();
199
200        let mut root = div().flex().flex_col();
201        root = match self.width {
202            Some(w) => root.w(px(w)),
203            None => root.w_full(),
204        };
205        root = root.child(body);
206        if !legend.is_empty() {
207            root = root.child(legend_row(t, &legend));
208        }
209        root.probe("AreaChart")
210    }
211}