1#![allow(unpredictable_function_pointer_comparisons)]
4
5use crate::theme::tokens::Color;
6
7#[derive(Clone, PartialEq, Debug)]
9pub struct ChartDataPoint {
10 pub label: String,
12 pub value: f64,
14 pub color: Option<Color>,
16}
17
18impl ChartDataPoint {
19 pub fn new(label: impl Into<String>, value: f64) -> Self {
21 Self {
22 label: label.into(),
23 value,
24 color: None,
25 }
26 }
27
28 pub fn with_color(label: impl Into<String>, value: f64, color: Color) -> Self {
30 Self {
31 label: label.into(),
32 value,
33 color: Some(color),
34 }
35 }
36}
37
38#[derive(Clone, PartialEq, Debug)]
40pub struct ChartSeries {
41 pub name: String,
43 pub color: Color,
45 pub data: Vec<ChartDataPoint>,
47}
48
49impl ChartSeries {
50 pub fn new(name: impl Into<String>, color: Color, data: Vec<ChartDataPoint>) -> Self {
52 Self {
53 name: name.into(),
54 color,
55 data,
56 }
57 }
58
59 pub fn min_value(&self) -> f64 {
61 self.data
62 .iter()
63 .map(|p| p.value)
64 .fold(f64::INFINITY, f64::min)
65 }
66
67 pub fn max_value(&self) -> f64 {
69 self.data
70 .iter()
71 .map(|p| p.value)
72 .fold(f64::NEG_INFINITY, f64::max)
73 }
74}
75
76#[derive(Clone, PartialEq, Debug)]
78pub struct ChartMargin {
79 pub top: u16,
80 pub right: u16,
81 pub bottom: u16,
82 pub left: u16,
83}
84
85impl Default for ChartMargin {
86 fn default() -> Self {
87 Self {
88 top: 20,
89 right: 20,
90 bottom: 40,
91 left: 50,
92 }
93 }
94}
95
96impl ChartMargin {
97 pub fn uniform(margin: u16) -> Self {
99 Self {
100 top: margin,
101 right: margin,
102 bottom: margin,
103 left: margin,
104 }
105 }
106
107 pub fn symmetric(vertical: u16, horizontal: u16) -> Self {
109 Self {
110 top: vertical,
111 right: horizontal,
112 bottom: vertical,
113 left: horizontal,
114 }
115 }
116}
117
118#[derive(Clone, PartialEq, Debug)]
120pub struct ChartAxis {
121 pub show_line: bool,
123 pub show_ticks: bool,
125 pub show_grid: bool,
127 pub tick_count: u8,
129 pub label_format: Option<fn(f64) -> String>,
131 pub min: Option<f64>,
133 pub max: Option<f64>,
135}
136
137impl Default for ChartAxis {
138 fn default() -> Self {
139 Self {
140 show_line: true,
141 show_ticks: true,
142 show_grid: true,
143 tick_count: 5,
144 label_format: None,
145 min: None,
146 max: None,
147 }
148 }
149}
150
151impl ChartAxis {
152 pub fn hidden() -> Self {
154 Self {
155 show_line: false,
156 show_ticks: false,
157 show_grid: false,
158 tick_count: 0,
159 label_format: None,
160 min: None,
161 max: None,
162 }
163 }
164}
165
166#[derive(Default, Clone, PartialEq, Debug)]
168pub enum LegendPosition {
169 None,
171 #[default]
173 Top,
174 Bottom,
176 Left,
178 Right,
180}
181
182#[derive(Clone, PartialEq, Debug)]
184pub struct ChartTooltip {
185 pub enabled: bool,
187 pub formatter: Option<fn(&ChartDataPoint, Option<&str>) -> String>,
189 pub custom_values: Option<std::collections::HashMap<String, String>>,
192 pub show_series_name: bool,
194 pub show_value: bool,
196 pub value_format: Option<fn(f64) -> String>,
198}
199
200impl Default for ChartTooltip {
201 fn default() -> Self {
202 Self {
203 enabled: true,
204 formatter: None,
205 custom_values: None,
206 show_series_name: true,
207 show_value: true,
208 value_format: None,
209 }
210 }
211}
212
213impl ChartTooltip {
214 pub fn with_formatter(formatter: fn(&ChartDataPoint, Option<&str>) -> String) -> Self {
216 Self {
217 enabled: true,
218 formatter: Some(formatter),
219 ..Default::default()
220 }
221 }
222
223 pub fn with_custom_values(values: std::collections::HashMap<String, String>) -> Self {
225 Self {
226 enabled: true,
227 custom_values: Some(values),
228 ..Default::default()
229 }
230 }
231
232 pub fn disabled() -> Self {
234 Self {
235 enabled: false,
236 ..Default::default()
237 }
238 }
239
240 pub fn get_content(&self, point: &ChartDataPoint, series_name: Option<&str>) -> String {
242 if let Some(formatter) = self.formatter {
244 return formatter(point, series_name);
245 }
246
247 let mut parts = Vec::new();
249
250 if self.show_series_name && series_name.is_some() {
252 parts.push(series_name.unwrap().to_string());
253 }
254
255 parts.push(point.label.clone());
257
258 if self.show_value {
260 let value_str = if let Some(format) = self.value_format {
261 format(point.value)
262 } else {
263 format_compact_number(point.value)
264 };
265 parts.push(format!(": {}", value_str));
266 }
267
268 parts.join("")
269 }
270}
271
272#[derive(Clone, PartialEq, Debug)]
274pub struct ChartAnimation {
275 pub enabled: bool,
277 pub duration_ms: u16,
279 pub easing: AnimationEasing,
281}
282
283impl Default for ChartAnimation {
284 fn default() -> Self {
285 Self {
286 enabled: true,
287 duration_ms: 500,
288 easing: AnimationEasing::EaseOut,
289 }
290 }
291}
292
293#[derive(Default, Clone, PartialEq, Debug)]
295pub enum AnimationEasing {
296 #[default]
297 Linear,
298 Ease,
299 EaseIn,
300 EaseOut,
301 EaseInOut,
302}
303
304impl AnimationEasing {
305 pub fn as_css(&self) -> &'static str {
306 match self {
307 AnimationEasing::Linear => "linear",
308 AnimationEasing::Ease => "ease",
309 AnimationEasing::EaseIn => "ease-in",
310 AnimationEasing::EaseOut => "ease-out",
311 AnimationEasing::EaseInOut => "ease-in-out",
312 }
313 }
314}
315
316pub fn calculate_nice_ticks(min: f64, max: f64, count: u8) -> Vec<f64> {
318 if min == max {
319 return vec![min];
320 }
321
322 let range = max - min;
323 let step = nice_number(range / count as f64, false);
324 let nice_min = (min / step).floor() * step;
325 let nice_max = (max / step).ceil() * step;
326
327 let mut ticks = Vec::new();
328 let mut current = nice_min;
329 while current <= nice_max + step / 2.0 {
330 ticks.push(current);
331 current += step;
332 }
333
334 ticks
335}
336
337fn nice_number(x: f64, round: bool) -> f64 {
339 let exp = x.log10().floor() as i32;
340 let f = x / 10.0_f64.powi(exp);
341
342 let nf = if round {
343 if f < 1.5 {
344 1.0
345 } else if f < 3.0 {
346 2.0
347 } else if f < 7.0 {
348 5.0
349 } else {
350 10.0
351 }
352 } else {
353 if f <= 1.0 {
354 1.0
355 } else if f <= 2.0 {
356 2.0
357 } else if f <= 5.0 {
358 5.0
359 } else {
360 10.0
361 }
362 };
363
364 nf * 10.0_f64.powi(exp)
365}
366
367pub fn format_compact_number(value: f64) -> String {
369 if value.abs() >= 1_000_000_000.0 {
370 format!("{:.1}B", value / 1_000_000_000.0)
371 } else if value.abs() >= 1_000_000.0 {
372 format!("{:.1}M", value / 1_000_000.0)
373 } else if value.abs() >= 1_000.0 {
374 format!("{:.1}K", value / 1_000.0)
375 } else {
376 format!("{:.0}", value)
377 }
378}
379
380pub fn format_currency(value: f64) -> String {
382 format!("${:.2}", value)
383}
384
385pub fn format_percentage(value: f64) -> String {
387 format!("{:.1}%", value)
388}
389
390pub fn color_to_css(color: &Color) -> String {
392 color.to_rgba()
393}
394
395pub fn calculate_smooth_line(points: &[(f64, f64)]) -> String {
397 if points.len() < 2 {
398 return String::new();
399 }
400
401 let mut path = format!("M {},{} ", points[0].0, points[0].1);
402
403 for i in 1..points.len() {
404 let prev = if i > 0 { points[i - 1] } else { points[0] };
405 let curr = points[i];
406 let next = if i < points.len() - 1 {
407 points[i + 1]
408 } else {
409 curr
410 };
411
412 let cp1x = prev.0 + (curr.0 - prev.0) * 0.5;
413 let cp1y = prev.1;
414 let cp2x = curr.0 - (next.0 - prev.0) * 0.5;
415 let cp2y = curr.1;
416
417 path.push_str(&format!(
418 "C {},{} {},{} {},{} ",
419 cp1x, cp1y, cp2x, cp2y, curr.0, curr.1
420 ));
421 }
422
423 path
424}
425
426pub fn calculate_area_path(points: &[(f64, f64)], baseline_y: f64) -> String {
428 if points.is_empty() {
429 return String::new();
430 }
431
432 let mut path = format!("M {},{} ", points[0].0, baseline_y);
433 path.push_str(&format!("L {},{} ", points[0].0, points[0].1));
434
435 for i in 1..points.len() {
436 path.push_str(&format!("L {},{} ", points[i].0, points[i].1));
437 }
438
439 path.push_str(&format!(
440 "L {},{} Z",
441 points[points.len() - 1].0,
442 baseline_y
443 ));
444 path
445}
446
447#[cfg(test)]
448mod tests {
449 use super::*;
450
451 #[test]
452 fn test_chart_data_point() {
453 let point = ChartDataPoint::new("Jan", 100.0);
454 assert_eq!(point.label, "Jan");
455 assert_eq!(point.value, 100.0);
456 assert!(point.color.is_none());
457 }
458
459 #[test]
460 fn test_chart_data_point_with_color() {
461 let color = Color::new(255, 0, 0);
462 let point = ChartDataPoint::with_color("Feb", 200.0, color.clone());
463 assert_eq!(point.color, Some(color));
464 }
465
466 #[test]
467 fn test_chart_series() {
468 let data = vec![
469 ChartDataPoint::new("A", 10.0),
470 ChartDataPoint::new("B", 20.0),
471 ChartDataPoint::new("C", 30.0),
472 ];
473 let series = ChartSeries::new("Test", Color::new(0, 0, 255), data);
474
475 assert_eq!(series.name, "Test");
476 assert_eq!(series.min_value(), 10.0);
477 assert_eq!(series.max_value(), 30.0);
478 }
479
480 #[test]
481 fn test_nice_ticks() {
482 let ticks = calculate_nice_ticks(0.0, 100.0, 5);
483 assert!(!ticks.is_empty());
484 assert!(ticks[0] <= 0.0);
485 assert!(ticks[ticks.len() - 1] >= 100.0);
486 }
487
488 #[test]
489 fn test_format_compact_number() {
490 assert_eq!(format_compact_number(1500.0), "1.5K");
491 assert_eq!(format_compact_number(1500000.0), "1.5M");
492 assert_eq!(format_compact_number(1500000000.0), "1.5B");
493 assert_eq!(format_compact_number(150.0), "150");
494 }
495}