1use crate::color_scale::ColorScale;
4use crate::error::ChartError;
5use crate::{
6 DEFAULT_HEIGHT, DEFAULT_TITLE_FONT_SIZE, DEFAULT_WIDTH, ScaleType, TITLE_AREA_HEIGHT,
7 extent_padded, validate_data_array, validate_dimensions, validate_grid_dimensions,
8 validate_monotonic, validate_positive,
9};
10use d3rs::contour::ContourGenerator;
11use d3rs::scale::{LinearScale, LogScale};
12use d3rs::shape::{ContourConfig, render_contour_bands};
13use d3rs::text::{VectorFontConfig, render_vector_text};
14use gpui::prelude::*;
15use gpui::*;
16
17#[derive(Clone)]
19pub struct ContourChart {
20 z: Vec<f64>,
21 grid_width: usize,
22 grid_height: usize,
23 x_values: Option<Vec<f64>>,
24 y_values: Option<Vec<f64>>,
25 x_scale_type: ScaleType,
26 y_scale_type: ScaleType,
27 thresholds: Option<Vec<f64>>,
28 color_scale: ColorScale,
29 title: Option<String>,
30 opacity: f32,
31 width: f32,
32 height: f32,
33}
34
35impl std::fmt::Debug for ContourChart {
36 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37 f.debug_struct("ContourChart")
38 .field("grid_width", &self.grid_width)
39 .field("grid_height", &self.grid_height)
40 .field("x_scale_type", &self.x_scale_type)
41 .field("y_scale_type", &self.y_scale_type)
42 .field("thresholds", &self.thresholds)
43 .field("color_scale", &self.color_scale)
44 .field("title", &self.title)
45 .field("opacity", &self.opacity)
46 .field("width", &self.width)
47 .field("height", &self.height)
48 .finish()
49 }
50}
51
52impl ContourChart {
53 pub fn x(mut self, values: &[f64]) -> Self {
58 self.x_values = Some(values.to_vec());
59 self
60 }
61
62 pub fn y(mut self, values: &[f64]) -> Self {
67 self.y_values = Some(values.to_vec());
68 self
69 }
70
71 pub fn x_scale(mut self, scale: ScaleType) -> Self {
73 self.x_scale_type = scale;
74 self
75 }
76
77 pub fn y_scale(mut self, scale: ScaleType) -> Self {
79 self.y_scale_type = scale;
80 self
81 }
82
83 pub fn thresholds(mut self, thresholds: Vec<f64>) -> Self {
88 self.thresholds = Some(thresholds);
89 self
90 }
91
92 pub fn color_scale(mut self, scale: ColorScale) -> Self {
94 self.color_scale = scale;
95 self
96 }
97
98 pub fn title(mut self, title: impl Into<String>) -> Self {
100 self.title = Some(title.into());
101 self
102 }
103
104 pub fn opacity(mut self, opacity: f32) -> Self {
106 self.opacity = opacity.clamp(0.0, 1.0);
107 self
108 }
109
110 pub fn size(mut self, width: f32, height: f32) -> Self {
112 self.width = width;
113 self.height = height;
114 self
115 }
116
117 pub fn build(self) -> Result<impl IntoElement, ChartError> {
119 validate_data_array(&self.z, "z")?;
121 validate_grid_dimensions(&self.z, self.grid_width, self.grid_height)?;
122 validate_dimensions(self.width, self.height)?;
123
124 let x_values = match self.x_values {
126 Some(ref v) => {
127 if v.len() != self.grid_width {
128 return Err(ChartError::DataLengthMismatch {
129 x_field: "x",
130 y_field: "grid_width",
131 x_len: v.len(),
132 y_len: self.grid_width,
133 });
134 }
135 validate_data_array(v, "x")?;
136 validate_monotonic(v, "x")?;
137 if self.x_scale_type == ScaleType::Log {
138 validate_positive(v, "x")?;
139 }
140 v.clone()
141 }
142 None => (0..self.grid_width).map(|i| i as f64).collect(),
143 };
144
145 let y_values = match self.y_values {
147 Some(ref v) => {
148 if v.len() != self.grid_height {
149 return Err(ChartError::DataLengthMismatch {
150 x_field: "y",
151 y_field: "grid_height",
152 x_len: v.len(),
153 y_len: self.grid_height,
154 });
155 }
156 validate_data_array(v, "y")?;
157 validate_monotonic(v, "y")?;
158 if self.y_scale_type == ScaleType::Log {
159 validate_positive(v, "y")?;
160 }
161 v.clone()
162 }
163 None => (0..self.grid_height).map(|i| i as f64).collect(),
164 };
165
166 let title_height = if self.title.is_some() {
168 TITLE_AREA_HEIGHT
169 } else {
170 0.0
171 };
172 let plot_height = self.height - title_height;
173
174 let (x_min, x_max) = extent_padded(&x_values, 0.0);
176 let (y_min, y_max) = extent_padded(&y_values, 0.0);
177
178 let (z_min, z_max) = extent_padded(&self.z, 0.0);
180
181 let thresholds = match self.thresholds {
183 Some(t) => t,
184 None => {
185 let n = 10;
187 (0..=n)
188 .map(|i| z_min + (z_max - z_min) * (i as f64) / (n as f64))
189 .collect()
190 }
191 };
192
193 let generator = ContourGenerator::new(self.grid_width, self.grid_height)
195 .x_values(x_values)
196 .y_values(y_values);
197 let bands = generator.contour_bands(&self.z, &thresholds);
198
199 let color_fn = self.color_scale.to_fn();
201 let config = ContourConfig::new()
202 .fill(true)
203 .fill_opacity(self.opacity)
204 .stroke_width(0.5)
205 .stroke_opacity(0.3)
206 .color_scale(color_fn);
207
208 let contour_element: AnyElement = match (self.x_scale_type, self.y_scale_type) {
210 (ScaleType::Linear, ScaleType::Linear) => {
211 let x_scale = LinearScale::new()
212 .domain(x_min, x_max)
213 .range(0.0, self.width as f64);
214 let y_scale = LinearScale::new()
215 .domain(y_min, y_max)
216 .range(plot_height as f64, 0.0);
217 render_contour_bands(bands, &x_scale, &y_scale, &config).into_any_element()
218 }
219 (ScaleType::Log, ScaleType::Linear) => {
220 let x_scale = LogScale::new()
221 .domain(x_min.max(1e-10), x_max)
222 .range(0.0, self.width as f64);
223 let y_scale = LinearScale::new()
224 .domain(y_min, y_max)
225 .range(plot_height as f64, 0.0);
226 render_contour_bands(bands, &x_scale, &y_scale, &config).into_any_element()
227 }
228 (ScaleType::Linear, ScaleType::Log) => {
229 let x_scale = LinearScale::new()
230 .domain(x_min, x_max)
231 .range(0.0, self.width as f64);
232 let y_scale = LogScale::new()
233 .domain(y_min.max(1e-10), y_max)
234 .range(plot_height as f64, 0.0);
235 render_contour_bands(bands, &x_scale, &y_scale, &config).into_any_element()
236 }
237 (ScaleType::Log, ScaleType::Log) => {
238 let x_scale = LogScale::new()
239 .domain(x_min.max(1e-10), x_max)
240 .range(0.0, self.width as f64);
241 let y_scale = LogScale::new()
242 .domain(y_min.max(1e-10), y_max)
243 .range(plot_height as f64, 0.0);
244 render_contour_bands(bands, &x_scale, &y_scale, &config).into_any_element()
245 }
246 };
247
248 let mut container = div()
250 .w(px(self.width))
251 .h(px(self.height))
252 .relative()
253 .flex()
254 .flex_col();
255
256 if let Some(title) = &self.title {
258 let font_config =
259 VectorFontConfig::horizontal(DEFAULT_TITLE_FONT_SIZE, hsla(0.0, 0.0, 0.2, 1.0));
260 container = container.child(
261 div()
262 .w_full()
263 .h(px(title_height))
264 .flex()
265 .justify_center()
266 .items_center()
267 .child(render_vector_text(title, &font_config)),
268 );
269 }
270
271 container = container.child(
273 div()
274 .w(px(self.width))
275 .h(px(plot_height))
276 .relative()
277 .child(contour_element),
278 );
279
280 Ok(container)
281 }
282}
283
284pub fn contour(z: &[f64], grid_width: usize, grid_height: usize) -> ContourChart {
308 ContourChart {
309 z: z.to_vec(),
310 grid_width,
311 grid_height,
312 x_values: None,
313 y_values: None,
314 x_scale_type: ScaleType::Linear,
315 y_scale_type: ScaleType::Linear,
316 thresholds: None,
317 color_scale: ColorScale::default(),
318 title: None,
319 opacity: 0.8,
320 width: DEFAULT_WIDTH,
321 height: DEFAULT_HEIGHT,
322 }
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328
329 #[test]
330 fn test_contour_empty_z() {
331 let result = contour(&[], 0, 0).build();
332 assert!(matches!(result, Err(ChartError::EmptyData { field: "z" })));
333 }
334
335 #[test]
336 fn test_contour_grid_mismatch() {
337 let z = vec![1.0, 2.0, 3.0, 4.0, 5.0]; let result = contour(&z, 2, 3).build(); assert!(matches!(
340 result,
341 Err(ChartError::GridDimensionMismatch {
342 z_len: 5,
343 width: 2,
344 height: 3,
345 expected: 6,
346 })
347 ));
348 }
349
350 #[test]
351 fn test_contour_successful_build() {
352 let z = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]; let result = contour(&z, 3, 3)
354 .title("Test Contour")
355 .color_scale(ColorScale::Viridis)
356 .build();
357 assert!(result.is_ok());
358 }
359
360 #[test]
361 fn test_contour_with_custom_thresholds() {
362 let z = vec![1.0; 9]; let result = contour(&z, 3, 3)
364 .thresholds(vec![0.0, 0.5, 1.0, 1.5])
365 .build();
366 assert!(result.is_ok());
367 }
368
369 #[test]
370 fn test_contour_with_custom_axes() {
371 let z = vec![1.0; 6]; let x = vec![10.0, 100.0];
373 let y = vec![0.0, 1.0, 2.0];
374 let result = contour(&z, 2, 3).x(&x).y(&y).build();
375 assert!(result.is_ok());
376 }
377
378 #[test]
379 fn test_contour_log_scale() {
380 let z = vec![1.0; 4]; let x = vec![10.0, 100.0];
382 let y = vec![1.0, 10.0];
383 let result = contour(&z, 2, 2)
384 .x(&x)
385 .y(&y)
386 .x_scale(ScaleType::Log)
387 .y_scale(ScaleType::Log)
388 .build();
389 assert!(result.is_ok());
390 }
391
392 #[test]
393 fn test_contour_builder_chain() {
394 let z = vec![1.0; 9]; let result = contour(&z, 3, 3)
396 .title("My Contour")
397 .color_scale(ColorScale::Plasma)
398 .thresholds(vec![0.0, 0.5, 1.0])
399 .opacity(0.8)
400 .size(800.0, 600.0)
401 .build();
402 assert!(result.is_ok());
403 }
404}