use super::types::{Series, SeriesData, SeriesRenderContext, SeriesType, idx_to_x, price_to_y};
use crate::tokens::DESIGN_TOKENS;
use egui::{Color32, Painter, Pos2, Shape, Stroke};
#[derive(Debug, Clone)]
pub struct BaselineSeriesOptions {
pub top_line_color: Color32,
pub bottom_line_color: Color32,
pub top_fill_color: Color32,
pub bottom_fill_color: Color32,
pub line_width: f32,
pub baseline: f64,
}
impl Default for BaselineSeriesOptions {
fn default() -> Self {
Self {
top_line_color: DESIGN_TOKENS.semantic.extended.bullish,
bottom_line_color: DESIGN_TOKENS.semantic.extended.bearish,
top_fill_color: DESIGN_TOKENS
.semantic
.extended
.bullish
.gamma_multiply(50_f32 / 255.0),
bottom_fill_color: DESIGN_TOKENS
.semantic
.extended
.bearish
.gamma_multiply(50_f32 / 255.0),
line_width: 2.0,
baseline: 0.0,
}
}
}
pub struct BaselineSeries {
data: Vec<SeriesData>,
options: BaselineSeriesOptions,
name: String,
}
impl BaselineSeries {
pub fn new(name: impl Into<String>, data: Vec<SeriesData>) -> Self {
Self {
data,
options: BaselineSeriesOptions::default(),
name: name.into(),
}
}
pub fn with_options(mut self, options: BaselineSeriesOptions) -> Self {
self.options = options;
self
}
pub fn with_baseline(mut self, baseline: f64) -> Self {
self.options.baseline = baseline;
self
}
}
impl Series for BaselineSeries {
fn series_type(&self) -> SeriesType {
SeriesType::Baseline
}
fn data(&self) -> &[SeriesData] {
&self.data
}
fn price_range(&self, start_idx: usize, end_idx: usize) -> Option<(f64, f64)> {
let visible = &self.data[start_idx.min(self.data.len())..end_idx.min(self.data.len())];
let mut min = f64::MAX;
let mut max = f64::MIN;
let mut found = false;
for point in visible {
if let Some(value) = point.main_val() {
min = min.min(value).min(self.options.baseline);
max = max.max(value).max(self.options.baseline);
found = true;
}
}
if found { Some((min, max)) } else { None }
}
fn render(&self, ctx: &SeriesRenderContext) {
let visible =
&self.data[ctx.start_idx.min(self.data.len())..ctx.end_idx.min(self.data.len())];
if visible.is_empty() {
return;
}
let last_idx = self.data.len().saturating_sub(1);
let baseline_y = price_to_y(
self.options.baseline,
ctx.price_min,
ctx.price_max,
ctx.rect,
);
let mut curr_segment: Vec<Pos2> = Vec::new();
let mut is_above = false;
for (i, point) in visible.iter().enumerate() {
if let Some(value) = point.main_val() {
let global_idx = ctx.start_idx + i;
let x = idx_to_x(
global_idx,
last_idx,
ctx.bar_spacing,
ctx.right_offset,
ctx.rect,
);
let y = price_to_y(value, ctx.price_min, ctx.price_max, ctx.rect);
let point_pos = Pos2::new(x, y);
let curr_is_above = value >= self.options.baseline;
if curr_segment.is_empty() {
curr_segment.push(point_pos);
is_above = curr_is_above;
} else if curr_is_above == is_above {
curr_segment.push(point_pos);
} else {
self.render_segment(ctx.painter, &curr_segment, baseline_y, is_above);
curr_segment.clear();
curr_segment.push(point_pos);
is_above = curr_is_above;
}
}
}
if !curr_segment.is_empty() {
self.render_segment(ctx.painter, &curr_segment, baseline_y, is_above);
}
}
fn name(&self) -> &str {
&self.name
}
fn color(&self) -> Color32 {
self.options.top_line_color
}
}
impl BaselineSeries {
fn render_segment(&self, painter: &Painter, points: &[Pos2], baseline_y: f32, is_above: bool) {
if points.len() < 2 {
return;
}
let (line_color, fill_color) = if is_above {
(self.options.top_line_color, self.options.top_fill_color)
} else {
(
self.options.bottom_line_color,
self.options.bottom_fill_color,
)
};
let (Some(first), Some(last)) = (points.first(), points.last()) else {
return;
};
let mut fill_points = points.to_vec();
let last_x = last.x;
let first_x = first.x;
fill_points.push(Pos2::new(last_x, baseline_y));
fill_points.push(Pos2::new(first_x, baseline_y));
painter.add(Shape::convex_polygon(fill_points, fill_color, Stroke::NONE));
painter.add(Shape::line(
points.to_vec(),
Stroke::new(self.options.line_width, line_color),
));
}
}