1use std::rc::Rc;
2
3use gpui::{App, Bounds, Hsla, Pixels, SharedString, TextAlign, Window, point};
4use gpui_component_macros::IntoPlot;
5use num_traits::Zero;
6
7use crate::{
8 ActiveTheme,
9 plot::{
10 Plot,
11 label::{PlotLabel, TEXT_HEIGHT, TEXT_SIZE, Text},
12 polygon,
13 shape::{Arc, ArcData, Pie},
14 },
15};
16
17const DEFAULT_LABEL_GAP: f32 = 15.;
19
20#[derive(IntoPlot)]
21pub struct PieChart<T: 'static> {
22 data: Vec<T>,
23 inner_radius: f32,
24 inner_radius_fn: Option<Rc<dyn Fn(&ArcData<T>) -> f32 + 'static>>,
25 outer_radius: f32,
26 outer_radius_fn: Option<Rc<dyn Fn(&ArcData<T>) -> f32 + 'static>>,
27 pad_angle: f32,
28 value: Option<Rc<dyn Fn(&T) -> f32>>,
29 color: Option<Rc<dyn Fn(&T) -> Hsla>>,
30 label: Option<Rc<dyn Fn(&T) -> SharedString + 'static>>,
31 label_line_color: Option<Rc<dyn Fn(&T) -> Hsla + 'static>>,
32 label_color: Option<Hsla>,
33 label_gap: f32,
34}
35
36impl<T> PieChart<T> {
37 pub fn new<I>(data: I) -> Self
38 where
39 I: IntoIterator<Item = T>,
40 {
41 Self {
42 data: data.into_iter().collect(),
43 inner_radius: 0.,
44 inner_radius_fn: None,
45 outer_radius: 0.,
46 outer_radius_fn: None,
47 pad_angle: 0.,
48 value: None,
49 color: None,
50 label: None,
51 label_line_color: None,
52 label_color: None,
53 label_gap: DEFAULT_LABEL_GAP,
54 }
55 }
56
57 pub fn inner_radius(mut self, inner_radius: f32) -> Self {
59 self.inner_radius = inner_radius;
60 self
61 }
62
63 pub fn inner_radius_fn(
65 mut self,
66 inner_radius_fn: impl Fn(&ArcData<T>) -> f32 + 'static,
67 ) -> Self {
68 self.inner_radius_fn = Some(Rc::new(inner_radius_fn));
69 self
70 }
71
72 fn get_inner_radius(&self, arc: &ArcData<T>) -> f32 {
73 if let Some(inner_radius_fn) = self.inner_radius_fn.as_ref() {
74 inner_radius_fn(arc)
75 } else {
76 self.inner_radius
77 }
78 }
79
80 pub fn outer_radius(mut self, outer_radius: f32) -> Self {
82 self.outer_radius = outer_radius;
83 self
84 }
85
86 pub fn outer_radius_fn(
88 mut self,
89 outer_radius_fn: impl Fn(&ArcData<T>) -> f32 + 'static,
90 ) -> Self {
91 self.outer_radius_fn = Some(Rc::new(outer_radius_fn));
92 self
93 }
94
95 fn get_outer_radius(&self, arc: &ArcData<T>) -> f32 {
96 if let Some(outer_radius_fn) = self.outer_radius_fn.as_ref() {
97 outer_radius_fn(arc)
98 } else {
99 self.outer_radius
100 }
101 }
102
103 pub fn pad_angle(mut self, pad_angle: f32) -> Self {
105 self.pad_angle = pad_angle;
106 self
107 }
108
109 pub fn value(mut self, value: impl Fn(&T) -> f32 + 'static) -> Self {
110 self.value = Some(Rc::new(value));
111 self
112 }
113
114 pub fn color<H>(mut self, color: impl Fn(&T) -> H + 'static) -> Self
116 where
117 H: Into<Hsla> + 'static,
118 {
119 self.color = Some(Rc::new(move |t| color(t).into()));
120 self
121 }
122
123 pub fn label(mut self, label: impl Fn(&T) -> SharedString + 'static) -> Self {
128 self.label = Some(Rc::new(label));
129 self
130 }
131
132 pub fn label_line_color(mut self, color: impl Fn(&T) -> Hsla + 'static) -> Self {
134 self.label_line_color = Some(Rc::new(color));
135 self
136 }
137
138 pub fn label_color(mut self, color: Hsla) -> Self {
140 self.label_color = Some(color);
141 self
142 }
143
144 pub fn label_gap(mut self, gap: f32) -> Self {
147 self.label_gap = gap;
148 self
149 }
150}
151
152impl<T> Plot for PieChart<T> {
153 fn paint(&mut self, bounds: Bounds<Pixels>, window: &mut Window, cx: &mut App) {
154 let Some(value_fn) = self.value.as_ref() else {
155 return;
156 };
157
158 let outer_radius = if self.outer_radius.is_zero() {
159 bounds.size.height.as_f32() * 0.4
160 } else {
161 self.outer_radius
162 };
163
164 let arc = Arc::new()
165 .inner_radius(self.inner_radius)
166 .outer_radius(outer_radius);
167 let value_fn = value_fn.clone();
168 let mut pie = Pie::<T>::new().value(move |d| Some(value_fn(d)));
169 pie = pie.pad_angle(self.pad_angle);
170 let arcs = pie.arcs(&self.data);
171
172 for a in &arcs {
173 let inner_radius = self.get_inner_radius(a);
174 let outer_radius = self.get_outer_radius(a);
175 arc.paint(
176 a,
177 if let Some(color_fn) = self.color.as_ref() {
178 color_fn(a.data)
179 } else {
180 cx.theme().chart_2
181 },
182 Some(inner_radius),
183 Some(outer_radius),
184 &bounds,
185 window,
186 );
187 }
188
189 let Some(label_fn) = self.label.as_ref() else {
191 return;
192 };
193
194 let label_radius = outer_radius + self.label_gap;
195 let center_x = bounds.size.width.as_f32() / 2.;
196 let center_y = bounds.size.height.as_f32() / 2.;
197 let label_arc = Arc::new()
198 .inner_radius(label_radius)
199 .outer_radius(label_radius);
200 let edge_arc = Arc::new()
201 .inner_radius(outer_radius)
202 .outer_radius(outer_radius);
203
204 let label_color = self.label_color.unwrap_or(cx.theme().foreground);
205 let default_line_color = cx.theme().border;
206
207 let mut right: Vec<LabelLayout> = vec![];
211 let mut left: Vec<LabelLayout> = vec![];
212 for a in &arcs {
213 if a.end_angle - a.start_angle < std::f32::consts::PI / 360. {
215 continue;
216 }
217
218 let centroid = label_arc.centroid(a);
219 let edge = edge_arc.centroid(a);
220 let is_right = centroid.x > 0.;
221 let line_color = self
222 .label_line_color
223 .as_ref()
224 .map(|f| f(a.data))
225 .unwrap_or(default_line_color);
226
227 let layout = LabelLayout {
228 arc_x: edge.x,
229 arc_y: edge.y,
230 label_x: centroid.x,
231 y: centroid.y,
232 text: label_fn(a.data),
233 line_color,
234 };
235 if is_right { &mut right } else { &mut left }.push(layout);
236 }
237
238 let top = -center_y + TEXT_HEIGHT / 2.;
241 let bottom = center_y - TEXT_HEIGHT / 2.;
242 spread_labels(&mut right, top, bottom);
243 spread_labels(&mut left, top, bottom);
244
245 let mut labels = vec![];
247 for (side, items) in [(1., &right), (-1., &left)] {
248 for item in items {
249 let pts = [
252 point(item.arc_x + center_x, item.arc_y + center_y),
253 point(item.label_x + center_x, item.y + center_y),
254 point(side * label_radius + center_x, item.y + center_y),
255 ];
256 if let Some(p) = polygon(&pts, &bounds) {
257 window.paint_path(p, item.line_color);
258 }
259
260 let origin = point(
262 side * (label_radius + 4.) + center_x,
263 item.y - TEXT_SIZE / 2. + center_y,
264 );
265 let align = if side > 0. {
266 TextAlign::Left
267 } else {
268 TextAlign::Right
269 };
270 labels.push(Text::new(item.text.clone(), origin, label_color).align(align));
271 }
272 }
273
274 PlotLabel::new(labels).paint(&bounds, window, cx);
275 }
276}
277
278struct LabelLayout {
280 arc_x: f32,
282 arc_y: f32,
283 label_x: f32,
285 y: f32,
287 text: SharedString,
288 line_color: Hsla,
289}
290
291fn spread_labels(items: &mut [LabelLayout], top: f32, bottom: f32) {
298 let n = items.len();
299 if n == 0 {
300 return;
301 }
302
303 items.sort_by(|a, b| a.y.total_cmp(&b.y));
305
306 for i in 1..n {
308 let min_y = items[i - 1].y + TEXT_HEIGHT;
309 if items[i].y < min_y {
310 items[i].y = min_y;
311 }
312 }
313
314 if items[n - 1].y > bottom {
316 items[n - 1].y = bottom;
317 }
318 for i in (0..n - 1).rev() {
319 let max_y = items[i + 1].y - TEXT_HEIGHT;
320 if items[i].y > max_y {
321 items[i].y = max_y;
322 }
323 }
324
325 if items[0].y < top {
327 items[0].y = top;
328 }
329}