gpui_px/
pie.rs

1//! Pie chart - Plotly Express style API.
2
3use 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
14/// Default color palette (Plotly)
15const DEFAULT_PALETTE: [u32; 10] = [
16    0x1f77b4, 0xff7f0e, 0x2ca02c, 0xd62728, 0x9467bd, 0x8c564b, 0xe377c2, 0x7f7f7f, 0xbcbd22,
17    0x17becf,
18];
19
20/// Pie chart builder.
21#[derive(Clone)]
22pub struct PieChart {
23    labels: Option<Vec<String>>,
24    values: Vec<f64>,
25    title: Option<String>,
26    inner_radius_fraction: f64, // 0.0 to 1.0 of outer radius
27    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    /// Set chart title (rendered at top of chart).
37    pub fn title(mut self, title: impl Into<String>) -> Self {
38        self.title = Some(title.into());
39        self
40    }
41
42    /// Set custom colors for slices.
43    pub fn colors(mut self, colors: &[u32]) -> Self {
44        self.colors = Some(colors.to_vec());
45        self
46    }
47
48    /// Set hole size fraction (0.0 to 1.0).
49    /// 0.0 = full pie, 0.5 = donut with hole half the radius.
50    pub fn hole(mut self, fraction: f64) -> Self {
51        self.inner_radius_fraction = fraction.clamp(0.0, 0.99);
52        self
53    }
54
55    /// Set padding angle between slices (in radians).
56    pub fn pad_angle(mut self, angle: f64) -> Self {
57        self.pad_angle = angle;
58        self
59    }
60
61    /// Set corner radius for slices.
62    pub fn corner_radius(mut self, radius: f64) -> Self {
63        self.corner_radius = radius;
64        self
65    }
66
67    /// Sort slices by value (descending). Default is true.
68    pub fn sort(mut self, sort: bool) -> Self {
69        self.sort = sort;
70        self
71    }
72
73    /// Set chart dimensions.
74    pub fn size(mut self, width: f32, height: f32) -> Self {
75        self.width = width;
76        self.height = height;
77        self
78    }
79
80    /// Build and validate the chart, returning renderable element.
81    pub fn build(self) -> Result<impl IntoElement, ChartError> {
82        // Validate inputs
83        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        // Calculate plot area
91        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        // Calculate radius
100        let radius = (plot_width.min(plot_height) / 2.0) as f64 * 0.9; // 90% fit
101        let inner_radius = radius * self.inner_radius_fraction;
102
103        // Prepare pie generator
104        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        // Generate slices
112        let slices = pie.generate(&self.values, |v| *v);
113
114        // Determine colors
115        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        // Create arc generator
126        let arc_gen = Arc::new();
127
128        // Render function
129        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        // Build container
167        let mut container = div()
168            .w(px(self.width))
169            .h(px(self.height))
170            .relative()
171            .flex()
172            .flex_col();
173
174        // Add title if present
175        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        // Add plot area
190        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
202/// Create a pie chart from values.
203///
204/// # Example
205///
206/// ```rust,no_run
207/// use gpui_px::pie;
208///
209/// let values = vec![10.0, 20.0, 30.0, 40.0];
210/// let labels = vec!["A", "B", "C", "D"];
211///
212/// let chart = pie(&values)
213///     .labels(&labels)
214///     .title("My Pie Chart")
215///     .build()?;
216/// # Ok::<(), gpui_px::ChartError>(())
217/// ```
218pub 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    /// Set labels for slices (used for tooltips/legend - currently unused).
235    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
241/// Create a donut chart from values (shorthand for pie with hole).
242///
243/// # Example
244///
245/// ```rust,no_run
246/// use gpui_px::donut;
247///
248/// let values = vec![10.0, 20.0, 30.0];
249/// let chart = donut(&values).title("My Donut").build()?;
250/// # Ok::<(), gpui_px::ChartError>(())
251/// ```
252pub fn donut(values: &[f64]) -> PieChart {
253    pie(values).hole(0.5)
254}