1use std::{hash::Hash, rc::Rc};
2
3use gpui::{
4 AnyElement, App, Bounds, ElementId, Hsla, IntoElement, Pixels, Point, SharedString, Size,
5 Window, point, px,
6};
7use gpui_component_macros::IntoPlot;
8
9use crate::{
10 ActiveTheme,
11 plot::{
12 AxisLabelPlacement, Curve, PathCaches, Plot, PlotAppear, PlotAxis,
13 scale::{PlotValue, Scale, ScaleLinear, ScalePoint},
14 shape::Line,
15 tooltip::{CrossLine, Dot, Tooltip, TooltipState},
16 },
17};
18
19use super::{
20 AXIS_GAP, ChartAppear, HOVER_DOT_SIZE, HOVER_HALO_SIZE, PointAxes, TooltipContent, ValueExtent,
21 axis_point_count, build_point_x_labels, caller_id, labeled_items, pinned_plot_mask,
22 point_range, point_value_scale, reveal_mask,
23};
24
25#[derive(IntoPlot)]
26pub struct LineChart<T, X, Y>
27where
28 T: 'static,
29 X: PartialEq + Into<SharedString> + 'static,
30 Y: PlotValue,
31{
32 data: Vec<T>,
33 x: Option<Rc<dyn Fn(&T) -> X>>,
34 y: Option<Rc<dyn Fn(&T) -> Y>>,
35 stroke: Option<Hsla>,
36 curve: Curve,
37 dot: bool,
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 appear: ChartAppear,
47 name: Option<SharedString>,
48 tooltip_content: TooltipContent<T>,
49}
50
51impl<T, X, Y> LineChart<T, X, Y>
52where
53 X: PartialEq + Into<SharedString> + 'static,
54 Y: PlotValue,
55{
56 #[track_caller]
57 pub fn new<I>(data: I) -> Self
58 where
59 I: IntoIterator<Item = T>,
60 {
61 Self {
62 data: data.into_iter().collect(),
63 stroke: None,
64 curve: Default::default(),
65 dot: false,
66 x: None,
67 y: None,
68 tick_margin: 1,
69 x_axis: true,
70 grid: true,
71 y_domain: None,
72 point_count: None,
73 axes: PointAxes::default(),
74 id: caller_id(),
75 interactive: true,
76 appear: ChartAppear::default(),
77 name: None,
78 tooltip_content: TooltipContent::default(),
79 }
80 }
81
82 pub fn id(mut self, id: impl Into<ElementId>) -> Self {
89 self.id = id.into();
90 self
91 }
92
93 pub fn interactive(mut self, interactive: bool) -> Self {
101 self.interactive = interactive;
102 self
103 }
104
105 pub fn appear(mut self, appear: bool) -> Self {
112 self.appear.set_enabled(appear);
113 self
114 }
115
116 pub fn appear_key(mut self, key: impl Hash) -> Self {
121 self.appear.set_key(key);
122 self
123 }
124
125 pub fn name(mut self, name: impl Into<SharedString>) -> Self {
127 self.name = Some(name.into());
128 self
129 }
130
131 pub fn tooltip_title(mut self, title: impl Fn(&T) -> SharedString + 'static) -> Self {
133 self.tooltip_content.set_title(title);
134 self
135 }
136
137 pub fn tooltip_value(mut self, value: impl Fn(&T, f64) -> SharedString + 'static) -> Self {
141 self.tooltip_content.set_value(move |d, _, v| value(d, v));
142 self
143 }
144
145 pub fn tooltip_value_color<H>(mut self, color: impl Fn(&T, f64) -> H + 'static) -> Self
151 where
152 H: Into<Hsla>,
153 {
154 self.tooltip_content
155 .set_value_color(move |d, _, value| color(d, value));
156 self
157 }
158
159 pub fn tooltip_content<E>(
166 mut self,
167 content: impl Fn(&T, &mut Window, &mut App) -> E + 'static,
168 ) -> Self
169 where
170 E: IntoElement,
171 {
172 self.tooltip_content.set_content(content);
173 self
174 }
175
176 pub fn x(mut self, x: impl Fn(&T) -> X + 'static) -> Self {
177 self.x = Some(Rc::new(x));
178 self
179 }
180
181 pub fn y(mut self, y: impl Fn(&T) -> Y + 'static) -> Self {
182 self.y = Some(Rc::new(y));
183 self
184 }
185
186 pub fn stroke(mut self, stroke: impl Into<Hsla>) -> Self {
187 self.stroke = Some(stroke.into());
188 self
189 }
190
191 pub fn natural(mut self) -> Self {
192 self.curve = Curve::Natural;
193 self
194 }
195
196 pub fn linear(mut self) -> Self {
197 self.curve = Curve::Linear;
198 self
199 }
200
201 pub fn step_after(mut self) -> Self {
202 self.curve = Curve::StepAfter;
203 self
204 }
205
206 pub fn dot(mut self) -> Self {
207 self.dot = true;
208 self
209 }
210
211 pub fn tick_margin(mut self, tick_margin: usize) -> Self {
212 self.tick_margin = tick_margin;
213 self
214 }
215
216 pub fn x_axis(mut self, x_axis: bool) -> Self {
220 self.x_axis = x_axis;
221 self
222 }
223
224 pub fn grid(mut self, grid: bool) -> Self {
225 self.grid = grid;
226 self
227 }
228
229 pub fn y_domain(mut self, min: Y, max: Y) -> Self {
237 self.y_domain = Some((min, max));
238 self
239 }
240
241 pub fn point_count(mut self, count: usize) -> Self {
250 self.point_count = Some(count);
251 self
252 }
253
254 pub fn y_axis(mut self, y_axis: bool) -> Self {
258 self.axes.y_axis = y_axis;
259 self
260 }
261
262 pub fn y_axis_label_placement(mut self, placement: AxisLabelPlacement) -> Self {
267 self.axes.y_axis_label_placement = placement;
268 self
269 }
270
271 pub fn y_tick_count(mut self, count: usize) -> Self {
280 self.axes.y_tick_count = count.max(2);
281 self
282 }
283
284 pub fn y_tick_format<S>(mut self, format: impl Fn(f64) -> S + 'static) -> Self
286 where
287 S: Into<SharedString> + 'static,
288 {
289 self.axes.y_tick_format = Some(Rc::new(move |value| format(value).into()));
290 self
291 }
292
293 pub fn x_tick_count(mut self, count: usize) -> Self {
300 self.axes.x_tick_count = Some(count);
301 self
302 }
303
304 pub fn grid_columns(mut self, count: usize) -> Self {
309 self.axes.grid_columns = count;
310 self
311 }
312
313 pub fn grid_dashed(mut self, dashed: bool) -> Self {
317 self.axes.grid_dashed = dashed;
318 self
319 }
320
321 pub fn reference_line(mut self, value: Y) -> Self {
325 if let Some(value) = value.to_f64() {
326 self.axes.reference_lines.push(value);
327 }
328 self
329 }
330
331 pub fn y_padding(mut self, top: f32, bottom: f32) -> Self {
336 self.axes.y_padding = (top, bottom);
337 self
338 }
339
340 fn scales(
345 &self,
346 bounds: Bounds<Pixels>,
347 ) -> Option<(ScalePoint<X>, ScaleLinear<Y>, ValueExtent)> {
348 let (x_fn, y_fn) = (self.x.as_ref()?, self.y.as_ref()?);
349
350 let width = bounds.size.width.as_f32();
351 let axis_gap = if self.x_axis { AXIS_GAP } else { 0. };
352 let height = bounds.size.height.as_f32() - axis_gap;
353
354 let len = self.data.len();
355 let x = ScalePoint::new(
356 self.data.iter().map(|v| x_fn(v)),
357 point_range(
358 self.axes.plot_left(),
359 width - self.axes.plot_left(),
360 len,
361 axis_point_count(self.point_count, len),
362 ),
363 );
364 let (y, extent) = point_value_scale(
365 self.data.iter().map(|v| y_fn(v)),
366 self.y_domain,
367 height,
368 self.axes.y_padding,
369 );
370
371 Some((x, y, extent))
372 }
373}
374
375impl<T, X, Y> Plot for LineChart<T, X, Y>
376where
377 X: PartialEq + Into<SharedString> + 'static,
378 Y: PlotValue,
379{
380 fn prepaint(
381 &mut self,
382 bounds: Bounds<Pixels>,
383 window: &mut Window,
384 _cx: &mut App,
385 ) -> Vec<AnyElement> {
386 if let Some((_, _, extent)) = self.scales(bounds) {
388 let axis_gap = if self.x_axis { AXIS_GAP } else { 0. };
389 let height = bounds.size.height.as_f32() - axis_gap;
390 self.axes.measure_y_labels(extent, height, window);
391 }
392 vec![]
393 }
394
395 fn paint(&mut self, bounds: Bounds<Pixels>, window: &mut Window, cx: &mut App) {
396 let (Some(x_fn), Some(y_fn)) = (self.x.as_ref(), self.y.as_ref()) else {
397 return;
398 };
399 let Some((x, y, extent)) = self.scales(bounds) else {
400 return;
401 };
402
403 let axis_gap = if self.x_axis { AXIS_GAP } else { 0. };
404 let height = bounds.size.height.as_f32() - axis_gap;
405
406 let left = self.axes.plot_left();
410 let axis_bounds = Bounds {
411 origin: bounds.origin + point(px(left), px(0.)),
412 size: Size::new(bounds.size.width - px(left), bounds.size.height),
413 };
414 let mut axis = PlotAxis::new().stroke(cx.theme().border);
415 if self.x_axis {
416 let labeled = labeled_items(
417 axis_point_count(self.point_count, self.data.len()),
418 self.axes.x_tick_count,
419 self.tick_margin,
420 );
421 let labels = build_point_x_labels(
422 &self.data,
423 x_fn.as_ref(),
424 &x,
425 axis_point_count(self.point_count, self.data.len()),
426 &labeled,
427 cx.theme().muted_foreground,
428 )
429 .into_iter()
430 .map(|mut label| {
431 label.tick -= px(left);
432 label
433 });
434 axis = axis.x(height).x_label(labels);
435 }
436 axis.paint(&axis_bounds, window, cx);
437
438 if self.grid {
439 self.axes.paint_grid(bounds, height, window, cx);
440 }
441
442 let stroke = self.stroke.unwrap_or(cx.theme().chart_2);
444 let y_fn = y_fn.clone();
447 let mut line = Line::new()
448 .data(self.data.iter().enumerate())
449 .x(move |(i, _)| x.tick_at(*i))
450 .y(move |(_, d)| y.tick(&y_fn(d)))
451 .stroke(stroke)
452 .curve(self.curve)
453 .stroke_width(2.);
454
455 if self.dot {
456 line = line.dot().dot_size(8.).dot_fill(stroke);
457 }
458
459 let mask = self
460 .y_domain
461 .is_some()
462 .then(|| pinned_plot_mask(bounds, height));
463 let reveal = reveal_mask(bounds, left, self.appear.get().progress());
466 window.with_content_mask(mask, |window| {
467 window.with_content_mask(reveal, |window| {
468 let caches = PathCaches::for_paint("line", window, cx);
469 caches.update(cx, |caches, _| {
470 line.paint_cached(&bounds, caches.slot(0), window);
471 });
472 });
473 });
474
475 self.axes
476 .paint_reference_lines(extent, bounds, height, window, cx);
477 self.axes.paint_y_labels(extent, bounds, height, window, cx);
478 }
479
480 fn id(&self) -> Option<ElementId> {
481 Some(self.id.clone())
482 }
483
484 fn interactive(&self) -> bool {
485 self.interactive
486 }
487
488 fn appear(&mut self, appear: PlotAppear, _window: &mut Window, _cx: &mut App) {
489 self.appear.update(appear);
490 }
491
492 fn appear_generation(&self) -> Option<u64> {
493 self.appear.generation()
494 }
495
496 fn tooltip_state(
497 &self,
498 position: Point<Pixels>,
499 bounds: Bounds<Pixels>,
500 _cx: &App,
501 ) -> Option<TooltipState> {
502 let y_fn = self.y.as_ref()?;
503 let (x, y, _) = self.scales(bounds)?;
504
505 let axis_gap = if self.x_axis { AXIS_GAP } else { 0. };
507 if position.y.as_f32() > bounds.size.height.as_f32() - axis_gap
508 || position.x.as_f32() < self.axes.plot_left()
509 {
510 return None;
511 }
512
513 let index = x.nearest_index(position.x.as_f32());
514 let d = self.data.get(index)?;
515 let x_tick = x.tick_at(index)?;
516 let y_tick = y.tick(&y_fn(d))?;
517
518 Some(TooltipState::new(
519 index,
520 point(px(x_tick), position.y),
521 vec![point(px(x_tick), px(y_tick))],
522 ))
523 }
524
525 fn tooltip(
526 &self,
527 state: &TooltipState,
528 cursor: Point<Pixels>,
529 bounds: Bounds<Pixels>,
530 window: &mut Window,
531 cx: &mut App,
532 ) -> Option<AnyElement> {
533 let (x_fn, y_fn) = (self.x.as_ref()?, self.y.as_ref()?);
534 let d = self.data.get(state.index)?;
535 let stroke = self.stroke.unwrap_or(cx.theme().chart_2);
536 let name = self.name.clone().unwrap_or_default();
537 let dot = *state.dots.first()?;
538
539 let tooltip = Tooltip::new(cursor, bounds.size)
541 .gap(px(8.))
542 .cross_line(
544 CrossLine::new(state.cross_line)
545 .height(bounds.size.height.as_f32() - if self.x_axis { AXIS_GAP } else { 0. }),
546 )
547 .dots(Some(
548 Dot::new(dot)
549 .size(HOVER_DOT_SIZE)
550 .halo(HOVER_HALO_SIZE)
551 .stroke(cx.theme().background)
552 .fill(stroke),
553 ));
554
555 let tooltip = self.tooltip_content.apply(
556 tooltip,
557 d,
558 || Some(x_fn(d).into()),
559 || Some([(stroke, name, y_fn(d).to_f64()?)]),
560 window,
561 cx,
562 )?;
563
564 Some(tooltip.into_any_element())
565 }
566}
567
568#[cfg(test)]
569mod tests {
570 use gpui::{Bounds, point, px, size};
571
572 use super::LineChart;
573 use crate::plot::scale::Scale;
574
575 #[test]
576 fn test_f32_values_scale_like_f64() {
577 let bounds = Bounds::new(point(px(0.), px(0.)), size(px(100.), px(50.)));
578 let chart: LineChart<(usize, f32), String, f32> =
579 LineChart::new([2f32, 4.].into_iter().enumerate())
580 .x(|(i, _)| i.to_string())
581 .y(|(_, v)| *v)
582 .x_axis(false);
583 let (_, y, _) = chart.scales(bounds).unwrap();
584 let y64 = LineChart::new([2f64, 4.].into_iter().enumerate())
585 .x(|(i, _): &(usize, f64)| i.to_string())
586 .y(|(_, v)| *v)
587 .x_axis(false)
588 .scales(bounds)
589 .unwrap()
590 .1;
591 assert_eq!(y.tick(&4.), y64.tick(&4.));
592 assert_eq!(y.tick(&0.), y64.tick(&0.));
593 }
594}