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 let Some((_, _, extent)) = self.scales(bounds) {
377 let axis_gap = if self.x_axis { AXIS_GAP } else { 0. };
378 let height = bounds.size.height.as_f32() - axis_gap;
379 self.axes.measure_y_labels(extent, height, window);
380 }
381 vec![]
382 }
383
384 fn paint(&mut self, bounds: Bounds<Pixels>, window: &mut Window, cx: &mut App) {
385 let Some(x_fn) = self.x.as_ref() else {
386 return;
387 };
388 let Some((x, y, extent)) = self.scales(bounds) else {
389 return;
390 };
391
392 let axis_gap = if self.x_axis { AXIS_GAP } else { 0. };
393 let height = bounds.size.height.as_f32() - axis_gap;
394
395 let left = self.axes.plot_left();
399 let axis_bounds = Bounds {
400 origin: bounds.origin + point(px(left), px(0.)),
401 size: Size::new(bounds.size.width - px(left), bounds.size.height),
402 };
403 let mut axis = PlotAxis::new().stroke(cx.macos().border);
404 if self.x_axis {
405 let labeled = labeled_items(
406 axis_point_count(self.point_count, self.data.len()),
407 self.axes.x_tick_count,
408 self.tick_margin,
409 );
410 let labels = build_point_x_labels(
411 &self.data,
412 x_fn.as_ref(),
413 &x,
414 axis_point_count(self.point_count, self.data.len()),
415 &labeled,
416 cx.macos().secondary,
417 )
418 .into_iter()
419 .map(|mut label| {
420 label.tick -= px(left);
421 label
422 });
423 axis = axis.x(height).x_label(labels);
424 }
425 axis.paint(&axis_bounds, window, cx);
426
427 if self.grid {
428 self.axes.paint_grid(bounds, height, window, cx);
429 }
430
431 let chart = cx.macos().chart;
434 let areas = self.y.iter().enumerate().map(|(i, y_fn)| {
435 let x = x.clone();
436 let y = y.clone();
437 let x_fn = x_fn.clone();
438 let y_fn = y_fn.clone();
439
440 let stroke = self
441 .strokes
442 .get(i)
443 .copied()
444 .unwrap_or(chart[i % chart.len()]);
445 let fill = self
446 .fills
447 .get(i)
448 .copied()
449 .unwrap_or_else(|| stroke.opacity(0.2).into());
450 let curve = *self
451 .curves
452 .get(i)
453 .unwrap_or(self.curves.first().unwrap_or(&Default::default()));
454
455 Area::new()
456 .data(&self.data)
457 .x(move |d| x.tick(&x_fn(d)))
458 .y0(height)
459 .y1(move |d| y.tick(&y_fn(d)))
460 .stroke(stroke)
461 .curve(curve)
462 .fill(fill)
463 });
464
465 let mask = self
466 .y_domain
467 .is_some()
468 .then(|| pinned_plot_mask(bounds, height));
469 window.with_content_mask(mask, |window| {
470 if self.interactive {
474 let caches = PathCaches::for_paint("areas", window, cx);
475 caches.update(cx, |caches, _| {
476 for (i, area) in areas.enumerate() {
477 let (fill, line) = caches.slot_pair(i);
478 area.paint_cached(&bounds, fill, line, window);
479 }
480 });
481 } else {
482 for area in areas {
483 area.paint(&bounds, window);
484 }
485 }
486 });
487
488 self.axes
489 .paint_reference_lines(extent, bounds, height, window, cx);
490 self.axes.paint_y_labels(extent, bounds, height, window, cx);
491 }
492
493 fn id(&self) -> Option<ElementId> {
494 self.interactive.then(|| self.id.clone())
495 }
496
497 fn tooltip_state(
498 &self,
499 position: Point<Pixels>,
500 bounds: Bounds<Pixels>,
501 _cx: &App,
502 ) -> Option<TooltipState> {
503 let x_fn = self.x.as_ref()?;
504 let (x, y, _) = self.scales(bounds)?;
505
506 let axis_gap = if self.x_axis { AXIS_GAP } else { 0. };
508 if position.y.as_f32() > bounds.size.height.as_f32() - axis_gap
509 || position.x.as_f32() < self.axes.plot_left()
510 {
511 return None;
512 }
513
514 let index = x.nearest_index(position.x.as_f32());
515 let d = self.data.get(index)?;
516 let x_tick = x.tick(&x_fn(d))?;
517
518 let dots = self
520 .y
521 .iter()
522 .filter_map(|y_fn| Some(point(px(x_tick), px(y.tick(&y_fn(d))?))))
523 .collect();
524
525 Some(TooltipState::new(
526 index,
527 point(px(x_tick), position.y),
528 dots,
529 ))
530 }
531
532 fn tooltip(
533 &self,
534 state: &TooltipState,
535 cursor: Point<Pixels>,
536 bounds: Bounds<Pixels>,
537 window: &mut Window,
538 cx: &mut App,
539 ) -> Option<AnyElement> {
540 let x_fn = self.x.as_ref()?;
541 let d = self.data.get(state.index)?;
542
543 let chart = cx.macos().chart;
544 let dot_stroke = cx.macos().background;
545 let color = |i: usize| {
546 self.strokes
547 .get(i)
548 .copied()
549 .unwrap_or(chart[i % chart.len()])
550 };
551
552 let tooltip = Tooltip::new(cursor, bounds.size)
554 .gap(px(8.))
555 .cross_line(
557 CrossLine::new(state.cross_line)
558 .height(bounds.size.height.as_f32() - if self.x_axis { AXIS_GAP } else { 0. }),
559 )
560 .dots(state.dots.iter().enumerate().map(|(i, p)| {
561 Dot::new(*p)
562 .size(HOVER_DOT_SIZE)
563 .halo(HOVER_HALO_SIZE)
564 .stroke(dot_stroke)
565 .fill(color(i))
566 }));
567
568 let tooltip = self.tooltip_content.apply(
569 tooltip,
570 d,
571 || Some(x_fn(d).into()),
572 || {
574 self.y
575 .iter()
576 .enumerate()
577 .map(|(i, y_fn)| {
578 let name = self.names.get(i).cloned().unwrap_or_default();
579 Some((color(i), name, y_fn(d).to_f64()?))
580 })
581 .collect::<Option<Vec<_>>>()
582 },
583 window,
584 cx,
585 )?;
586
587 Some(tooltip.into_any_element())
588 }
589}
590
591#[cfg(test)]
592mod tests {
593 use gpui_kit::{Bounds, point, px, size};
594
595 use super::AreaChart;
596 use crate::plot::scale::Scale;
597
598 fn bounds() -> Bounds<gpui_kit::Pixels> {
599 Bounds::new(point(px(0.), px(0.)), size(px(100.), px(50.)))
600 }
601
602 fn chart(data: Vec<f64>) -> AreaChart<(usize, f64), String, f64> {
603 AreaChart::new(data.into_iter().enumerate())
604 .x(|(i, _)| i.to_string())
605 .y(|(_, v)| *v)
606 .x_axis(false)
607 }
608
609 #[test]
610 fn test_point_count_fills_the_leading_part() {
611 let (x, _, _) = chart(vec![1., 2., 3.])
612 .point_count(5)
613 .scales(bounds())
614 .unwrap();
615 assert_eq!(x.tick(&"0".to_string()), Some(0.));
616 assert_eq!(x.tick(&"2".to_string()), Some(50.));
617
618 let (x, _, _) = chart(vec![1., 2., 3.])
619 .point_count(2)
620 .scales(bounds())
621 .unwrap();
622 assert_eq!(x.tick(&"2".to_string()), Some(100.));
623 }
624
625 #[test]
626 fn test_y_domain_replaces_the_fit_from_zero() {
627 let (_, y, _) = chart(vec![10., 20.])
628 .y_domain(10., 20.)
629 .scales(bounds())
630 .unwrap();
631 assert_eq!(y.tick(&10.), Some(50.));
632 assert_eq!(y.tick(&20.), Some(10.));
633
634 let (_, y, _) = chart(vec![10., 20.]).scales(bounds()).unwrap();
635 assert_eq!(y.tick(&0.), Some(50.));
636 assert_eq!(y.tick(&20.), Some(10.));
637 }
638}