1use crate::error::ChartError;
4use crate::{
5 DEFAULT_COLOR, DEFAULT_HEIGHT, DEFAULT_PADDING_FRACTION, DEFAULT_TITLE_FONT_SIZE,
6 DEFAULT_WIDTH, TITLE_AREA_HEIGHT, ScaleType, extent_padded, validate_data_array,
7 validate_data_length, validate_dimensions, validate_positive,
8};
9use d3rs::color::D3Color;
10use d3rs::scale::{LinearScale, LogScale};
11use d3rs::shape::{CurveType, LineConfig, LinePoint, render_line};
12use d3rs::text::{VectorFontConfig, render_vector_text};
13use gpui::prelude::*;
14use gpui::*;
15
16#[derive(Debug, Clone)]
18pub struct LineChart {
19 x: Vec<f64>,
20 y: Vec<f64>,
21 title: Option<String>,
22 color: u32,
23 stroke_width: f32,
24 opacity: f32,
25 curve: CurveType,
26 show_points: bool,
27 width: f32,
28 height: f32,
29 x_scale_type: ScaleType,
30 y_scale_type: ScaleType,
31}
32
33impl LineChart {
34 pub fn title(mut self, title: impl Into<String>) -> Self {
36 self.title = Some(title.into());
37 self
38 }
39
40 pub fn color(mut self, hex: u32) -> Self {
50 self.color = hex;
51 self
52 }
53
54 pub fn stroke_width(mut self, width: f32) -> Self {
56 self.stroke_width = width;
57 self
58 }
59
60 pub fn opacity(mut self, opacity: f32) -> Self {
62 self.opacity = opacity.clamp(0.0, 1.0);
63 self
64 }
65
66 pub fn curve(mut self, curve: CurveType) -> Self {
68 self.curve = curve;
69 self
70 }
71
72 pub fn show_points(mut self, show: bool) -> Self {
74 self.show_points = show;
75 self
76 }
77
78 pub fn size(mut self, width: f32, height: f32) -> Self {
80 self.width = width;
81 self.height = height;
82 self
83 }
84
85 pub fn x_scale(mut self, scale: ScaleType) -> Self {
95 self.x_scale_type = scale;
96 self
97 }
98
99 pub fn y_scale(mut self, scale: ScaleType) -> Self {
101 self.y_scale_type = scale;
102 self
103 }
104
105 pub fn build(self) -> Result<impl IntoElement, ChartError> {
107 validate_data_array(&self.x, "x")?;
109 validate_data_array(&self.y, "y")?;
110 validate_data_length(self.x.len(), self.y.len(), "x", "y")?;
111 validate_dimensions(self.width, self.height)?;
112
113 if self.x_scale_type == ScaleType::Log {
115 validate_positive(&self.x, "x")?;
116 }
117 if self.y_scale_type == ScaleType::Log {
118 validate_positive(&self.y, "y")?;
119 }
120
121 let title_height = if self.title.is_some() {
123 TITLE_AREA_HEIGHT
124 } else {
125 0.0
126 };
127 let plot_height = self.height - title_height;
128
129 let (x_min, x_max) = extent_padded(&self.x, DEFAULT_PADDING_FRACTION);
131 let (y_min, y_max) = extent_padded(&self.y, DEFAULT_PADDING_FRACTION);
132
133 let data: Vec<LinePoint> = self
135 .x
136 .iter()
137 .zip(self.y.iter())
138 .map(|(&x, &y)| LinePoint::new(x, y))
139 .collect();
140
141 let config = LineConfig::new()
143 .stroke_color(D3Color::from_hex(self.color))
144 .stroke_width(self.stroke_width)
145 .opacity(self.opacity)
146 .curve(self.curve)
147 .show_points(self.show_points);
148
149 let line_element: AnyElement = match (self.x_scale_type, self.y_scale_type) {
151 (ScaleType::Linear, ScaleType::Linear) => {
152 let x_scale = LinearScale::new()
153 .domain(x_min, x_max)
154 .range(0.0, self.width as f64);
155 let y_scale = LinearScale::new()
156 .domain(y_min, y_max)
157 .range(plot_height as f64, 0.0);
158 render_line(&x_scale, &y_scale, &data, &config).into_any_element()
159 }
160 (ScaleType::Log, ScaleType::Linear) => {
161 let x_scale = LogScale::new()
162 .domain(x_min.max(1e-10), x_max)
163 .range(0.0, self.width as f64);
164 let y_scale = LinearScale::new()
165 .domain(y_min, y_max)
166 .range(plot_height as f64, 0.0);
167 render_line(&x_scale, &y_scale, &data, &config).into_any_element()
168 }
169 (ScaleType::Linear, ScaleType::Log) => {
170 let x_scale = LinearScale::new()
171 .domain(x_min, x_max)
172 .range(0.0, self.width as f64);
173 let y_scale = LogScale::new()
174 .domain(y_min.max(1e-10), y_max)
175 .range(plot_height as f64, 0.0);
176 render_line(&x_scale, &y_scale, &data, &config).into_any_element()
177 }
178 (ScaleType::Log, ScaleType::Log) => {
179 let x_scale = LogScale::new()
180 .domain(x_min.max(1e-10), x_max)
181 .range(0.0, self.width as f64);
182 let y_scale = LogScale::new()
183 .domain(y_min.max(1e-10), y_max)
184 .range(plot_height as f64, 0.0);
185 render_line(&x_scale, &y_scale, &data, &config).into_any_element()
186 }
187 };
188
189 let mut container = div()
191 .w(px(self.width))
192 .h(px(self.height))
193 .relative()
194 .flex()
195 .flex_col();
196
197 if let Some(title) = &self.title {
199 let font_config =
200 VectorFontConfig::horizontal(DEFAULT_TITLE_FONT_SIZE, hsla(0.0, 0.0, 0.2, 1.0));
201 container = container.child(
202 div()
203 .w_full()
204 .h(px(title_height))
205 .flex()
206 .justify_center()
207 .items_center()
208 .child(render_vector_text(title, &font_config)),
209 );
210 }
211
212 container = container.child(
214 div()
215 .w(px(self.width))
216 .h(px(plot_height))
217 .relative()
218 .child(line_element),
219 );
220
221 Ok(container)
222 }
223}
224
225pub fn line(x: &[f64], y: &[f64]) -> LineChart {
244 LineChart {
245 x: x.to_vec(),
246 y: y.to_vec(),
247 title: None,
248 color: DEFAULT_COLOR,
249 stroke_width: 2.0,
250 opacity: 1.0,
251 curve: CurveType::Linear,
252 show_points: false,
253 width: DEFAULT_WIDTH,
254 height: DEFAULT_HEIGHT,
255 x_scale_type: ScaleType::Linear,
256 y_scale_type: ScaleType::Linear,
257 }
258}
259
260#[cfg(test)]
261mod tests {
262 use super::*;
263
264 #[test]
265 fn test_line_empty_x_data() {
266 let result = line(&[], &[1.0, 2.0, 3.0]).build();
267 assert!(matches!(result, Err(ChartError::EmptyData { field: "x" })));
268 }
269
270 #[test]
271 fn test_line_empty_y_data() {
272 let result = line(&[1.0, 2.0, 3.0], &[]).build();
273 assert!(matches!(result, Err(ChartError::EmptyData { field: "y" })));
274 }
275
276 #[test]
277 fn test_line_data_length_mismatch() {
278 let result = line(&[1.0, 2.0, 3.0, 4.0], &[1.0, 2.0]).build();
279 assert!(matches!(
280 result,
281 Err(ChartError::DataLengthMismatch {
282 x_field: "x",
283 y_field: "y",
284 x_len: 4,
285 y_len: 2,
286 })
287 ));
288 }
289
290 #[test]
291 fn test_line_infinity_in_x() {
292 let result = line(&[1.0, 2.0, f64::NEG_INFINITY], &[1.0, 2.0, 3.0]).build();
293 assert!(matches!(
294 result,
295 Err(ChartError::InvalidData {
296 field: "x",
297 reason: "contains NaN or Infinity"
298 })
299 ));
300 }
301
302 #[test]
303 fn test_line_nan_in_y() {
304 let result = line(&[1.0, 2.0, 3.0], &[1.0, f64::NAN, 3.0]).build();
305 assert!(matches!(
306 result,
307 Err(ChartError::InvalidData {
308 field: "y",
309 reason: "contains NaN or Infinity"
310 })
311 ));
312 }
313
314 #[test]
315 fn test_line_successful_build() {
316 let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
317 let y = vec![2.0, 4.0, 3.0, 5.0, 4.5];
318 let result = line(&x, &y).title("Test Line").color(0xff7f0e).build();
319 assert!(result.is_ok());
320 }
321
322 #[test]
323 fn test_line_builder_chain() {
324 let result = line(&[1.0, 2.0, 3.0], &[4.0, 5.0, 6.0])
325 .title("My Line")
326 .color(0x00ff00)
327 .stroke_width(3.0)
328 .opacity(0.8)
329 .curve(CurveType::Linear)
330 .show_points(true)
331 .size(800.0, 600.0)
332 .build();
333 assert!(result.is_ok());
334 }
335
336 #[test]
337 fn test_line_log_x_scale() {
338 let x = vec![10.0, 100.0, 1000.0, 10000.0];
339 let y = vec![1.0, 2.0, 3.0, 4.0];
340 let result = line(&x, &y)
341 .x_scale(ScaleType::Log)
342 .build();
343 assert!(result.is_ok());
344 }
345
346 #[test]
347 fn test_line_log_y_scale() {
348 let x = vec![1.0, 2.0, 3.0, 4.0];
349 let y = vec![10.0, 100.0, 1000.0, 10000.0];
350 let result = line(&x, &y)
351 .y_scale(ScaleType::Log)
352 .build();
353 assert!(result.is_ok());
354 }
355
356 #[test]
357 fn test_line_log_xy_scale() {
358 let x = vec![10.0, 100.0, 1000.0];
359 let y = vec![20.0, 200.0, 2000.0];
360 let result = line(&x, &y)
361 .x_scale(ScaleType::Log)
362 .y_scale(ScaleType::Log)
363 .build();
364 assert!(result.is_ok());
365 }
366
367 #[test]
368 fn test_line_log_x_negative_values() {
369 let x = vec![-10.0, -5.0, 5.0, 10.0];
370 let y = vec![1.0, 2.0, 3.0, 4.0];
371 let result = line(&x, &y)
372 .x_scale(ScaleType::Log)
373 .build();
374 assert!(matches!(
375 result,
376 Err(ChartError::InvalidData {
377 field: "x",
378 reason: "contains non-positive values for log scale"
379 })
380 ));
381 }
382
383 #[test]
384 fn test_line_log_y_zero_value() {
385 let x = vec![1.0, 2.0, 3.0, 4.0];
386 let y = vec![0.0, 1.0, 2.0, 3.0];
387 let result = line(&x, &y)
388 .y_scale(ScaleType::Log)
389 .build();
390 assert!(matches!(
391 result,
392 Err(ChartError::InvalidData {
393 field: "y",
394 reason: "contains non-positive values for log scale"
395 })
396 ));
397 }
398
399 #[test]
400 fn test_line_log_scale_with_curve() {
401 let x = vec![10.0, 100.0, 1000.0];
402 let y = vec![1.0, 2.0, 3.0];
403 let result = line(&x, &y)
404 .title("Log Scale Line")
405 .x_scale(ScaleType::Log)
406 .curve(CurveType::Linear)
407 .show_points(true)
408 .build();
409 assert!(result.is_ok());
410 }
411}