Skip to main content

astrelis_geometry/chart/
style.rs

1//! Chart styling types.
2//!
3//! Provides comprehensive styling options for chart elements:
4//! - Line configuration with dash patterns and caps/joins
5//! - Marker configuration with various shapes
6//! - Fill configuration with gradients and targets
7//! - Axis styling
8
9use super::grid::DashPattern;
10use super::types::SeriesId;
11use astrelis_render::Color;
12
13/// Line cap style.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
15pub enum LineCap {
16    /// Flat cap (line ends at endpoint).
17    #[default]
18    Butt,
19    /// Rounded cap (semicircle at endpoint).
20    Round,
21    /// Square cap (extends beyond endpoint by half line width).
22    Square,
23}
24
25/// Line join style for connecting line segments.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
27pub enum LineJoin {
28    /// Sharp corner (miter).
29    #[default]
30    Miter,
31    /// Rounded corner.
32    Round,
33    /// Beveled corner.
34    Bevel,
35}
36
37/// Line style for series (legacy enum, kept for backward compatibility).
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
39pub enum LineStyle {
40    /// Solid line
41    #[default]
42    Solid,
43    /// Dashed line
44    Dashed,
45    /// Dotted line
46    Dotted,
47}
48
49impl LineStyle {
50    /// Convert to a DashPattern.
51    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/// Enhanced line configuration.
61#[derive(Debug, Clone, PartialEq)]
62pub struct LineConfig {
63    /// Line color.
64    pub color: Color,
65    /// Line thickness in pixels.
66    pub thickness: f32,
67    /// Dash pattern.
68    pub dash: DashPattern,
69    /// Line cap style.
70    pub cap: LineCap,
71    /// Line join style.
72    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    /// Create a line config with the specified color.
89    pub fn with_color(color: Color) -> Self {
90        Self {
91            color,
92            ..Default::default()
93        }
94    }
95
96    /// Set the line thickness.
97    pub fn thickness(mut self, thickness: f32) -> Self {
98        self.thickness = thickness;
99        self
100    }
101
102    /// Set the dash pattern.
103    pub fn dash(mut self, dash: DashPattern) -> Self {
104        self.dash = dash;
105        self
106    }
107
108    /// Make this a dashed line.
109    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    /// Make this a dotted line.
115    pub fn dotted(mut self, dot_size: f32) -> Self {
116        self.dash = DashPattern::dotted(dot_size);
117        self
118    }
119
120    /// Set the line cap.
121    pub fn cap(mut self, cap: LineCap) -> Self {
122        self.cap = cap;
123        self
124    }
125
126    /// Set the line join.
127    pub fn join(mut self, join: LineJoin) -> Self {
128        self.join = join;
129        self
130    }
131}
132
133/// Marker shape for data points.
134#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
135pub enum MarkerShape {
136    /// Circle
137    #[default]
138    Circle,
139    /// Square
140    Square,
141    /// Triangle pointing up
142    Triangle,
143    /// Triangle pointing down
144    TriangleDown,
145    /// Diamond (rotated square)
146    Diamond,
147    /// Cross (+)
148    Cross,
149    /// X shape
150    X,
151    /// Star
152    Star,
153    /// No marker (invisible)
154    None,
155}
156
157/// Point shape (alias for backward compatibility).
158pub type PointShape = MarkerShape;
159
160/// Enhanced marker configuration.
161#[derive(Debug, Clone, PartialEq)]
162pub struct MarkerConfig {
163    /// Marker shape.
164    pub shape: MarkerShape,
165    /// Marker size in pixels.
166    pub size: f32,
167    /// Fill color (None = no fill).
168    pub fill: Option<Color>,
169    /// Stroke color (None = no stroke).
170    pub stroke: Option<Color>,
171    /// Stroke thickness.
172    pub stroke_thickness: f32,
173    /// Show marker every Nth point (1 = all points).
174    pub interval: usize,
175    /// Only show markers on hover.
176    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    /// Create a new marker config with the specified shape.
195    pub fn new(shape: MarkerShape) -> Self {
196        Self {
197            shape,
198            ..Default::default()
199        }
200    }
201
202    /// Create a circle marker.
203    pub fn circle() -> Self {
204        Self::new(MarkerShape::Circle)
205    }
206
207    /// Create a square marker.
208    pub fn square() -> Self {
209        Self::new(MarkerShape::Square)
210    }
211
212    /// Create a diamond marker.
213    pub fn diamond() -> Self {
214        Self::new(MarkerShape::Diamond)
215    }
216
217    /// Set the marker size.
218    pub fn size(mut self, size: f32) -> Self {
219        self.size = size;
220        self
221    }
222
223    /// Set the fill color.
224    pub fn fill(mut self, color: Color) -> Self {
225        self.fill = Some(color);
226        self
227    }
228
229    /// Set no fill (outline only).
230    pub fn no_fill(mut self) -> Self {
231        self.fill = None;
232        self
233    }
234
235    /// Set the stroke color.
236    pub fn stroke(mut self, color: Color) -> Self {
237        self.stroke = Some(color);
238        self
239    }
240
241    /// Set the stroke thickness.
242    pub fn stroke_thickness(mut self, thickness: f32) -> Self {
243        self.stroke_thickness = thickness;
244        self
245    }
246
247    /// Show markers at intervals (every Nth point).
248    pub fn every(mut self, n: usize) -> Self {
249        self.interval = n.max(1);
250        self
251    }
252
253    /// Only show markers on hover.
254    pub fn on_hover_only(mut self) -> Self {
255        self.hover_only = true;
256        self
257    }
258}
259
260/// Fill target for area fills.
261#[derive(Debug, Clone, PartialEq, Default)]
262pub enum FillTarget {
263    /// Fill to a constant Y value.
264    ToValue(f64),
265    /// Fill to the X axis baseline (Y = 0 or axis minimum).
266    #[default]
267    ToBaseline,
268    /// Fill to another series.
269    ToSeries { series_id: SeriesId },
270    /// Fill a band between two series.
271    Band { lower: SeriesId, upper: SeriesId },
272}
273
274/// Gradient definition.
275#[derive(Debug, Clone, PartialEq)]
276pub struct Gradient {
277    /// Gradient stops (position 0.0-1.0, color).
278    pub stops: Vec<(f32, Color)>,
279    /// Whether the gradient is vertical (true) or horizontal (false).
280    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    /// Create a two-color vertical gradient.
294    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    /// Create a two-color horizontal gradient.
302    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    /// Add a gradient stop.
310    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    /// Get the color at a position (0.0-1.0).
317    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        // Find the two stops to interpolate between
328        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/// Enhanced fill configuration.
350#[derive(Debug, Clone, PartialEq)]
351pub struct FillConfig {
352    /// Fill target.
353    pub target: FillTarget,
354    /// Solid fill color.
355    pub color: Color,
356    /// Optional gradient (overrides solid color if present).
357    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    /// Create a fill to baseline with the specified color.
372    pub fn to_baseline(color: Color) -> Self {
373        Self {
374            target: FillTarget::ToBaseline,
375            color,
376            gradient: None,
377        }
378    }
379
380    /// Create a fill to a constant value.
381    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    /// Create a fill to another series.
390    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    /// Set a gradient fill.
399    pub fn with_gradient(mut self, gradient: Gradient) -> Self {
400        self.gradient = Some(gradient);
401        self
402    }
403
404    /// Get the effective fill color at a position (considering gradient).
405    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/// Point style for scatter/line charts (legacy, kept for compatibility).
415#[derive(Debug, Clone, Copy)]
416pub struct PointStyle {
417    /// Point size
418    pub size: f32,
419    /// Point shape
420    pub shape: PointShape,
421    /// Fill color
422    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/// Fill style for area charts (legacy, kept for compatibility).
436#[derive(Debug, Clone, Copy)]
437pub struct FillStyle {
438    /// Fill color
439    pub color: Color,
440    /// Opacity (0.0 to 1.0)
441    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/// Series visual style.
454#[derive(Debug, Clone)]
455pub struct SeriesStyle {
456    /// Line color
457    pub color: Color,
458    /// Line width
459    pub line_width: f32,
460    /// Line style (legacy)
461    pub line_style: LineStyle,
462    /// Point style (None = no points) - legacy
463    pub point_style: Option<PointStyle>,
464    /// Fill style (for area charts) - legacy
465    pub fill: Option<FillStyle>,
466    /// Z-order for rendering (higher = on top)
467    pub z_order: i32,
468    /// Whether this series is visible
469    pub visible: bool,
470    /// Whether to show in legend
471    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, // Thinner lines for better visibility with dense data
479            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    /// Create a style with a specific color.
491    pub fn with_color(color: Color) -> Self {
492        Self {
493            color,
494            ..Default::default()
495        }
496    }
497
498    /// Set line width.
499    pub fn line_width(mut self, width: f32) -> Self {
500        self.line_width = width;
501        self
502    }
503
504    /// Set line style.
505    pub fn line_style(mut self, style: LineStyle) -> Self {
506        self.line_style = style;
507        self
508    }
509
510    /// Add points.
511    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    /// Add points with custom style.
520    pub fn with_point_style(mut self, style: PointStyle) -> Self {
521        self.point_style = Some(style);
522        self
523    }
524
525    /// Add area fill.
526    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    /// Add area fill with custom style.
535    pub fn with_fill_style(mut self, style: FillStyle) -> Self {
536        self.fill = Some(style);
537        self
538    }
539
540    /// Set z-order (higher = rendered on top).
541    pub fn z_order(mut self, z_order: i32) -> Self {
542        self.z_order = z_order;
543        self
544    }
545
546    /// Set visibility.
547    pub fn visible(mut self, visible: bool) -> Self {
548        self.visible = visible;
549        self
550    }
551
552    /// Hide from legend.
553    pub fn hide_from_legend(mut self) -> Self {
554        self.show_in_legend = false;
555        self
556    }
557
558    /// Make this a dashed line.
559    pub fn dashed(mut self) -> Self {
560        self.line_style = LineStyle::Dashed;
561        self
562    }
563
564    /// Make this a dotted line.
565    pub fn dotted(mut self) -> Self {
566        self.line_style = LineStyle::Dotted;
567        self
568    }
569
570    /// Get the line configuration.
571    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    /// Get the marker configuration.
582    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    /// Get the fill configuration.
595    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/// Enhanced series style with full configuration.
605#[derive(Debug, Clone)]
606pub struct EnhancedSeriesStyle {
607    /// Line configuration.
608    pub line: LineConfig,
609    /// Marker configuration (None = no markers).
610    pub markers: Option<MarkerConfig>,
611    /// Fill configuration (None = no fill).
612    pub fill: Option<FillConfig>,
613    /// Z-order for rendering.
614    pub z_order: i32,
615    /// Whether this series is visible.
616    pub visible: bool,
617    /// Whether to show in legend.
618    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    /// Create a style with the specified line color.
636    pub fn with_color(color: Color) -> Self {
637        Self {
638            line: LineConfig::with_color(color),
639            ..Default::default()
640        }
641    }
642
643    /// Set the line configuration.
644    pub fn line(mut self, line: LineConfig) -> Self {
645        self.line = line;
646        self
647    }
648
649    /// Set the marker configuration.
650    pub fn markers(mut self, markers: MarkerConfig) -> Self {
651        self.markers = Some(markers);
652        self
653    }
654
655    /// Set the fill configuration.
656    pub fn fill(mut self, fill: FillConfig) -> Self {
657        self.fill = Some(fill);
658        self
659    }
660
661    /// Set z-order.
662    pub fn z_order(mut self, z_order: i32) -> Self {
663        self.z_order = z_order;
664        self
665    }
666
667    /// Set visibility.
668    pub fn visible(mut self, visible: bool) -> Self {
669        self.visible = visible;
670        self
671    }
672
673    /// Hide from legend.
674    pub fn hide_from_legend(mut self) -> Self {
675        self.show_in_legend = false;
676        self
677    }
678
679    /// Convert to legacy SeriesStyle.
680    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/// Axis visual style.
710#[derive(Debug, Clone)]
711pub struct AxisStyle {
712    /// Axis line color
713    pub line_color: Color,
714    /// Axis line width
715    pub line_width: f32,
716    /// Tick color
717    pub tick_color: Color,
718    /// Tick length
719    pub tick_length: f32,
720    /// Grid line color
721    pub grid_color: Color,
722    /// Grid line width
723    pub grid_width: f32,
724    /// Label color
725    pub label_color: Color,
726    /// Label font size
727    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
745/// Modern, minimal color palette for series.
746/// Inspired by contemporary data visualization tools.
747pub const SERIES_COLORS: [Color; 8] = [
748    Color::rgba(0.36, 0.67, 0.93, 1.0), // Soft blue
749    Color::rgba(0.95, 0.55, 0.38, 1.0), // Coral
750    Color::rgba(0.45, 0.80, 0.69, 1.0), // Mint/teal
751    Color::rgba(0.91, 0.70, 0.41, 1.0), // Warm gold
752    Color::rgba(0.70, 0.55, 0.85, 1.0), // Soft purple
753    Color::rgba(0.95, 0.60, 0.60, 1.0), // Soft red/pink
754    Color::rgba(0.55, 0.75, 0.50, 1.0), // Sage green
755    Color::rgba(0.60, 0.60, 0.65, 1.0), // Neutral gray
756];
757
758/// Get a color from the palette by index.
759pub fn palette_color(index: usize) -> Color {
760    SERIES_COLORS[index % SERIES_COLORS.len()]
761}