use super::grid::DashPattern;
use super::types::SeriesId;
use astrelis_render::Color;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LineCap {
#[default]
Butt,
Round,
Square,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LineJoin {
#[default]
Miter,
Round,
Bevel,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LineStyle {
#[default]
Solid,
Dashed,
Dotted,
}
impl LineStyle {
pub fn to_dash_pattern(&self) -> DashPattern {
match self {
Self::Solid => DashPattern::SOLID,
Self::Dashed => DashPattern::medium_dash(),
Self::Dotted => DashPattern::dotted(2.0),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct LineConfig {
pub color: Color,
pub thickness: f32,
pub dash: DashPattern,
pub cap: LineCap,
pub join: LineJoin,
}
impl Default for LineConfig {
fn default() -> Self {
Self {
color: Color::BLUE,
thickness: 1.5,
dash: DashPattern::SOLID,
cap: LineCap::Butt,
join: LineJoin::Miter,
}
}
}
impl LineConfig {
pub fn with_color(color: Color) -> Self {
Self {
color,
..Default::default()
}
}
pub fn thickness(mut self, thickness: f32) -> Self {
self.thickness = thickness;
self
}
pub fn dash(mut self, dash: DashPattern) -> Self {
self.dash = dash;
self
}
pub fn dashed(mut self, dash_len: f32, gap_len: f32) -> Self {
self.dash = DashPattern::dashed(dash_len, gap_len);
self
}
pub fn dotted(mut self, dot_size: f32) -> Self {
self.dash = DashPattern::dotted(dot_size);
self
}
pub fn cap(mut self, cap: LineCap) -> Self {
self.cap = cap;
self
}
pub fn join(mut self, join: LineJoin) -> Self {
self.join = join;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum MarkerShape {
#[default]
Circle,
Square,
Triangle,
TriangleDown,
Diamond,
Cross,
X,
Star,
None,
}
pub type PointShape = MarkerShape;
#[derive(Debug, Clone, PartialEq)]
pub struct MarkerConfig {
pub shape: MarkerShape,
pub size: f32,
pub fill: Option<Color>,
pub stroke: Option<Color>,
pub stroke_thickness: f32,
pub interval: usize,
pub hover_only: bool,
}
impl Default for MarkerConfig {
fn default() -> Self {
Self {
shape: MarkerShape::Circle,
size: 6.0,
fill: Some(Color::WHITE),
stroke: None,
stroke_thickness: 1.0,
interval: 1,
hover_only: false,
}
}
}
impl MarkerConfig {
pub fn new(shape: MarkerShape) -> Self {
Self {
shape,
..Default::default()
}
}
pub fn circle() -> Self {
Self::new(MarkerShape::Circle)
}
pub fn square() -> Self {
Self::new(MarkerShape::Square)
}
pub fn diamond() -> Self {
Self::new(MarkerShape::Diamond)
}
pub fn size(mut self, size: f32) -> Self {
self.size = size;
self
}
pub fn fill(mut self, color: Color) -> Self {
self.fill = Some(color);
self
}
pub fn no_fill(mut self) -> Self {
self.fill = None;
self
}
pub fn stroke(mut self, color: Color) -> Self {
self.stroke = Some(color);
self
}
pub fn stroke_thickness(mut self, thickness: f32) -> Self {
self.stroke_thickness = thickness;
self
}
pub fn every(mut self, n: usize) -> Self {
self.interval = n.max(1);
self
}
pub fn on_hover_only(mut self) -> Self {
self.hover_only = true;
self
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub enum FillTarget {
ToValue(f64),
#[default]
ToBaseline,
ToSeries { series_id: SeriesId },
Band { lower: SeriesId, upper: SeriesId },
}
#[derive(Debug, Clone, PartialEq)]
pub struct Gradient {
pub stops: Vec<(f32, Color)>,
pub vertical: bool,
}
impl Default for Gradient {
fn default() -> Self {
Self {
stops: vec![(0.0, Color::WHITE), (1.0, Color::BLACK)],
vertical: true,
}
}
}
impl Gradient {
pub fn vertical(top: Color, bottom: Color) -> Self {
Self {
stops: vec![(0.0, top), (1.0, bottom)],
vertical: true,
}
}
pub fn horizontal(left: Color, right: Color) -> Self {
Self {
stops: vec![(0.0, left), (1.0, right)],
vertical: false,
}
}
pub fn with_stop(mut self, position: f32, color: Color) -> Self {
self.stops.push((position.clamp(0.0, 1.0), color));
self.stops.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
self
}
pub fn color_at(&self, position: f32) -> Color {
if self.stops.is_empty() {
return Color::WHITE;
}
if self.stops.len() == 1 {
return self.stops[0].1;
}
let pos = position.clamp(0.0, 1.0);
let mut prev = &self.stops[0];
for stop in &self.stops {
if stop.0 >= pos {
if (stop.0 - prev.0).abs() < f32::EPSILON {
return stop.1;
}
let t = (pos - prev.0) / (stop.0 - prev.0);
return Color::rgba(
prev.1.r + (stop.1.r - prev.1.r) * t,
prev.1.g + (stop.1.g - prev.1.g) * t,
prev.1.b + (stop.1.b - prev.1.b) * t,
prev.1.a + (stop.1.a - prev.1.a) * t,
);
}
prev = stop;
}
self.stops.last().unwrap().1
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct FillConfig {
pub target: FillTarget,
pub color: Color,
pub gradient: Option<Gradient>,
}
impl Default for FillConfig {
fn default() -> Self {
Self {
target: FillTarget::ToBaseline,
color: Color::rgba(0.0, 0.5, 1.0, 0.3),
gradient: None,
}
}
}
impl FillConfig {
pub fn to_baseline(color: Color) -> Self {
Self {
target: FillTarget::ToBaseline,
color,
gradient: None,
}
}
pub fn to_value(value: f64, color: Color) -> Self {
Self {
target: FillTarget::ToValue(value),
color,
gradient: None,
}
}
pub fn to_series(series_id: SeriesId, color: Color) -> Self {
Self {
target: FillTarget::ToSeries { series_id },
color,
gradient: None,
}
}
pub fn with_gradient(mut self, gradient: Gradient) -> Self {
self.gradient = Some(gradient);
self
}
pub fn color_at(&self, position: f32) -> Color {
if let Some(gradient) = &self.gradient {
gradient.color_at(position)
} else {
self.color
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct PointStyle {
pub size: f32,
pub shape: PointShape,
pub color: Color,
}
impl Default for PointStyle {
fn default() -> Self {
Self {
size: 6.0,
shape: PointShape::Circle,
color: Color::WHITE,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct FillStyle {
pub color: Color,
pub opacity: f32,
}
impl Default for FillStyle {
fn default() -> Self {
Self {
color: Color::BLUE,
opacity: 0.3,
}
}
}
#[derive(Debug, Clone)]
pub struct SeriesStyle {
pub color: Color,
pub line_width: f32,
pub line_style: LineStyle,
pub point_style: Option<PointStyle>,
pub fill: Option<FillStyle>,
pub z_order: i32,
pub visible: bool,
pub show_in_legend: bool,
}
impl Default for SeriesStyle {
fn default() -> Self {
Self {
color: Color::BLUE,
line_width: 1.0, line_style: LineStyle::Solid,
point_style: None,
fill: None,
z_order: 0,
visible: true,
show_in_legend: true,
}
}
}
impl SeriesStyle {
pub fn with_color(color: Color) -> Self {
Self {
color,
..Default::default()
}
}
pub fn line_width(mut self, width: f32) -> Self {
self.line_width = width;
self
}
pub fn line_style(mut self, style: LineStyle) -> Self {
self.line_style = style;
self
}
pub fn with_points(mut self) -> Self {
self.point_style = Some(PointStyle {
color: self.color,
..Default::default()
});
self
}
pub fn with_point_style(mut self, style: PointStyle) -> Self {
self.point_style = Some(style);
self
}
pub fn with_fill(mut self) -> Self {
self.fill = Some(FillStyle {
color: self.color,
opacity: 0.3,
});
self
}
pub fn with_fill_style(mut self, style: FillStyle) -> Self {
self.fill = Some(style);
self
}
pub fn z_order(mut self, z_order: i32) -> Self {
self.z_order = z_order;
self
}
pub fn visible(mut self, visible: bool) -> Self {
self.visible = visible;
self
}
pub fn hide_from_legend(mut self) -> Self {
self.show_in_legend = false;
self
}
pub fn dashed(mut self) -> Self {
self.line_style = LineStyle::Dashed;
self
}
pub fn dotted(mut self) -> Self {
self.line_style = LineStyle::Dotted;
self
}
pub fn to_line_config(&self) -> LineConfig {
LineConfig {
color: self.color,
thickness: self.line_width,
dash: self.line_style.to_dash_pattern(),
cap: LineCap::default(),
join: LineJoin::default(),
}
}
pub fn to_marker_config(&self) -> Option<MarkerConfig> {
self.point_style.as_ref().map(|ps| MarkerConfig {
shape: ps.shape,
size: ps.size,
fill: Some(ps.color),
stroke: None,
stroke_thickness: 1.0,
interval: 1,
hover_only: false,
})
}
pub fn to_fill_config(&self) -> Option<FillConfig> {
self.fill.as_ref().map(|fs| FillConfig {
target: FillTarget::ToBaseline,
color: Color::rgba(fs.color.r, fs.color.g, fs.color.b, fs.opacity),
gradient: None,
})
}
}
#[derive(Debug, Clone)]
pub struct EnhancedSeriesStyle {
pub line: LineConfig,
pub markers: Option<MarkerConfig>,
pub fill: Option<FillConfig>,
pub z_order: i32,
pub visible: bool,
pub show_in_legend: bool,
}
impl Default for EnhancedSeriesStyle {
fn default() -> Self {
Self {
line: LineConfig::default(),
markers: None,
fill: None,
z_order: 0,
visible: true,
show_in_legend: true,
}
}
}
impl EnhancedSeriesStyle {
pub fn with_color(color: Color) -> Self {
Self {
line: LineConfig::with_color(color),
..Default::default()
}
}
pub fn line(mut self, line: LineConfig) -> Self {
self.line = line;
self
}
pub fn markers(mut self, markers: MarkerConfig) -> Self {
self.markers = Some(markers);
self
}
pub fn fill(mut self, fill: FillConfig) -> Self {
self.fill = Some(fill);
self
}
pub fn z_order(mut self, z_order: i32) -> Self {
self.z_order = z_order;
self
}
pub fn visible(mut self, visible: bool) -> Self {
self.visible = visible;
self
}
pub fn hide_from_legend(mut self) -> Self {
self.show_in_legend = false;
self
}
pub fn to_legacy(&self) -> SeriesStyle {
SeriesStyle {
color: self.line.color,
line_width: self.line.thickness,
line_style: if self.line.dash.is_solid() {
LineStyle::Solid
} else if self.line.dash.segments.len() == 2
&& self.line.dash.segments[0] == self.line.dash.segments[1]
{
LineStyle::Dotted
} else {
LineStyle::Dashed
},
point_style: self.markers.as_ref().map(|m| PointStyle {
size: m.size,
shape: m.shape,
color: m.fill.unwrap_or(self.line.color),
}),
fill: self.fill.as_ref().map(|f| FillStyle {
color: f.color,
opacity: f.color.a,
}),
z_order: self.z_order,
visible: self.visible,
show_in_legend: self.show_in_legend,
}
}
}
#[derive(Debug, Clone)]
pub struct AxisStyle {
pub line_color: Color,
pub line_width: f32,
pub tick_color: Color,
pub tick_length: f32,
pub grid_color: Color,
pub grid_width: f32,
pub label_color: Color,
pub label_size: f32,
}
impl Default for AxisStyle {
fn default() -> Self {
Self {
line_color: Color::rgba(0.4, 0.4, 0.45, 1.0),
line_width: 1.0,
tick_color: Color::rgba(0.4, 0.4, 0.45, 1.0),
tick_length: 4.0,
grid_color: Color::rgba(0.25, 0.25, 0.28, 1.0),
grid_width: 0.5,
label_color: Color::rgba(0.6, 0.6, 0.65, 1.0),
label_size: 11.0,
}
}
}
pub const SERIES_COLORS: [Color; 8] = [
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), ];
pub fn palette_color(index: usize) -> Color {
SERIES_COLORS[index % SERIES_COLORS.len()]
}