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_component_macros::IntoPlot;
8use num_traits::{Num, ToPrimitive};
9
10use crate::{
11 ActiveTheme,
12 plot::{
13 AXIS_GAP, Grid, Plot, PlotAxis, StrokeStyle,
14 scale::{Scale, ScaleLinear, ScalePoint, Sealed},
15 shape::Area,
16 tooltip::{CrossLine, Dot, Tooltip, TooltipState},
17 },
18};
19
20use super::build_point_x_labels;
21
22#[derive(IntoPlot)]
23pub struct AreaChart<T, X, Y>
24where
25 T: 'static,
26 X: Clone + PartialEq + Into<SharedString> + 'static,
27 Y: Clone + Copy + PartialOrd + Num + ToPrimitive + Sealed + 'static,
28{
29 data: Vec<T>,
30 x: Option<Rc<dyn Fn(&T) -> X>>,
31 y: Vec<Rc<dyn Fn(&T) -> Y>>,
32 strokes: Vec<Hsla>,
33 stroke_styles: Vec<StrokeStyle>,
34 fills: Vec<Background>,
35 names: Vec<SharedString>,
36 tick_margin: usize,
37 x_axis: bool,
38 grid: bool,
39 id: Option<ElementId>,
40}
41
42impl<T, X, Y> AreaChart<T, X, Y>
43where
44 X: Clone + PartialEq + Into<SharedString> + 'static,
45 Y: Clone + Copy + PartialOrd + Num + ToPrimitive + Sealed + 'static,
46{
47 pub fn new<I>(data: I) -> Self
48 where
49 I: IntoIterator<Item = T>,
50 {
51 Self {
52 data: data.into_iter().collect(),
53 stroke_styles: vec![],
54 strokes: vec![],
55 fills: vec![],
56 names: vec![],
57 tick_margin: 1,
58 x: None,
59 y: vec![],
60 x_axis: true,
61 grid: true,
62 id: None,
63 }
64 }
65
66 pub fn id(mut self, id: impl Into<ElementId>) -> Self {
71 self.id = Some(id.into());
72 self
73 }
74
75 pub fn name(mut self, name: impl Into<SharedString>) -> Self {
79 self.names.push(name.into());
80 self
81 }
82
83 pub fn x(mut self, x: impl Fn(&T) -> X + 'static) -> Self {
84 self.x = Some(Rc::new(x));
85 self
86 }
87
88 pub fn y(mut self, y: impl Fn(&T) -> Y + 'static) -> Self {
89 self.y.push(Rc::new(y));
90 self
91 }
92
93 pub fn stroke(mut self, stroke: impl Into<Hsla>) -> Self {
94 self.strokes.push(stroke.into());
95 self
96 }
97
98 pub fn fill(mut self, fill: impl Into<Background>) -> Self {
99 self.fills.push(fill.into());
100 self
101 }
102
103 pub fn natural(mut self) -> Self {
104 self.stroke_styles.push(StrokeStyle::Natural);
105 self
106 }
107
108 pub fn linear(mut self) -> Self {
109 self.stroke_styles.push(StrokeStyle::Linear);
110 self
111 }
112
113 pub fn step_after(mut self) -> Self {
114 self.stroke_styles.push(StrokeStyle::StepAfter);
115 self
116 }
117
118 pub fn tick_margin(mut self, tick_margin: usize) -> Self {
119 self.tick_margin = tick_margin;
120 self
121 }
122
123 pub fn x_axis(mut self, x_axis: bool) -> Self {
127 self.x_axis = x_axis;
128 self
129 }
130
131 pub fn grid(mut self, grid: bool) -> Self {
132 self.grid = grid;
133 self
134 }
135
136 fn scales(&self, bounds: Bounds<Pixels>) -> Option<(ScalePoint<X>, ScaleLinear<Y>)> {
141 let x_fn = self.x.as_ref()?;
142 if self.y.is_empty() {
143 return None;
144 }
145
146 let width = bounds.size.width.as_f32();
147 let axis_gap = if self.x_axis { AXIS_GAP } else { 0. };
148 let height = bounds.size.height.as_f32() - axis_gap;
149
150 let x = ScalePoint::new(self.data.iter().map(|v| x_fn(v)).collect(), vec![0., width]);
151 let domain = self
152 .data
153 .iter()
154 .flat_map(|v| self.y.iter().map(|y_fn| y_fn(v)))
155 .chain(Some(Y::zero()))
156 .collect::<Vec<_>>();
157 let y = ScaleLinear::new(domain, vec![height, 10.]);
158
159 Some((x, y))
160 }
161}
162
163impl<T, X, Y> Plot for AreaChart<T, X, Y>
164where
165 X: Clone + PartialEq + Into<SharedString> + 'static,
166 Y: Clone + Copy + PartialOrd + Num + ToPrimitive + Sealed + 'static,
167{
168 fn paint(&mut self, bounds: Bounds<Pixels>, window: &mut Window, cx: &mut App) {
169 let Some(x_fn) = self.x.as_ref() else {
170 return;
171 };
172 let Some((x, y)) = self.scales(bounds) else {
173 return;
174 };
175
176 let axis_gap = if self.x_axis { AXIS_GAP } else { 0. };
177 let height = bounds.size.height.as_f32() - axis_gap;
178
179 let mut axis = PlotAxis::new().stroke(cx.theme().border);
181 if self.x_axis {
182 let labels = build_point_x_labels(
183 &self.data,
184 x_fn.as_ref(),
185 &x,
186 self.tick_margin,
187 cx.theme().muted_foreground,
188 );
189 axis = axis.x(height).x_label(labels);
190 }
191 axis.paint(&bounds, window, cx);
192
193 if self.grid {
195 Grid::new()
196 .y((0..=3).map(|i| height * i as f32 / 4.0).collect())
197 .stroke(cx.theme().border)
198 .dash_array(&[px(4.), px(2.)])
199 .paint(&bounds, window);
200 }
201
202 for (i, y_fn) in self.y.iter().enumerate() {
204 let x = x.clone();
205 let y = y.clone();
206 let x_fn = x_fn.clone();
207 let y_fn = y_fn.clone();
208
209 let fill = *self
210 .fills
211 .get(i)
212 .unwrap_or(&cx.theme().chart_2.opacity(0.4).into());
213
214 let stroke = *self.strokes.get(i).unwrap_or(&cx.theme().chart_2);
215
216 let stroke_style = *self
217 .stroke_styles
218 .get(i)
219 .unwrap_or(self.stroke_styles.first().unwrap_or(&Default::default()));
220
221 Area::new()
222 .data(&self.data)
223 .x(move |d| x.tick(&x_fn(d)))
224 .y0(height)
225 .y1(move |d| y.tick(&y_fn(d)))
226 .stroke(stroke)
227 .stroke_style(stroke_style)
228 .fill(fill)
229 .paint(&bounds, window);
230 }
231 }
232
233 fn id(&self) -> Option<ElementId> {
234 self.id.clone()
235 }
236
237 fn tooltip_state(
238 &self,
239 position: Point<Pixels>,
240 bounds: Bounds<Pixels>,
241 _cx: &App,
242 ) -> Option<TooltipState> {
243 let x_fn = self.x.as_ref()?;
244 let (x, y) = self.scales(bounds)?;
245
246 let axis_gap = if self.x_axis { AXIS_GAP } else { 0. };
248 if position.y.as_f32() > bounds.size.height.as_f32() - axis_gap {
249 return None;
250 }
251
252 let index = x.least_index(position.x.as_f32());
253 let d = self.data.get(index)?;
254 let x_tick = x.tick(&x_fn(d))?;
255
256 let dots = self
258 .y
259 .iter()
260 .filter_map(|y_fn| Some(point(px(x_tick), px(y.tick(&y_fn(d))?))))
261 .collect();
262
263 Some(TooltipState::new(
264 index,
265 point(px(x_tick), position.y),
266 dots,
267 ))
268 }
269
270 fn tooltip(
271 &self,
272 state: &TooltipState,
273 cursor: Point<Pixels>,
274 bounds: Bounds<Pixels>,
275 _window: &mut Window,
276 cx: &mut App,
277 ) -> Option<AnyElement> {
278 let x_fn = self.x.as_ref()?;
279 let d = self.data.get(state.index)?;
280 let title: SharedString = x_fn(d).into();
281
282 let default_color = cx.theme().chart_2;
283 let dot_stroke = cx.theme().background;
284 let color = |i: usize| *self.strokes.get(i).unwrap_or(&default_color);
285
286 let mut tooltip = Tooltip::new(cursor, bounds.size)
288 .gap(px(8.))
289 .cross_line(
291 CrossLine::new(state.cross_line)
292 .height(bounds.size.height.as_f32() - if self.x_axis { AXIS_GAP } else { 0. }),
293 )
294 .dots(
295 state
296 .dots
297 .iter()
298 .enumerate()
299 .map(|(i, p)| Dot::new(*p).stroke(dot_stroke).fill(color(i))),
300 )
301 .title(title);
302
303 for (i, y_fn) in self.y.iter().enumerate() {
305 let name = self.names.get(i).cloned().unwrap_or_default();
306 let value = y_fn(d).to_f64()?;
307 tooltip = tooltip.row(color(i), name, format!("{}", value));
308 }
309
310 Some(tooltip.into_any_element())
311 }
312}