1use 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::{hover_slots, legend_row, x_label_row, y_axis_column};
26use super::{
27 min_max, normalize_between, paint_polyline, paint_polyline_ys, resolve_color, series_color,
28 tick_label,
29};
30use crate::devtools::Probed;
31
32const GRIDLINES: usize = 4;
34
35#[derive(IntoElement)]
38pub struct LineChart {
39 series: Vec<(Option<SharedString>, Vec<f32>)>,
40 colors: Vec<ColorValue>,
41 stroke: f32,
42 fill: bool,
43 axis: bool,
44 hover: bool,
45 labels: Vec<SharedString>,
46 width: Option<f32>,
47 height: f32,
48}
49
50impl LineChart {
51 pub fn new(values: impl IntoIterator<Item = f32>) -> Self {
52 LineChart {
53 series: vec![(None, values.into_iter().collect())],
54 colors: Vec::new(),
55 stroke: 2.0,
56 fill: false,
57 axis: false,
58 hover: false,
59 labels: Vec::new(),
60 width: None,
61 height: 140.0,
62 }
63 }
64
65 pub fn series(label: impl Into<SharedString>, values: impl IntoIterator<Item = f32>) -> Self {
69 let mut chart = LineChart::new(values);
70 chart.series[0].0 = Some(label.into());
71 chart
72 }
73
74 pub fn add_series(
76 mut self,
77 label: impl Into<SharedString>,
78 values: impl IntoIterator<Item = f32>,
79 ) -> Self {
80 self
81 .series
82 .push((Some(label.into()), values.into_iter().collect()));
83 self
84 }
85
86 pub fn color(mut self, color: impl Into<ColorValue>) -> Self {
88 self.colors = vec![color.into()];
89 self
90 }
91
92 pub fn colors(mut self, colors: impl IntoIterator<Item = impl Into<ColorValue>>) -> Self {
94 self.colors = colors.into_iter().map(Into::into).collect();
95 self
96 }
97
98 pub fn stroke(mut self, width: f32) -> Self {
100 self.stroke = width.max(0.5);
101 self
102 }
103
104 pub fn fill(mut self) -> Self {
106 self.fill = true;
107 self
108 }
109
110 pub fn axis(mut self) -> Self {
113 self.axis = true;
114 self
115 }
116
117 pub fn hover(mut self) -> Self {
119 self.hover = true;
120 self
121 }
122
123 pub fn labels(mut self, labels: impl IntoIterator<Item = impl Into<SharedString>>) -> Self {
125 self.labels = labels.into_iter().map(Into::into).collect();
126 self
127 }
128
129 pub fn width(mut self, width: f32) -> Self {
131 self.width = Some(width);
132 self
133 }
134
135 pub fn height(mut self, height: f32) -> Self {
137 self.height = height;
138 self
139 }
140}
141
142impl RenderOnce for LineChart {
143 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
144 let t = theme(cx);
145 let multi = self.series.len() > 1;
146 let line_colors: Vec<Hsla> = (0..self.series.len())
147 .map(|i| {
148 if !multi && self.colors.is_empty() {
149 t.primary().hsla()
150 } else if self.colors.len() == 1 && !multi {
151 resolve_color(t, self.colors[0])
152 } else {
153 series_color(t, &self.colors, i)
154 }
155 })
156 .collect();
157 let grid = t.border().alpha(0.5);
158 let stroke = self.stroke;
159 let filled = self.fill;
160
161 let all: Vec<f32> = self
163 .series
164 .iter()
165 .flat_map(|(_, values)| values.iter().copied())
166 .collect();
167 let ticks = if self.axis {
168 let (lo, hi) = min_max(&all).unwrap_or((0.0, 1.0));
169 nice_ticks(lo, hi, 4)
170 } else {
171 Vec::new()
172 };
173 let scale = (!ticks.is_empty()).then(|| (*ticks.first().unwrap(), *ticks.last().unwrap()));
174
175 let point_count = self.series.iter().map(|(_, v)| v.len()).max().unwrap_or(0);
176 let gridline_count = if self.axis {
177 ticks.len().max(2)
178 } else {
179 GRIDLINES
180 };
181
182 let series = self.series.clone();
183 let paint_colors = line_colors.clone();
184 let plot = canvas(
185 |_, _, _| (),
186 move |bounds, _, window, _cx| {
187 let w = f32::from(bounds.size.width);
188 let h = f32::from(bounds.size.height);
189 if w <= 0.0 || h <= 0.0 {
190 return;
191 }
192 for i in 0..gridline_count {
193 let y = (h - 1.0) * (i as f32 / (gridline_count - 1) as f32);
194 window.paint_quad(fill(
195 Bounds::new(bounds.origin + point(px(0.0), px(y)), size(px(w), px(1.0))),
196 grid,
197 ));
198 }
199 for (i, (_, values)) in series.iter().enumerate() {
200 let color = paint_colors[i];
201 let area = filled.then_some(Hsla { a: 0.15, ..color });
202 match scale {
203 Some((lo, hi)) => paint_polyline_ys(
204 window,
205 bounds,
206 &normalize_between(values, lo, hi),
207 stroke,
208 color,
209 area,
210 ),
211 None => paint_polyline(window, bounds, values, stroke, color, area),
212 }
213 }
214 },
215 )
216 .w_full()
217 .h(px(self.height));
218
219 let mut plot_wrap = div().relative().flex_1().child(plot);
221 if self.hover && point_count > 0 {
222 let texts: Vec<SharedString> = (0..point_count)
223 .map(|i| {
224 let parts: Vec<String> = self
225 .series
226 .iter()
227 .map(|(label, values)| {
228 let value = values
229 .get(i)
230 .map(|v| tick_label(*v))
231 .unwrap_or_else(|| "–".into());
232 match label {
233 Some(name) => format!("{name}: {value}"),
234 None => value,
235 }
236 })
237 .collect();
238 let text = match self.labels.get(i) {
239 Some(cat) => format!("{cat} — {}", parts.join(" ")),
240 None => parts.join(" "),
241 };
242 text.into()
243 })
244 .collect();
245 plot_wrap = plot_wrap.child(hover_slots("guise-linechart-hover".into(), texts));
246 }
247
248 let mut body = div().flex().flex_row().w_full();
249 if self.axis {
250 body = body.child(y_axis_column(t, &ticks, self.height));
251 }
252 let mut plot_column = div()
253 .flex_1()
254 .flex()
255 .flex_col()
256 .gap(px(4.0))
257 .child(plot_wrap);
258 if !self.labels.is_empty() {
259 plot_column = plot_column.child(x_label_row(t, &self.labels));
260 }
261 body = body.child(plot_column);
262
263 let legend: Vec<(SharedString, Hsla)> = self
264 .series
265 .iter()
266 .enumerate()
267 .filter_map(|(i, (label, _))| label.clone().map(|l| (l, line_colors[i])))
268 .collect();
269
270 let mut root = div().flex().flex_col();
271 root = match self.width {
272 Some(w) => root.w(px(w)),
273 None => root.w_full(),
274 };
275 root = root.child(body);
276 if !legend.is_empty() {
277 root = root.child(legend_row(t, &legend));
278 }
279 root.probe("LineChart")
280 }
281}