liecharts 0.1.0-beta.1

A Rust charting library with PNG and SVG rendering support
Documentation
use vello_cpu::kurbo::{BezPath, Point, Rect};

use crate::{
    component::{ChartComponent, SeriesComponent, SeriesContext},
    layout::LayoutOutput,
    model::{CandlestickSeries, ChartModel},
    visual::{FillStrokeStyle, Stroke, VisualElement},
};

pub struct CandlestickSeriesComponent {
    series: CandlestickSeries,
    series_index: usize,
    grid_index: usize,
}

impl CandlestickSeriesComponent {
    pub fn new(series: &CandlestickSeries, series_index: usize, grid_index: usize) -> Self {
        Self {
            series: series.clone(),
            series_index,
            grid_index,
        }
    }

    fn build_with_context(&self, ctx: &SeriesContext) -> Vec<VisualElement> {
        let coord = ctx.coord;

        let cat_width = coord.category_width();
        let candle_width = cat_width * 0.6;
        let shadow_width = 1.5;

        let mut elements = Vec::new();

        for (idx, item) in self.series.data.iter().enumerate() {
            let x = coord.x_to_pixel(idx as f64 + 0.5);

            let high_y = coord.y_to_pixel(item.high, self.series.y_axis_index);
            let low_y = coord.y_to_pixel(item.low, self.series.y_axis_index);
            let open_y = coord.y_to_pixel(item.open, self.series.y_axis_index);
            let close_y = coord.y_to_pixel(item.close, self.series.y_axis_index);

            let is_up = item.close >= item.open;

            let (body_top, body_bottom) = if is_up {
                (close_y.min(open_y), close_y.max(open_y))
            } else {
                (open_y.min(close_y), open_y.max(close_y))
            };

            let body_color = if is_up {
                self.series.color_up
            } else {
                self.series.color_down
            };
            let border_color = if is_up {
                self.series.item_style.border_color.unwrap_or(body_color)
            } else {
                self.series.item_style.border_color0.unwrap_or(body_color)
            };

            let body_rect = Rect::new(
                x - candle_width / 2.0,
                body_top,
                x + candle_width / 2.0,
                body_bottom,
            );

            elements.push(VisualElement::Rect {
                rect: body_rect,
                style: FillStrokeStyle {
                    fill: Some(body_color),
                    stroke: Some(Stroke {
                        color: border_color,
                        width: 1.0,
                    }),
                },
            });

            let mut shadow = BezPath::new();
            shadow.move_to(Point::new(x, high_y));
            shadow.line_to(Point::new(x, body_top));
            shadow.move_to(Point::new(x, body_bottom));
            shadow.line_to(Point::new(x, low_y));

            elements.push(VisualElement::Path {
                path: shadow,
                style: FillStrokeStyle {
                    fill: None,
                    stroke: Some(Stroke {
                        color: border_color,
                        width: shadow_width,
                    }),
                },
            });
        }

        elements
    }
}

impl SeriesComponent for CandlestickSeriesComponent {
    fn series_index(&self) -> usize {
        self.series_index
    }

    fn grid_index(&self) -> usize {
        self.grid_index
    }

    fn is_empty(&self) -> bool {
        self.series.data.is_empty()
    }
}

impl ChartComponent for CandlestickSeriesComponent {
    fn build_visual_elements(
        &self,
        resolved: &ChartModel,
        layout: &LayoutOutput,
    ) -> Vec<VisualElement> {
        let ctx = match self.create_context(resolved, layout) {
            Some(ctx) => ctx,
            None => return Vec::new(),
        };
        self.build_with_context(&ctx)
    }
}