gpui_px/
area.rs

1//! Area chart - Plotly Express style API.
2
3use crate::error::ChartError;
4use crate::{
5    DEFAULT_COLOR, DEFAULT_HEIGHT, DEFAULT_PADDING_FRACTION, DEFAULT_TITLE_FONT_SIZE,
6    DEFAULT_WIDTH, ScaleType, TITLE_AREA_HEIGHT, extent_padded, validate_data_array,
7    validate_data_length, validate_dimensions, validate_positive,
8};
9use d3rs::color::D3Color;
10use d3rs::scale::{LinearScale, LogScale, Scale};
11use d3rs::shape::{Area, Curve};
12use d3rs::text::{VectorFontConfig, render_vector_text};
13use gpui::prelude::*;
14use gpui::*;
15use std::sync::Arc;
16
17/// Area chart builder.
18#[derive(Clone)]
19pub struct AreaChart {
20    x: Vec<f64>,
21    y: Vec<f64>,
22    y0: Option<Vec<f64>>,
23    title: Option<String>,
24    color: u32,
25    opacity: f32,
26    curve: Curve,
27    width: f32,
28    height: f32,
29    x_scale_type: ScaleType,
30    y_scale_type: ScaleType,
31}
32
33impl AreaChart {
34    /// Set chart title (rendered at top of chart).
35    pub fn title(mut self, title: impl Into<String>) -> Self {
36        self.title = Some(title.into());
37        self
38    }
39
40    /// Set fill color as 24-bit RGB hex value (format: 0xRRGGBB).
41    pub fn color(mut self, hex: u32) -> Self {
42        self.color = hex;
43        self
44    }
45
46    /// Set fill opacity (0.0 - 1.0).
47    pub fn opacity(mut self, opacity: f32) -> Self {
48        self.opacity = opacity.clamp(0.0, 1.0);
49        self
50    }
51
52    /// Set curve interpolation type.
53    pub fn curve(mut self, curve: Curve) -> Self {
54        self.curve = curve;
55        self
56    }
57
58    /// Set chart dimensions.
59    pub fn size(mut self, width: f32, height: f32) -> Self {
60        self.width = width;
61        self.height = height;
62        self
63    }
64
65    /// Set X-axis scale type (linear or log).
66    pub fn x_scale(mut self, scale: ScaleType) -> Self {
67        self.x_scale_type = scale;
68        self
69    }
70
71    /// Set Y-axis scale type (linear or log).
72    pub fn y_scale(mut self, scale: ScaleType) -> Self {
73        self.y_scale_type = scale;
74        self
75    }
76
77    /// Set baseline Y values (y0). Defaults to 0.0 if not specified.
78    pub fn y0(mut self, y0: &[f64]) -> Self {
79        self.y0 = Some(y0.to_vec());
80        self
81    }
82
83    /// Build and validate the chart, returning renderable element.
84    pub fn build(self) -> Result<impl IntoElement, ChartError> {
85        // Validate inputs
86        validate_data_array(&self.x, "x")?;
87        validate_data_array(&self.y, "y")?;
88        validate_data_length(self.x.len(), self.y.len(), "x", "y")?;
89        validate_dimensions(self.width, self.height)?;
90
91        if let Some(ref y0) = self.y0 {
92            validate_data_array(y0, "y0")?;
93            validate_data_length(self.x.len(), y0.len(), "x", "y0")?;
94        }
95
96        // Validate positive values for log scales
97        if self.x_scale_type == ScaleType::Log {
98            validate_positive(&self.x, "x")?;
99        }
100        if self.y_scale_type == ScaleType::Log {
101            validate_positive(&self.y, "y")?;
102            if let Some(ref y0) = self.y0 {
103                validate_positive(y0, "y0")?;
104            }
105        }
106
107        // Calculate plot area (reserve space for title if present)
108        let title_height = if self.title.is_some() {
109            TITLE_AREA_HEIGHT
110        } else {
111            0.0
112        };
113        let plot_height = self.height - title_height;
114
115        // Calculate domains with padding
116        let (x_min, x_max) = extent_padded(&self.x, DEFAULT_PADDING_FRACTION);
117
118        // Calculate Y domain considering y and y0
119        let y_iter = self.y.iter();
120        let (y_min, y_max) = if let Some(ref y0) = self.y0 {
121            let all_y: Vec<f64> = y_iter.chain(y0.iter()).copied().collect();
122            extent_padded(&all_y, DEFAULT_PADDING_FRACTION)
123        } else {
124            let mut all_y: Vec<f64> = y_iter.copied().collect();
125            all_y.push(0.0); // Include baseline 0
126            extent_padded(&all_y, DEFAULT_PADDING_FRACTION)
127        };
128
129        // Prepare data for rendering
130        struct AreaDatum {
131            x: f64,
132            y0: f64,
133            y1: f64,
134        }
135
136        let data: Vec<AreaDatum> = match &self.y0 {
137            Some(y0) => self
138                .x
139                .iter()
140                .zip(self.y.iter())
141                .zip(y0.iter())
142                .map(|((&x, &y1), &y0)| AreaDatum { x, y0, y1 })
143                .collect(),
144            None => self
145                .x
146                .iter()
147                .zip(self.y.iter())
148                .map(|(&x, &y1)| AreaDatum { x, y0: 0.0, y1 })
149                .collect(),
150        };
151
152        let color = D3Color::from_hex(self.color);
153        let fill_color = color.to_rgba();
154        let opacity = self.opacity;
155        let curve = self.curve;
156
157        // Create render function
158        let render_element = move |x_scale: Arc<dyn Scale<f64, f64>>,
159                                   y_scale: Arc<dyn Scale<f64, f64>>| {
160            let x_scale_prepaint = x_scale.clone();
161            let y_scale_prepaint = y_scale.clone();
162
163            canvas(
164                move |bounds, _, _| (x_scale_prepaint.clone(), y_scale_prepaint.clone(), bounds),
165                move |_, (x_scale, y_scale, bounds), window, _| {
166                    let x_scale_x = x_scale.clone();
167                    let y_scale_y0 = y_scale.clone();
168                    let y_scale_y1 = y_scale.clone();
169
170                    let area = Area::new()
171                        .x(move |d: &AreaDatum| x_scale_x.scale(d.x))
172                        .y0(move |d: &AreaDatum| y_scale_y0.scale(d.y0))
173                        .y1(move |d: &AreaDatum| y_scale_y1.scale(d.y1))
174                        .curve(curve);
175
176                    let path = area.generate(&data);
177                    let points = path.flatten(0.5);
178
179                    let origin_x: f32 = bounds.origin.x.into();
180                    let origin_y: f32 = bounds.origin.y.into();
181
182                    if points.is_empty() {
183                        return;
184                    }
185
186                    let mut path_builder = PathBuilder::fill();
187
188                    let first = points[0];
189                    path_builder.move_to(gpui::point(
190                        px(origin_x + first.x as f32),
191                        px(origin_y + first.y as f32),
192                    ));
193
194                    for p in points.iter().skip(1) {
195                        path_builder.line_to(gpui::point(
196                            px(origin_x + p.x as f32),
197                            px(origin_y + p.y as f32),
198                        ));
199                    }
200
201                    path_builder.close();
202
203                    if let Ok(gpui_path) = path_builder.build() {
204                        window.paint_path(
205                            gpui_path,
206                            Rgba {
207                                r: fill_color.r,
208                                g: fill_color.g,
209                                b: fill_color.b,
210                                a: fill_color.a * opacity,
211                            },
212                        );
213                    }
214                },
215            )
216        };
217
218        // Build the element based on scale types
219        let area_element: AnyElement = match (self.x_scale_type, self.y_scale_type) {
220            (ScaleType::Linear, ScaleType::Linear) => {
221                let x_scale = LinearScale::new()
222                    .domain(x_min, x_max)
223                    .range(0.0, self.width as f64);
224                let y_scale = LinearScale::new()
225                    .domain(y_min, y_max)
226                    .range(plot_height as f64, 0.0);
227                render_element(Arc::new(x_scale), Arc::new(y_scale)).into_any_element()
228            }
229            (ScaleType::Log, ScaleType::Linear) => {
230                let x_scale = LogScale::new()
231                    .domain(x_min.max(1e-10), x_max)
232                    .range(0.0, self.width as f64);
233                let y_scale = LinearScale::new()
234                    .domain(y_min, y_max)
235                    .range(plot_height as f64, 0.0);
236                render_element(Arc::new(x_scale), Arc::new(y_scale)).into_any_element()
237            }
238            (ScaleType::Linear, ScaleType::Log) => {
239                let x_scale = LinearScale::new()
240                    .domain(x_min, x_max)
241                    .range(0.0, self.width as f64);
242                let y_scale = LogScale::new()
243                    .domain(y_min.max(1e-10), y_max)
244                    .range(plot_height as f64, 0.0);
245                render_element(Arc::new(x_scale), Arc::new(y_scale)).into_any_element()
246            }
247            (ScaleType::Log, ScaleType::Log) => {
248                let x_scale = LogScale::new()
249                    .domain(x_min.max(1e-10), x_max)
250                    .range(0.0, self.width as f64);
251                let y_scale = LogScale::new()
252                    .domain(y_min.max(1e-10), y_max)
253                    .range(plot_height as f64, 0.0);
254                render_element(Arc::new(x_scale), Arc::new(y_scale)).into_any_element()
255            }
256        };
257
258        // Build container with optional title
259        let mut container = div()
260            .w(px(self.width))
261            .h(px(self.height))
262            .relative()
263            .flex()
264            .flex_col();
265
266        // Add title if present
267        if let Some(title) = &self.title {
268            let font_config =
269                VectorFontConfig::horizontal(DEFAULT_TITLE_FONT_SIZE, hsla(0.0, 0.0, 0.2, 1.0));
270            container = container.child(
271                div()
272                    .w_full()
273                    .h(px(title_height))
274                    .flex()
275                    .justify_center()
276                    .items_center()
277                    .child(render_vector_text(title, &font_config)),
278            );
279        }
280
281        // Add plot area
282        container = container.child(
283            div()
284                .w(px(self.width))
285                .h(px(plot_height))
286                .relative()
287                .child(area_element),
288        );
289
290        Ok(container)
291    }
292}
293
294/// Create an area chart from x and y data.
295///
296/// # Example
297///
298/// ```rust,no_run
299/// use gpui_px::{area, Curve};
300///
301/// let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
302/// let y = vec![2.0, 4.0, 3.0, 5.0, 4.5];
303///
304/// let chart = area(&x, &y)
305///     .title("My Area Chart")
306///     .color(0xff7f0e)
307///     .curve(Curve::MonotoneX)
308///     .build()?;
309/// # Ok::<(), gpui_px::ChartError>(())
310/// ```
311pub fn area(x: &[f64], y: &[f64]) -> AreaChart {
312    AreaChart {
313        x: x.to_vec(),
314        y: y.to_vec(),
315        y0: None,
316        title: None,
317        color: DEFAULT_COLOR,
318        opacity: 0.6,
319        curve: Curve::Linear,
320        width: DEFAULT_WIDTH,
321        height: DEFAULT_HEIGHT,
322        x_scale_type: ScaleType::Linear,
323        y_scale_type: ScaleType::Linear,
324    }
325}