1use crate::error::ChartError;
4use crate::{
5 DEFAULT_COLOR, DEFAULT_HEIGHT, DEFAULT_TITLE_FONT_SIZE, DEFAULT_WIDTH, ScaleType,
6 TITLE_AREA_HEIGHT, extent_padded, validate_data_array, validate_dimensions,
7 validate_grid_dimensions, validate_monotonic, validate_positive,
8};
9use d3rs::axis::{AxisConfig, DefaultAxisTheme, render_axis};
10use d3rs::color::D3Color;
11use d3rs::contour::ContourGenerator;
12use d3rs::grid::{GridConfig, render_grid};
13use d3rs::scale::{LinearScale, LogScale};
14use d3rs::shape::{ContourConfig, render_contour};
15use d3rs::text::{VectorFontConfig, render_vector_text};
16use gpui::prelude::*;
17use gpui::*;
18
19#[derive(Debug, Clone)]
21pub struct IsolineChart {
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 levels: Option<Vec<f64>>,
30 color: u32,
31 stroke_width: f32,
32 opacity: f32,
33 title: Option<String>,
34 width: f32,
35 height: f32,
36}
37
38impl IsolineChart {
39 pub fn x(mut self, values: &[f64]) -> Self {
44 self.x_values = Some(values.to_vec());
45 self
46 }
47
48 pub fn y(mut self, values: &[f64]) -> Self {
53 self.y_values = Some(values.to_vec());
54 self
55 }
56
57 pub fn x_scale(mut self, scale: ScaleType) -> Self {
59 self.x_scale_type = scale;
60 self
61 }
62
63 pub fn y_scale(mut self, scale: ScaleType) -> Self {
65 self.y_scale_type = scale;
66 self
67 }
68
69 pub fn levels(mut self, levels: Vec<f64>) -> Self {
74 self.levels = Some(levels);
75 self
76 }
77
78 pub fn color(mut self, hex: u32) -> Self {
80 self.color = hex;
81 self
82 }
83
84 pub fn stroke_width(mut self, width: f32) -> Self {
86 self.stroke_width = width;
87 self
88 }
89
90 pub fn opacity(mut self, opacity: f32) -> Self {
92 self.opacity = opacity.clamp(0.0, 1.0);
93 self
94 }
95
96 pub fn title(mut self, title: impl Into<String>) -> Self {
98 self.title = Some(title.into());
99 self
100 }
101
102 pub fn size(mut self, width: f32, height: f32) -> Self {
104 self.width = width;
105 self.height = height;
106 self
107 }
108
109 pub fn build(self) -> Result<impl IntoElement, ChartError> {
111 validate_data_array(&self.z, "z")?;
113 validate_grid_dimensions(&self.z, self.grid_width, self.grid_height)?;
114 validate_dimensions(self.width, self.height)?;
115
116 let x_values = match self.x_values {
118 Some(ref v) => {
119 if v.len() != self.grid_width {
120 return Err(ChartError::DataLengthMismatch {
121 x_field: "x",
122 y_field: "grid_width",
123 x_len: v.len(),
124 y_len: self.grid_width,
125 });
126 }
127 validate_data_array(v, "x")?;
128 validate_monotonic(v, "x")?;
129 if self.x_scale_type == ScaleType::Log {
130 validate_positive(v, "x")?;
131 }
132 v.clone()
133 }
134 None => (0..self.grid_width).map(|i| i as f64).collect(),
135 };
136
137 let y_values = match self.y_values {
139 Some(ref v) => {
140 if v.len() != self.grid_height {
141 return Err(ChartError::DataLengthMismatch {
142 x_field: "y",
143 y_field: "grid_height",
144 x_len: v.len(),
145 y_len: self.grid_height,
146 });
147 }
148 validate_data_array(v, "y")?;
149 validate_monotonic(v, "y")?;
150 if self.y_scale_type == ScaleType::Log {
151 validate_positive(v, "y")?;
152 }
153 v.clone()
154 }
155 None => (0..self.grid_height).map(|i| i as f64).collect(),
156 };
157
158 let title_height = if self.title.is_some() {
160 TITLE_AREA_HEIGHT
161 } else {
162 0.0
163 };
164
165 let left_margin = 60.0_f64;
167 let bottom_margin = 40.0_f64;
168 let plot_width = (self.width as f64) - left_margin;
169 let plot_height = (self.height as f64) - title_height as f64 - bottom_margin;
170
171 let theme = DefaultAxisTheme;
172
173 let (x_min, x_max) = extent_padded(&x_values, 0.0);
175 let (y_min, y_max) = extent_padded(&y_values, 0.0);
176
177 let (z_min, z_max) = extent_padded(&self.z, 0.0);
179
180 let levels = match self.levels {
182 Some(l) => l,
183 None => {
184 let n = 10;
186 (0..=n)
187 .map(|i| z_min + (z_max - z_min) * (i as f64) / (n as f64))
188 .collect()
189 }
190 };
191
192 let generator = ContourGenerator::new(self.grid_width, self.grid_height)
194 .x_values(x_values)
195 .y_values(y_values);
196 let contours = generator.contours(&self.z, &levels);
197
198 let config = ContourConfig::new()
200 .fill(false)
201 .stroke_color(D3Color::from_hex(self.color))
202 .stroke_width(self.stroke_width)
203 .stroke_opacity(self.opacity);
204
205 let isoline_element: AnyElement = match (self.x_scale_type, self.y_scale_type) {
207 (ScaleType::Linear, ScaleType::Linear) => {
208 let x_scale = LinearScale::new()
209 .domain(x_min, x_max)
210 .range(0.0, plot_width);
211 let y_scale = LinearScale::new()
212 .domain(y_min, y_max)
213 .range(plot_height, 0.0);
214
215 div()
216 .flex()
217 .child(render_axis(
218 &y_scale,
219 &AxisConfig::left(),
220 plot_height as f32,
221 &theme,
222 ))
223 .child(
224 div()
225 .flex()
226 .flex_col()
227 .child(
228 div()
229 .w(px(plot_width as f32))
230 .h(px(plot_height as f32))
231 .relative()
232 .bg(rgb(0xf8f8f8))
233 .child(render_grid(
234 &x_scale,
235 &y_scale,
236 &GridConfig::default(),
237 plot_width as f32,
238 plot_height as f32,
239 &theme,
240 ))
241 .child(div().absolute().inset_0().child(render_contour(
242 contours, &x_scale, &y_scale, &config,
243 ))),
244 )
245 .child(render_axis(
246 &x_scale,
247 &AxisConfig::bottom(),
248 plot_width as f32,
249 &theme,
250 )),
251 )
252 .into_any_element()
253 }
254 (ScaleType::Log, ScaleType::Linear) => {
255 let x_scale = LogScale::new()
256 .domain(x_min.max(1e-10), x_max)
257 .range(0.0, plot_width);
258 let y_scale = LinearScale::new()
259 .domain(y_min, y_max)
260 .range(plot_height, 0.0);
261
262 div()
263 .flex()
264 .child(render_axis(
265 &y_scale,
266 &AxisConfig::left(),
267 plot_height as f32,
268 &theme,
269 ))
270 .child(
271 div()
272 .flex()
273 .flex_col()
274 .child(
275 div()
276 .w(px(plot_width as f32))
277 .h(px(plot_height as f32))
278 .relative()
279 .bg(rgb(0xf8f8f8))
280 .child(render_grid(
281 &x_scale,
282 &y_scale,
283 &GridConfig::default(),
284 plot_width as f32,
285 plot_height as f32,
286 &theme,
287 ))
288 .child(div().absolute().inset_0().child(render_contour(
289 contours, &x_scale, &y_scale, &config,
290 ))),
291 )
292 .child(render_axis(
293 &x_scale,
294 &AxisConfig::bottom(),
295 plot_width as f32,
296 &theme,
297 )),
298 )
299 .into_any_element()
300 }
301 (ScaleType::Linear, ScaleType::Log) => {
302 let x_scale = LinearScale::new()
303 .domain(x_min, x_max)
304 .range(0.0, plot_width);
305 let y_scale = LogScale::new()
306 .domain(y_min.max(1e-10), y_max)
307 .range(plot_height, 0.0);
308
309 div()
310 .flex()
311 .child(render_axis(
312 &y_scale,
313 &AxisConfig::left(),
314 plot_height as f32,
315 &theme,
316 ))
317 .child(
318 div()
319 .flex()
320 .flex_col()
321 .child(
322 div()
323 .w(px(plot_width as f32))
324 .h(px(plot_height as f32))
325 .relative()
326 .bg(rgb(0xf8f8f8))
327 .child(render_grid(
328 &x_scale,
329 &y_scale,
330 &GridConfig::default(),
331 plot_width as f32,
332 plot_height as f32,
333 &theme,
334 ))
335 .child(div().absolute().inset_0().child(render_contour(
336 contours, &x_scale, &y_scale, &config,
337 ))),
338 )
339 .child(render_axis(
340 &x_scale,
341 &AxisConfig::bottom(),
342 plot_width as f32,
343 &theme,
344 )),
345 )
346 .into_any_element()
347 }
348 (ScaleType::Log, ScaleType::Log) => {
349 let x_scale = LogScale::new()
350 .domain(x_min.max(1e-10), x_max)
351 .range(0.0, plot_width);
352 let y_scale = LogScale::new()
353 .domain(y_min.max(1e-10), y_max)
354 .range(plot_height, 0.0);
355
356 div()
357 .flex()
358 .child(render_axis(
359 &y_scale,
360 &AxisConfig::left(),
361 plot_height as f32,
362 &theme,
363 ))
364 .child(
365 div()
366 .flex()
367 .flex_col()
368 .child(
369 div()
370 .w(px(plot_width as f32))
371 .h(px(plot_height as f32))
372 .relative()
373 .bg(rgb(0xf8f8f8))
374 .child(render_grid(
375 &x_scale,
376 &y_scale,
377 &GridConfig::default(),
378 plot_width as f32,
379 plot_height as f32,
380 &theme,
381 ))
382 .child(div().absolute().inset_0().child(render_contour(
383 contours, &x_scale, &y_scale, &config,
384 ))),
385 )
386 .child(render_axis(
387 &x_scale,
388 &AxisConfig::bottom(),
389 plot_width as f32,
390 &theme,
391 )),
392 )
393 .into_any_element()
394 }
395 };
396
397 let mut container = div()
399 .w(px(self.width))
400 .h(px(self.height))
401 .relative()
402 .flex()
403 .flex_col();
404
405 if let Some(title) = &self.title {
407 let font_config =
408 VectorFontConfig::horizontal(DEFAULT_TITLE_FONT_SIZE, hsla(0.0, 0.0, 0.2, 1.0));
409 container = container.child(
410 div()
411 .w_full()
412 .h(px(title_height))
413 .flex()
414 .justify_center()
415 .items_center()
416 .child(render_vector_text(title, &font_config)),
417 );
418 }
419
420 container = container.child(isoline_element);
422
423 Ok(container)
424 }
425}
426
427pub fn isoline(z: &[f64], grid_width: usize, grid_height: usize) -> IsolineChart {
452 IsolineChart {
453 z: z.to_vec(),
454 grid_width,
455 grid_height,
456 x_values: None,
457 y_values: None,
458 x_scale_type: ScaleType::Linear,
459 y_scale_type: ScaleType::Linear,
460 levels: None,
461 color: DEFAULT_COLOR,
462 stroke_width: 1.5,
463 opacity: 1.0,
464 title: None,
465 width: DEFAULT_WIDTH,
466 height: DEFAULT_HEIGHT,
467 }
468}
469
470#[cfg(test)]
471mod tests {
472 use super::*;
473
474 #[test]
475 fn test_isoline_empty_z() {
476 let result = isoline(&[], 0, 0).build();
477 assert!(matches!(result, Err(ChartError::EmptyData { field: "z" })));
478 }
479
480 #[test]
481 fn test_isoline_grid_mismatch() {
482 let z = vec![1.0, 2.0, 3.0, 4.0, 5.0]; let result = isoline(&z, 2, 3).build(); assert!(matches!(
485 result,
486 Err(ChartError::GridDimensionMismatch {
487 z_len: 5,
488 width: 2,
489 height: 3,
490 expected: 6,
491 })
492 ));
493 }
494
495 #[test]
496 fn test_isoline_successful_build() {
497 let z = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]; let result = isoline(&z, 3, 3)
499 .title("Test Isolines")
500 .color(0x333333)
501 .build();
502 assert!(result.is_ok());
503 }
504
505 #[test]
506 fn test_isoline_with_custom_levels() {
507 let z = vec![1.0; 9]; let result = isoline(&z, 3, 3).levels(vec![0.5, 1.0, 1.5]).build();
509 assert!(result.is_ok());
510 }
511
512 #[test]
513 fn test_isoline_with_custom_axes() {
514 let z = vec![1.0; 6]; let x = vec![10.0, 100.0];
516 let y = vec![0.0, 1.0, 2.0];
517 let result = isoline(&z, 2, 3).x(&x).y(&y).build();
518 assert!(result.is_ok());
519 }
520
521 #[test]
522 fn test_isoline_log_scale() {
523 let z = vec![1.0; 4]; let x = vec![10.0, 100.0];
525 let y = vec![1.0, 10.0];
526 let result = isoline(&z, 2, 2)
527 .x(&x)
528 .y(&y)
529 .x_scale(ScaleType::Log)
530 .y_scale(ScaleType::Log)
531 .build();
532 assert!(result.is_ok());
533 }
534
535 #[test]
536 fn test_isoline_builder_chain() {
537 let z = vec![1.0; 9]; let result = isoline(&z, 3, 3)
539 .title("My Isolines")
540 .color(0xff0000)
541 .stroke_width(2.0)
542 .opacity(0.8)
543 .levels(vec![0.5, 1.0, 1.5])
544 .size(800.0, 600.0)
545 .build();
546 assert!(result.is_ok());
547 }
548}