rgpui_component/plot/shape/
bar.rs1use rgpui::{
2 App, Background, Bounds, Corners, PaintQuad, Pixels, Point, Size, Window, fill, point, px,
3};
4
5use crate::plot::{
6 label::{PlotLabel, TEXT_GAP, TEXT_HEIGHT, TEXT_SIZE, Text},
7 origin_point,
8};
9
10#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)]
13pub enum BarAlignment {
14 #[default]
16 Bottom,
17 Top,
19 Left,
21 Right,
23}
24
25impl BarAlignment {
26 pub fn is_horizontal(self) -> bool {
27 matches!(self, Self::Left | Self::Right)
28 }
29
30 pub fn is_vertical(self) -> bool {
31 !self.is_horizontal()
32 }
33
34 pub fn gradient_angle(self) -> f32 {
40 match self {
41 Self::Bottom => 0.,
42 Self::Top => 180.,
43 Self::Left => 90.,
44 Self::Right => 270.,
45 }
46 }
47}
48
49#[allow(clippy::type_complexity)]
50pub struct Bar<T> {
51 data: Vec<T>,
52 alignment: BarAlignment,
53 cross: Box<dyn Fn(&T) -> Option<f32>>,
54 band_width: f32,
55 base: Box<dyn Fn(&T) -> f32>,
56 value: Box<dyn Fn(&T) -> Option<f32>>,
57 fill: Box<dyn Fn(&T, Bounds<f32>, BarAlignment) -> Background>,
58 label: Option<Box<dyn Fn(&T, Point<Pixels>) -> Vec<Text>>>,
59 corner_radii: Corners<Pixels>,
60}
61
62impl<T> Default for Bar<T> {
63 fn default() -> Self {
64 Self {
65 data: Vec::new(),
66 alignment: BarAlignment::default(),
67 cross: Box::new(|_| None),
68 band_width: 0.,
69 base: Box::new(|_| 0.),
70 value: Box::new(|_| None),
71 fill: Box::new(|_, _, _| rgpui::black().into()),
72 label: None,
73 corner_radii: Corners::all(px(0.)),
74 }
75 }
76}
77
78impl<T> Bar<T> {
79 pub fn new() -> Self {
80 Self::default()
81 }
82
83 pub fn data<I>(mut self, data: I) -> Self
85 where
86 I: IntoIterator<Item = T>,
87 {
88 self.data = data.into_iter().collect();
89 self
90 }
91
92 pub fn alignment(mut self, alignment: BarAlignment) -> Self {
96 self.alignment = alignment;
97 self
98 }
99
100 pub fn cross<F>(mut self, cross: F) -> Self
105 where
106 F: Fn(&T) -> Option<f32> + 'static,
107 {
108 self.cross = Box::new(cross);
109 self
110 }
111
112 pub fn band_width(mut self, band_width: f32) -> Self {
114 self.band_width = band_width;
115 self
116 }
117
118 pub fn base<F>(mut self, base: F) -> Self
120 where
121 F: Fn(&T) -> f32 + 'static,
122 {
123 self.base = Box::new(base);
124 self
125 }
126
127 pub fn value<F>(mut self, value: F) -> Self
129 where
130 F: Fn(&T) -> Option<f32> + 'static,
131 {
132 self.value = Box::new(value);
133 self
134 }
135
136 pub fn fill<F, B>(mut self, fill: F) -> Self
148 where
149 F: Fn(&T, Bounds<f32>, BarAlignment) -> B + 'static,
150 B: Into<Background>,
151 {
152 self.fill = Box::new(move |v, frame, alignment| fill(v, frame, alignment).into());
153 self
154 }
155
156 pub fn label<F>(mut self, label: F) -> Self
158 where
159 F: Fn(&T, Point<Pixels>) -> Vec<Text> + 'static,
160 {
161 self.label = Some(Box::new(label));
162 self
163 }
164
165 pub fn corner_radii(mut self, corner_radii: impl Into<Corners<Pixels>>) -> Self {
170 self.corner_radii = corner_radii.into();
171 self
172 }
173
174 fn path(&self, bounds: &Bounds<Pixels>) -> (Vec<PaintQuad>, PlotLabel) {
175 let origin = bounds.origin;
176 let mut graph = vec![];
177 let mut labels = vec![];
178
179 for v in &self.data {
180 let Some(cross) = (self.cross)(v) else {
181 continue;
182 };
183 let Some(value) = (self.value)(v) else {
184 continue;
185 };
186 let base = (self.base)(v);
187
188 let bw = self.band_width;
189 let (frame, p1, p2) = if self.alignment.is_vertical() {
190 let x0 = cross;
191 let x1 = cross + bw;
192 let y_min = value.min(base);
193 let y_max = value.max(base);
194 let frame = Bounds {
195 origin: Point::new(x0, y_min),
196 size: Size::new(x1 - x0, y_max - y_min),
197 };
198 (
199 frame,
200 origin_point(px(x0), px(y_min), origin),
201 origin_point(px(x1), px(y_max), origin),
202 )
203 } else {
204 let y0 = cross;
205 let y1 = cross + bw;
206 let x_min = value.min(base);
207 let x_max = value.max(base);
208 let frame = Bounds {
209 origin: Point::new(x_min, y0),
210 size: Size::new(x_max - x_min, y1 - y0),
211 };
212 (
213 frame,
214 origin_point(px(x_min), px(y0), origin),
215 origin_point(px(x_max), px(y1), origin),
216 )
217 };
218
219 let bg = (self.fill)(v, frame, self.alignment);
220 graph.push(fill(Bounds::from_corners(p1, p2), bg).corner_radii(self.corner_radii));
221
222 if let Some(label) = &self.label {
223 let label_origin = label_origin(self.alignment, cross, base, value, bw);
224 labels.extend(label(v, label_origin));
225 }
226 }
227
228 (graph, PlotLabel::new(labels))
229 }
230
231 pub fn paint(&self, bounds: &Bounds<Pixels>, window: &mut Window, cx: &mut App) {
233 let (graph, labels) = self.path(bounds);
234 for quad in graph {
235 window.paint_quad(quad);
236 }
237 labels.paint(bounds, window, cx);
238 }
239}
240
241fn label_origin(
246 alignment: BarAlignment,
247 cross: f32,
248 base: f32,
249 value: f32,
250 band_width: f32,
251) -> Point<Pixels> {
252 match alignment {
253 BarAlignment::Bottom => {
254 let cx = cross + band_width / 2.;
255 if value <= base {
257 point(px(cx), px(value - TEXT_HEIGHT))
258 } else {
259 point(px(cx), px(value + TEXT_GAP))
260 }
261 }
262 BarAlignment::Top => {
263 let cx = cross + band_width / 2.;
264 if value >= base {
266 point(px(cx), px(value + TEXT_GAP))
267 } else {
268 point(px(cx), px(value - TEXT_HEIGHT))
269 }
270 }
271 BarAlignment::Left => {
272 let cy = cross + band_width / 2. - TEXT_SIZE / 2.;
274 if value >= base {
276 point(px(value + TEXT_GAP), px(cy))
277 } else {
278 point(px(value - TEXT_GAP), px(cy))
279 }
280 }
281 BarAlignment::Right => {
282 let cy = cross + band_width / 2. - TEXT_SIZE / 2.;
283 if value <= base {
285 point(px(value - TEXT_GAP), px(cy))
286 } else {
287 point(px(value + TEXT_GAP), px(cy))
288 }
289 }
290 }
291}