1use std::{hash::Hash, ops::RangeInclusive, rc::Rc};
2
3use gpui::{
4 AnyElement, App, Background, Bounds, Corners, ElementId, Hsla, IntoElement, LinearColorStop,
5 Pixels, Point, SharedString, Size, TextAlign, Window, linear_gradient, point, px,
6};
7use gpui_component_macros::IntoPlot;
8
9use crate::{
10 ActiveTheme,
11 plot::{
12 AxisLabelPlacement, AxisLabelSide, AxisText, Grid, Plot, PlotAppear, PlotAxis, PlotLabel,
13 label::{TEXT_GAP, TEXT_HEIGHT, TEXT_SIZE, Text, measure_text_width},
14 scale::{PlotValue, Scale, ScaleBand, ScaleLinear},
15 shape::{Bar, BarAlignment},
16 tooltip::{CrossLine, PlotHover, Tooltip, TooltipState},
17 },
18};
19
20use super::{
21 AXIS_GAP, ChartAppear, MAX_BAND_WIDTH, TickFormat, TooltipContent, VALUE_AXIS_GAP,
22 build_band_labels, caller_id, format_tick, labeled_items, value_axis_gap,
23};
24
25const HOVER_DIM: f32 = 0.45;
27
28#[derive(Clone, Copy)]
30struct BarHover {
31 center: f32,
33 focus: f32,
35}
36
37#[derive(IntoPlot)]
38pub struct BarChart<T, B, V>
39where
40 T: 'static,
41 B: Eq + Hash + Into<SharedString> + 'static,
42 V: PlotValue,
43{
44 data: Vec<T>,
45 band: Option<Rc<dyn Fn(&T) -> B>>,
46 value: Option<Rc<dyn Fn(&T) -> V>>,
47 fill: Option<Rc<dyn Fn(&T, Bounds<f32>, Bounds<f32>, BarAlignment) -> Background>>,
48 #[allow(clippy::type_complexity)]
49 fill_gradient:
50 Option<Rc<dyn Fn(&T, RangeInclusive<f32>, &dyn Fn(f32) -> f32) -> [LinearColorStop; 2]>>,
51 tick_margin: usize,
52 label: Option<Rc<dyn Fn(&T) -> SharedString>>,
53 label_color: Option<Rc<dyn Fn(&T) -> Hsla>>,
54 label_axis: bool,
55 value_axis: bool,
56 value_axis_label_placement: AxisLabelPlacement,
57 value_tick_count: usize,
58 value_tick_format: Option<TickFormat>,
59 band_count: Option<usize>,
60 band_tick_count: Option<usize>,
61 grid: bool,
62 grid_dashed: bool,
63 alignment: BarAlignment,
64 corner_radii: Corners<Pixels>,
65 padding_inner: f32,
66 padding_outer: f32,
67 max_band_width: Pixels,
68 min_length: f32,
69 id: ElementId,
70 interactive: bool,
71 appear: ChartAppear,
72 name: Option<SharedString>,
73 tooltip_content: TooltipContent<T>,
74 horizontal_gaps: (f32, f32),
77 value_label_gap: f32,
80 hover: Option<BarHover>,
81}
82
83impl<T, B, V> BarChart<T, B, V>
84where
85 B: Eq + Hash + Into<SharedString> + 'static,
86 V: PlotValue,
87{
88 #[track_caller]
89 pub fn new<I>(data: I) -> Self
90 where
91 I: IntoIterator<Item = T>,
92 {
93 Self {
94 data: data.into_iter().collect(),
95 band: None,
96 value: None,
97 fill: None,
98 fill_gradient: None,
99 tick_margin: 1,
100 label: None,
101 label_color: None,
102 label_axis: true,
103 value_axis: false,
104 value_axis_label_placement: AxisLabelPlacement::default(),
105 value_tick_count: 5,
106 value_tick_format: None,
107 band_count: None,
108 band_tick_count: None,
109 grid: true,
110 grid_dashed: true,
111 alignment: BarAlignment::default(),
112 corner_radii: Corners::all(px(0.)),
113 padding_inner: 0.4,
114 padding_outer: 0.2,
115 max_band_width: px(MAX_BAND_WIDTH),
116 min_length: 0.,
117 id: caller_id(),
118 interactive: true,
119 appear: ChartAppear::default(),
120 name: None,
121 tooltip_content: TooltipContent::default(),
122 horizontal_gaps: (0., 0.),
123 value_label_gap: VALUE_AXIS_GAP,
124 hover: None,
125 }
126 }
127
128 pub fn id(mut self, id: impl Into<ElementId>) -> Self {
135 self.id = id.into();
136 self
137 }
138
139 pub fn interactive(mut self, interactive: bool) -> Self {
147 self.interactive = interactive;
148 self
149 }
150
151 pub fn appear(mut self, appear: bool) -> Self {
158 self.appear.set_enabled(appear);
159 self
160 }
161
162 pub fn appear_key(mut self, key: impl Hash) -> Self {
167 self.appear.set_key(key);
168 self
169 }
170
171 pub fn name(mut self, name: impl Into<SharedString>) -> Self {
173 self.name = Some(name.into());
174 self
175 }
176
177 pub fn tooltip_title(mut self, title: impl Fn(&T) -> SharedString + 'static) -> Self {
179 self.tooltip_content.set_title(title);
180 self
181 }
182
183 pub fn tooltip_value(mut self, value: impl Fn(&T, f64) -> SharedString + 'static) -> Self {
187 self.tooltip_content.set_value(move |d, _, v| value(d, v));
188 self
189 }
190
191 pub fn tooltip_value_color<H>(mut self, color: impl Fn(&T, f64) -> H + 'static) -> Self
197 where
198 H: Into<Hsla>,
199 {
200 self.tooltip_content
201 .set_value_color(move |d, _, value| color(d, value));
202 self
203 }
204
205 pub fn tooltip_content<E>(
212 mut self,
213 content: impl Fn(&T, &mut Window, &mut App) -> E + 'static,
214 ) -> Self
215 where
216 E: IntoElement,
217 {
218 self.tooltip_content.set_content(content);
219 self
220 }
221
222 pub fn band(mut self, band: impl Fn(&T) -> B + 'static) -> Self {
224 self.band = Some(Rc::new(band));
225 self
226 }
227
228 pub fn value(mut self, value: impl Fn(&T) -> V + 'static) -> Self {
230 self.value = Some(Rc::new(value));
231 self
232 }
233
234 pub fn fill<Bg>(
254 mut self,
255 fill: impl Fn(&T, Bounds<f32>, Bounds<f32>, BarAlignment) -> Bg + 'static,
256 ) -> Self
257 where
258 Bg: Into<Background> + 'static,
259 {
260 self.fill = Some(Rc::new(move |t, bar_bounds, chart_bounds, alignment| {
261 fill(t, bar_bounds, chart_bounds, alignment).into()
262 }));
263 self.fill_gradient = None;
264 self
265 }
266
267 pub fn fill_gradient(
303 mut self,
304 fill: impl Fn(&T, RangeInclusive<f32>, &dyn Fn(f32) -> f32) -> [LinearColorStop; 2] + 'static,
305 ) -> Self {
306 self.fill_gradient = Some(Rc::new(fill));
307 self.fill = None;
308 self
309 }
310
311 pub fn tick_margin(mut self, tick_margin: usize) -> Self {
312 self.tick_margin = tick_margin;
313 self
314 }
315
316 pub fn label<S>(mut self, label: impl Fn(&T) -> S + 'static) -> Self
317 where
318 S: Into<SharedString> + 'static,
319 {
320 self.label = Some(Rc::new(move |t| label(t).into()));
321 self
322 }
323
324 pub fn label_color<H>(mut self, color: impl Fn(&T) -> H + 'static) -> Self
330 where
331 H: Into<Hsla> + 'static,
332 {
333 self.label_color = Some(Rc::new(move |t| color(t).into()));
334 self
335 }
336
337 pub fn label_axis(mut self, label_axis: bool) -> Self {
341 self.label_axis = label_axis;
342 self
343 }
344
345 pub fn value_axis(mut self, value_axis: bool) -> Self {
353 self.value_axis = value_axis;
354 self
355 }
356
357 pub fn value_tick_count(mut self, count: usize) -> Self {
366 self.value_tick_count = count.max(2);
367 self
368 }
369
370 pub fn value_axis_label_placement(mut self, placement: AxisLabelPlacement) -> Self {
375 self.value_axis_label_placement = placement;
376 self
377 }
378
379 pub fn value_tick_format<S>(mut self, format: impl Fn(f64) -> S + 'static) -> Self
383 where
384 S: Into<SharedString> + 'static,
385 {
386 self.value_tick_format = Some(Rc::new(move |value| format(value).into()));
387 self
388 }
389
390 pub fn band_count(mut self, count: usize) -> Self {
397 self.band_count = Some(count);
398 self
399 }
400
401 pub fn band_tick_count(mut self, count: usize) -> Self {
408 self.band_tick_count = Some(count);
409 self
410 }
411
412 pub fn grid(mut self, grid: bool) -> Self {
413 self.grid = grid;
414 self
415 }
416
417 pub fn grid_dashed(mut self, dashed: bool) -> Self {
421 self.grid_dashed = dashed;
422 self
423 }
424
425 pub fn alignment(mut self, alignment: BarAlignment) -> Self {
429 self.alignment = alignment;
430 self
431 }
432
433 pub fn corner_radii(mut self, corner_radii: impl Into<Corners<Pixels>>) -> Self {
438 self.corner_radii = corner_radii.into();
439 self
440 }
441
442 pub fn padding_inner(mut self, padding: f32) -> Self {
446 self.padding_inner = padding;
447 self
448 }
449
450 pub fn padding_outer(mut self, padding: f32) -> Self {
454 self.padding_outer = padding;
455 self
456 }
457
458 pub fn max_band_width(mut self, width: impl Into<Pixels>) -> Self {
463 self.max_band_width = width.into();
464 self
465 }
466
467 pub fn min_length(mut self, length: f32) -> Self {
476 self.min_length = length;
477 self
478 }
479
480 fn band_scale(&self, bounds: Bounds<Pixels>) -> Option<ScaleBand<B>> {
483 let band_fn = self.band.as_ref()?;
484 let band_extent = if self.alignment.is_horizontal() {
485 bounds.size.height.as_f32()
486 } else {
487 bounds.size.width.as_f32()
488 };
489 let extent = (band_extent - self.value_axis_gap()).max(0.);
492 Some(
493 ScaleBand::new(self.data.iter().map(|v| band_fn(v)), [0., extent])
494 .band_count(self.band_count.unwrap_or(0))
495 .max_band_width(self.max_band_width.as_f32())
496 .padding_inner(self.padding_inner)
497 .padding_outer(self.padding_outer),
498 )
499 }
500
501 fn band_offset(&self) -> f32 {
508 if self.alignment.is_horizontal() {
509 0.
510 } else {
511 self.value_axis_gap()
512 }
513 }
514
515 fn value_scale(&self, bounds: Bounds<Pixels>) -> Option<(ScaleLinear<V>, f32, f32)> {
519 let value_fn = self.value.as_ref()?;
520 let value_dim = if self.alignment.is_horizontal() {
521 bounds.size.width.as_f32()
522 } else {
523 bounds.size.height.as_f32()
524 };
525 let axis_gap = if self.label_axis { AXIS_GAP } else { 0. };
526 let far_gap = if self.label.is_some() {
534 TEXT_HEIGHT
535 } else {
536 10.
537 };
538 let (band_gap, value_end_gap) = if self.alignment.is_horizontal() {
539 self.horizontal_gaps
540 } else {
541 (axis_gap, far_gap)
542 };
543 let (baseline, far) = match self.alignment {
545 BarAlignment::Bottom => (value_dim - axis_gap, far_gap),
546 BarAlignment::Top => (axis_gap, value_dim - far_gap),
547 BarAlignment::Left => (band_gap, value_dim - value_end_gap),
548 BarAlignment::Right => (value_dim - band_gap, value_end_gap),
549 };
550 let scale = ScaleLinear::new(
551 self.data.iter().map(|v| value_fn(v)).chain(Some(V::zero())),
552 [baseline, far],
553 );
554 Some((scale, baseline, far))
555 }
556
557 fn bar_frame(
559 &self,
560 d: &T,
561 band_scale: &ScaleBand<B>,
562 bounds: Bounds<Pixels>,
563 ) -> Option<Bounds<f32>> {
564 let (band_fn, value_fn) = (self.band.as_ref()?, self.value.as_ref()?);
565 let (value_scale, baseline, _) = self.value_scale(bounds)?;
566 let zero = value_scale.tick(&V::zero()).unwrap_or(baseline);
567 let cross = band_scale.tick(&band_fn(d))? + self.band_offset();
568 let end = bar_end(
569 &value_scale,
570 value_fn(d),
571 zero,
572 self.alignment,
573 self.min_length,
574 )?;
575 let (lo, length) = (end.min(zero), (end - zero).abs());
576 let band_width = band_scale.band_width();
577 Some(if self.alignment.is_horizontal() {
578 Bounds {
579 origin: Point::new(lo, cross),
580 size: Size::new(length, band_width),
581 }
582 } else {
583 Bounds {
584 origin: Point::new(cross, lo),
585 size: Size::new(band_width, length),
586 }
587 })
588 }
589
590 fn gradient_range(&self) -> RangeInclusive<f32> {
592 let Some(value_fn) = self.value.as_ref() else {
593 return 0.0..=0.0;
594 };
595 let mut lo = 0.0_f32;
596 let mut hi = 0.0_f32;
597 for v in &self.data {
598 if let Some(f) = value_fn(v).to_f32() {
599 lo = lo.min(f);
600 hi = hi.max(f);
601 }
602 }
603 lo..=hi
604 }
605
606 fn bar_color(&self, d: &T, frame: Bounds<f32>, bounds: Bounds<Pixels>, cx: &App) -> Hsla {
611 let default = cx.theme().chart_2;
612 if let Some(fill) = self.fill_gradient.as_ref() {
613 let value = self
614 .value
615 .as_ref()
616 .and_then(|value_fn| value_fn(d).to_f32())
617 .unwrap_or(0.);
618 let [first, _] = bar_gradient(fill.as_ref(), d, value, self.gradient_range());
619 return first.color;
620 }
621 let Some(fill) = self.fill.as_ref() else {
622 return default;
623 };
624 let chart_bounds = Bounds {
625 origin: Point::new(0., 0.),
626 size: Size::new(bounds.size.width.as_f32(), bounds.size.height.as_f32()),
627 };
628 fill(d, frame, chart_bounds, self.alignment)
629 .as_solid()
630 .unwrap_or(default)
631 }
632
633 fn value_axis_gap(&self) -> f32 {
636 if !self.value_axis || self.value_axis_label_placement != AxisLabelPlacement::Outside {
637 0.
638 } else if self.alignment.is_horizontal() {
639 VALUE_AXIS_GAP
641 } else {
642 self.value_label_gap
643 }
644 }
645
646 fn band_slots(&self) -> usize {
649 self.band_count.unwrap_or(0).max(self.data.len())
650 }
651
652 fn value_tick_labels(&self) -> Vec<SharedString> {
655 let Some(value_fn) = self.value.as_ref() else {
656 return vec![];
657 };
658 let (lo, hi) = self.data.iter().fold((0.0_f32, 0.0_f32), |(lo, hi), v| {
660 let f = value_fn(v).to_f32().unwrap_or(0.);
661 (lo.min(f), hi.max(f))
662 });
663 let steps = (self.value_tick_count - 1) as f32;
664 (0..self.value_tick_count)
665 .map(|i| {
666 let value = (hi - (hi - lo) * i as f32 / steps) as f64;
667 match self.value_tick_format.as_ref() {
668 Some(format) => format(value),
669 None => format_tick(value),
670 }
671 })
672 .collect()
673 }
674
675 fn measure_horizontal_gaps(&self, window: &mut Window) -> (f32, f32) {
680 let Some(band_fn) = self.band.as_ref() else {
681 return (0., 0.);
682 };
683 let font_size = px(TEXT_SIZE);
684 let band_gap = if self.label_axis {
685 self.data
686 .iter()
687 .map(|v| {
688 let s: SharedString = band_fn(v).into();
689 measure_text_width(&s, font_size, window)
690 })
691 .fold(0f32, f32::max)
692 + TEXT_GAP * 2.
693 } else {
694 0.
695 };
696 let value_end_gap = if let Some(label_fn) = self.label.as_ref() {
697 self.data
698 .iter()
699 .map(|v| measure_text_width(&label_fn(v), font_size, window))
700 .fold(0f32, f32::max)
701 + TEXT_GAP * 2.
702 } else {
703 TEXT_GAP * 4.
704 };
705 (band_gap, value_end_gap)
706 }
707
708 fn value_extent(&self, bounds: Bounds<Pixels>) -> (f32, f32) {
711 if self.alignment.is_horizontal() {
712 let (band_gap, value_end_gap) = self.horizontal_gaps;
713 let length = (bounds.size.width.as_f32() - band_gap - value_end_gap).max(0.);
714 let start = if matches!(self.alignment, BarAlignment::Left) {
715 band_gap
716 } else {
717 value_end_gap
718 };
719 (start, length)
720 } else {
721 let axis_gap = if self.label_axis { AXIS_GAP } else { 0. };
722 let length = bounds.size.height.as_f32() - axis_gap;
723 let start = if matches!(self.alignment, BarAlignment::Top) {
724 axis_gap
725 } else {
726 0.
727 };
728 (start, length)
729 }
730 }
731
732 fn is_over_bars(&self, position: Point<Pixels>, bounds: Bounds<Pixels>) -> bool {
734 let (start, length) = self.value_extent(bounds);
735 if self.alignment.is_horizontal() {
736 let value_labels_top = bounds.size.height.as_f32() - VALUE_AXIS_GAP;
737 (start..=start + length).contains(&position.x.as_f32())
738 && !(self.value_axis_gap() > 0. && position.y.as_f32() > value_labels_top)
739 } else {
740 (start..=start + length).contains(&position.y.as_f32())
741 && position.x.as_f32() >= self.band_offset()
742 }
743 }
744}
745
746impl<T, B, V> Plot for BarChart<T, B, V>
747where
748 B: Eq + Hash + Into<SharedString> + 'static,
749 V: PlotValue,
750{
751 fn prepaint(
752 &mut self,
753 _bounds: Bounds<Pixels>,
754 window: &mut Window,
755 _cx: &mut App,
756 ) -> Vec<AnyElement> {
757 self.horizontal_gaps = if self.alignment.is_horizontal() {
758 self.measure_horizontal_gaps(window)
759 } else {
760 (0., 0.)
761 };
762 if self.value_axis && !self.alignment.is_horizontal() {
763 self.value_label_gap = value_axis_gap(self.value_tick_labels(), window);
764 }
765 vec![]
766 }
767
768 fn paint(&mut self, bounds: Bounds<Pixels>, window: &mut Window, cx: &mut App) {
769 let (Some(band_fn), Some(value_fn)) = (self.band.as_ref(), self.value.as_ref()) else {
770 return;
771 };
772
773 let total_width = bounds.size.width.as_f32();
774 let total_height = bounds.size.height.as_f32();
775 let alignment = self.alignment;
776 let is_horizontal = alignment.is_horizontal();
777
778 let Some(band_scale) = self.band_scale(bounds) else {
781 return;
782 };
783 let band_width = band_scale.band_width();
784
785 let Some((value_scale, baseline, far)) = self.value_scale(bounds) else {
786 return;
787 };
788
789 let zero_pixel = value_scale.tick(&V::zero()).unwrap_or(baseline);
793 let band_offset = self.band_offset();
794
795 let value_axis_gap = self.value_axis_gap();
799 let plot_bounds = if is_horizontal {
800 Bounds {
801 origin: bounds.origin,
802 size: Size::new(bounds.size.width, bounds.size.height - px(value_axis_gap)),
803 }
804 } else {
805 Bounds {
806 origin: bounds.origin + point(px(value_axis_gap), px(0.)),
807 size: Size::new(bounds.size.width - px(value_axis_gap), bounds.size.height),
808 }
809 };
810
811 let mut axis = PlotAxis::new().stroke(cx.theme().border);
813 if self.label_axis {
814 match alignment {
815 BarAlignment::Bottom | BarAlignment::Top => {
816 axis = axis.x(zero_pixel);
817
818 let labeled =
823 labeled_items(self.band_slots(), self.band_tick_count, self.tick_margin);
824 let labels = self
825 .data
826 .iter()
827 .enumerate()
828 .filter(|(i, _)| labeled[*i])
829 .filter_map(|(_, d)| {
830 let band_x = band_scale.tick(&band_fn(d))?;
831 let value = value_fn(d).to_f32().unwrap_or(0.);
832 let label_y = if label_below_zero_line(value, alignment) {
833 zero_pixel + TEXT_GAP
834 } else {
835 zero_pixel - TEXT_GAP - TEXT_SIZE
836 };
837
838 Some(
839 Text::new(
840 band_fn(d).into(),
841 point(px(band_x + band_offset + band_width / 2.), px(label_y)),
842 cx.theme().muted_foreground,
843 )
844 .align(TextAlign::Center),
845 )
846 })
847 .collect();
848 PlotLabel::new(labels).paint(&bounds, window, cx);
849 }
850 BarAlignment::Left | BarAlignment::Right => {
851 let labels = build_band_labels(
852 &self.data,
853 band_fn.as_ref(),
854 &band_scale,
855 band_width,
856 &labeled_items(self.band_slots(), self.band_tick_count, self.tick_margin),
857 cx.theme().muted_foreground,
858 );
859 let (side, align) = if matches!(alignment, BarAlignment::Left) {
860 (AxisLabelSide::Start, TextAlign::Right)
861 } else {
862 (AxisLabelSide::End, TextAlign::Left)
863 };
864 axis = axis
865 .y(zero_pixel)
866 .y_label_side(side)
867 .y_label(labels.into_iter().map(|t| t.align(align)));
868 }
869 }
870 }
871 axis.paint(&plot_bounds, window, cx);
872
873 let value_ticks = value_tick_positions(far, baseline, self.value_tick_count);
874 let steps = value_ticks.len() - 1;
875
876 if self.grid {
878 let grid = Grid::new().stroke(cx.theme().chart_grid);
879 let grid = if self.grid_dashed {
880 grid.dash_array(&[px(4.), px(2.)])
881 } else {
882 grid
883 };
884 let lines = value_ticks[..steps].to_vec();
885 let grid = if is_horizontal {
886 grid.x(lines)
887 } else {
888 grid.y(lines)
889 };
890 grid.paint(&plot_bounds, window);
891 }
892
893 let mut inside_labels = None;
895 if self.value_axis {
896 let color = cx.theme().muted_foreground;
899 let texts = self
900 .value_tick_labels()
901 .into_iter()
902 .zip(value_ticks.iter().copied());
903
904 match self.value_axis_label_placement {
905 AxisLabelPlacement::Outside => {
909 let labels = texts.map(|(text, tick)| AxisText::new(text, px(tick), color));
910 let value_axis = if is_horizontal {
911 PlotAxis::new()
912 .x_axis(false)
913 .x(px(total_height - VALUE_AXIS_GAP))
914 .x_label(labels.map(|t| t.align(TextAlign::Center)))
915 } else {
916 PlotAxis::new()
917 .y_axis(false)
918 .y(px(value_axis_gap - TEXT_GAP * 2.))
919 .y_label(labels.map(|t| t.align(TextAlign::Right)))
920 };
921 value_axis.paint(&bounds, window, cx);
922 }
923 AxisLabelPlacement::Inside => {
927 let labels = texts
928 .map(|(text, tick)| {
929 if is_horizontal {
930 Text::new(text, point(tick, total_height - TEXT_HEIGHT), color)
931 .align(TextAlign::Center)
932 } else {
933 let top = if tick < TEXT_HEIGHT {
934 tick + TEXT_GAP
935 } else {
936 tick - TEXT_HEIGHT
937 };
938 Text::new(text, point(TEXT_GAP, top), color)
939 }
940 })
941 .collect();
942 inside_labels = Some(PlotLabel::new(labels));
943 }
944 }
945 }
946
947 let band_fn_cloned = band_fn.clone();
949 let value_fn_cloned = value_fn.clone();
950 let default_fill: Background = cx.theme().chart_2.into();
951 let fill = self.fill.clone();
952 let fill_gradient = self.fill_gradient.clone();
953 let label_color = cx.theme().foreground;
954 let label_color_fn = self.label_color.clone();
955 let min_length = self.min_length;
956
957 let chart_bounds: Bounds<f32> = Bounds {
961 origin: Point::new(0., 0.),
962 size: Size::new(total_width, total_height),
963 };
964
965 let chart_range = self.gradient_range();
968
969 let hover = self.hover;
973 let step = band_scale.step().max(f32::EPSILON);
974 let emphasis = move |frame: Bounds<f32>| -> f32 {
975 let Some(hover) = hover else {
976 return 1.;
977 };
978 let center = if is_horizontal {
979 frame.origin.y + frame.size.height / 2.
980 } else {
981 frame.origin.x + frame.size.width / 2.
982 };
983 let distance = ((center - hover.center).abs() / step).min(1.);
984 1. - HOVER_DIM * hover.focus * distance
985 };
986
987 let appear = self.appear.get().progress();
990
991 let mut bar = Bar::new()
992 .data(&self.data)
993 .alignment(alignment)
994 .band_width(band_width)
995 .cross(move |d| band_scale.tick(&band_fn_cloned(d)).map(|t| t + band_offset))
996 .base(move |_| zero_pixel)
997 .value(move |d| {
998 let end = bar_end(
999 &value_scale,
1000 value_fn_cloned(d),
1001 zero_pixel,
1002 alignment,
1003 min_length,
1004 )?;
1005 Some(zero_pixel + (end - zero_pixel) * appear)
1006 })
1007 .corner_radii(self.corner_radii);
1008
1009 bar = match (fill, fill_gradient) {
1010 (_, Some(fg)) => {
1011 let value_fn_for_grad = value_fn.clone();
1012 bar.fill(move |d, frame, alignment| {
1013 let v = value_fn_for_grad(d).to_f32().unwrap_or(0.);
1014 let [s0, s1] = bar_gradient(fg.as_ref(), d, v, chart_range.clone());
1015 let bg: Background = linear_gradient(alignment.gradient_angle(), s0, s1);
1016 bg.opacity(emphasis(frame))
1017 })
1018 }
1019 (Some(f), _) => bar.fill(move |d, frame, alignment| {
1020 f(d, frame, chart_bounds, alignment).opacity(emphasis(frame))
1021 }),
1022 _ => bar.fill(move |_, frame, _| default_fill.opacity(emphasis(frame))),
1023 };
1024
1025 if let Some(label) = self.label.as_ref() {
1026 let label = label.clone();
1027 let text_align = match alignment {
1028 BarAlignment::Bottom | BarAlignment::Top => TextAlign::Center,
1029 BarAlignment::Left => TextAlign::Left,
1030 BarAlignment::Right => TextAlign::Right,
1031 };
1032 bar = bar.label(move |d, p| {
1033 let color = label_color_fn
1035 .as_ref()
1036 .map_or(label_color, |f| f(d))
1037 .opacity(appear);
1038 vec![Text::new(label(d), p, color).align(text_align)]
1039 });
1040 }
1041
1042 bar.paint(&bounds, window, cx);
1043 if let Some(labels) = inside_labels {
1044 labels.paint(&bounds, window, cx);
1045 }
1046 }
1047
1048 fn id(&self) -> Option<ElementId> {
1049 Some(self.id.clone())
1050 }
1051
1052 fn interactive(&self) -> bool {
1053 self.interactive
1054 }
1055
1056 fn appear(&mut self, appear: PlotAppear, _window: &mut Window, _cx: &mut App) {
1057 self.appear.update(appear);
1058 }
1059
1060 fn appear_generation(&self) -> Option<u64> {
1061 self.appear.generation()
1062 }
1063
1064 fn tooltip_state(
1065 &self,
1066 position: Point<Pixels>,
1067 bounds: Bounds<Pixels>,
1068 _cx: &App,
1069 ) -> Option<TooltipState> {
1070 let band_fn = self.band.as_ref()?;
1071 self.value.as_ref()?;
1072
1073 if !self.is_over_bars(position, bounds) {
1075 return None;
1076 }
1077
1078 let is_horizontal = self.alignment.is_horizontal();
1081 let band_scale = self.band_scale(bounds)?;
1082 let band_width = band_scale.band_width();
1083
1084 let band_offset = self.band_offset();
1085 let cursor_band = if is_horizontal {
1086 position.y
1087 } else {
1088 position.x
1089 };
1090 let index = band_scale.nearest_index(cursor_band.as_f32() - band_offset);
1091 let d = self.data.get(index)?;
1092 let center = band_scale.tick(&band_fn(d))? + band_offset + band_width / 2.;
1093
1094 let cross_line = if is_horizontal {
1097 point(position.x, px(center))
1098 } else {
1099 point(px(center), position.y)
1100 };
1101
1102 Some(TooltipState::new(index, cross_line, vec![]))
1103 }
1104
1105 fn hover(&mut self, hover: Option<&PlotHover>, window: &mut Window, cx: &mut App) {
1106 self.hover = hover.map(|hover| {
1107 let target = if self.alignment.is_horizontal() {
1110 hover.state().cross_line.y
1111 } else {
1112 hover.state().cross_line.x
1113 };
1114 let center = hover.glide(("bar-chart", "band"), target, window, cx);
1115 BarHover {
1116 center: center.as_f32(),
1117 focus: hover.progress(),
1118 }
1119 });
1120 }
1121
1122 fn tooltip(
1123 &self,
1124 state: &TooltipState,
1125 cursor: Point<Pixels>,
1126 bounds: Bounds<Pixels>,
1127 window: &mut Window,
1128 cx: &mut App,
1129 ) -> Option<AnyElement> {
1130 let (band_fn, value_fn) = (self.band.as_ref()?, self.value.as_ref()?);
1131 let d = self.data.get(state.index)?;
1132 let name = self.name.clone().unwrap_or_default();
1133
1134 let band_scale = self.band_scale(bounds)?;
1138 let band_width = band_scale.band_width();
1139 let center = self.hover.map_or(state.cross_line, |hover| {
1140 if self.alignment.is_horizontal() {
1141 point(state.cross_line.x, px(hover.center))
1142 } else {
1143 point(px(hover.center), state.cross_line.y)
1144 }
1145 });
1146 let (start, length) = self.value_extent(bounds);
1147 let cross_line = if self.alignment.is_horizontal() {
1148 CrossLine::new(center)
1149 .horizontal()
1150 .h_span(start, length)
1151 .band(px(band_width))
1152 } else {
1153 CrossLine::new(center)
1154 .span(start, length)
1155 .band(px(band_width))
1156 };
1157
1158 let frame = self.bar_frame(d, &band_scale, bounds).unwrap_or_default();
1159 let swatch = self.bar_color(d, frame, bounds, cx);
1160
1161 let tooltip = Tooltip::new(cursor, bounds.size)
1163 .glide(false)
1164 .gap(px(8.))
1165 .cross_line(cross_line);
1166
1167 let tooltip = self.tooltip_content.apply(
1168 tooltip,
1169 d,
1170 || Some(band_fn(d).into()),
1171 || Some([(swatch, name, value_fn(d).to_f64()?)]),
1172 window,
1173 cx,
1174 )?;
1175
1176 Some(tooltip.into_any_element())
1177 }
1178}
1179
1180fn bar_end<V>(
1183 scale: &ScaleLinear<V>,
1184 value: V,
1185 zero: f32,
1186 alignment: BarAlignment,
1187 min_length: f32,
1188) -> Option<f32>
1189where
1190 V: PlotValue,
1191{
1192 let tick = scale.tick(&value)?;
1193 Some(extend_to_min_length(
1194 tick,
1195 zero,
1196 value < V::zero(),
1197 alignment,
1198 min_length,
1199 ))
1200}
1201
1202fn extend_to_min_length(
1205 tick: f32,
1206 zero: f32,
1207 negative: bool,
1208 alignment: BarAlignment,
1209 min: f32,
1210) -> f32 {
1211 if (tick - zero).abs() >= min {
1212 return tick;
1213 }
1214 let grows_toward_origin = matches!(alignment, BarAlignment::Bottom | BarAlignment::Right);
1215 if grows_toward_origin != negative {
1216 zero - min
1217 } else {
1218 zero + min
1219 }
1220}
1221
1222fn bar_gradient<T>(
1225 fill: &dyn Fn(&T, RangeInclusive<f32>, &dyn Fn(f32) -> f32) -> [LinearColorStop; 2],
1226 d: &T,
1227 value: f32,
1228 range: RangeInclusive<f32>,
1229) -> [LinearColorStop; 2] {
1230 let bar_lo = value.min(0.);
1231 let bar_span = (value.max(0.) - bar_lo).max(f32::EPSILON);
1232 let chart_to_bar = |chart_value: f32| (chart_value - bar_lo) / bar_span;
1233 clip_stops_to_bar(fill(d, range, &chart_to_bar))
1234}
1235
1236fn clip_stops_to_bar(stops: [LinearColorStop; 2]) -> [LinearColorStop; 2] {
1247 let [a, b] = stops;
1248 let p0 = a.percentage;
1249 let p1 = b.percentage;
1250 let lerp = |t: f32| -> Hsla {
1251 Hsla {
1252 h: a.color.h + (b.color.h - a.color.h) * t,
1253 s: a.color.s + (b.color.s - a.color.s) * t,
1254 l: a.color.l + (b.color.l - a.color.l) * t,
1255 a: a.color.a + (b.color.a - a.color.a) * t,
1256 }
1257 };
1258 let span = p1 - p0;
1259 let sample = |target: f32| -> Hsla {
1260 if span.abs() < f32::EPSILON {
1261 a.color
1262 } else {
1263 lerp((target - p0) / span)
1264 }
1265 };
1266 let new_a = if (0. ..=1.).contains(&p0) {
1267 a
1268 } else {
1269 LinearColorStop {
1270 color: sample(p0.clamp(0., 1.)),
1271 percentage: p0.clamp(0., 1.),
1272 }
1273 };
1274 let new_b = if (0. ..=1.).contains(&p1) {
1275 b
1276 } else {
1277 LinearColorStop {
1278 color: sample(p1.clamp(0., 1.)),
1279 percentage: p1.clamp(0., 1.),
1280 }
1281 };
1282 [new_a, new_b]
1283}
1284
1285fn label_below_zero_line(value: f32, alignment: BarAlignment) -> bool {
1292 (value < 0.) == (alignment == BarAlignment::Top)
1293}
1294
1295fn value_tick_positions(far: f32, baseline: f32, count: usize) -> Vec<f32> {
1300 let steps = (count - 1) as f32;
1301 (0..count)
1302 .map(|i| far + (baseline - far) * i as f32 / steps)
1303 .collect()
1304}
1305
1306#[cfg(test)]
1307mod tests {
1308 use super::*;
1309
1310 #[test]
1311 fn test_label_below_zero_line() {
1312 assert!(label_below_zero_line(5., BarAlignment::Bottom));
1314 assert!(label_below_zero_line(0., BarAlignment::Bottom));
1315 assert!(!label_below_zero_line(-5., BarAlignment::Bottom));
1316
1317 assert!(!label_below_zero_line(5., BarAlignment::Top));
1319 assert!(!label_below_zero_line(0., BarAlignment::Top));
1320 assert!(label_below_zero_line(-5., BarAlignment::Top));
1321 }
1322
1323 #[test]
1324 fn test_value_tick_positions() {
1325 assert_eq!(
1327 value_tick_positions(10., 110., 5),
1328 vec![10., 35., 60., 85., 110.]
1329 );
1330
1331 assert_eq!(value_tick_positions(110., 10., 3), vec![110., 60., 10.]);
1333
1334 assert_eq!(value_tick_positions(0., 50., 2), vec![0., 50.]);
1335 }
1336
1337 #[test]
1338 fn test_min_length_extends_away_from_zero() {
1339 assert_eq!(
1341 extend_to_min_length(100., 100., false, BarAlignment::Bottom, 2.),
1342 98.
1343 );
1344 assert_eq!(
1345 extend_to_min_length(10., 10., false, BarAlignment::Top, 2.),
1346 12.
1347 );
1348 assert_eq!(
1349 extend_to_min_length(10., 10., false, BarAlignment::Left, 2.),
1350 12.
1351 );
1352 assert_eq!(
1353 extend_to_min_length(90., 90., false, BarAlignment::Right, 2.),
1354 88.
1355 );
1356
1357 assert_eq!(
1359 extend_to_min_length(50.5, 50., true, BarAlignment::Bottom, 2.),
1360 52.
1361 );
1362
1363 assert_eq!(
1365 extend_to_min_length(40., 100., false, BarAlignment::Bottom, 2.),
1366 40.
1367 );
1368 }
1369
1370 #[test]
1371 fn value_tick_labels_walk_the_domain_from_the_far_end() {
1372 use super::BarChart;
1373
1374 let chart = BarChart::new([10., 20.])
1375 .band(|v| format!("{v}"))
1376 .value(|v| *v)
1377 .value_tick_count(3);
1378 assert_eq!(chart.value_tick_labels(), vec!["20", "10", "0"]);
1379
1380 let money = chart.value_tick_format(|v| format!("${v:.0}"));
1381 assert_eq!(money.value_tick_labels(), vec!["$20", "$10", "$0"]);
1382
1383 assert_eq!(money.band_count(12).band_slots(), 12);
1385 }
1386
1387 #[test]
1388 fn a_band_count_keeps_each_bar_in_its_band() {
1389 use gpui::{Bounds, point, px, size};
1390
1391 use super::BarChart;
1392 use crate::plot::{AxisLabelPlacement, scale::Scale};
1393
1394 let bounds = Bounds::new(point(px(0.), px(0.)), size(px(40.), px(100.)));
1395 let chart = |data: &[f64], count| {
1396 BarChart::new(data.to_vec())
1397 .band(|v| format!("{v}"))
1398 .value(|v| *v)
1399 .band_count(count)
1400 };
1401
1402 let wide = chart(&[1., 2.], 2).band_scale(bounds).unwrap();
1404 let narrow = chart(&[1., 2.], 4).band_scale(bounds).unwrap();
1405 assert_eq!(narrow.band_width() * 2., wide.band_width());
1406 assert!(narrow.tick(&"2".to_string()).unwrap() < 20.);
1407
1408 let grown = chart(&[1., 2., 3.], 4).band_scale(bounds).unwrap();
1410 assert_eq!(grown.tick(&"2".to_string()), narrow.tick(&"2".to_string()));
1411 assert_eq!(grown.band_width(), narrow.band_width());
1412
1413 let outside = chart(&[1., 2.], 2).value_axis(true);
1415 let inside = chart(&[1., 2.], 2)
1416 .value_axis(true)
1417 .value_axis_label_placement(AxisLabelPlacement::Inside);
1418 assert_eq!(outside.value_axis_gap(), super::VALUE_AXIS_GAP);
1419 assert_eq!(inside.value_axis_gap(), 0.);
1420 }
1421
1422 #[gpui::test]
1425 fn the_tooltip_swatch_follows_the_bar_color(cx: &mut gpui::TestAppContext) {
1426 cx.update(crate::init);
1427 let bars = || {
1428 BarChart::new([1., -2.])
1429 .band(|d: &f64| SharedString::from(format!("{d}")))
1430 .value(|d: &f64| *d)
1431 };
1432 let frame = Bounds::default();
1433 let bounds = Bounds::new(point(px(0.), px(0.)), gpui::size(px(100.), px(100.)));
1434 let (default, solid, gradient, stops) = cx.update(|cx| {
1435 let gain = gpui::green();
1436 let loss = gpui::red();
1437 let default = bars().bar_color(&1., frame, bounds, cx);
1438 let solid = bars()
1439 .fill(move |d: &f64, _, _, _| if *d >= 0. { gain } else { loss })
1440 .bar_color(&-2., frame, bounds, cx);
1441 let gradient = bars()
1442 .fill(move |_: &f64, _, _, _| {
1443 linear_gradient(
1444 0.,
1445 gpui::linear_color_stop(gain, 0.),
1446 gpui::linear_color_stop(loss, 1.),
1447 )
1448 })
1449 .bar_color(&1., frame, bounds, cx);
1450 let stops = bars()
1451 .fill_gradient(move |_: &f64, _, _| {
1452 [
1453 gpui::linear_color_stop(gain, 0.),
1454 gpui::linear_color_stop(loss, 1.),
1455 ]
1456 })
1457 .bar_color(&1., frame, bounds, cx);
1458 (default, solid, gradient, stops)
1459 });
1460 let chart_2 = cx.update(|cx| cx.theme().chart_2);
1461
1462 assert_eq!(default, chart_2);
1463 assert_eq!(solid, gpui::red());
1464 assert_eq!(gradient, chart_2);
1465 assert_eq!(stops, gpui::green());
1466 }
1467
1468 #[test]
1471 fn the_tooltip_reads_the_painted_bar_frame() {
1472 let bars = BarChart::new([1., -2.])
1473 .band(|d: &f64| SharedString::from(format!("{d}")))
1474 .value(|d: &f64| *d);
1475 let bounds = Bounds::new(point(px(0.), px(0.)), gpui::size(px(100.), px(100.)));
1476 let band_scale = bars.band_scale(bounds).expect("bars have a band scale");
1477 let up = bars.bar_frame(&1., &band_scale, bounds).expect("a frame");
1478 let down = bars.bar_frame(&-2., &band_scale, bounds).expect("a frame");
1479
1480 assert_eq!(up.origin.y + up.size.height, down.origin.y);
1482 assert!((down.size.height - 2. * up.size.height).abs() < 0.01);
1483 assert!(up.origin.x < down.origin.x);
1484 assert_eq!(up.size.width, band_scale.band_width());
1485 }
1486}