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
72      .series
73      .push((Some(label.into()), values.into_iter().collect()));
74    self
75  }
76
77  /// Draw the raw series over each other instead of stacking.
78  pub fn overlaid(mut self) -> Self {
79    self.stacked = false;
80    self
81  }
82
83  /// Per-series colors, cycled when shorter than the series list.
84  pub fn colors(mut self, colors: impl IntoIterator<Item = impl Into<ColorValue>>) -> Self {
85    self.colors = colors.into_iter().map(Into::into).collect();
86    self
87  }
88
89  /// Show a y-axis with nice-number ticks and aligned gridlines.
90  pub fn axis(mut self) -> Self {
91    self.axis = true;
92    self
93  }
94
95  /// Category labels under the plot, one per data point.
96  pub fn labels(mut self, labels: impl IntoIterator<Item = impl Into<SharedString>>) -> Self {
97    self.labels = labels.into_iter().map(Into::into).collect();
98    self
99  }
100
101  /// Fixed width in px. Defaults to the parent's full width.
102  pub fn width(mut self, width: f32) -> Self {
103    self.width = Some(width);
104    self
105  }
106
107  /// Plot height in px (default 140), excluding labels and legend.
108  pub fn height(mut self, height: f32) -> Self {
109    self.height = height;
110    self
111  }
112}
113
114impl RenderOnce for AreaChart {
115  fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
116    let t = theme(cx);
117    let colors: Vec<Hsla> = (0..self.series.len())
118      .map(|i| series_color(t, &self.colors, i))
119      .collect();
120    let grid = t.border().alpha(0.5);
121    let stroke = self.stroke;
122    let stacked = self.stacked;
123
124    let raw: Vec<Vec<f32>> = self.series.iter().map(|(_, v)| v.clone()).collect();
125    let layers = if stacked {
126      stack_layers(&raw)
127    } else {
128      raw.clone()
129    };
130
131    // Scale: stacked charts run 0..=top-layer max; overlaid use the
132    // combined data range.
133    let flat: Vec<f32> = layers.iter().flatten().copied().collect();
134    let (lo, hi) = if stacked {
135      (0.0, min_max(&flat).map(|(_, hi)| hi).unwrap_or(1.0))
136    } else {
137      min_max(&flat).unwrap_or((0.0, 1.0))
138    };
139    let ticks = nice_ticks(lo, hi, 4);
140    let (lo, hi) = (*ticks.first().unwrap(), *ticks.last().unwrap());
141    let gridline_count = ticks.len().max(2);
142
143    let paint_colors = colors.clone();
144    let plot = canvas(
145      |_, _, _| (),
146      move |bounds, _, window, _cx| {
147        let w = f32::from(bounds.size.width);
148        let h = f32::from(bounds.size.height);
149        if w <= 0.0 || h <= 0.0 {
150          return;
151        }
152        for i in 0..gridline_count {
153          let y = (h - 1.0) * (i as f32 / (gridline_count - 1) as f32);
154          window.paint_quad(fill(
155            Bounds::new(bounds.origin + point(px(0.0), px(y)), size(px(w), px(1.0))),
156            grid,
157          ));
158        }
159        let zero = normalize_between(&[0.0; 2], lo, hi)[0];
160        let mut below: Option<Vec<f32>> = None;
161        for (i, layer) in layers.iter().enumerate() {
162          let ys = normalize_between(layer, lo, hi);
163          let color = paint_colors[i];
164          let band = Hsla { a: 0.25, ..color };
165          if stacked {
166            let lower = below.clone().unwrap_or_else(|| vec![zero; ys.len()]);
167            paint_band(window, bounds, &ys, &lower, band);
168            below = Some(ys.clone());
169          }
170          paint_polyline_ys(
171            window,
172            bounds,
173            &ys,
174            stroke,
175            color,
176            (!stacked).then_some(band),
177          );
178        }
179      },
180    )
181    .w_full()
182    .h(px(self.height));
183
184    let mut body = div().flex().flex_row().w_full();
185    if self.axis {
186      body = body.child(y_axis_column(t, &ticks, self.height));
187    }
188    let mut plot_column = div().flex_1().flex().flex_col().gap(px(4.0)).child(plot);
189    if !self.labels.is_empty() {
190      plot_column = plot_column.child(x_label_row(t, &self.labels));
191    }
192    body = body.child(plot_column);
193
194    let legend: Vec<(SharedString, Hsla)> = self
195      .series
196      .iter()
197      .enumerate()
198      .filter_map(|(i, (label, _))| label.clone().map(|l| (l, colors[i])))
199      .collect();
200
201    let mut root = div().flex().flex_col();
202    root = match self.width {
203      Some(w) => root.w(px(w)),
204      None => root.w_full(),
205    };
206    root = root.child(body);
207    if !legend.is_empty() {
208      root = root.child(legend_row(t, &legend));
209    }
210    root.probe("AreaChart")
211  }
212}