1use crate::error::ChartError;
4use crate::{
5 DEFAULT_COLOR, DEFAULT_HEIGHT, DEFAULT_PADDING_FRACTION, DEFAULT_TITLE_FONT_SIZE,
6 DEFAULT_WIDTH, ScaleType, TITLE_AREA_HEIGHT, extent_padded, validate_data_array,
7 validate_data_length, validate_dimensions, validate_positive,
8};
9use d3rs::axis::{AxisConfig, DefaultAxisTheme, render_axis};
10use d3rs::color::D3Color;
11use d3rs::grid::{GridConfig, render_grid};
12use d3rs::scale::{LinearScale, LogScale};
13use d3rs::shape::{CurveType, LineConfig, LinePoint, render_line};
14use d3rs::text::{VectorFontConfig, render_vector_text};
15use gpui::prelude::*;
16use gpui::*;
17
18#[derive(Debug, Clone)]
20pub struct LineChart {
21 x: Vec<f64>,
22 y: Vec<f64>,
23 title: Option<String>,
24 color: u32,
25 stroke_width: f32,
26 opacity: f32,
27 curve: CurveType,
28 show_points: bool,
29 width: f32,
30 height: f32,
31 x_scale_type: ScaleType,
32 y_scale_type: ScaleType,
33}
34
35impl LineChart {
36 pub fn title(mut self, title: impl Into<String>) -> Self {
38 self.title = Some(title.into());
39 self
40 }
41
42 pub fn color(mut self, hex: u32) -> Self {
52 self.color = hex;
53 self
54 }
55
56 pub fn stroke_width(mut self, width: f32) -> Self {
58 self.stroke_width = width;
59 self
60 }
61
62 pub fn opacity(mut self, opacity: f32) -> Self {
64 self.opacity = opacity.clamp(0.0, 1.0);
65 self
66 }
67
68 pub fn curve(mut self, curve: CurveType) -> Self {
70 self.curve = curve;
71 self
72 }
73
74 pub fn show_points(mut self, show: bool) -> Self {
76 self.show_points = show;
77 self
78 }
79
80 pub fn size(mut self, width: f32, height: f32) -> Self {
82 self.width = width;
83 self.height = height;
84 self
85 }
86
87 pub fn x_scale(mut self, scale: ScaleType) -> Self {
97 self.x_scale_type = scale;
98 self
99 }
100
101 pub fn y_scale(mut self, scale: ScaleType) -> Self {
103 self.y_scale_type = scale;
104 self
105 }
106
107 pub fn build(self) -> Result<impl IntoElement, ChartError> {
109 validate_data_array(&self.x, "x")?;
111 validate_data_array(&self.y, "y")?;
112 validate_data_length(self.x.len(), self.y.len(), "x", "y")?;
113 validate_dimensions(self.width, self.height)?;
114
115 if self.x_scale_type == ScaleType::Log {
117 validate_positive(&self.x, "x")?;
118 }
119 if self.y_scale_type == ScaleType::Log {
120 validate_positive(&self.y, "y")?;
121 }
122
123 let margin_left = 50.0;
125 let margin_bottom = 30.0;
126 let margin_top = 10.0;
127 let margin_right = 20.0;
128
129 let title_height = if self.title.is_some() {
131 TITLE_AREA_HEIGHT
132 } else {
133 0.0
134 };
135
136 let plot_width = (self.width as f64 - margin_left - margin_right).max(0.0);
137 let plot_height =
138 (self.height as f64 - title_height as f64 - margin_top - margin_bottom).max(0.0);
139
140 let (x_min, x_max) = extent_padded(&self.x, DEFAULT_PADDING_FRACTION);
142 let (y_min, y_max) = extent_padded(&self.y, DEFAULT_PADDING_FRACTION);
143
144 let data: Vec<LinePoint> = self
146 .x
147 .iter()
148 .zip(self.y.iter())
149 .map(|(&x, &y)| LinePoint::new(x, y))
150 .collect();
151
152 let config = LineConfig::new()
154 .stroke_color(D3Color::from_hex(self.color))
155 .stroke_width(self.stroke_width)
156 .opacity(self.opacity)
157 .curve(self.curve)
158 .show_points(self.show_points);
159
160 let theme = DefaultAxisTheme;
161
162 let chart_content: AnyElement = match (self.x_scale_type, self.y_scale_type) {
164 (ScaleType::Linear, ScaleType::Linear) => {
165 let x_scale = LinearScale::new()
166 .domain(x_min, x_max)
167 .range(0.0, plot_width);
168 let y_scale = LinearScale::new()
169 .domain(y_min, y_max)
170 .range(plot_height, 0.0);
171
172 div()
173 .flex()
174 .child(render_axis(
175 &y_scale,
176 &AxisConfig::left(),
177 plot_height as f32,
178 &theme,
179 ))
180 .child(
181 div()
182 .flex()
183 .flex_col()
184 .child(
185 div()
186 .w(px(plot_width as f32))
187 .h(px(plot_height as f32))
188 .relative()
189 .bg(rgb(0xf8f8f8))
190 .child(render_grid(
191 &x_scale,
192 &y_scale,
193 &GridConfig::default(),
194 plot_width as f32,
195 plot_height as f32,
196 &theme,
197 ))
198 .child(render_line(&x_scale, &y_scale, &data, &config)),
199 )
200 .child(render_axis(
201 &x_scale,
202 &AxisConfig::bottom(),
203 plot_width as f32,
204 &theme,
205 )),
206 )
207 .into_any_element()
208 }
209 (ScaleType::Log, ScaleType::Linear) => {
210 let x_scale = LogScale::new()
211 .domain(x_min.max(1e-10), x_max)
212 .range(0.0, plot_width);
213 let y_scale = LinearScale::new()
214 .domain(y_min, y_max)
215 .range(plot_height, 0.0);
216
217 let x_axis_config = AxisConfig::bottom().with_label_angle(-45.0);
219
220 div()
221 .flex()
222 .child(render_axis(
223 &y_scale,
224 &AxisConfig::left(),
225 plot_height as f32,
226 &theme,
227 ))
228 .child(
229 div()
230 .flex()
231 .flex_col()
232 .child(
233 div()
234 .w(px(plot_width as f32))
235 .h(px(plot_height as f32))
236 .relative()
237 .bg(rgb(0xf8f8f8))
238 .child(render_grid(
239 &x_scale,
240 &y_scale,
241 &GridConfig::default(),
242 plot_width as f32,
243 plot_height as f32,
244 &theme,
245 ))
246 .child(render_line(&x_scale, &y_scale, &data, &config)),
247 )
248 .child(render_axis(
249 &x_scale,
250 &x_axis_config,
251 plot_width as f32,
252 &theme,
253 )),
254 )
255 .into_any_element()
256 }
257 (ScaleType::Linear, ScaleType::Log) => {
258 let x_scale = LinearScale::new()
259 .domain(x_min, x_max)
260 .range(0.0, plot_width);
261 let y_scale = LogScale::new()
262 .domain(y_min.max(1e-10), y_max)
263 .range(plot_height, 0.0);
264
265 div()
266 .flex()
267 .child(render_axis(
268 &y_scale,
269 &AxisConfig::left(),
270 plot_height as f32,
271 &theme,
272 ))
273 .child(
274 div()
275 .flex()
276 .flex_col()
277 .child(
278 div()
279 .w(px(plot_width as f32))
280 .h(px(plot_height as f32))
281 .relative()
282 .bg(rgb(0xf8f8f8))
283 .child(render_grid(
284 &x_scale,
285 &y_scale,
286 &GridConfig::default(),
287 plot_width as f32,
288 plot_height as f32,
289 &theme,
290 ))
291 .child(render_line(&x_scale, &y_scale, &data, &config)),
292 )
293 .child(render_axis(
294 &x_scale,
295 &AxisConfig::bottom(),
296 plot_width as f32,
297 &theme,
298 )),
299 )
300 .into_any_element()
301 }
302 (ScaleType::Log, ScaleType::Log) => {
303 let x_scale = LogScale::new()
304 .domain(x_min.max(1e-10), x_max)
305 .range(0.0, plot_width);
306 let y_scale = LogScale::new()
307 .domain(y_min.max(1e-10), y_max)
308 .range(plot_height, 0.0);
309
310 let x_axis_config = AxisConfig::bottom().with_label_angle(-45.0);
312
313 div()
314 .flex()
315 .child(render_axis(
316 &y_scale,
317 &AxisConfig::left(),
318 plot_height as f32,
319 &theme,
320 ))
321 .child(
322 div()
323 .flex()
324 .flex_col()
325 .child(
326 div()
327 .w(px(plot_width as f32))
328 .h(px(plot_height as f32))
329 .relative()
330 .bg(rgb(0xf8f8f8))
331 .child(render_grid(
332 &x_scale,
333 &y_scale,
334 &GridConfig::default(),
335 plot_width as f32,
336 plot_height as f32,
337 &theme,
338 ))
339 .child(render_line(&x_scale, &y_scale, &data, &config)),
340 )
341 .child(render_axis(
342 &x_scale,
343 &x_axis_config,
344 plot_width as f32,
345 &theme,
346 )),
347 )
348 .into_any_element()
349 }
350 };
351
352 let mut container = div()
354 .w(px(self.width))
355 .h(px(self.height))
356 .relative()
357 .flex()
358 .flex_col();
359
360 if let Some(title) = &self.title {
362 let font_config =
363 VectorFontConfig::horizontal(DEFAULT_TITLE_FONT_SIZE, hsla(0.0, 0.0, 0.2, 1.0));
364 container = container.child(
365 div()
366 .w_full()
367 .h(px(title_height))
368 .flex()
369 .justify_center()
370 .items_center()
371 .child(render_vector_text(title, &font_config)),
372 );
373 }
374
375 container = container.child(div().relative().child(chart_content));
377
378 Ok(container)
379 }
380}
381
382pub fn line(x: &[f64], y: &[f64]) -> LineChart {
401 LineChart {
402 x: x.to_vec(),
403 y: y.to_vec(),
404 title: None,
405 color: DEFAULT_COLOR,
406 stroke_width: 2.0,
407 opacity: 1.0,
408 curve: CurveType::Linear,
409 show_points: false,
410 width: DEFAULT_WIDTH,
411 height: DEFAULT_HEIGHT,
412 x_scale_type: ScaleType::Linear,
413 y_scale_type: ScaleType::Linear,
414 }
415}
416
417#[cfg(test)]
418mod tests {
419 use super::*;
420
421 #[test]
422 fn test_line_empty_x_data() {
423 let result = line(&[], &[1.0, 2.0, 3.0]).build();
424 assert!(matches!(result, Err(ChartError::EmptyData { field: "x" })));
425 }
426
427 #[test]
428 fn test_line_empty_y_data() {
429 let result = line(&[1.0, 2.0, 3.0], &[]).build();
430 assert!(matches!(result, Err(ChartError::EmptyData { field: "y" })));
431 }
432
433 #[test]
434 fn test_line_data_length_mismatch() {
435 let result = line(&[1.0, 2.0, 3.0, 4.0], &[1.0, 2.0]).build();
436 assert!(matches!(
437 result,
438 Err(ChartError::DataLengthMismatch {
439 x_field: "x",
440 y_field: "y",
441 x_len: 4,
442 y_len: 2,
443 })
444 ));
445 }
446
447 #[test]
448 fn test_line_infinity_in_x() {
449 let result = line(&[1.0, 2.0, f64::NEG_INFINITY], &[1.0, 2.0, 3.0]).build();
450 assert!(matches!(
451 result,
452 Err(ChartError::InvalidData {
453 field: "x",
454 reason: "contains NaN or Infinity"
455 })
456 ));
457 }
458
459 #[test]
460 fn test_line_nan_in_y() {
461 let result = line(&[1.0, 2.0, 3.0], &[1.0, f64::NAN, 3.0]).build();
462 assert!(matches!(
463 result,
464 Err(ChartError::InvalidData {
465 field: "y",
466 reason: "contains NaN or Infinity"
467 })
468 ));
469 }
470
471 #[test]
472 fn test_line_successful_build() {
473 let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
474 let y = vec![2.0, 4.0, 3.0, 5.0, 4.5];
475 let result = line(&x, &y).title("Test Line").color(0xff7f0e).build();
476 assert!(result.is_ok());
477 }
478
479 #[test]
480 fn test_line_builder_chain() {
481 let result = line(&[1.0, 2.0, 3.0], &[4.0, 5.0, 6.0])
482 .title("My Line")
483 .color(0x00ff00)
484 .stroke_width(3.0)
485 .opacity(0.8)
486 .curve(CurveType::Linear)
487 .show_points(true)
488 .size(800.0, 600.0)
489 .build();
490 assert!(result.is_ok());
491 }
492
493 #[test]
494 fn test_line_log_x_scale() {
495 let x = vec![10.0, 100.0, 1000.0, 10000.0];
496 let y = vec![1.0, 2.0, 3.0, 4.0];
497 let result = line(&x, &y).x_scale(ScaleType::Log).build();
498 assert!(result.is_ok());
499 }
500
501 #[test]
502 fn test_line_log_y_scale() {
503 let x = vec![1.0, 2.0, 3.0, 4.0];
504 let y = vec![10.0, 100.0, 1000.0, 10000.0];
505 let result = line(&x, &y).y_scale(ScaleType::Log).build();
506 assert!(result.is_ok());
507 }
508
509 #[test]
510 fn test_line_log_xy_scale() {
511 let x = vec![10.0, 100.0, 1000.0];
512 let y = vec![20.0, 200.0, 2000.0];
513 let result = line(&x, &y)
514 .x_scale(ScaleType::Log)
515 .y_scale(ScaleType::Log)
516 .build();
517 assert!(result.is_ok());
518 }
519
520 #[test]
521 fn test_line_log_x_negative_values() {
522 let x = vec![-10.0, -5.0, 5.0, 10.0];
523 let y = vec![1.0, 2.0, 3.0, 4.0];
524 let result = line(&x, &y).x_scale(ScaleType::Log).build();
525 assert!(matches!(
526 result,
527 Err(ChartError::InvalidData {
528 field: "x",
529 reason: "contains non-positive values for log scale"
530 })
531 ));
532 }
533
534 #[test]
535 fn test_line_log_y_zero_value() {
536 let x = vec![1.0, 2.0, 3.0, 4.0];
537 let y = vec![0.0, 1.0, 2.0, 3.0];
538 let result = line(&x, &y).y_scale(ScaleType::Log).build();
539 assert!(matches!(
540 result,
541 Err(ChartError::InvalidData {
542 field: "y",
543 reason: "contains non-positive values for log scale"
544 })
545 ));
546 }
547
548 #[test]
549 fn test_line_log_scale_with_curve() {
550 let x = vec![10.0, 100.0, 1000.0];
551 let y = vec![1.0, 2.0, 3.0];
552 let result = line(&x, &y)
553 .title("Log Scale Line")
554 .x_scale(ScaleType::Log)
555 .curve(CurveType::Linear)
556 .show_points(true)
557 .build();
558 assert!(result.is_ok());
559 }
560}