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, 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, MAX_BAND_WIDTH, TickFormat, TooltipContent, VALUE_AXIS_GAP, build_band_labels,
22 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 name: Option<SharedString>,
72 tooltip_content: TooltipContent<T>,
73 horizontal_gaps: (f32, f32),
76 value_label_gap: f32,
79 hover: Option<BarHover>,
80}
81
82impl<T, B, V> BarChart<T, B, V>
83where
84 B: Eq + Hash + Into<SharedString> + 'static,
85 V: PlotValue,
86{
87 #[track_caller]
88 pub fn new<I>(data: I) -> Self
89 where
90 I: IntoIterator<Item = T>,
91 {
92 Self {
93 data: data.into_iter().collect(),
94 band: None,
95 value: None,
96 fill: None,
97 fill_gradient: None,
98 tick_margin: 1,
99 label: None,
100 label_color: None,
101 label_axis: true,
102 value_axis: false,
103 value_axis_label_placement: AxisLabelPlacement::default(),
104 value_tick_count: 5,
105 value_tick_format: None,
106 band_count: None,
107 band_tick_count: None,
108 grid: true,
109 grid_dashed: true,
110 alignment: BarAlignment::default(),
111 corner_radii: Corners::all(px(0.)),
112 padding_inner: 0.4,
113 padding_outer: 0.2,
114 max_band_width: px(MAX_BAND_WIDTH),
115 min_length: 0.,
116 id: caller_id(),
117 interactive: true,
118 name: None,
119 tooltip_content: TooltipContent::default(),
120 horizontal_gaps: (0., 0.),
121 value_label_gap: VALUE_AXIS_GAP,
122 hover: None,
123 }
124 }
125
126 pub fn id(mut self, id: impl Into<ElementId>) -> Self {
133 self.id = id.into();
134 self
135 }
136
137 pub fn interactive(mut self, interactive: bool) -> Self {
146 self.interactive = interactive;
147 self
148 }
149
150 pub fn name(mut self, name: impl Into<SharedString>) -> Self {
152 self.name = Some(name.into());
153 self
154 }
155
156 pub fn tooltip_title(mut self, title: impl Fn(&T) -> SharedString + 'static) -> Self {
158 self.tooltip_content.set_title(title);
159 self
160 }
161
162 pub fn tooltip_value(mut self, value: impl Fn(&T, f64) -> SharedString + 'static) -> Self {
166 self.tooltip_content.set_value(move |d, _, v| value(d, v));
167 self
168 }
169
170 pub fn tooltip_value_color<H>(mut self, color: impl Fn(&T, f64) -> H + 'static) -> Self
176 where
177 H: Into<Hsla>,
178 {
179 self.tooltip_content
180 .set_value_color(move |d, _, value| color(d, value));
181 self
182 }
183
184 pub fn tooltip_content<E>(
191 mut self,
192 content: impl Fn(&T, &mut Window, &mut App) -> E + 'static,
193 ) -> Self
194 where
195 E: IntoElement,
196 {
197 self.tooltip_content.set_content(content);
198 self
199 }
200
201 pub fn band(mut self, band: impl Fn(&T) -> B + 'static) -> Self {
203 self.band = Some(Rc::new(band));
204 self
205 }
206
207 pub fn value(mut self, value: impl Fn(&T) -> V + 'static) -> Self {
209 self.value = Some(Rc::new(value));
210 self
211 }
212
213 pub fn fill<Bg>(
233 mut self,
234 fill: impl Fn(&T, Bounds<f32>, Bounds<f32>, BarAlignment) -> Bg + 'static,
235 ) -> Self
236 where
237 Bg: Into<Background> + 'static,
238 {
239 self.fill = Some(Rc::new(move |t, bar_bounds, chart_bounds, alignment| {
240 fill(t, bar_bounds, chart_bounds, alignment).into()
241 }));
242 self.fill_gradient = None;
243 self
244 }
245
246 pub fn fill_gradient(
282 mut self,
283 fill: impl Fn(&T, RangeInclusive<f32>, &dyn Fn(f32) -> f32) -> [LinearColorStop; 2] + 'static,
284 ) -> Self {
285 self.fill_gradient = Some(Rc::new(fill));
286 self.fill = None;
287 self
288 }
289
290 pub fn tick_margin(mut self, tick_margin: usize) -> Self {
291 self.tick_margin = tick_margin;
292 self
293 }
294
295 pub fn label<S>(mut self, label: impl Fn(&T) -> S + 'static) -> Self
296 where
297 S: Into<SharedString> + 'static,
298 {
299 self.label = Some(Rc::new(move |t| label(t).into()));
300 self
301 }
302
303 pub fn label_color<H>(mut self, color: impl Fn(&T) -> H + 'static) -> Self
309 where
310 H: Into<Hsla> + 'static,
311 {
312 self.label_color = Some(Rc::new(move |t| color(t).into()));
313 self
314 }
315
316 pub fn label_axis(mut self, label_axis: bool) -> Self {
320 self.label_axis = label_axis;
321 self
322 }
323
324 pub fn value_axis(mut self, value_axis: bool) -> Self {
332 self.value_axis = value_axis;
333 self
334 }
335
336 pub fn value_tick_count(mut self, count: usize) -> Self {
345 self.value_tick_count = count.max(2);
346 self
347 }
348
349 pub fn value_axis_label_placement(mut self, placement: AxisLabelPlacement) -> Self {
354 self.value_axis_label_placement = placement;
355 self
356 }
357
358 pub fn value_tick_format<S>(mut self, format: impl Fn(f64) -> S + 'static) -> Self
362 where
363 S: Into<SharedString> + 'static,
364 {
365 self.value_tick_format = Some(Rc::new(move |value| format(value).into()));
366 self
367 }
368
369 pub fn band_count(mut self, count: usize) -> Self {
376 self.band_count = Some(count);
377 self
378 }
379
380 pub fn band_tick_count(mut self, count: usize) -> Self {
387 self.band_tick_count = Some(count);
388 self
389 }
390
391 pub fn grid(mut self, grid: bool) -> Self {
392 self.grid = grid;
393 self
394 }
395
396 pub fn grid_dashed(mut self, dashed: bool) -> Self {
400 self.grid_dashed = dashed;
401 self
402 }
403
404 pub fn alignment(mut self, alignment: BarAlignment) -> Self {
408 self.alignment = alignment;
409 self
410 }
411
412 pub fn corner_radii(mut self, corner_radii: impl Into<Corners<Pixels>>) -> Self {
417 self.corner_radii = corner_radii.into();
418 self
419 }
420
421 pub fn padding_inner(mut self, padding: f32) -> Self {
425 self.padding_inner = padding;
426 self
427 }
428
429 pub fn padding_outer(mut self, padding: f32) -> Self {
433 self.padding_outer = padding;
434 self
435 }
436
437 pub fn max_band_width(mut self, width: impl Into<Pixels>) -> Self {
442 self.max_band_width = width.into();
443 self
444 }
445
446 pub fn min_length(mut self, length: f32) -> Self {
455 self.min_length = length;
456 self
457 }
458
459 fn band_scale(&self, bounds: Bounds<Pixels>) -> Option<ScaleBand<B>> {
462 let band_fn = self.band.as_ref()?;
463 let band_extent = if self.alignment.is_horizontal() {
464 bounds.size.height.as_f32()
465 } else {
466 bounds.size.width.as_f32()
467 };
468 let extent = (band_extent - self.value_axis_gap()).max(0.);
471 Some(
472 ScaleBand::new(self.data.iter().map(|v| band_fn(v)), [0., extent])
473 .band_count(self.band_count.unwrap_or(0))
474 .max_band_width(self.max_band_width.as_f32())
475 .padding_inner(self.padding_inner)
476 .padding_outer(self.padding_outer),
477 )
478 }
479
480 fn band_offset(&self) -> f32 {
487 if self.alignment.is_horizontal() {
488 0.
489 } else {
490 self.value_axis_gap()
491 }
492 }
493
494 fn value_scale(&self, bounds: Bounds<Pixels>) -> Option<(ScaleLinear<V>, f32, f32)> {
498 let value_fn = self.value.as_ref()?;
499 let value_dim = if self.alignment.is_horizontal() {
500 bounds.size.width.as_f32()
501 } else {
502 bounds.size.height.as_f32()
503 };
504 let axis_gap = if self.label_axis { AXIS_GAP } else { 0. };
505 let far_gap = if self.label.is_some() {
513 TEXT_HEIGHT
514 } else {
515 10.
516 };
517 let (band_gap, value_end_gap) = if self.alignment.is_horizontal() {
518 self.horizontal_gaps
519 } else {
520 (axis_gap, far_gap)
521 };
522 let (baseline, far) = match self.alignment {
524 BarAlignment::Bottom => (value_dim - axis_gap, far_gap),
525 BarAlignment::Top => (axis_gap, value_dim - far_gap),
526 BarAlignment::Left => (band_gap, value_dim - value_end_gap),
527 BarAlignment::Right => (value_dim - band_gap, value_end_gap),
528 };
529 let scale = ScaleLinear::new(
530 self.data.iter().map(|v| value_fn(v)).chain(Some(V::zero())),
531 [baseline, far],
532 );
533 Some((scale, baseline, far))
534 }
535
536 fn bar_frame(
538 &self,
539 d: &T,
540 band_scale: &ScaleBand<B>,
541 bounds: Bounds<Pixels>,
542 ) -> Option<Bounds<f32>> {
543 let (band_fn, value_fn) = (self.band.as_ref()?, self.value.as_ref()?);
544 let (value_scale, baseline, _) = self.value_scale(bounds)?;
545 let zero = value_scale.tick(&V::zero()).unwrap_or(baseline);
546 let cross = band_scale.tick(&band_fn(d))? + self.band_offset();
547 let end = bar_end(
548 &value_scale,
549 value_fn(d),
550 zero,
551 self.alignment,
552 self.min_length,
553 )?;
554 let (lo, length) = (end.min(zero), (end - zero).abs());
555 let band_width = band_scale.band_width();
556 Some(if self.alignment.is_horizontal() {
557 Bounds {
558 origin: Point::new(lo, cross),
559 size: Size::new(length, band_width),
560 }
561 } else {
562 Bounds {
563 origin: Point::new(cross, lo),
564 size: Size::new(band_width, length),
565 }
566 })
567 }
568
569 fn gradient_range(&self) -> RangeInclusive<f32> {
571 let Some(value_fn) = self.value.as_ref() else {
572 return 0.0..=0.0;
573 };
574 let mut lo = 0.0_f32;
575 let mut hi = 0.0_f32;
576 for v in &self.data {
577 if let Some(f) = value_fn(v).to_f32() {
578 lo = lo.min(f);
579 hi = hi.max(f);
580 }
581 }
582 lo..=hi
583 }
584
585 fn bar_color(&self, d: &T, frame: Bounds<f32>, bounds: Bounds<Pixels>, cx: &App) -> Hsla {
590 let default = cx.theme().chart_2;
591 if let Some(fill) = self.fill_gradient.as_ref() {
592 let value = self
593 .value
594 .as_ref()
595 .and_then(|value_fn| value_fn(d).to_f32())
596 .unwrap_or(0.);
597 let [first, _] = bar_gradient(fill.as_ref(), d, value, self.gradient_range());
598 return first.color;
599 }
600 let Some(fill) = self.fill.as_ref() else {
601 return default;
602 };
603 let chart_bounds = Bounds {
604 origin: Point::new(0., 0.),
605 size: Size::new(bounds.size.width.as_f32(), bounds.size.height.as_f32()),
606 };
607 fill(d, frame, chart_bounds, self.alignment)
608 .as_solid()
609 .unwrap_or(default)
610 }
611
612 fn value_axis_gap(&self) -> f32 {
615 if !self.value_axis || self.value_axis_label_placement != AxisLabelPlacement::Outside {
616 0.
617 } else if self.alignment.is_horizontal() {
618 VALUE_AXIS_GAP
620 } else {
621 self.value_label_gap
622 }
623 }
624
625 fn band_slots(&self) -> usize {
628 self.band_count.unwrap_or(0).max(self.data.len())
629 }
630
631 fn value_tick_labels(&self) -> Vec<SharedString> {
634 let Some(value_fn) = self.value.as_ref() else {
635 return vec![];
636 };
637 let (lo, hi) = self.data.iter().fold((0.0_f32, 0.0_f32), |(lo, hi), v| {
639 let f = value_fn(v).to_f32().unwrap_or(0.);
640 (lo.min(f), hi.max(f))
641 });
642 let steps = (self.value_tick_count - 1) as f32;
643 (0..self.value_tick_count)
644 .map(|i| {
645 let value = (hi - (hi - lo) * i as f32 / steps) as f64;
646 match self.value_tick_format.as_ref() {
647 Some(format) => format(value),
648 None => format_tick(value),
649 }
650 })
651 .collect()
652 }
653
654 fn measure_horizontal_gaps(&self, window: &mut Window) -> (f32, f32) {
659 let Some(band_fn) = self.band.as_ref() else {
660 return (0., 0.);
661 };
662 let font_size = px(TEXT_SIZE);
663 let band_gap = if self.label_axis {
664 self.data
665 .iter()
666 .map(|v| {
667 let s: SharedString = band_fn(v).into();
668 measure_text_width(&s, font_size, window)
669 })
670 .fold(0f32, f32::max)
671 + TEXT_GAP * 2.
672 } else {
673 0.
674 };
675 let value_end_gap = if let Some(label_fn) = self.label.as_ref() {
676 self.data
677 .iter()
678 .map(|v| measure_text_width(&label_fn(v), font_size, window))
679 .fold(0f32, f32::max)
680 + TEXT_GAP * 2.
681 } else {
682 TEXT_GAP * 4.
683 };
684 (band_gap, value_end_gap)
685 }
686
687 fn value_extent(&self, bounds: Bounds<Pixels>) -> (f32, f32) {
690 if self.alignment.is_horizontal() {
691 let (band_gap, value_end_gap) = self.horizontal_gaps;
692 let length = (bounds.size.width.as_f32() - band_gap - value_end_gap).max(0.);
693 let start = if matches!(self.alignment, BarAlignment::Left) {
694 band_gap
695 } else {
696 value_end_gap
697 };
698 (start, length)
699 } else {
700 let axis_gap = if self.label_axis { AXIS_GAP } else { 0. };
701 let length = bounds.size.height.as_f32() - axis_gap;
702 let start = if matches!(self.alignment, BarAlignment::Top) {
703 axis_gap
704 } else {
705 0.
706 };
707 (start, length)
708 }
709 }
710
711 fn is_over_bars(&self, position: Point<Pixels>, bounds: Bounds<Pixels>) -> bool {
713 let (start, length) = self.value_extent(bounds);
714 if self.alignment.is_horizontal() {
715 let value_labels_top = bounds.size.height.as_f32() - VALUE_AXIS_GAP;
716 (start..=start + length).contains(&position.x.as_f32())
717 && !(self.value_axis_gap() > 0. && position.y.as_f32() > value_labels_top)
718 } else {
719 (start..=start + length).contains(&position.y.as_f32())
720 && position.x.as_f32() >= self.band_offset()
721 }
722 }
723}
724
725impl<T, B, V> Plot for BarChart<T, B, V>
726where
727 B: Eq + Hash + Into<SharedString> + 'static,
728 V: PlotValue,
729{
730 fn prepaint(
731 &mut self,
732 _bounds: Bounds<Pixels>,
733 window: &mut Window,
734 _cx: &mut App,
735 ) -> Vec<AnyElement> {
736 self.horizontal_gaps = if self.alignment.is_horizontal() {
737 self.measure_horizontal_gaps(window)
738 } else {
739 (0., 0.)
740 };
741 if self.value_axis && !self.alignment.is_horizontal() {
742 self.value_label_gap = value_axis_gap(self.value_tick_labels(), window);
743 }
744 vec![]
745 }
746
747 fn paint(&mut self, bounds: Bounds<Pixels>, window: &mut Window, cx: &mut App) {
748 let (Some(band_fn), Some(value_fn)) = (self.band.as_ref(), self.value.as_ref()) else {
749 return;
750 };
751
752 let total_width = bounds.size.width.as_f32();
753 let total_height = bounds.size.height.as_f32();
754 let alignment = self.alignment;
755 let is_horizontal = alignment.is_horizontal();
756
757 let Some(band_scale) = self.band_scale(bounds) else {
760 return;
761 };
762 let band_width = band_scale.band_width();
763
764 let Some((value_scale, baseline, far)) = self.value_scale(bounds) else {
765 return;
766 };
767
768 let zero_pixel = value_scale.tick(&V::zero()).unwrap_or(baseline);
772 let band_offset = self.band_offset();
773
774 let value_axis_gap = self.value_axis_gap();
778 let plot_bounds = if is_horizontal {
779 Bounds {
780 origin: bounds.origin,
781 size: Size::new(bounds.size.width, bounds.size.height - px(value_axis_gap)),
782 }
783 } else {
784 Bounds {
785 origin: bounds.origin + point(px(value_axis_gap), px(0.)),
786 size: Size::new(bounds.size.width - px(value_axis_gap), bounds.size.height),
787 }
788 };
789
790 let mut axis = PlotAxis::new().stroke(cx.theme().border);
792 if self.label_axis {
793 match alignment {
794 BarAlignment::Bottom | BarAlignment::Top => {
795 axis = axis.x(zero_pixel);
796
797 let labeled =
802 labeled_items(self.band_slots(), self.band_tick_count, self.tick_margin);
803 let labels = self
804 .data
805 .iter()
806 .enumerate()
807 .filter(|(i, _)| labeled[*i])
808 .filter_map(|(_, d)| {
809 let band_x = band_scale.tick(&band_fn(d))?;
810 let value = value_fn(d).to_f32().unwrap_or(0.);
811 let label_y = if label_below_zero_line(value, alignment) {
812 zero_pixel + TEXT_GAP
813 } else {
814 zero_pixel - TEXT_GAP - TEXT_SIZE
815 };
816
817 Some(
818 Text::new(
819 band_fn(d).into(),
820 point(px(band_x + band_offset + band_width / 2.), px(label_y)),
821 cx.theme().muted_foreground,
822 )
823 .align(TextAlign::Center),
824 )
825 })
826 .collect();
827 PlotLabel::new(labels).paint(&bounds, window, cx);
828 }
829 BarAlignment::Left | BarAlignment::Right => {
830 let labels = build_band_labels(
831 &self.data,
832 band_fn.as_ref(),
833 &band_scale,
834 band_width,
835 &labeled_items(self.band_slots(), self.band_tick_count, self.tick_margin),
836 cx.theme().muted_foreground,
837 );
838 let (side, align) = if matches!(alignment, BarAlignment::Left) {
839 (AxisLabelSide::Start, TextAlign::Right)
840 } else {
841 (AxisLabelSide::End, TextAlign::Left)
842 };
843 axis = axis
844 .y(zero_pixel)
845 .y_label_side(side)
846 .y_label(labels.into_iter().map(|t| t.align(align)));
847 }
848 }
849 }
850 axis.paint(&plot_bounds, window, cx);
851
852 let value_ticks = value_tick_positions(far, baseline, self.value_tick_count);
853 let steps = value_ticks.len() - 1;
854
855 if self.grid {
857 let grid = Grid::new().stroke(cx.theme().chart_grid);
858 let grid = if self.grid_dashed {
859 grid.dash_array(&[px(4.), px(2.)])
860 } else {
861 grid
862 };
863 let lines = value_ticks[..steps].to_vec();
864 let grid = if is_horizontal {
865 grid.x(lines)
866 } else {
867 grid.y(lines)
868 };
869 grid.paint(&plot_bounds, window);
870 }
871
872 let mut inside_labels = None;
874 if self.value_axis {
875 let color = cx.theme().muted_foreground;
878 let texts = self
879 .value_tick_labels()
880 .into_iter()
881 .zip(value_ticks.iter().copied());
882
883 match self.value_axis_label_placement {
884 AxisLabelPlacement::Outside => {
888 let labels = texts.map(|(text, tick)| AxisText::new(text, px(tick), color));
889 let value_axis = if is_horizontal {
890 PlotAxis::new()
891 .x_axis(false)
892 .x(px(total_height - VALUE_AXIS_GAP))
893 .x_label(labels.map(|t| t.align(TextAlign::Center)))
894 } else {
895 PlotAxis::new()
896 .y_axis(false)
897 .y(px(value_axis_gap - TEXT_GAP * 2.))
898 .y_label(labels.map(|t| t.align(TextAlign::Right)))
899 };
900 value_axis.paint(&bounds, window, cx);
901 }
902 AxisLabelPlacement::Inside => {
906 let labels = texts
907 .map(|(text, tick)| {
908 if is_horizontal {
909 Text::new(text, point(tick, total_height - TEXT_HEIGHT), color)
910 .align(TextAlign::Center)
911 } else {
912 let top = if tick < TEXT_HEIGHT {
913 tick + TEXT_GAP
914 } else {
915 tick - TEXT_HEIGHT
916 };
917 Text::new(text, point(TEXT_GAP, top), color)
918 }
919 })
920 .collect();
921 inside_labels = Some(PlotLabel::new(labels));
922 }
923 }
924 }
925
926 let band_fn_cloned = band_fn.clone();
928 let value_fn_cloned = value_fn.clone();
929 let default_fill: Background = cx.theme().chart_2.into();
930 let fill = self.fill.clone();
931 let fill_gradient = self.fill_gradient.clone();
932 let label_color = cx.theme().foreground;
933 let label_color_fn = self.label_color.clone();
934 let min_length = self.min_length;
935
936 let chart_bounds: Bounds<f32> = Bounds {
940 origin: Point::new(0., 0.),
941 size: Size::new(total_width, total_height),
942 };
943
944 let chart_range = self.gradient_range();
947
948 let hover = self.hover;
952 let step = band_scale.step().max(f32::EPSILON);
953 let emphasis = move |frame: Bounds<f32>| -> f32 {
954 let Some(hover) = hover else {
955 return 1.;
956 };
957 let center = if is_horizontal {
958 frame.origin.y + frame.size.height / 2.
959 } else {
960 frame.origin.x + frame.size.width / 2.
961 };
962 let distance = ((center - hover.center).abs() / step).min(1.);
963 1. - HOVER_DIM * hover.focus * distance
964 };
965
966 let mut bar = Bar::new()
967 .data(&self.data)
968 .alignment(alignment)
969 .band_width(band_width)
970 .cross(move |d| band_scale.tick(&band_fn_cloned(d)).map(|t| t + band_offset))
971 .base(move |_| zero_pixel)
972 .value(move |d| {
973 bar_end(
974 &value_scale,
975 value_fn_cloned(d),
976 zero_pixel,
977 alignment,
978 min_length,
979 )
980 })
981 .corner_radii(self.corner_radii);
982
983 bar = match (fill, fill_gradient) {
984 (_, Some(fg)) => {
985 let value_fn_for_grad = value_fn.clone();
986 bar.fill(move |d, frame, alignment| {
987 let v = value_fn_for_grad(d).to_f32().unwrap_or(0.);
988 let [s0, s1] = bar_gradient(fg.as_ref(), d, v, chart_range.clone());
989 let bg: Background = linear_gradient(alignment.gradient_angle(), s0, s1);
990 bg.opacity(emphasis(frame))
991 })
992 }
993 (Some(f), _) => bar.fill(move |d, frame, alignment| {
994 f(d, frame, chart_bounds, alignment).opacity(emphasis(frame))
995 }),
996 _ => bar.fill(move |_, frame, _| default_fill.opacity(emphasis(frame))),
997 };
998
999 if let Some(label) = self.label.as_ref() {
1000 let label = label.clone();
1001 let text_align = match alignment {
1002 BarAlignment::Bottom | BarAlignment::Top => TextAlign::Center,
1003 BarAlignment::Left => TextAlign::Left,
1004 BarAlignment::Right => TextAlign::Right,
1005 };
1006 bar = bar.label(move |d, p| {
1007 let color = label_color_fn.as_ref().map_or(label_color, |f| f(d));
1008 vec![Text::new(label(d), p, color).align(text_align)]
1009 });
1010 }
1011
1012 bar.paint(&bounds, window, cx);
1013 if let Some(labels) = inside_labels {
1014 labels.paint(&bounds, window, cx);
1015 }
1016 }
1017
1018 fn id(&self) -> Option<ElementId> {
1019 self.interactive.then(|| self.id.clone())
1020 }
1021
1022 fn tooltip_state(
1023 &self,
1024 position: Point<Pixels>,
1025 bounds: Bounds<Pixels>,
1026 _cx: &App,
1027 ) -> Option<TooltipState> {
1028 let band_fn = self.band.as_ref()?;
1029 self.value.as_ref()?;
1030
1031 if !self.is_over_bars(position, bounds) {
1033 return None;
1034 }
1035
1036 let is_horizontal = self.alignment.is_horizontal();
1039 let band_scale = self.band_scale(bounds)?;
1040 let band_width = band_scale.band_width();
1041
1042 let band_offset = self.band_offset();
1043 let cursor_band = if is_horizontal {
1044 position.y
1045 } else {
1046 position.x
1047 };
1048 let index = band_scale.nearest_index(cursor_band.as_f32() - band_offset);
1049 let d = self.data.get(index)?;
1050 let center = band_scale.tick(&band_fn(d))? + band_offset + band_width / 2.;
1051
1052 let cross_line = if is_horizontal {
1055 point(position.x, px(center))
1056 } else {
1057 point(px(center), position.y)
1058 };
1059
1060 Some(TooltipState::new(index, cross_line, vec![]))
1061 }
1062
1063 fn hover(&mut self, hover: Option<&PlotHover>, window: &mut Window, cx: &mut App) {
1064 self.hover = hover.map(|hover| {
1065 let target = if self.alignment.is_horizontal() {
1068 hover.state().cross_line.y
1069 } else {
1070 hover.state().cross_line.x
1071 };
1072 let center = hover.glide(("bar-chart", "band"), target, window, cx);
1073 BarHover {
1074 center: center.as_f32(),
1075 focus: hover.progress(),
1076 }
1077 });
1078 }
1079
1080 fn tooltip(
1081 &self,
1082 state: &TooltipState,
1083 cursor: Point<Pixels>,
1084 bounds: Bounds<Pixels>,
1085 window: &mut Window,
1086 cx: &mut App,
1087 ) -> Option<AnyElement> {
1088 let (band_fn, value_fn) = (self.band.as_ref()?, self.value.as_ref()?);
1089 let d = self.data.get(state.index)?;
1090 let name = self.name.clone().unwrap_or_default();
1091
1092 let band_scale = self.band_scale(bounds)?;
1096 let band_width = band_scale.band_width();
1097 let center = self.hover.map_or(state.cross_line, |hover| {
1098 if self.alignment.is_horizontal() {
1099 point(state.cross_line.x, px(hover.center))
1100 } else {
1101 point(px(hover.center), state.cross_line.y)
1102 }
1103 });
1104 let (start, length) = self.value_extent(bounds);
1105 let cross_line = if self.alignment.is_horizontal() {
1106 CrossLine::new(center)
1107 .horizontal()
1108 .h_span(start, length)
1109 .band(px(band_width))
1110 } else {
1111 CrossLine::new(center)
1112 .span(start, length)
1113 .band(px(band_width))
1114 };
1115
1116 let frame = self.bar_frame(d, &band_scale, bounds).unwrap_or_default();
1117 let swatch = self.bar_color(d, frame, bounds, cx);
1118
1119 let tooltip = Tooltip::new(cursor, bounds.size)
1121 .glide(false)
1122 .gap(px(8.))
1123 .cross_line(cross_line);
1124
1125 let tooltip = self.tooltip_content.apply(
1126 tooltip,
1127 d,
1128 || Some(band_fn(d).into()),
1129 || Some([(swatch, name, value_fn(d).to_f64()?)]),
1130 window,
1131 cx,
1132 )?;
1133
1134 Some(tooltip.into_any_element())
1135 }
1136}
1137
1138fn bar_end<V>(
1141 scale: &ScaleLinear<V>,
1142 value: V,
1143 zero: f32,
1144 alignment: BarAlignment,
1145 min_length: f32,
1146) -> Option<f32>
1147where
1148 V: PlotValue,
1149{
1150 let tick = scale.tick(&value)?;
1151 Some(extend_to_min_length(
1152 tick,
1153 zero,
1154 value < V::zero(),
1155 alignment,
1156 min_length,
1157 ))
1158}
1159
1160fn extend_to_min_length(
1163 tick: f32,
1164 zero: f32,
1165 negative: bool,
1166 alignment: BarAlignment,
1167 min: f32,
1168) -> f32 {
1169 if (tick - zero).abs() >= min {
1170 return tick;
1171 }
1172 let grows_toward_origin = matches!(alignment, BarAlignment::Bottom | BarAlignment::Right);
1173 if grows_toward_origin != negative {
1174 zero - min
1175 } else {
1176 zero + min
1177 }
1178}
1179
1180fn bar_gradient<T>(
1183 fill: &dyn Fn(&T, RangeInclusive<f32>, &dyn Fn(f32) -> f32) -> [LinearColorStop; 2],
1184 d: &T,
1185 value: f32,
1186 range: RangeInclusive<f32>,
1187) -> [LinearColorStop; 2] {
1188 let bar_lo = value.min(0.);
1189 let bar_span = (value.max(0.) - bar_lo).max(f32::EPSILON);
1190 let chart_to_bar = |chart_value: f32| (chart_value - bar_lo) / bar_span;
1191 clip_stops_to_bar(fill(d, range, &chart_to_bar))
1192}
1193
1194fn clip_stops_to_bar(stops: [LinearColorStop; 2]) -> [LinearColorStop; 2] {
1205 let [a, b] = stops;
1206 let p0 = a.percentage;
1207 let p1 = b.percentage;
1208 let lerp = |t: f32| -> Hsla {
1209 Hsla {
1210 h: a.color.h + (b.color.h - a.color.h) * t,
1211 s: a.color.s + (b.color.s - a.color.s) * t,
1212 l: a.color.l + (b.color.l - a.color.l) * t,
1213 a: a.color.a + (b.color.a - a.color.a) * t,
1214 }
1215 };
1216 let span = p1 - p0;
1217 let sample = |target: f32| -> Hsla {
1218 if span.abs() < f32::EPSILON {
1219 a.color
1220 } else {
1221 lerp((target - p0) / span)
1222 }
1223 };
1224 let new_a = if (0. ..=1.).contains(&p0) {
1225 a
1226 } else {
1227 LinearColorStop {
1228 color: sample(p0.clamp(0., 1.)),
1229 percentage: p0.clamp(0., 1.),
1230 }
1231 };
1232 let new_b = if (0. ..=1.).contains(&p1) {
1233 b
1234 } else {
1235 LinearColorStop {
1236 color: sample(p1.clamp(0., 1.)),
1237 percentage: p1.clamp(0., 1.),
1238 }
1239 };
1240 [new_a, new_b]
1241}
1242
1243fn label_below_zero_line(value: f32, alignment: BarAlignment) -> bool {
1250 (value < 0.) == (alignment == BarAlignment::Top)
1251}
1252
1253fn value_tick_positions(far: f32, baseline: f32, count: usize) -> Vec<f32> {
1258 let steps = (count - 1) as f32;
1259 (0..count)
1260 .map(|i| far + (baseline - far) * i as f32 / steps)
1261 .collect()
1262}
1263
1264#[cfg(test)]
1265mod tests {
1266 use super::*;
1267
1268 #[test]
1269 fn test_label_below_zero_line() {
1270 assert!(label_below_zero_line(5., BarAlignment::Bottom));
1272 assert!(label_below_zero_line(0., BarAlignment::Bottom));
1273 assert!(!label_below_zero_line(-5., BarAlignment::Bottom));
1274
1275 assert!(!label_below_zero_line(5., BarAlignment::Top));
1277 assert!(!label_below_zero_line(0., BarAlignment::Top));
1278 assert!(label_below_zero_line(-5., BarAlignment::Top));
1279 }
1280
1281 #[test]
1282 fn test_value_tick_positions() {
1283 assert_eq!(
1285 value_tick_positions(10., 110., 5),
1286 vec![10., 35., 60., 85., 110.]
1287 );
1288
1289 assert_eq!(value_tick_positions(110., 10., 3), vec![110., 60., 10.]);
1291
1292 assert_eq!(value_tick_positions(0., 50., 2), vec![0., 50.]);
1293 }
1294
1295 #[test]
1296 fn test_min_length_extends_away_from_zero() {
1297 assert_eq!(
1299 extend_to_min_length(100., 100., false, BarAlignment::Bottom, 2.),
1300 98.
1301 );
1302 assert_eq!(
1303 extend_to_min_length(10., 10., false, BarAlignment::Top, 2.),
1304 12.
1305 );
1306 assert_eq!(
1307 extend_to_min_length(10., 10., false, BarAlignment::Left, 2.),
1308 12.
1309 );
1310 assert_eq!(
1311 extend_to_min_length(90., 90., false, BarAlignment::Right, 2.),
1312 88.
1313 );
1314
1315 assert_eq!(
1317 extend_to_min_length(50.5, 50., true, BarAlignment::Bottom, 2.),
1318 52.
1319 );
1320
1321 assert_eq!(
1323 extend_to_min_length(40., 100., false, BarAlignment::Bottom, 2.),
1324 40.
1325 );
1326 }
1327
1328 #[test]
1329 fn value_tick_labels_walk_the_domain_from_the_far_end() {
1330 use super::BarChart;
1331
1332 let chart = BarChart::new([10., 20.])
1333 .band(|v| format!("{v}"))
1334 .value(|v| *v)
1335 .value_tick_count(3);
1336 assert_eq!(chart.value_tick_labels(), vec!["20", "10", "0"]);
1337
1338 let money = chart.value_tick_format(|v| format!("${v:.0}"));
1339 assert_eq!(money.value_tick_labels(), vec!["$20", "$10", "$0"]);
1340
1341 assert_eq!(money.band_count(12).band_slots(), 12);
1343 }
1344
1345 #[test]
1346 fn a_band_count_keeps_each_bar_in_its_band() {
1347 use gpui::{Bounds, point, px, size};
1348
1349 use super::BarChart;
1350 use crate::plot::{AxisLabelPlacement, scale::Scale};
1351
1352 let bounds = Bounds::new(point(px(0.), px(0.)), size(px(40.), px(100.)));
1353 let chart = |data: &[f64], count| {
1354 BarChart::new(data.to_vec())
1355 .band(|v| format!("{v}"))
1356 .value(|v| *v)
1357 .band_count(count)
1358 };
1359
1360 let wide = chart(&[1., 2.], 2).band_scale(bounds).unwrap();
1362 let narrow = chart(&[1., 2.], 4).band_scale(bounds).unwrap();
1363 assert_eq!(narrow.band_width() * 2., wide.band_width());
1364 assert!(narrow.tick(&"2".to_string()).unwrap() < 20.);
1365
1366 let grown = chart(&[1., 2., 3.], 4).band_scale(bounds).unwrap();
1368 assert_eq!(grown.tick(&"2".to_string()), narrow.tick(&"2".to_string()));
1369 assert_eq!(grown.band_width(), narrow.band_width());
1370
1371 let outside = chart(&[1., 2.], 2).value_axis(true);
1373 let inside = chart(&[1., 2.], 2)
1374 .value_axis(true)
1375 .value_axis_label_placement(AxisLabelPlacement::Inside);
1376 assert_eq!(outside.value_axis_gap(), super::VALUE_AXIS_GAP);
1377 assert_eq!(inside.value_axis_gap(), 0.);
1378 }
1379
1380 #[gpui::test]
1383 fn the_tooltip_swatch_follows_the_bar_color(cx: &mut gpui::TestAppContext) {
1384 cx.update(crate::init);
1385 let bars = || {
1386 BarChart::new([1., -2.])
1387 .band(|d: &f64| SharedString::from(format!("{d}")))
1388 .value(|d: &f64| *d)
1389 };
1390 let frame = Bounds::default();
1391 let bounds = Bounds::new(point(px(0.), px(0.)), gpui::size(px(100.), px(100.)));
1392 let (default, solid, gradient, stops) = cx.update(|cx| {
1393 let gain = gpui::green();
1394 let loss = gpui::red();
1395 let default = bars().bar_color(&1., frame, bounds, cx);
1396 let solid = bars()
1397 .fill(move |d: &f64, _, _, _| if *d >= 0. { gain } else { loss })
1398 .bar_color(&-2., frame, bounds, cx);
1399 let gradient = bars()
1400 .fill(move |_: &f64, _, _, _| {
1401 linear_gradient(
1402 0.,
1403 gpui::linear_color_stop(gain, 0.),
1404 gpui::linear_color_stop(loss, 1.),
1405 )
1406 })
1407 .bar_color(&1., frame, bounds, cx);
1408 let stops = bars()
1409 .fill_gradient(move |_: &f64, _, _| {
1410 [
1411 gpui::linear_color_stop(gain, 0.),
1412 gpui::linear_color_stop(loss, 1.),
1413 ]
1414 })
1415 .bar_color(&1., frame, bounds, cx);
1416 (default, solid, gradient, stops)
1417 });
1418 let chart_2 = cx.update(|cx| cx.theme().chart_2);
1419
1420 assert_eq!(default, chart_2);
1421 assert_eq!(solid, gpui::red());
1422 assert_eq!(gradient, chart_2);
1423 assert_eq!(stops, gpui::green());
1424 }
1425
1426 #[test]
1429 fn the_tooltip_reads_the_painted_bar_frame() {
1430 let bars = BarChart::new([1., -2.])
1431 .band(|d: &f64| SharedString::from(format!("{d}")))
1432 .value(|d: &f64| *d);
1433 let bounds = Bounds::new(point(px(0.), px(0.)), gpui::size(px(100.), px(100.)));
1434 let band_scale = bars.band_scale(bounds).expect("bars have a band scale");
1435 let up = bars.bar_frame(&1., &band_scale, bounds).expect("a frame");
1436 let down = bars.bar_frame(&-2., &band_scale, bounds).expect("a frame");
1437
1438 assert_eq!(up.origin.y + up.size.height, down.origin.y);
1440 assert!((down.size.height - 2. * up.size.height).abs() < 0.01);
1441 assert!(up.origin.x < down.origin.x);
1442 assert_eq!(up.size.width, band_scale.band_width());
1443 }
1444}