1use std::rc::Rc;
2
3use gpui::{
4 AnyElement, App, Background, Bounds, ElementId, Hsla, IntoElement, Pixels, Point, SharedString,
5 Window, point, px,
6};
7use gpui_base::motion::spring;
8use gpui_component_macros::IntoPlot;
9use num_traits::{Num, ToPrimitive};
10
11use crate::{
12 ActiveTheme,
13 plot::{
14 AXIS_GAP, Grid, PathCaches, Plot, PlotAxis, StrokeStyle,
15 scale::{Scale, ScaleLinear, ScalePoint, Sealed},
16 shape::Area,
17 tooltip::{CrossLine, Dot, PlotHover, Tooltip, TooltipState},
18 },
19};
20
21use super::{HOVER_DOT_SIZE, build_point_x_labels, hover_halo_size, pointer_spring};
22
23#[derive(Clone)]
25struct AreaHover {
26 x: Pixels,
28 dots: Vec<Point<Pixels>>,
30 focus: f32,
32}
33
34#[derive(IntoPlot)]
35pub struct AreaChart<T, X, Y>
36where
37 T: 'static,
38 X: Clone + PartialEq + Into<SharedString> + 'static,
39 Y: Clone + Copy + PartialOrd + Num + ToPrimitive + Sealed + 'static,
40{
41 data: Vec<T>,
42 x: Option<Rc<dyn Fn(&T) -> X>>,
43 y: Vec<Rc<dyn Fn(&T) -> Y>>,
44 strokes: Vec<Hsla>,
45 stroke_styles: Vec<StrokeStyle>,
46 fills: Vec<Background>,
47 names: Vec<SharedString>,
48 tick_margin: usize,
49 x_axis: bool,
50 grid: bool,
51 id: Option<ElementId>,
52 hover: Option<AreaHover>,
53}
54
55impl<T, X, Y> AreaChart<T, X, Y>
56where
57 X: Clone + PartialEq + Into<SharedString> + 'static,
58 Y: Clone + Copy + PartialOrd + Num + ToPrimitive + Sealed + 'static,
59{
60 pub fn new<I>(data: I) -> Self
61 where
62 I: IntoIterator<Item = T>,
63 {
64 Self {
65 data: data.into_iter().collect(),
66 stroke_styles: vec![],
67 strokes: vec![],
68 fills: vec![],
69 names: vec![],
70 tick_margin: 1,
71 x: None,
72 y: vec![],
73 x_axis: true,
74 grid: true,
75 id: None,
76 hover: None,
77 }
78 }
79
80 pub fn id(mut self, id: impl Into<ElementId>) -> Self {
85 self.id = Some(id.into());
86 self
87 }
88
89 pub fn name(mut self, name: impl Into<SharedString>) -> Self {
93 self.names.push(name.into());
94 self
95 }
96
97 pub fn x(mut self, x: impl Fn(&T) -> X + 'static) -> Self {
98 self.x = Some(Rc::new(x));
99 self
100 }
101
102 pub fn y(mut self, y: impl Fn(&T) -> Y + 'static) -> Self {
103 self.y.push(Rc::new(y));
104 self
105 }
106
107 pub fn stroke(mut self, stroke: impl Into<Hsla>) -> Self {
108 self.strokes.push(stroke.into());
109 self
110 }
111
112 pub fn fill(mut self, fill: impl Into<Background>) -> Self {
113 self.fills.push(fill.into());
114 self
115 }
116
117 pub fn natural(mut self) -> Self {
118 self.stroke_styles.push(StrokeStyle::Natural);
119 self
120 }
121
122 pub fn linear(mut self) -> Self {
123 self.stroke_styles.push(StrokeStyle::Linear);
124 self
125 }
126
127 pub fn step_after(mut self) -> Self {
128 self.stroke_styles.push(StrokeStyle::StepAfter);
129 self
130 }
131
132 pub fn tick_margin(mut self, tick_margin: usize) -> Self {
133 self.tick_margin = tick_margin;
134 self
135 }
136
137 pub fn x_axis(mut self, x_axis: bool) -> Self {
141 self.x_axis = x_axis;
142 self
143 }
144
145 pub fn grid(mut self, grid: bool) -> Self {
146 self.grid = grid;
147 self
148 }
149
150 fn scales(&self, bounds: Bounds<Pixels>) -> Option<(ScalePoint<X>, ScaleLinear<Y>)> {
155 let x_fn = self.x.as_ref()?;
156 if self.y.is_empty() {
157 return None;
158 }
159
160 let width = bounds.size.width.as_f32();
161 let axis_gap = if self.x_axis { AXIS_GAP } else { 0. };
162 let height = bounds.size.height.as_f32() - axis_gap;
163
164 let x = ScalePoint::new(self.data.iter().map(|v| x_fn(v)).collect(), vec![0., width]);
165 let domain = self
166 .data
167 .iter()
168 .flat_map(|v| self.y.iter().map(|y_fn| y_fn(v)))
169 .chain(Some(Y::zero()))
170 .collect::<Vec<_>>();
171 let y = ScaleLinear::new(domain, vec![height, 10.]);
172
173 Some((x, y))
174 }
175}
176
177impl<T, X, Y> Plot for AreaChart<T, X, Y>
178where
179 X: Clone + PartialEq + Into<SharedString> + 'static,
180 Y: Clone + Copy + PartialOrd + Num + ToPrimitive + Sealed + 'static,
181{
182 fn paint(&mut self, bounds: Bounds<Pixels>, window: &mut Window, cx: &mut App) {
183 let Some(x_fn) = self.x.as_ref() else {
184 return;
185 };
186 let Some((x, y)) = self.scales(bounds) else {
187 return;
188 };
189
190 let axis_gap = if self.x_axis { AXIS_GAP } else { 0. };
191 let height = bounds.size.height.as_f32() - axis_gap;
192
193 let mut axis = PlotAxis::new().stroke(cx.theme().border);
195 if self.x_axis {
196 let labels = build_point_x_labels(
197 &self.data,
198 x_fn.as_ref(),
199 &x,
200 self.tick_margin,
201 cx.theme().muted_foreground,
202 );
203 axis = axis.x(height).x_label(labels);
204 }
205 axis.paint(&bounds, window, cx);
206
207 if self.grid {
209 Grid::new()
210 .y((0..=3).map(|i| height * i as f32 / 4.0).collect())
211 .stroke(cx.theme().border)
212 .dash_array(&[px(4.), px(2.)])
213 .paint(&bounds, window);
214 }
215
216 let default_fill: Background = cx.theme().chart_2.opacity(0.4).into();
218 let default_stroke = cx.theme().chart_2;
219 let areas = self.y.iter().enumerate().map(|(i, y_fn)| {
220 let x = x.clone();
221 let y = y.clone();
222 let x_fn = x_fn.clone();
223 let y_fn = y_fn.clone();
224
225 let fill = *self.fills.get(i).unwrap_or(&default_fill);
226 let stroke = *self.strokes.get(i).unwrap_or(&default_stroke);
227 let stroke_style = *self
228 .stroke_styles
229 .get(i)
230 .unwrap_or(self.stroke_styles.first().unwrap_or(&Default::default()));
231
232 Area::new()
233 .data(&self.data)
234 .x(move |d| x.tick(&x_fn(d)))
235 .y0(height)
236 .y1(move |d| y.tick(&y_fn(d)))
237 .stroke(stroke)
238 .stroke_style(stroke_style)
239 .fill(fill)
240 });
241
242 if self.id.is_some() {
245 let caches = PathCaches::for_paint("areas", window, cx);
246 caches.update(cx, |caches, _| {
247 for (i, area) in areas.enumerate() {
248 let (fill, line) = caches.slot_pair(i);
249 area.paint_cached(&bounds, fill, line, window);
250 }
251 });
252 } else {
253 for area in areas {
254 area.paint(&bounds, window);
255 }
256 }
257 }
258
259 fn id(&self) -> Option<ElementId> {
260 self.id.clone()
261 }
262
263 fn tooltip_state(
264 &self,
265 position: Point<Pixels>,
266 bounds: Bounds<Pixels>,
267 _cx: &App,
268 ) -> Option<TooltipState> {
269 let x_fn = self.x.as_ref()?;
270 let (x, y) = self.scales(bounds)?;
271
272 let axis_gap = if self.x_axis { AXIS_GAP } else { 0. };
274 if position.y.as_f32() > bounds.size.height.as_f32() - axis_gap {
275 return None;
276 }
277
278 let index = x.least_index(position.x.as_f32());
279 let d = self.data.get(index)?;
280 let x_tick = x.tick(&x_fn(d))?;
281
282 let dots = self
284 .y
285 .iter()
286 .filter_map(|y_fn| Some(point(px(x_tick), px(y.tick(&y_fn(d))?))))
287 .collect();
288
289 Some(TooltipState::new(
290 index,
291 point(px(x_tick), position.y),
292 dots,
293 ))
294 }
295
296 fn hover(&mut self, hover: Option<&PlotHover>, window: &mut Window, cx: &mut App) {
297 self.hover = hover.map(|hover| {
298 let state = hover.state();
302 let policy = pointer_spring(cx).with_travel(!hover.is_entering());
303 let x = spring(("area-chart", "x"), state.cross_line.x, policy, window, cx);
304 let dots = state
305 .dots
306 .iter()
307 .enumerate()
308 .map(|(i, dot)| {
309 let id = ElementId::named_usize("area-chart-dot", i);
310 point(x, spring(id, dot.y, policy, window, cx))
311 })
312 .collect();
313 AreaHover {
314 x,
315 dots,
316 focus: hover.focus(),
317 }
318 });
319 }
320
321 fn tooltip(
322 &self,
323 state: &TooltipState,
324 cursor: Point<Pixels>,
325 bounds: Bounds<Pixels>,
326 _window: &mut Window,
327 cx: &mut App,
328 ) -> Option<AnyElement> {
329 let x_fn = self.x.as_ref()?;
330 let d = self.data.get(state.index)?;
331 let title: SharedString = x_fn(d).into();
332
333 let default_color = cx.theme().chart_2;
334 let dot_stroke = cx.theme().background;
335 let color = |i: usize| *self.strokes.get(i).unwrap_or(&default_color);
336
337 let (x, dots, focus) = match self.hover.as_ref() {
340 Some(hover) => (hover.x, &hover.dots, hover.focus),
341 None => (state.cross_line.x, &state.dots, 1.),
342 };
343
344 let mut tooltip = Tooltip::new(cursor, bounds.size)
346 .gap(px(8.))
347 .cross_line(
349 CrossLine::new(point(x, state.cross_line.y))
350 .height(bounds.size.height.as_f32() - if self.x_axis { AXIS_GAP } else { 0. }),
351 )
352 .dots(dots.iter().enumerate().map(|(i, p)| {
353 Dot::new(*p)
354 .size(HOVER_DOT_SIZE)
355 .halo(hover_halo_size(focus))
356 .stroke(dot_stroke)
357 .fill(color(i))
358 }))
359 .title(title);
360
361 for (i, y_fn) in self.y.iter().enumerate() {
363 let name = self.names.get(i).cloned().unwrap_or_default();
364 let value = y_fn(d).to_f64()?;
365 tooltip = tooltip.row(color(i), name, format!("{}", value));
366 }
367
368 Some(tooltip.into_any_element())
369 }
370}