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::color::D3Color;
10use d3rs::contour::ContourGenerator;
11use d3rs::scale::{LinearScale, LogScale};
12use d3rs::shape::{ContourConfig, render_contour};
13use d3rs::text::{VectorFontConfig, render_vector_text};
14use gpui::prelude::*;
15use gpui::*;
16
17#[derive(Debug, Clone)]
19pub struct IsolineChart {
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 levels: Option<Vec<f64>>,
28 color: u32,
29 stroke_width: f32,
30 opacity: f32,
31 title: Option<String>,
32 width: f32,
33 height: f32,
34}
35
36impl IsolineChart {
37 pub fn x(mut self, values: &[f64]) -> Self {
42 self.x_values = Some(values.to_vec());
43 self
44 }
45
46 pub fn y(mut self, values: &[f64]) -> Self {
51 self.y_values = Some(values.to_vec());
52 self
53 }
54
55 pub fn x_scale(mut self, scale: ScaleType) -> Self {
57 self.x_scale_type = scale;
58 self
59 }
60
61 pub fn y_scale(mut self, scale: ScaleType) -> Self {
63 self.y_scale_type = scale;
64 self
65 }
66
67 pub fn levels(mut self, levels: Vec<f64>) -> Self {
72 self.levels = Some(levels);
73 self
74 }
75
76 pub fn color(mut self, hex: u32) -> Self {
78 self.color = hex;
79 self
80 }
81
82 pub fn stroke_width(mut self, width: f32) -> Self {
84 self.stroke_width = width;
85 self
86 }
87
88 pub fn opacity(mut self, opacity: f32) -> Self {
90 self.opacity = opacity.clamp(0.0, 1.0);
91 self
92 }
93
94 pub fn title(mut self, title: impl Into<String>) -> Self {
96 self.title = Some(title.into());
97 self
98 }
99
100 pub fn size(mut self, width: f32, height: f32) -> Self {
102 self.width = width;
103 self.height = height;
104 self
105 }
106
107 pub fn build(self) -> Result<impl IntoElement, ChartError> {
109 validate_data_array(&self.z, "z")?;
111 validate_grid_dimensions(&self.z, self.grid_width, self.grid_height)?;
112 validate_dimensions(self.width, self.height)?;
113
114 let x_values = match self.x_values {
116 Some(ref v) => {
117 if v.len() != self.grid_width {
118 return Err(ChartError::DataLengthMismatch {
119 x_field: "x",
120 y_field: "grid_width",
121 x_len: v.len(),
122 y_len: self.grid_width,
123 });
124 }
125 validate_data_array(v, "x")?;
126 validate_monotonic(v, "x")?;
127 if self.x_scale_type == ScaleType::Log {
128 validate_positive(v, "x")?;
129 }
130 v.clone()
131 }
132 None => (0..self.grid_width).map(|i| i as f64).collect(),
133 };
134
135 let y_values = match self.y_values {
137 Some(ref v) => {
138 if v.len() != self.grid_height {
139 return Err(ChartError::DataLengthMismatch {
140 x_field: "y",
141 y_field: "grid_height",
142 x_len: v.len(),
143 y_len: self.grid_height,
144 });
145 }
146 validate_data_array(v, "y")?;
147 validate_monotonic(v, "y")?;
148 if self.y_scale_type == ScaleType::Log {
149 validate_positive(v, "y")?;
150 }
151 v.clone()
152 }
153 None => (0..self.grid_height).map(|i| i as f64).collect(),
154 };
155
156 let title_height = if self.title.is_some() {
158 TITLE_AREA_HEIGHT
159 } else {
160 0.0
161 };
162 let plot_height = self.height - title_height;
163
164 let (x_min, x_max) = extent_padded(&x_values, 0.0);
166 let (y_min, y_max) = extent_padded(&y_values, 0.0);
167
168 let (z_min, z_max) = extent_padded(&self.z, 0.0);
170
171 let levels = match self.levels {
173 Some(l) => l,
174 None => {
175 let n = 10;
177 (0..=n)
178 .map(|i| z_min + (z_max - z_min) * (i as f64) / (n as f64))
179 .collect()
180 }
181 };
182
183 let generator = ContourGenerator::new(self.grid_width, self.grid_height)
185 .x_values(x_values)
186 .y_values(y_values);
187 let contours = generator.contours(&self.z, &levels);
188
189 let config = ContourConfig::new()
191 .fill(false)
192 .stroke_color(D3Color::from_hex(self.color))
193 .stroke_width(self.stroke_width)
194 .stroke_opacity(self.opacity);
195
196 let isoline_element: AnyElement = match (self.x_scale_type, self.y_scale_type) {
198 (ScaleType::Linear, ScaleType::Linear) => {
199 let x_scale = LinearScale::new()
200 .domain(x_min, x_max)
201 .range(0.0, self.width as f64);
202 let y_scale = LinearScale::new()
203 .domain(y_min, y_max)
204 .range(plot_height as f64, 0.0);
205 render_contour(contours, &x_scale, &y_scale, &config).into_any_element()
206 }
207 (ScaleType::Log, ScaleType::Linear) => {
208 let x_scale = LogScale::new()
209 .domain(x_min.max(1e-10), x_max)
210 .range(0.0, self.width as f64);
211 let y_scale = LinearScale::new()
212 .domain(y_min, y_max)
213 .range(plot_height as f64, 0.0);
214 render_contour(contours, &x_scale, &y_scale, &config).into_any_element()
215 }
216 (ScaleType::Linear, ScaleType::Log) => {
217 let x_scale = LinearScale::new()
218 .domain(x_min, x_max)
219 .range(0.0, self.width as f64);
220 let y_scale = LogScale::new()
221 .domain(y_min.max(1e-10), y_max)
222 .range(plot_height as f64, 0.0);
223 render_contour(contours, &x_scale, &y_scale, &config).into_any_element()
224 }
225 (ScaleType::Log, ScaleType::Log) => {
226 let x_scale = LogScale::new()
227 .domain(x_min.max(1e-10), x_max)
228 .range(0.0, self.width as f64);
229 let y_scale = LogScale::new()
230 .domain(y_min.max(1e-10), y_max)
231 .range(plot_height as f64, 0.0);
232 render_contour(contours, &x_scale, &y_scale, &config).into_any_element()
233 }
234 };
235
236 let mut container = div()
238 .w(px(self.width))
239 .h(px(self.height))
240 .relative()
241 .flex()
242 .flex_col();
243
244 if let Some(title) = &self.title {
246 let font_config =
247 VectorFontConfig::horizontal(DEFAULT_TITLE_FONT_SIZE, hsla(0.0, 0.0, 0.2, 1.0));
248 container = container.child(
249 div()
250 .w_full()
251 .h(px(title_height))
252 .flex()
253 .justify_center()
254 .items_center()
255 .child(render_vector_text(title, &font_config)),
256 );
257 }
258
259 container = container.child(
261 div()
262 .w(px(self.width))
263 .h(px(plot_height))
264 .relative()
265 .child(isoline_element),
266 );
267
268 Ok(container)
269 }
270}
271
272pub fn isoline(z: &[f64], grid_width: usize, grid_height: usize) -> IsolineChart {
297 IsolineChart {
298 z: z.to_vec(),
299 grid_width,
300 grid_height,
301 x_values: None,
302 y_values: None,
303 x_scale_type: ScaleType::Linear,
304 y_scale_type: ScaleType::Linear,
305 levels: None,
306 color: DEFAULT_COLOR,
307 stroke_width: 1.5,
308 opacity: 1.0,
309 title: None,
310 width: DEFAULT_WIDTH,
311 height: DEFAULT_HEIGHT,
312 }
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318
319 #[test]
320 fn test_isoline_empty_z() {
321 let result = isoline(&[], 0, 0).build();
322 assert!(matches!(result, Err(ChartError::EmptyData { field: "z" })));
323 }
324
325 #[test]
326 fn test_isoline_grid_mismatch() {
327 let z = vec![1.0, 2.0, 3.0, 4.0, 5.0]; let result = isoline(&z, 2, 3).build(); assert!(matches!(
330 result,
331 Err(ChartError::GridDimensionMismatch {
332 z_len: 5,
333 width: 2,
334 height: 3,
335 expected: 6,
336 })
337 ));
338 }
339
340 #[test]
341 fn test_isoline_successful_build() {
342 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)
344 .title("Test Isolines")
345 .color(0x333333)
346 .build();
347 assert!(result.is_ok());
348 }
349
350 #[test]
351 fn test_isoline_with_custom_levels() {
352 let z = vec![1.0; 9]; let result = isoline(&z, 3, 3).levels(vec![0.5, 1.0, 1.5]).build();
354 assert!(result.is_ok());
355 }
356
357 #[test]
358 fn test_isoline_with_custom_axes() {
359 let z = vec![1.0; 6]; let x = vec![10.0, 100.0];
361 let y = vec![0.0, 1.0, 2.0];
362 let result = isoline(&z, 2, 3).x(&x).y(&y).build();
363 assert!(result.is_ok());
364 }
365
366 #[test]
367 fn test_isoline_log_scale() {
368 let z = vec![1.0; 4]; let x = vec![10.0, 100.0];
370 let y = vec![1.0, 10.0];
371 let result = isoline(&z, 2, 2)
372 .x(&x)
373 .y(&y)
374 .x_scale(ScaleType::Log)
375 .y_scale(ScaleType::Log)
376 .build();
377 assert!(result.is_ok());
378 }
379
380 #[test]
381 fn test_isoline_builder_chain() {
382 let z = vec![1.0; 9]; let result = isoline(&z, 3, 3)
384 .title("My Isolines")
385 .color(0xff0000)
386 .stroke_width(2.0)
387 .opacity(0.8)
388 .levels(vec![0.5, 1.0, 1.5])
389 .size(800.0, 600.0)
390 .build();
391 assert!(result.is_ok());
392 }
393}