1use super::grid::DashPattern;
10use super::types::SeriesId;
11use astrelis_render::Color;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
15pub enum LineCap {
16 #[default]
18 Butt,
19 Round,
21 Square,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
27pub enum LineJoin {
28 #[default]
30 Miter,
31 Round,
33 Bevel,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
39pub enum LineStyle {
40 #[default]
42 Solid,
43 Dashed,
45 Dotted,
47}
48
49impl LineStyle {
50 pub fn to_dash_pattern(&self) -> DashPattern {
52 match self {
53 Self::Solid => DashPattern::SOLID,
54 Self::Dashed => DashPattern::medium_dash(),
55 Self::Dotted => DashPattern::dotted(2.0),
56 }
57 }
58}
59
60#[derive(Debug, Clone, PartialEq)]
62pub struct LineConfig {
63 pub color: Color,
65 pub thickness: f32,
67 pub dash: DashPattern,
69 pub cap: LineCap,
71 pub join: LineJoin,
73}
74
75impl Default for LineConfig {
76 fn default() -> Self {
77 Self {
78 color: Color::BLUE,
79 thickness: 1.5,
80 dash: DashPattern::SOLID,
81 cap: LineCap::Butt,
82 join: LineJoin::Miter,
83 }
84 }
85}
86
87impl LineConfig {
88 pub fn with_color(color: Color) -> Self {
90 Self {
91 color,
92 ..Default::default()
93 }
94 }
95
96 pub fn thickness(mut self, thickness: f32) -> Self {
98 self.thickness = thickness;
99 self
100 }
101
102 pub fn dash(mut self, dash: DashPattern) -> Self {
104 self.dash = dash;
105 self
106 }
107
108 pub fn dashed(mut self, dash_len: f32, gap_len: f32) -> Self {
110 self.dash = DashPattern::dashed(dash_len, gap_len);
111 self
112 }
113
114 pub fn dotted(mut self, dot_size: f32) -> Self {
116 self.dash = DashPattern::dotted(dot_size);
117 self
118 }
119
120 pub fn cap(mut self, cap: LineCap) -> Self {
122 self.cap = cap;
123 self
124 }
125
126 pub fn join(mut self, join: LineJoin) -> Self {
128 self.join = join;
129 self
130 }
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
135pub enum MarkerShape {
136 #[default]
138 Circle,
139 Square,
141 Triangle,
143 TriangleDown,
145 Diamond,
147 Cross,
149 X,
151 Star,
153 None,
155}
156
157pub type PointShape = MarkerShape;
159
160#[derive(Debug, Clone, PartialEq)]
162pub struct MarkerConfig {
163 pub shape: MarkerShape,
165 pub size: f32,
167 pub fill: Option<Color>,
169 pub stroke: Option<Color>,
171 pub stroke_thickness: f32,
173 pub interval: usize,
175 pub hover_only: bool,
177}
178
179impl Default for MarkerConfig {
180 fn default() -> Self {
181 Self {
182 shape: MarkerShape::Circle,
183 size: 6.0,
184 fill: Some(Color::WHITE),
185 stroke: None,
186 stroke_thickness: 1.0,
187 interval: 1,
188 hover_only: false,
189 }
190 }
191}
192
193impl MarkerConfig {
194 pub fn new(shape: MarkerShape) -> Self {
196 Self {
197 shape,
198 ..Default::default()
199 }
200 }
201
202 pub fn circle() -> Self {
204 Self::new(MarkerShape::Circle)
205 }
206
207 pub fn square() -> Self {
209 Self::new(MarkerShape::Square)
210 }
211
212 pub fn diamond() -> Self {
214 Self::new(MarkerShape::Diamond)
215 }
216
217 pub fn size(mut self, size: f32) -> Self {
219 self.size = size;
220 self
221 }
222
223 pub fn fill(mut self, color: Color) -> Self {
225 self.fill = Some(color);
226 self
227 }
228
229 pub fn no_fill(mut self) -> Self {
231 self.fill = None;
232 self
233 }
234
235 pub fn stroke(mut self, color: Color) -> Self {
237 self.stroke = Some(color);
238 self
239 }
240
241 pub fn stroke_thickness(mut self, thickness: f32) -> Self {
243 self.stroke_thickness = thickness;
244 self
245 }
246
247 pub fn every(mut self, n: usize) -> Self {
249 self.interval = n.max(1);
250 self
251 }
252
253 pub fn on_hover_only(mut self) -> Self {
255 self.hover_only = true;
256 self
257 }
258}
259
260#[derive(Debug, Clone, PartialEq, Default)]
262pub enum FillTarget {
263 ToValue(f64),
265 #[default]
267 ToBaseline,
268 ToSeries { series_id: SeriesId },
270 Band { lower: SeriesId, upper: SeriesId },
272}
273
274#[derive(Debug, Clone, PartialEq)]
276pub struct Gradient {
277 pub stops: Vec<(f32, Color)>,
279 pub vertical: bool,
281}
282
283impl Default for Gradient {
284 fn default() -> Self {
285 Self {
286 stops: vec![(0.0, Color::WHITE), (1.0, Color::BLACK)],
287 vertical: true,
288 }
289 }
290}
291
292impl Gradient {
293 pub fn vertical(top: Color, bottom: Color) -> Self {
295 Self {
296 stops: vec![(0.0, top), (1.0, bottom)],
297 vertical: true,
298 }
299 }
300
301 pub fn horizontal(left: Color, right: Color) -> Self {
303 Self {
304 stops: vec![(0.0, left), (1.0, right)],
305 vertical: false,
306 }
307 }
308
309 pub fn with_stop(mut self, position: f32, color: Color) -> Self {
311 self.stops.push((position.clamp(0.0, 1.0), color));
312 self.stops.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
313 self
314 }
315
316 pub fn color_at(&self, position: f32) -> Color {
318 if self.stops.is_empty() {
319 return Color::WHITE;
320 }
321 if self.stops.len() == 1 {
322 return self.stops[0].1;
323 }
324
325 let pos = position.clamp(0.0, 1.0);
326
327 let mut prev = &self.stops[0];
329 for stop in &self.stops {
330 if stop.0 >= pos {
331 if (stop.0 - prev.0).abs() < f32::EPSILON {
332 return stop.1;
333 }
334 let t = (pos - prev.0) / (stop.0 - prev.0);
335 return Color::rgba(
336 prev.1.r + (stop.1.r - prev.1.r) * t,
337 prev.1.g + (stop.1.g - prev.1.g) * t,
338 prev.1.b + (stop.1.b - prev.1.b) * t,
339 prev.1.a + (stop.1.a - prev.1.a) * t,
340 );
341 }
342 prev = stop;
343 }
344
345 self.stops.last().unwrap().1
346 }
347}
348
349#[derive(Debug, Clone, PartialEq)]
351pub struct FillConfig {
352 pub target: FillTarget,
354 pub color: Color,
356 pub gradient: Option<Gradient>,
358}
359
360impl Default for FillConfig {
361 fn default() -> Self {
362 Self {
363 target: FillTarget::ToBaseline,
364 color: Color::rgba(0.0, 0.5, 1.0, 0.3),
365 gradient: None,
366 }
367 }
368}
369
370impl FillConfig {
371 pub fn to_baseline(color: Color) -> Self {
373 Self {
374 target: FillTarget::ToBaseline,
375 color,
376 gradient: None,
377 }
378 }
379
380 pub fn to_value(value: f64, color: Color) -> Self {
382 Self {
383 target: FillTarget::ToValue(value),
384 color,
385 gradient: None,
386 }
387 }
388
389 pub fn to_series(series_id: SeriesId, color: Color) -> Self {
391 Self {
392 target: FillTarget::ToSeries { series_id },
393 color,
394 gradient: None,
395 }
396 }
397
398 pub fn with_gradient(mut self, gradient: Gradient) -> Self {
400 self.gradient = Some(gradient);
401 self
402 }
403
404 pub fn color_at(&self, position: f32) -> Color {
406 if let Some(gradient) = &self.gradient {
407 gradient.color_at(position)
408 } else {
409 self.color
410 }
411 }
412}
413
414#[derive(Debug, Clone, Copy)]
416pub struct PointStyle {
417 pub size: f32,
419 pub shape: PointShape,
421 pub color: Color,
423}
424
425impl Default for PointStyle {
426 fn default() -> Self {
427 Self {
428 size: 6.0,
429 shape: PointShape::Circle,
430 color: Color::WHITE,
431 }
432 }
433}
434
435#[derive(Debug, Clone, Copy)]
437pub struct FillStyle {
438 pub color: Color,
440 pub opacity: f32,
442}
443
444impl Default for FillStyle {
445 fn default() -> Self {
446 Self {
447 color: Color::BLUE,
448 opacity: 0.3,
449 }
450 }
451}
452
453#[derive(Debug, Clone)]
455pub struct SeriesStyle {
456 pub color: Color,
458 pub line_width: f32,
460 pub line_style: LineStyle,
462 pub point_style: Option<PointStyle>,
464 pub fill: Option<FillStyle>,
466 pub z_order: i32,
468 pub visible: bool,
470 pub show_in_legend: bool,
472}
473
474impl Default for SeriesStyle {
475 fn default() -> Self {
476 Self {
477 color: Color::BLUE,
478 line_width: 1.0, line_style: LineStyle::Solid,
480 point_style: None,
481 fill: None,
482 z_order: 0,
483 visible: true,
484 show_in_legend: true,
485 }
486 }
487}
488
489impl SeriesStyle {
490 pub fn with_color(color: Color) -> Self {
492 Self {
493 color,
494 ..Default::default()
495 }
496 }
497
498 pub fn line_width(mut self, width: f32) -> Self {
500 self.line_width = width;
501 self
502 }
503
504 pub fn line_style(mut self, style: LineStyle) -> Self {
506 self.line_style = style;
507 self
508 }
509
510 pub fn with_points(mut self) -> Self {
512 self.point_style = Some(PointStyle {
513 color: self.color,
514 ..Default::default()
515 });
516 self
517 }
518
519 pub fn with_point_style(mut self, style: PointStyle) -> Self {
521 self.point_style = Some(style);
522 self
523 }
524
525 pub fn with_fill(mut self) -> Self {
527 self.fill = Some(FillStyle {
528 color: self.color,
529 opacity: 0.3,
530 });
531 self
532 }
533
534 pub fn with_fill_style(mut self, style: FillStyle) -> Self {
536 self.fill = Some(style);
537 self
538 }
539
540 pub fn z_order(mut self, z_order: i32) -> Self {
542 self.z_order = z_order;
543 self
544 }
545
546 pub fn visible(mut self, visible: bool) -> Self {
548 self.visible = visible;
549 self
550 }
551
552 pub fn hide_from_legend(mut self) -> Self {
554 self.show_in_legend = false;
555 self
556 }
557
558 pub fn dashed(mut self) -> Self {
560 self.line_style = LineStyle::Dashed;
561 self
562 }
563
564 pub fn dotted(mut self) -> Self {
566 self.line_style = LineStyle::Dotted;
567 self
568 }
569
570 pub fn to_line_config(&self) -> LineConfig {
572 LineConfig {
573 color: self.color,
574 thickness: self.line_width,
575 dash: self.line_style.to_dash_pattern(),
576 cap: LineCap::default(),
577 join: LineJoin::default(),
578 }
579 }
580
581 pub fn to_marker_config(&self) -> Option<MarkerConfig> {
583 self.point_style.as_ref().map(|ps| MarkerConfig {
584 shape: ps.shape,
585 size: ps.size,
586 fill: Some(ps.color),
587 stroke: None,
588 stroke_thickness: 1.0,
589 interval: 1,
590 hover_only: false,
591 })
592 }
593
594 pub fn to_fill_config(&self) -> Option<FillConfig> {
596 self.fill.as_ref().map(|fs| FillConfig {
597 target: FillTarget::ToBaseline,
598 color: Color::rgba(fs.color.r, fs.color.g, fs.color.b, fs.opacity),
599 gradient: None,
600 })
601 }
602}
603
604#[derive(Debug, Clone)]
606pub struct EnhancedSeriesStyle {
607 pub line: LineConfig,
609 pub markers: Option<MarkerConfig>,
611 pub fill: Option<FillConfig>,
613 pub z_order: i32,
615 pub visible: bool,
617 pub show_in_legend: bool,
619}
620
621impl Default for EnhancedSeriesStyle {
622 fn default() -> Self {
623 Self {
624 line: LineConfig::default(),
625 markers: None,
626 fill: None,
627 z_order: 0,
628 visible: true,
629 show_in_legend: true,
630 }
631 }
632}
633
634impl EnhancedSeriesStyle {
635 pub fn with_color(color: Color) -> Self {
637 Self {
638 line: LineConfig::with_color(color),
639 ..Default::default()
640 }
641 }
642
643 pub fn line(mut self, line: LineConfig) -> Self {
645 self.line = line;
646 self
647 }
648
649 pub fn markers(mut self, markers: MarkerConfig) -> Self {
651 self.markers = Some(markers);
652 self
653 }
654
655 pub fn fill(mut self, fill: FillConfig) -> Self {
657 self.fill = Some(fill);
658 self
659 }
660
661 pub fn z_order(mut self, z_order: i32) -> Self {
663 self.z_order = z_order;
664 self
665 }
666
667 pub fn visible(mut self, visible: bool) -> Self {
669 self.visible = visible;
670 self
671 }
672
673 pub fn hide_from_legend(mut self) -> Self {
675 self.show_in_legend = false;
676 self
677 }
678
679 pub fn to_legacy(&self) -> SeriesStyle {
681 SeriesStyle {
682 color: self.line.color,
683 line_width: self.line.thickness,
684 line_style: if self.line.dash.is_solid() {
685 LineStyle::Solid
686 } else if self.line.dash.segments.len() == 2
687 && self.line.dash.segments[0] == self.line.dash.segments[1]
688 {
689 LineStyle::Dotted
690 } else {
691 LineStyle::Dashed
692 },
693 point_style: self.markers.as_ref().map(|m| PointStyle {
694 size: m.size,
695 shape: m.shape,
696 color: m.fill.unwrap_or(self.line.color),
697 }),
698 fill: self.fill.as_ref().map(|f| FillStyle {
699 color: f.color,
700 opacity: f.color.a,
701 }),
702 z_order: self.z_order,
703 visible: self.visible,
704 show_in_legend: self.show_in_legend,
705 }
706 }
707}
708
709#[derive(Debug, Clone)]
711pub struct AxisStyle {
712 pub line_color: Color,
714 pub line_width: f32,
716 pub tick_color: Color,
718 pub tick_length: f32,
720 pub grid_color: Color,
722 pub grid_width: f32,
724 pub label_color: Color,
726 pub label_size: f32,
728}
729
730impl Default for AxisStyle {
731 fn default() -> Self {
732 Self {
733 line_color: Color::rgba(0.4, 0.4, 0.45, 1.0),
734 line_width: 1.0,
735 tick_color: Color::rgba(0.4, 0.4, 0.45, 1.0),
736 tick_length: 4.0,
737 grid_color: Color::rgba(0.25, 0.25, 0.28, 1.0),
738 grid_width: 0.5,
739 label_color: Color::rgba(0.6, 0.6, 0.65, 1.0),
740 label_size: 11.0,
741 }
742 }
743}
744
745pub const SERIES_COLORS: [Color; 8] = [
748 Color::rgba(0.36, 0.67, 0.93, 1.0), Color::rgba(0.95, 0.55, 0.38, 1.0), Color::rgba(0.45, 0.80, 0.69, 1.0), Color::rgba(0.91, 0.70, 0.41, 1.0), Color::rgba(0.70, 0.55, 0.85, 1.0), Color::rgba(0.95, 0.60, 0.60, 1.0), Color::rgba(0.55, 0.75, 0.50, 1.0), Color::rgba(0.60, 0.60, 0.65, 1.0), ];
757
758pub fn palette_color(index: usize) -> Color {
760 SERIES_COLORS[index % SERIES_COLORS.len()]
761}