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::axis::{AxisConfig, DefaultAxisTheme, render_axis};
11use d3rs::contour::ContourGenerator;
12use d3rs::grid::{GridConfig, render_grid};
13use d3rs::scale::{LinearScale, LogScale};
14use d3rs::shape::{ContourConfig, render_contour_bands};
15use d3rs::text::{VectorFontConfig, render_vector_text};
16use gpui::prelude::*;
17use gpui::*;
18
19#[derive(Clone)]
21pub struct ContourChart {
22 z: Vec<f64>,
23 grid_width: usize,
24 grid_height: usize,
25 x_values: Option<Vec<f64>>,
26 y_values: Option<Vec<f64>>,
27 x_scale_type: ScaleType,
28 y_scale_type: ScaleType,
29 thresholds: Option<Vec<f64>>,
30 color_scale: ColorScale,
31 title: Option<String>,
32 opacity: f32,
33 width: f32,
34 height: f32,
35}
36
37impl std::fmt::Debug for ContourChart {
38 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39 f.debug_struct("ContourChart")
40 .field("grid_width", &self.grid_width)
41 .field("grid_height", &self.grid_height)
42 .field("x_scale_type", &self.x_scale_type)
43 .field("y_scale_type", &self.y_scale_type)
44 .field("thresholds", &self.thresholds)
45 .field("color_scale", &self.color_scale)
46 .field("title", &self.title)
47 .field("opacity", &self.opacity)
48 .field("width", &self.width)
49 .field("height", &self.height)
50 .finish()
51 }
52}
53
54impl ContourChart {
55 pub fn x(mut self, values: &[f64]) -> Self {
60 self.x_values = Some(values.to_vec());
61 self
62 }
63
64 pub fn y(mut self, values: &[f64]) -> Self {
69 self.y_values = Some(values.to_vec());
70 self
71 }
72
73 pub fn x_scale(mut self, scale: ScaleType) -> Self {
75 self.x_scale_type = scale;
76 self
77 }
78
79 pub fn y_scale(mut self, scale: ScaleType) -> Self {
81 self.y_scale_type = scale;
82 self
83 }
84
85 pub fn thresholds(mut self, thresholds: Vec<f64>) -> Self {
90 self.thresholds = Some(thresholds);
91 self
92 }
93
94 pub fn color_scale(mut self, scale: ColorScale) -> Self {
96 self.color_scale = scale;
97 self
98 }
99
100 pub fn title(mut self, title: impl Into<String>) -> Self {
102 self.title = Some(title.into());
103 self
104 }
105
106 pub fn opacity(mut self, opacity: f32) -> Self {
108 self.opacity = opacity.clamp(0.0, 1.0);
109 self
110 }
111
112 pub fn size(mut self, width: f32, height: f32) -> Self {
114 self.width = width;
115 self.height = height;
116 self
117 }
118
119 pub fn build(self) -> Result<impl IntoElement, ChartError> {
121 validate_data_array(&self.z, "z")?;
123 validate_grid_dimensions(&self.z, self.grid_width, self.grid_height)?;
124 validate_dimensions(self.width, self.height)?;
125
126 let x_values = match self.x_values {
128 Some(ref v) => {
129 if v.len() != self.grid_width {
130 return Err(ChartError::DataLengthMismatch {
131 x_field: "x",
132 y_field: "grid_width",
133 x_len: v.len(),
134 y_len: self.grid_width,
135 });
136 }
137 validate_data_array(v, "x")?;
138 validate_monotonic(v, "x")?;
139 if self.x_scale_type == ScaleType::Log {
140 validate_positive(v, "x")?;
141 }
142 v.clone()
143 }
144 None => (0..self.grid_width).map(|i| i as f64).collect(),
145 };
146
147 let y_values = match self.y_values {
149 Some(ref v) => {
150 if v.len() != self.grid_height {
151 return Err(ChartError::DataLengthMismatch {
152 x_field: "y",
153 y_field: "grid_height",
154 x_len: v.len(),
155 y_len: self.grid_height,
156 });
157 }
158 validate_data_array(v, "y")?;
159 validate_monotonic(v, "y")?;
160 if self.y_scale_type == ScaleType::Log {
161 validate_positive(v, "y")?;
162 }
163 v.clone()
164 }
165 None => (0..self.grid_height).map(|i| i as f64).collect(),
166 };
167
168 let title_height = if self.title.is_some() {
170 TITLE_AREA_HEIGHT
171 } else {
172 0.0
173 };
174
175 let left_margin = 60.0_f64;
177 let bottom_margin = 40.0_f64;
178 let plot_width = (self.width as f64) - left_margin;
179 let plot_height = (self.height as f64) - title_height as f64 - bottom_margin;
180
181 let theme = DefaultAxisTheme;
182
183 let (x_min, x_max) = extent_padded(&x_values, 0.0);
185 let (y_min, y_max) = extent_padded(&y_values, 0.0);
186
187 let (z_min, z_max) = extent_padded(&self.z, 0.0);
189
190 let thresholds = match self.thresholds {
192 Some(t) => t,
193 None => {
194 let n = 10;
196 (0..=n)
197 .map(|i| z_min + (z_max - z_min) * (i as f64) / (n as f64))
198 .collect()
199 }
200 };
201
202 let generator = ContourGenerator::new(self.grid_width, self.grid_height)
204 .x_values(x_values)
205 .y_values(y_values);
206 let bands = generator.contour_bands(&self.z, &thresholds);
207
208 let color_fn = self.color_scale.to_fn();
210 let config = ContourConfig::new()
211 .fill(true)
212 .fill_opacity(self.opacity)
213 .stroke_width(0.5)
214 .stroke_opacity(0.3)
215 .color_scale(color_fn);
216
217 let contour_element: AnyElement = match (self.x_scale_type, self.y_scale_type) {
219 (ScaleType::Linear, ScaleType::Linear) => {
220 let x_scale = LinearScale::new()
221 .domain(x_min, x_max)
222 .range(0.0, plot_width);
223 let y_scale = LinearScale::new()
224 .domain(y_min, y_max)
225 .range(plot_height, 0.0);
226
227 div()
228 .flex()
229 .child(render_axis(
230 &y_scale,
231 &AxisConfig::left(),
232 plot_height as f32,
233 &theme,
234 ))
235 .child(
236 div()
237 .flex()
238 .flex_col()
239 .child(
240 div()
241 .w(px(plot_width as f32))
242 .h(px(plot_height as f32))
243 .relative()
244 .bg(rgb(0xf8f8f8))
245 .child(render_grid(
246 &x_scale,
247 &y_scale,
248 &GridConfig::default(),
249 plot_width as f32,
250 plot_height as f32,
251 &theme,
252 ))
253 .child(div().absolute().inset_0().child(render_contour_bands(
254 bands, &x_scale, &y_scale, &config,
255 ))),
256 )
257 .child(render_axis(
258 &x_scale,
259 &AxisConfig::bottom(),
260 plot_width as f32,
261 &theme,
262 )),
263 )
264 .into_any_element()
265 }
266 (ScaleType::Log, ScaleType::Linear) => {
267 let x_scale = LogScale::new()
268 .domain(x_min.max(1e-10), x_max)
269 .range(0.0, plot_width);
270 let y_scale = LinearScale::new()
271 .domain(y_min, y_max)
272 .range(plot_height, 0.0);
273
274 div()
275 .flex()
276 .child(render_axis(
277 &y_scale,
278 &AxisConfig::left(),
279 plot_height as f32,
280 &theme,
281 ))
282 .child(
283 div()
284 .flex()
285 .flex_col()
286 .child(
287 div()
288 .w(px(plot_width as f32))
289 .h(px(plot_height as f32))
290 .relative()
291 .bg(rgb(0xf8f8f8))
292 .child(render_grid(
293 &x_scale,
294 &y_scale,
295 &GridConfig::default(),
296 plot_width as f32,
297 plot_height as f32,
298 &theme,
299 ))
300 .child(div().absolute().inset_0().child(render_contour_bands(
301 bands, &x_scale, &y_scale, &config,
302 ))),
303 )
304 .child(render_axis(
305 &x_scale,
306 &AxisConfig::bottom(),
307 plot_width as f32,
308 &theme,
309 )),
310 )
311 .into_any_element()
312 }
313 (ScaleType::Linear, ScaleType::Log) => {
314 let x_scale = LinearScale::new()
315 .domain(x_min, x_max)
316 .range(0.0, plot_width);
317 let y_scale = LogScale::new()
318 .domain(y_min.max(1e-10), y_max)
319 .range(plot_height, 0.0);
320
321 div()
322 .flex()
323 .child(render_axis(
324 &y_scale,
325 &AxisConfig::left(),
326 plot_height as f32,
327 &theme,
328 ))
329 .child(
330 div()
331 .flex()
332 .flex_col()
333 .child(
334 div()
335 .w(px(plot_width as f32))
336 .h(px(plot_height as f32))
337 .relative()
338 .bg(rgb(0xf8f8f8))
339 .child(render_grid(
340 &x_scale,
341 &y_scale,
342 &GridConfig::default(),
343 plot_width as f32,
344 plot_height as f32,
345 &theme,
346 ))
347 .child(div().absolute().inset_0().child(render_contour_bands(
348 bands, &x_scale, &y_scale, &config,
349 ))),
350 )
351 .child(render_axis(
352 &x_scale,
353 &AxisConfig::bottom(),
354 plot_width as f32,
355 &theme,
356 )),
357 )
358 .into_any_element()
359 }
360 (ScaleType::Log, ScaleType::Log) => {
361 let x_scale = LogScale::new()
362 .domain(x_min.max(1e-10), x_max)
363 .range(0.0, plot_width);
364 let y_scale = LogScale::new()
365 .domain(y_min.max(1e-10), y_max)
366 .range(plot_height, 0.0);
367
368 div()
369 .flex()
370 .child(render_axis(
371 &y_scale,
372 &AxisConfig::left(),
373 plot_height as f32,
374 &theme,
375 ))
376 .child(
377 div()
378 .flex()
379 .flex_col()
380 .child(
381 div()
382 .w(px(plot_width as f32))
383 .h(px(plot_height as f32))
384 .relative()
385 .bg(rgb(0xf8f8f8))
386 .child(render_grid(
387 &x_scale,
388 &y_scale,
389 &GridConfig::default(),
390 plot_width as f32,
391 plot_height as f32,
392 &theme,
393 ))
394 .child(div().absolute().inset_0().child(render_contour_bands(
395 bands, &x_scale, &y_scale, &config,
396 ))),
397 )
398 .child(render_axis(
399 &x_scale,
400 &AxisConfig::bottom(),
401 plot_width as f32,
402 &theme,
403 )),
404 )
405 .into_any_element()
406 }
407 };
408
409 let mut container = div()
411 .w(px(self.width))
412 .h(px(self.height))
413 .relative()
414 .flex()
415 .flex_col();
416
417 if let Some(title) = &self.title {
419 let font_config =
420 VectorFontConfig::horizontal(DEFAULT_TITLE_FONT_SIZE, hsla(0.0, 0.0, 0.2, 1.0));
421 container = container.child(
422 div()
423 .w_full()
424 .h(px(title_height))
425 .flex()
426 .justify_center()
427 .items_center()
428 .child(render_vector_text(title, &font_config)),
429 );
430 }
431
432 container = container.child(contour_element);
434
435 Ok(container)
436 }
437}
438
439pub fn contour(z: &[f64], grid_width: usize, grid_height: usize) -> ContourChart {
463 ContourChart {
464 z: z.to_vec(),
465 grid_width,
466 grid_height,
467 x_values: None,
468 y_values: None,
469 x_scale_type: ScaleType::Linear,
470 y_scale_type: ScaleType::Linear,
471 thresholds: None,
472 color_scale: ColorScale::default(),
473 title: None,
474 opacity: 0.8,
475 width: DEFAULT_WIDTH,
476 height: DEFAULT_HEIGHT,
477 }
478}
479
480#[cfg(test)]
481mod tests {
482 use super::*;
483
484 #[test]
485 fn test_contour_empty_z() {
486 let result = contour(&[], 0, 0).build();
487 assert!(matches!(result, Err(ChartError::EmptyData { field: "z" })));
488 }
489
490 #[test]
491 fn test_contour_grid_mismatch() {
492 let z = vec![1.0, 2.0, 3.0, 4.0, 5.0]; let result = contour(&z, 2, 3).build(); assert!(matches!(
495 result,
496 Err(ChartError::GridDimensionMismatch {
497 z_len: 5,
498 width: 2,
499 height: 3,
500 expected: 6,
501 })
502 ));
503 }
504
505 #[test]
506 fn test_contour_successful_build() {
507 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)
509 .title("Test Contour")
510 .color_scale(ColorScale::Viridis)
511 .build();
512 assert!(result.is_ok());
513 }
514
515 #[test]
516 fn test_contour_with_custom_thresholds() {
517 let z = vec![1.0; 9]; let result = contour(&z, 3, 3)
519 .thresholds(vec![0.0, 0.5, 1.0, 1.5])
520 .build();
521 assert!(result.is_ok());
522 }
523
524 #[test]
525 fn test_contour_with_custom_axes() {
526 let z = vec![1.0; 6]; let x = vec![10.0, 100.0];
528 let y = vec![0.0, 1.0, 2.0];
529 let result = contour(&z, 2, 3).x(&x).y(&y).build();
530 assert!(result.is_ok());
531 }
532
533 #[test]
534 fn test_contour_log_scale() {
535 let z = vec![1.0; 4]; let x = vec![10.0, 100.0];
537 let y = vec![1.0, 10.0];
538 let result = contour(&z, 2, 2)
539 .x(&x)
540 .y(&y)
541 .x_scale(ScaleType::Log)
542 .y_scale(ScaleType::Log)
543 .build();
544 assert!(result.is_ok());
545 }
546
547 #[test]
548 fn test_contour_builder_chain() {
549 let z = vec![1.0; 9]; let result = contour(&z, 3, 3)
551 .title("My Contour")
552 .color_scale(ColorScale::Plasma)
553 .thresholds(vec![0.0, 0.5, 1.0])
554 .opacity(0.8)
555 .size(800.0, 600.0)
556 .build();
557 assert!(result.is_ok());
558 }
559}