1use gpui::prelude::*;
11use gpui::{canvas, div, point, px, App, Hsla, IntoElement, PathBuilder, SharedString, Window};
12
13use crate::style::ColorValue;
14use crate::theme::{theme, Size};
15
16use super::{arc_point, series_color, slice_spans};
17
18#[derive(IntoElement)]
23pub struct PieChart {
24 values: Vec<f32>,
25 labels: Vec<SharedString>,
26 colors: Vec<ColorValue>,
27 size: f32,
28 donut: Option<f32>,
29}
30
31impl PieChart {
32 pub fn new(values: impl IntoIterator<Item = f32>) -> Self {
33 PieChart {
34 values: values.into_iter().collect(),
35 labels: Vec::new(),
36 colors: Vec::new(),
37 size: 160.0,
38 donut: None,
39 }
40 }
41
42 pub fn entries(entries: impl IntoIterator<Item = (impl Into<SharedString>, f32)>) -> Self {
44 let (labels, values): (Vec<SharedString>, Vec<f32>) = entries
45 .into_iter()
46 .map(|(label, value)| (label.into(), value))
47 .unzip();
48 PieChart {
49 labels,
50 ..PieChart::new(values)
51 }
52 }
53
54 pub fn color(mut self, color: impl Into<ColorValue>) -> Self {
56 self.colors = vec![color.into()];
57 self
58 }
59
60 pub fn colors(mut self, colors: impl IntoIterator<Item = impl Into<ColorValue>>) -> Self {
62 self.colors = colors.into_iter().map(Into::into).collect();
63 self
64 }
65
66 pub fn size(mut self, size: f32) -> Self {
68 self.size = size;
69 self
70 }
71
72 pub fn donut(mut self, inner_fraction: f32) -> Self {
75 self.donut = Some(inner_fraction.clamp(0.05, 0.95));
76 self
77 }
78}
79
80impl RenderOnce for PieChart {
81 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
82 let t = theme(cx);
83 let n = self.values.len();
84 let slice_colors: Vec<Hsla> = (0..n).map(|i| series_color(t, &self.colors, i)).collect();
85 let legend_colors = slice_colors.clone();
86 let dimmed = t.dimmed().hsla();
87 let font_xs = t.font_size(Size::Xs);
88 let spans = slice_spans(&self.values);
89 let donut = self.donut;
90 let diameter = self.size;
91
92 let plot = canvas(
93 |_, _, _| (),
94 move |bounds, _, window, _cx| {
95 let w = f32::from(bounds.size.width);
96 let h = f32::from(bounds.size.height);
97 let radius = w.min(h) / 2.0;
98 if radius <= 0.0 {
99 return;
100 }
101 let center = (
102 f32::from(bounds.origin.x) + w / 2.0,
103 f32::from(bounds.origin.y) + h / 2.0,
104 );
105 for (i, &(start, end)) in spans.iter().enumerate() {
106 if end - start <= f32::EPSILON {
107 continue;
108 }
109 let color = slice_colors[i];
110 let inner = donut.map(|f| radius * f);
111 if end - start >= 0.999 {
112 let mid = start + 0.5;
115 paint_slice(window, center, radius, inner, start, mid, color);
116 paint_slice(window, center, radius, inner, mid, start + 1.0, color);
117 } else {
118 paint_slice(window, center, radius, inner, start, end, color);
119 }
120 }
121 },
122 )
123 .w(px(diameter))
124 .h(px(diameter));
125
126 let mut root = div()
127 .flex()
128 .flex_col()
129 .items_center()
130 .gap(px(8.0))
131 .child(plot);
132
133 if !self.labels.is_empty() && !legend_colors.is_empty() {
135 let items = self.labels.iter().enumerate().map(|(i, label)| {
136 div()
137 .flex()
138 .flex_row()
139 .items_center()
140 .gap(px(6.0))
141 .child(
142 div()
143 .w(px(8.0))
144 .h(px(8.0))
145 .rounded(px(4.0))
146 .bg(legend_colors[i % legend_colors.len()]),
147 )
148 .child(
149 div()
150 .text_size(px(font_xs))
151 .text_color(dimmed)
152 .child(label.clone()),
153 )
154 });
155 root = root.child(
156 div()
157 .flex()
158 .flex_row()
159 .flex_wrap()
160 .justify_center()
161 .gap(px(12.0))
162 .children(items),
163 );
164 }
165 root
166 }
167}
168
169fn paint_slice(
173 window: &mut Window,
174 center: (f32, f32),
175 radius: f32,
176 inner: Option<f32>,
177 start: f32,
178 end: f32,
179 color: Hsla,
180) {
181 let large = end - start > 0.5;
182 let to_pt = |(x, y): (f32, f32)| point(px(x), px(y));
183 let outer_start = to_pt(arc_point(center, radius, start));
184 let outer_end = to_pt(arc_point(center, radius, end));
185 let radii = point(px(radius), px(radius));
186
187 let mut pb = PathBuilder::fill();
188 match inner {
189 Some(hole) if hole > 0.0 => {
192 let inner_radii = point(px(hole), px(hole));
193 pb.move_to(outer_start);
194 pb.arc_to(radii, px(0.0), large, true, outer_end);
195 pb.line_to(to_pt(arc_point(center, hole, end)));
196 pb.arc_to(
197 inner_radii,
198 px(0.0),
199 large,
200 false,
201 to_pt(arc_point(center, hole, start)),
202 );
203 pb.close();
204 }
205 _ => {
207 pb.move_to(to_pt(center));
208 pb.line_to(outer_start);
209 pb.arc_to(radii, px(0.0), large, true, outer_end);
210 pb.close();
211 }
212 }
213 if let Ok(path) = pb.build() {
214 window.paint_path(path, color);
215 }
216}