Skip to main content

guise/chart/
line.rs

1//! `LineChart` — a sparkline grown up: light horizontal gridlines and an
2//! optional area fill, still axis-free.
3//!
4//! ```ignore
5//! use guise::chart::LineChart;
6//!
7//! LineChart::new([12.0, 18.0, 9.0, 24.0, 20.0, 31.0]).fill().height(180.0)
8//! ```
9
10use gpui::prelude::*;
11use gpui::{canvas, fill, point, px, size, App, Bounds, Hsla, IntoElement, Window};
12
13use crate::style::ColorValue;
14use crate::theme::theme;
15
16use super::{paint_polyline, resolve_color};
17
18/// How many horizontal gridlines a `LineChart` paints (top and bottom included).
19const GRIDLINES: usize = 4;
20
21/// A single-series line chart. Values are min/max normalized so the line spans
22/// the full height; fewer than two values paint only the gridlines.
23#[derive(IntoElement)]
24pub struct LineChart {
25    values: Vec<f32>,
26    color: Option<ColorValue>,
27    stroke: f32,
28    fill: bool,
29    width: Option<f32>,
30    height: f32,
31}
32
33impl LineChart {
34    pub fn new(values: impl IntoIterator<Item = f32>) -> Self {
35        LineChart {
36            values: values.into_iter().collect(),
37            color: None,
38            stroke: 2.0,
39            fill: false,
40            width: None,
41            height: 140.0,
42        }
43    }
44
45    /// Line color. Defaults to the theme primary.
46    pub fn color(mut self, color: impl Into<ColorValue>) -> Self {
47        self.color = Some(color.into());
48        self
49    }
50
51    /// Stroke width in px (default 2).
52    pub fn stroke(mut self, width: f32) -> Self {
53        self.stroke = width.max(0.5);
54        self
55    }
56
57    /// Fill the area between the line and the baseline (line color at 0.15 alpha).
58    pub fn fill(mut self) -> Self {
59        self.fill = true;
60        self
61    }
62
63    /// Fixed width in px. Defaults to the parent's full width.
64    pub fn width(mut self, width: f32) -> Self {
65        self.width = Some(width);
66        self
67    }
68
69    /// Height in px (default 140).
70    pub fn height(mut self, height: f32) -> Self {
71        self.height = height;
72        self
73    }
74}
75
76impl RenderOnce for LineChart {
77    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
78        let t = theme(cx);
79        let line = self
80            .color
81            .map(|c| resolve_color(t, c))
82            .unwrap_or_else(|| t.primary().hsla());
83        let area = self.fill.then_some(Hsla { a: 0.15, ..line });
84        let grid = t.border().alpha(0.5);
85        let stroke = self.stroke;
86        let values = self.values;
87
88        let plot = canvas(
89            |_, _, _| (),
90            move |bounds, _, window, _cx| {
91                let w = f32::from(bounds.size.width);
92                let h = f32::from(bounds.size.height);
93                if w <= 0.0 || h <= 0.0 {
94                    return;
95                }
96                // Evenly spaced 1px gridlines, top edge through bottom edge.
97                for i in 0..GRIDLINES {
98                    let y = (h - 1.0) * (i as f32 / (GRIDLINES - 1) as f32);
99                    window.paint_quad(fill(
100                        Bounds::new(bounds.origin + point(px(0.0), px(y)), size(px(w), px(1.0))),
101                        grid,
102                    ));
103                }
104                paint_polyline(window, bounds, &values, stroke, line, area);
105            },
106        )
107        .h(px(self.height));
108
109        match self.width {
110            Some(w) => plot.w(px(w)),
111            None => plot.w_full(),
112        }
113    }
114}