1use crate::error::ChartError;
4use crate::{
5 DEFAULT_HEIGHT, DEFAULT_TITLE_FONT_SIZE, DEFAULT_WIDTH, TITLE_AREA_HEIGHT, validate_data_array,
6 validate_data_length, validate_dimensions,
7};
8use d3rs::color::D3Color;
9use d3rs::shape::{Arc, Pie};
10use d3rs::text::{VectorFontConfig, render_vector_text};
11use gpui::prelude::*;
12use gpui::*;
13
14const DEFAULT_PALETTE: [u32; 10] = [
16 0x1f77b4, 0xff7f0e, 0x2ca02c, 0xd62728, 0x9467bd, 0x8c564b, 0xe377c2, 0x7f7f7f, 0xbcbd22,
17 0x17becf,
18];
19
20#[derive(Clone)]
22pub struct PieChart {
23 labels: Option<Vec<String>>,
24 values: Vec<f64>,
25 title: Option<String>,
26 inner_radius_fraction: f64, pad_angle: f64,
28 corner_radius: f64,
29 colors: Option<Vec<u32>>,
30 width: f32,
31 height: f32,
32 sort: bool,
33}
34
35impl PieChart {
36 pub fn title(mut self, title: impl Into<String>) -> Self {
38 self.title = Some(title.into());
39 self
40 }
41
42 pub fn colors(mut self, colors: &[u32]) -> Self {
44 self.colors = Some(colors.to_vec());
45 self
46 }
47
48 pub fn hole(mut self, fraction: f64) -> Self {
51 self.inner_radius_fraction = fraction.clamp(0.0, 0.99);
52 self
53 }
54
55 pub fn pad_angle(mut self, angle: f64) -> Self {
57 self.pad_angle = angle;
58 self
59 }
60
61 pub fn corner_radius(mut self, radius: f64) -> Self {
63 self.corner_radius = radius;
64 self
65 }
66
67 pub fn sort(mut self, sort: bool) -> Self {
69 self.sort = sort;
70 self
71 }
72
73 pub fn size(mut self, width: f32, height: f32) -> Self {
75 self.width = width;
76 self.height = height;
77 self
78 }
79
80 pub fn build(self) -> Result<impl IntoElement, ChartError> {
82 validate_data_array(&self.values, "values")?;
84 validate_dimensions(self.width, self.height)?;
85
86 if let Some(ref labels) = self.labels {
87 validate_data_length(labels.len(), self.values.len(), "labels", "values")?;
88 }
89
90 let title_height = if self.title.is_some() {
92 TITLE_AREA_HEIGHT
93 } else {
94 0.0
95 };
96 let plot_height = self.height - title_height;
97 let plot_width = self.width;
98
99 let radius = (plot_width.min(plot_height) / 2.0) as f64 * 0.9; let inner_radius = radius * self.inner_radius_fraction;
102
103 let pie = Pie::new()
105 .pad_angle(self.pad_angle)
106 .corner_radius(self.corner_radius)
107 .inner_radius(inner_radius)
108 .outer_radius(radius)
109 .sort(self.sort);
110
111 let slices = pie.generate(&self.values, |v| *v);
113
114 let colors: Vec<u32> = match self.colors {
116 Some(c) => c.iter().cycle().take(slices.len()).copied().collect(),
117 None => DEFAULT_PALETTE
118 .iter()
119 .cycle()
120 .take(slices.len())
121 .copied()
122 .collect(),
123 };
124
125 let arc_gen = Arc::new();
127
128 let render_element = canvas(
130 move |bounds, _, _| (slices, colors, arc_gen, bounds, plot_width, plot_height),
131 move |_, (slices, colors, arc_gen, bounds, plot_width, plot_height), window, _| {
132 let origin_x: f32 = bounds.origin.x.into();
133 let origin_y: f32 = bounds.origin.y.into();
134 let center_x = origin_x + plot_width / 2.0;
135 let center_y = origin_y + plot_height / 2.0;
136
137 let arc_gen = arc_gen.center(center_x as f64, center_y as f64);
138
139 for (i, slice) in slices.iter().enumerate() {
140 let color = D3Color::from_hex(colors[i % colors.len()]);
141 let fill_color = color.to_rgba();
142
143 let path = arc_gen.generate(&slice.arc);
144 let points = path.flatten(0.5);
145
146 if points.is_empty() {
147 continue;
148 }
149
150 let mut builder = PathBuilder::fill();
151
152 builder.move_to(point(px(points[0].x as f32), px(points[0].y as f32)));
153 for p in points.iter().skip(1) {
154 builder.line_to(point(px(p.x as f32), px(p.y as f32)));
155 }
156
157 builder.close();
158
159 if let Ok(gpui_path) = builder.build() {
160 window.paint_path(gpui_path, fill_color);
161 }
162 }
163 },
164 );
165
166 let mut container = div()
168 .w(px(self.width))
169 .h(px(self.height))
170 .relative()
171 .flex()
172 .flex_col();
173
174 if let Some(title) = &self.title {
176 let font_config =
177 VectorFontConfig::horizontal(DEFAULT_TITLE_FONT_SIZE, hsla(0.0, 0.0, 0.2, 1.0));
178 container = container.child(
179 div()
180 .w_full()
181 .h(px(title_height))
182 .flex()
183 .justify_center()
184 .items_center()
185 .child(render_vector_text(title, &font_config)),
186 );
187 }
188
189 container = container.child(
191 div()
192 .w(px(self.width))
193 .h(px(plot_height))
194 .relative()
195 .child(render_element),
196 );
197
198 Ok(container)
199 }
200}
201
202pub fn pie(values: &[f64]) -> PieChart {
219 PieChart {
220 labels: None,
221 values: values.to_vec(),
222 title: None,
223 inner_radius_fraction: 0.0,
224 pad_angle: 0.0,
225 corner_radius: 0.0,
226 colors: None,
227 width: DEFAULT_WIDTH,
228 height: DEFAULT_HEIGHT,
229 sort: true,
230 }
231}
232
233impl PieChart {
234 pub fn labels(mut self, labels: &[impl ToString]) -> Self {
236 self.labels = Some(labels.iter().map(|l| l.to_string()).collect());
237 self
238 }
239}
240
241pub fn donut(values: &[f64]) -> PieChart {
253 pie(values).hole(0.5)
254}