gpui_px/
surface3d.rs

1//! 3D Surface chart.
2
3use crate::error::ChartError;
4use crate::{
5    DEFAULT_HEIGHT, DEFAULT_TITLE_FONT_SIZE, DEFAULT_WIDTH, TITLE_AREA_HEIGHT, validate_data_array,
6    validate_dimensions, validate_grid_dimensions, validate_monotonic, validate_positive,
7};
8use d3rs::surface3d::{Colormap, Surface3DConfig, Surface3DElement, SurfaceData};
9use d3rs::text::{VectorFontConfig, render_vector_text};
10use gpui::prelude::*;
11use gpui::*;
12
13/// Surface 3D chart builder.
14#[derive(Clone)]
15pub struct Surface3DChart {
16    z: Vec<f64>,
17    grid_width: usize,
18    grid_height: usize,
19    x_values: Option<Vec<f64>>,
20    y_values: Option<Vec<f64>>,
21    title: Option<String>,
22    colormap: Colormap,
23    wireframe: bool,
24    width: f32,
25    height: f32,
26    x_log: bool,
27    y_log: bool,
28    z_min: Option<f64>,
29    z_max: Option<f64>,
30    x_label: Option<String>,
31    y_label: Option<String>,
32    z_label: Option<String>,
33}
34
35impl std::fmt::Debug for Surface3DChart {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        f.debug_struct("Surface3DChart")
38            .field("grid_width", &self.grid_width)
39            .field("grid_height", &self.grid_height)
40            .field("colormap", &self.colormap)
41            .field("title", &self.title)
42            .field("wireframe", &self.wireframe)
43            .field("width", &self.width)
44            .field("height", &self.height)
45            .finish()
46    }
47}
48
49impl Surface3DChart {
50    /// Set custom x axis values.
51    ///
52    /// Values must be strictly monotonically increasing.
53    /// Length must match grid_width.
54    pub fn x(mut self, values: &[f64]) -> Self {
55        self.x_values = Some(values.to_vec());
56        self
57    }
58
59    /// Set custom y axis values.
60    ///
61    /// Values must be strictly monotonically increasing.
62    /// Length must match grid_height.
63    pub fn y(mut self, values: &[f64]) -> Self {
64        self.y_values = Some(values.to_vec());
65        self
66    }
67
68    /// Set chart title (rendered at top of chart).
69    pub fn title(mut self, title: impl Into<String>) -> Self {
70        self.title = Some(title.into());
71        self
72    }
73
74    /// Set colormap.
75    pub fn colormap(mut self, colormap: Colormap) -> Self {
76        self.colormap = colormap;
77        self
78    }
79
80    /// Enable wireframe mode.
81    pub fn wireframe(mut self, wireframe: bool) -> Self {
82        self.wireframe = wireframe;
83        self
84    }
85
86    /// Set chart dimensions.
87    pub fn size(mut self, width: f32, height: f32) -> Self {
88        self.width = width;
89        self.height = height;
90        self
91    }
92
93    /// Set logarithmic X-axis.
94    pub fn x_log(mut self, log: bool) -> Self {
95        self.x_log = log;
96        self
97    }
98
99    /// Set logarithmic Y-axis.
100    pub fn y_log(mut self, log: bool) -> Self {
101        self.y_log = log;
102        self
103    }
104
105    /// Set Z-axis range manually.
106    pub fn z_range(mut self, min: f64, max: f64) -> Self {
107        self.z_min = Some(min);
108        self.z_max = Some(max);
109        self
110    }
111
112    /// Set X-axis label.
113    pub fn x_label(mut self, label: impl Into<String>) -> Self {
114        self.x_label = Some(label.into());
115        self
116    }
117
118    /// Set Y-axis label.
119    pub fn y_label(mut self, label: impl Into<String>) -> Self {
120        self.y_label = Some(label.into());
121        self
122    }
123
124    /// Set Z-axis label.
125    pub fn z_label(mut self, label: impl Into<String>) -> Self {
126        self.z_label = Some(label.into());
127        self
128    }
129
130    /// Build and validate the chart, returning renderable element.
131    pub fn build(self) -> Result<impl IntoElement, ChartError> {
132        // Validate inputs
133        validate_data_array(&self.z, "z")?;
134        validate_grid_dimensions(&self.z, self.grid_width, self.grid_height)?;
135        validate_dimensions(self.width, self.height)?;
136
137        // Generate or validate x values
138        let x_values = match self.x_values {
139            Some(ref v) => {
140                if v.len() != self.grid_width {
141                    return Err(ChartError::DataLengthMismatch {
142                        x_field: "x",
143                        y_field: "grid_width",
144                        x_len: v.len(),
145                        y_len: self.grid_width,
146                    });
147                }
148                validate_data_array(v, "x")?;
149                validate_monotonic(v, "x")?;
150                if self.x_log {
151                    validate_positive(v, "x")?;
152                }
153                v.clone()
154            }
155            None => (0..self.grid_width).map(|i| i as f64).collect(),
156        };
157
158        // Generate or validate y values
159        let y_values = match self.y_values {
160            Some(ref v) => {
161                if v.len() != self.grid_height {
162                    return Err(ChartError::DataLengthMismatch {
163                        x_field: "y",
164                        y_field: "grid_height",
165                        x_len: v.len(),
166                        y_len: self.grid_height,
167                    });
168                }
169                validate_data_array(v, "y")?;
170                validate_monotonic(v, "y")?;
171                if self.y_log {
172                    validate_positive(v, "y")?;
173                }
174                v.clone()
175            }
176            None => (0..self.grid_height).map(|i| i as f64).collect(),
177        };
178
179        // Reshape z into Vec<Vec<f64>>
180        // z is row-major (y varies slowly, x varies quickly)
181        let mut z_grid = Vec::with_capacity(self.grid_height);
182        for y_idx in 0..self.grid_height {
183            let start = y_idx * self.grid_width;
184            let end = start + self.grid_width;
185            z_grid.push(self.z[start..end].to_vec());
186        }
187
188        // Calculate plot area (reserve space for title if present)
189        let title_height = if self.title.is_some() {
190            TITLE_AREA_HEIGHT
191        } else {
192            0.0
193        };
194        let plot_height = self.height - title_height;
195
196        // Create SurfaceData
197        let mut data = SurfaceData::from_grid(x_values, y_values, z_grid);
198
199        // Apply configurations to data
200        if let Some(label) = self.x_label {
201            data = data.with_x_label(label);
202        }
203        if let Some(label) = self.y_label {
204            data = data.with_y_label(label);
205        }
206        if let Some(label) = self.z_label {
207            data = data.with_z_label(label);
208        }
209        data = data.with_log_x(self.x_log).with_log_y(self.y_log);
210        if let (Some(min), Some(max)) = (self.z_min, self.z_max) {
211            data = data.with_z_range(min, max);
212        }
213
214        // Create Surface3DConfig
215        let config = Surface3DConfig::new()
216            .colormap(self.colormap)
217            .wireframe(self.wireframe);
218
219        // Build container with optional title
220        let mut container = div()
221            .w(px(self.width))
222            .h(px(self.height))
223            .relative()
224            .flex()
225            .flex_col();
226
227        // Add title if present
228        if let Some(title) = &self.title {
229            let font_config =
230                VectorFontConfig::horizontal(DEFAULT_TITLE_FONT_SIZE, hsla(0.0, 0.0, 0.2, 1.0));
231            container = container.child(
232                div()
233                    .w_full()
234                    .h(px(title_height))
235                    .flex()
236                    .justify_center()
237                    .items_center()
238                    .child(render_vector_text(title, &font_config)),
239            );
240        }
241
242        // Add surface element
243        container = container.child(
244            div()
245                .w(px(self.width))
246                .h(px(plot_height))
247                .relative()
248                .child(Surface3DElement::new(data, config)),
249        );
250
251        Ok(container)
252    }
253}
254
255/// Create a 3D surface chart from z data with grid dimensions.
256///
257/// Data is in row-major order: `z[row * width + col]` where row 0 is at the bottom.
258///
259/// # Example
260///
261/// ```rust,no_run
262/// use gpui_px::{surface3d, Colormap};
263///
264/// // 3x3 grid
265/// let z = vec![
266///     1.0, 2.0, 3.0,  // row 0 (bottom)
267///     4.0, 5.0, 6.0,  // row 1
268///     7.0, 8.0, 9.0,  // row 2 (top)
269/// ];
270///
271/// let chart = surface3d(&z, 3, 3)
272///     .title("My Surface")
273///     .colormap(Colormap::Viridis)
274///     .build()?;
275/// # Ok::<(), gpui_px::ChartError>(())
276/// ```
277pub fn surface3d(z: &[f64], grid_width: usize, grid_height: usize) -> Surface3DChart {
278    Surface3DChart {
279        z: z.to_vec(),
280        grid_width,
281        grid_height,
282        x_values: None,
283        y_values: None,
284        title: None,
285        colormap: Colormap::Viridis,
286        wireframe: false,
287        width: DEFAULT_WIDTH,
288        height: DEFAULT_HEIGHT,
289        x_log: false,
290        y_log: false,
291        z_min: None,
292        z_max: None,
293        x_label: None,
294        y_label: None,
295        z_label: None,
296    }
297}