1use std::rc::Rc;
2
3use gpui_kit::{
4 AnyElement, App, Background, Bounds, ElementId, Hsla, IntoElement, Pixels, Point, SharedString,
5 Size, Window, point, px,
6};
7
8use crate::{
9 ActiveTheme,
10 plot::{
11 AxisLabelPlacement, Curve, PathCaches, Plot, PlotAxis,
12 scale::{PlotValue, Scale, ScaleLinear, ScalePoint},
13 shape::Area,
14 tooltip::{CrossLine, Dot, Tooltip, TooltipState},
15 },
16};
17
18use super::{
19 AXIS_GAP, HOVER_DOT_SIZE, HOVER_HALO_SIZE, PointAxes, TooltipContent, ValueExtent,
20 axis_point_count, build_point_x_labels, caller_id, labeled_items, pinned_plot_mask,
21 point_range, point_value_scale,
22};
23
24pub struct AreaChart<T, X, Y>
25where
26 T: 'static,
27 X: Clone + PartialEq + Into<SharedString> + 'static,
28 Y: PlotValue,
29{
30 data: Vec<T>,
31 x: Option<Rc<dyn Fn(&T) -> X>>,
32 y: Vec<Rc<dyn Fn(&T) -> Y>>,
33 strokes: Vec<Hsla>,
34 curves: Vec<Curve>,
35 fills: Vec<Background>,
36 names: Vec<SharedString>,
37 tooltip_content: TooltipContent<T>,
38 tick_margin: usize,
39 x_axis: bool,
40 grid: bool,
41 y_domain: Option<(Y, Y)>,
42 point_count: Option<usize>,
43 axes: PointAxes,
44 id: ElementId,
45 interactive: bool,
46}
47
48impl<T, X, Y> AreaChart<T, X, Y>
49where
50 X: Clone + PartialEq + Into<SharedString> + 'static,
51 Y: PlotValue,
52{
53 #[track_caller]
54 pub fn new<I>(data: I) -> Self
55 where
56 I: IntoIterator<Item = T>,
57 {
58 Self {
59 data: data.into_iter().collect(),
60 curves: vec![],
61 strokes: vec![],
62 fills: vec![],
63 names: vec![],
64 tooltip_content: TooltipContent::default(),
65 tick_margin: 1,
66 x: None,
67 y: vec![],
68 x_axis: true,
69 grid: true,
70 y_domain: None,
71 point_count: None,
72 axes: PointAxes::default(),
73 id: caller_id(),
74 interactive: true,
75 }
76 }
77
78 pub fn id(mut self, id: impl Into<ElementId>) -> Self {
85 self.id = id.into();
86 self
87 }
88
89 pub fn interactive(mut self, interactive: bool) -> Self {
98 self.interactive = interactive;
99 self
100 }
101
102 pub fn name(mut self, name: impl Into<SharedString>) -> Self {
106 self.names.push(name.into());
107 self
108 }
109
110 pub fn tooltip_title(mut self, title: impl Fn(&T) -> SharedString + 'static) -> Self {
112 self.tooltip_content.set_title(title);
113 self
114 }
115
116 pub fn tooltip_value(
121 mut self,
122 value: impl Fn(&T, usize, f64) -> SharedString + 'static,
123 ) -> Self {
124 self.tooltip_content.set_value(value);
125 self
126 }
127
128 pub fn tooltip_value_color<H>(mut self, color: impl Fn(&T, usize, f64) -> H + 'static) -> Self
134 where
135 H: Into<Hsla>,
136 {
137 self.tooltip_content.set_value_color(color);
138 self
139 }
140
141 pub fn tooltip_content<E>(
148 mut self,
149 content: impl Fn(&T, &mut Window, &mut App) -> E + 'static,
150 ) -> Self
151 where
152 E: IntoElement,
153 {
154 self.tooltip_content.set_content(content);
155 self
156 }
157
158 pub fn x(mut self, x: impl Fn(&T) -> X + 'static) -> Self {
159 self.x = Some(Rc::new(x));
160 self
161 }
162
163 pub fn y(mut self, y: impl Fn(&T) -> Y + 'static) -> Self {
164 self.y.push(Rc::new(y));
165 self
166 }
167
168 pub fn stroke(mut self, stroke: impl Into<Hsla>) -> Self {
169 self.strokes.push(stroke.into());
170 self
171 }
172
173 pub fn fill(mut self, fill: impl Into<Background>) -> Self {
174 self.fills.push(fill.into());
175 self
176 }
177
178 pub fn natural(mut self) -> Self {
179 self.curves.push(Curve::Natural);
180 self
181 }
182
183 pub fn linear(mut self) -> Self {
184 self.curves.push(Curve::Linear);
185 self
186 }
187
188 pub fn step_after(mut self) -> Self {
189 self.curves.push(Curve::StepAfter);
190 self
191 }
192
193 pub fn tick_margin(mut self, tick_margin: usize) -> Self {
194 self.tick_margin = tick_margin;
195 self
196 }
197
198 pub fn x_axis(mut self, x_axis: bool) -> Self {
202 self.x_axis = x_axis;
203 self
204 }
205
206 pub fn grid(mut self, grid: bool) -> Self {
207 self.grid = grid;
208 self
209 }
210
211 pub fn y_domain(mut self, min: Y, max: Y) -> Self {
219 self.y_domain = Some((min, max));
220 self
221 }
222
223 pub fn point_count(mut self, count: usize) -> Self {
232 self.point_count = Some(count);
233 self
234 }
235
236 pub fn y_axis(mut self, y_axis: bool) -> Self {
240 self.axes.y_axis = y_axis;
241 self
242 }
243
244 pub fn y_axis_label_placement(mut self, placement: AxisLabelPlacement) -> Self {
249 self.axes.y_axis_label_placement = placement;
250 self
251 }
252
253 pub fn y_tick_count(mut self, count: usize) -> Self {
262 self.axes.y_tick_count = count.max(2);
263 self
264 }
265
266 pub fn y_tick_format<S>(mut self, format: impl Fn(f64) -> S + 'static) -> Self
268 where
269 S: Into<SharedString> + 'static,
270 {
271 self.axes.y_tick_format = Some(Rc::new(move |value| format(value).into()));
272 self
273 }
274
275 pub fn x_tick_count(mut self, count: usize) -> Self {
282 self.axes.x_tick_count = Some(count);
283 self
284 }
285
286 pub fn grid_columns(mut self, count: usize) -> Self {
291 self.axes.grid_columns = count;
292 self
293 }
294
295 pub fn grid_dashed(mut self, dashed: bool) -> Self {
299 self.axes.grid_dashed = dashed;
300 self
301 }
302
303 pub fn reference_line(mut self, value: Y) -> Self {
307 if let Some(value) = value.to_f64() {
308 self.axes.reference_lines.push(value);
309 }
310 self
311 }
312
313 pub fn y_padding(mut self, top: f32, bottom: f32) -> Self {
318 self.axes.y_padding = (top, bottom);
319 self
320 }
321
322 fn scales(
327 &self,
328 bounds: Bounds<Pixels>,
329 ) -> Option<(ScalePoint<X>, ScaleLinear<Y>, ValueExtent)> {
330 let x_fn = self.x.as_ref()?;
331 if self.y.is_empty() {
332 return None;
333 }
334
335 let width = bounds.size.width.as_f32();
336 let axis_gap = if self.x_axis { AXIS_GAP } else { 0. };
337 let height = bounds.size.height.as_f32() - axis_gap;
338
339 let len = self.data.len();
340 let x = ScalePoint::new(
341 self.data.iter().map(|v| x_fn(v)),
342 point_range(
343 self.axes.plot_left(),
344 width - self.axes.plot_left(),
345 len,
346 axis_point_count(self.point_count, len),
347 ),
348 );
349 let (y, extent) = point_value_scale(
350 self.data
351 .iter()
352 .flat_map(|v| self.y.iter().map(|y_fn| y_fn(v))),
353 self.y_domain,
354 height,
355 self.axes.y_padding,
356 );
357
358 Some((x, y, extent))
359 }
360}
361
362crate::impl_into_plot!(impl<T, X, Y> AreaChart<T, X, Y> where X: Clone + PartialEq + Into<SharedString> + 'static, Y: PlotValue);
363
364impl<T, X, Y> Plot for AreaChart<T, X, Y>
365where
366 X: Clone + PartialEq + Into<SharedString> + 'static,
367 Y: PlotValue,
368{
369 fn prepaint(
370 &mut self,
371 bounds: Bounds<Pixels>,
372 window: &mut Window,
373 _cx: &mut App,
374 ) -> Vec<AnyElement> {
375 if self.axes.y_axis
377 && self.axes.y_axis_label_placement == AxisLabelPlacement::Outside
378 && let Some((_, _, extent)) = self.scales(bounds)
379 {
380 let axis_gap = if self.x_axis { AXIS_GAP } else { 0. };
381 let height = bounds.size.height.as_f32() - axis_gap;
382 self.axes.measure_y_labels(extent, height, window);
383 }
384 vec![]
385 }
386
387 fn paint(&mut self, bounds: Bounds<Pixels>, window: &mut Window, cx: &mut App) {
388 let Some(x_fn) = self.x.as_ref() else {
389 return;
390 };
391 let Some((x, y, extent)) = self.scales(bounds) else {
392 return;
393 };
394
395 let axis_gap = if self.x_axis { AXIS_GAP } else { 0. };
396 let height = bounds.size.height.as_f32() - axis_gap;
397
398 let left = self.axes.plot_left();
402 let axis_bounds = Bounds {
403 origin: bounds.origin + point(px(left), px(0.)),
404 size: Size::new(bounds.size.width - px(left), bounds.size.height),
405 };
406 let mut axis = PlotAxis::new().stroke(cx.macos().border);
407 if self.x_axis {
408 let labeled = labeled_items(
409 axis_point_count(self.point_count, self.data.len()),
410 self.axes.x_tick_count,
411 self.tick_margin,
412 );
413 let labels = build_point_x_labels(
414 &self.data,
415 x_fn.as_ref(),
416 &x,
417 axis_point_count(self.point_count, self.data.len()),
418 &labeled,
419 cx.macos().secondary,
420 )
421 .into_iter()
422 .map(|mut label| {
423 label.tick -= px(left);
424 label
425 });
426 axis = axis.x(height).x_label(labels);
427 }
428 axis.paint(&axis_bounds, window, cx);
429
430 if self.grid {
431 self.axes.paint_grid(bounds, height, window, cx);
432 }
433
434 let chart = cx.macos().chart;
437 let x = Rc::new(x);
438 let areas = self.y.iter().enumerate().map(|(i, y_fn)| {
439 let x = x.clone();
440 let y = y.clone();
441 let y_fn = y_fn.clone();
442
443 let stroke = self
444 .strokes
445 .get(i)
446 .copied()
447 .unwrap_or(chart[i % chart.len()]);
448 let fill = self
449 .fills
450 .get(i)
451 .copied()
452 .unwrap_or_else(|| stroke.opacity(0.2).into());
453 let curve = *self
454 .curves
455 .get(i)
456 .unwrap_or(self.curves.first().unwrap_or(&Default::default()));
457
458 Area::new()
459 .data(self.data.iter().enumerate())
460 .x(move |(index, _)| x.tick_at(*index))
461 .y0(height)
462 .y1(move |(_, d)| y.tick(&y_fn(d)))
463 .stroke(stroke)
464 .curve(curve)
465 .fill(fill)
466 });
467
468 let mask = self
469 .y_domain
470 .is_some()
471 .then(|| pinned_plot_mask(bounds, height));
472 window.with_content_mask(mask, |window| {
473 if self.interactive {
477 let caches = PathCaches::for_paint("areas", window, cx);
478 caches.update(cx, |caches, _| {
479 for (i, area) in areas.enumerate() {
480 let (fill, line) = caches.slot_pair(i);
481 area.paint_cached(&bounds, fill, line, window);
482 }
483 });
484 } else {
485 for area in areas {
486 area.paint(&bounds, window);
487 }
488 }
489 });
490
491 self.axes
492 .paint_reference_lines(extent, bounds, height, window, cx);
493 self.axes.paint_y_labels(extent, bounds, height, window, cx);
494 }
495
496 fn id(&self) -> Option<ElementId> {
497 self.interactive.then(|| self.id.clone())
498 }
499
500 fn tooltip_state(
501 &self,
502 position: Point<Pixels>,
503 bounds: Bounds<Pixels>,
504 _cx: &App,
505 ) -> Option<TooltipState> {
506 let (x, y, _) = self.scales(bounds)?;
507
508 let axis_gap = if self.x_axis { AXIS_GAP } else { 0. };
510 if position.y.as_f32() > bounds.size.height.as_f32() - axis_gap
511 || position.x.as_f32() < self.axes.plot_left()
512 {
513 return None;
514 }
515
516 let index = x.nearest_index(position.x.as_f32());
517 let d = self.data.get(index)?;
518 let x_tick = x.tick_at(index)?;
519
520 let dots = self
522 .y
523 .iter()
524 .filter_map(|y_fn| Some(point(px(x_tick), px(y.tick(&y_fn(d))?))))
525 .collect();
526
527 Some(TooltipState::new(
528 index,
529 point(px(x_tick), position.y),
530 dots,
531 ))
532 }
533
534 fn tooltip(
535 &self,
536 state: &TooltipState,
537 cursor: Point<Pixels>,
538 bounds: Bounds<Pixels>,
539 window: &mut Window,
540 cx: &mut App,
541 ) -> Option<AnyElement> {
542 let x_fn = self.x.as_ref()?;
543 let d = self.data.get(state.index)?;
544
545 let chart = cx.macos().chart;
546 let dot_stroke = cx.macos().background;
547 let color = |i: usize| {
548 self.strokes
549 .get(i)
550 .copied()
551 .unwrap_or(chart[i % chart.len()])
552 };
553
554 let tooltip = Tooltip::new(cursor, bounds.size)
556 .gap(px(8.))
557 .cross_line(
559 CrossLine::new(state.cross_line)
560 .height(bounds.size.height.as_f32() - if self.x_axis { AXIS_GAP } else { 0. }),
561 )
562 .dots(state.dots.iter().enumerate().map(|(i, p)| {
563 Dot::new(*p)
564 .size(HOVER_DOT_SIZE)
565 .halo(HOVER_HALO_SIZE)
566 .stroke(dot_stroke)
567 .fill(color(i))
568 }));
569
570 let tooltip = self.tooltip_content.apply(
571 tooltip,
572 d,
573 || Some(x_fn(d).into()),
574 || {
576 self.y
577 .iter()
578 .enumerate()
579 .map(|(i, y_fn)| {
580 let name = self.names.get(i).cloned().unwrap_or_default();
581 Some((color(i), name, y_fn(d).to_f64()?))
582 })
583 .collect::<Option<Vec<_>>>()
584 },
585 window,
586 cx,
587 )?;
588
589 Some(tooltip.into_any_element())
590 }
591}
592
593#[cfg(test)]
594mod tests {
595 use gpui_kit::{Bounds, point, px, size};
596
597 use super::AreaChart;
598 use crate::plot::scale::Scale;
599
600 fn bounds() -> Bounds<gpui_kit::Pixels> {
601 Bounds::new(point(px(0.), px(0.)), size(px(100.), px(50.)))
602 }
603
604 fn chart(data: Vec<f64>) -> AreaChart<(usize, f64), String, f64> {
605 AreaChart::new(data.into_iter().enumerate())
606 .x(|(i, _)| i.to_string())
607 .y(|(_, v)| *v)
608 .x_axis(false)
609 }
610
611 #[test]
612 fn test_point_count_fills_the_leading_part() {
613 let (x, _, _) = chart(vec![1., 2., 3.])
614 .point_count(5)
615 .scales(bounds())
616 .unwrap();
617 assert_eq!(x.tick(&"0".to_string()), Some(0.));
618 assert_eq!(x.tick(&"2".to_string()), Some(50.));
619
620 let (x, _, _) = chart(vec![1., 2., 3.])
621 .point_count(2)
622 .scales(bounds())
623 .unwrap();
624 assert_eq!(x.tick(&"2".to_string()), Some(100.));
625 }
626
627 #[test]
628 fn test_y_domain_replaces_the_fit_from_zero() {
629 let (_, y, _) = chart(vec![10., 20.])
630 .y_domain(10., 20.)
631 .scales(bounds())
632 .unwrap();
633 assert_eq!(y.tick(&10.), Some(50.));
634 assert_eq!(y.tick(&20.), Some(10.));
635
636 let (_, y, _) = chart(vec![10., 20.]).scales(bounds()).unwrap();
637 assert_eq!(y.tick(&0.), Some(50.));
638 assert_eq!(y.tick(&20.), Some(10.));
639 }
640}