liecharts 0.1.0-beta.1

A Rust charting library with PNG and SVG rendering support
Documentation
use std::f64::consts::PI;

use vello_cpu::kurbo::Point;

use crate::{
    component::{ChartComponent, SeriesComponent, SeriesContext},
    layout::LayoutOutput,
    model::{ChartModel, PolarScatterSeries},
    text::create_text_layout,
    visual::{Color, FillStrokeStyle, Stroke, StrokeStyle, TextAlign, TextBaseline, VisualElement},
};

pub struct PolarScatterSeriesComponent {
    series: PolarScatterSeries,
    series_index: usize,
    grid_index: usize,
}

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

    fn get_center_and_radius(&self, ctx: &SeriesContext) -> (Point, f64) {
        let plot_bounds = ctx.plot_bounds();
        let chart_width = plot_bounds.width();
        let chart_height = plot_bounds.height();
        let chart_x = plot_bounds.x0;
        let chart_y = plot_bounds.y0;

        let cx = chart_x + chart_width * 0.5;
        let cy = chart_y + chart_height * 0.5;
        let max_radius = chart_width.min(chart_height) / 2.0;

        (Point::new(cx, cy), max_radius)
    }

    fn build_grid(&self, center: Point, max_radius: f64) -> Vec<VisualElement> {
        let mut elements = Vec::new();
        let grid_color = Color::new(200, 200, 200);
        let stroke = Stroke::new(grid_color, 1.0);
        let stroke_style = StrokeStyle::new(grid_color, 1.0);

        let max_data_radius = self
            .series
            .data
            .iter()
            .map(|d| d.radius)
            .fold(0.0, f64::max)
            .max(1.0);

        let split_number = 5;
        for i in 1..=split_number {
            let r = max_radius * (i as f64 / split_number as f64);
            elements.push(VisualElement::Circle {
                center,
                radius: r,
                style: FillStrokeStyle {
                    fill: None,
                    stroke: Some(stroke.clone()),
                },
            });

            let label_value = max_data_radius * (i as f64 / split_number as f64);
            let label_text = format!("{:.0}", label_value);
            let label_pos = Point::new(center.x, center.y - r);

            let label_font = crate::model::TextStyle {
                font_size: 10.0,
                font_family: "sans-serif".to_string(),
                color: Color::new(100, 100, 100),
                font_weight: crate::option::FontWeight::Named(
                    crate::option::FontWeightNamed::Normal,
                ),
                ..Default::default()
            };

            let layout_obj = create_text_layout(&label_text, &label_font, None);
            let th = layout_obj.height() as f64;

            elements.push(VisualElement::TextRun {
                text: label_text,
                position: Point::new(label_pos.x + 3.0, label_pos.y - th / 2.0),
                style: crate::model::TextStyle {
                    color: label_font.color,
                    font_size: label_font.font_size,
                    font_family: label_font.font_family,
                    font_weight: label_font.font_weight,
                    font_style: label_font.font_style,
                    align: TextAlign::Left,
                    vertical_align: TextBaseline::Top,
                },
                rotation: 0.0,
                max_width: None,
                layout: Some(layout_obj),
            });
        }

        let directions = 8;
        for i in 0..directions {
            let angle = -PI / 2.0 + (i as f64 * 2.0 * PI / directions as f64);
            let end_x = center.x + angle.cos() * max_radius;
            let end_y = center.y + angle.sin() * max_radius;

            elements.push(VisualElement::Line {
                start: center,
                end: Point::new(end_x, end_y),
                style: stroke_style.clone(),
            });
        }

        elements
    }

    fn build_scatter_points(&self, center: Point, max_radius: f64) -> Vec<VisualElement> {
        let mut elements = Vec::new();

        let max_data_radius = self
            .series
            .data
            .iter()
            .map(|d| d.radius)
            .fold(0.0, f64::max)
            .max(1.0);

        let color = self
            .series
            .item_style
            .color
            .unwrap_or(Color::new(84, 112, 198));

        for data_item in &self.series.data {
            let r = (data_item.radius / max_data_radius).clamp(0.0, 1.0) * max_radius;
            let angle = data_item.angle;

            let x = center.x + angle.cos() * r;
            let y = center.y + angle.sin() * r;

            let point_center = Point::new(x, y);
            let symbol_size = data_item.symbol_size;

            match self.series.symbol {
                crate::model::Symbol::Circle => {
                    elements.push(VisualElement::Circle {
                        center: point_center,
                        radius: symbol_size,
                        style: FillStrokeStyle {
                            fill: Some(color),
                            stroke: Some(Stroke::new(Color::new(255, 255, 255), 1.0)),
                        },
                    });
                }
                crate::model::Symbol::Rect => {
                    let half_size = symbol_size;
                    let rect = vello_cpu::kurbo::Rect::new(
                        point_center.x - half_size,
                        point_center.y - half_size,
                        point_center.x + half_size,
                        point_center.y + half_size,
                    );
                    elements.push(VisualElement::Rect {
                        rect,
                        style: FillStrokeStyle {
                            fill: Some(color),
                            stroke: Some(Stroke::new(Color::new(255, 255, 255), 1.0)),
                        },
                    });
                }
                crate::model::Symbol::Diamond => {
                    let mut path = vello_cpu::kurbo::BezPath::new();
                    path.move_to(Point::new(point_center.x, point_center.y - symbol_size));
                    path.line_to(Point::new(point_center.x + symbol_size, point_center.y));
                    path.line_to(Point::new(point_center.x, point_center.y + symbol_size));
                    path.line_to(Point::new(point_center.x - symbol_size, point_center.y));
                    path.close_path();
                    elements.push(VisualElement::Path {
                        path,
                        style: FillStrokeStyle {
                            fill: Some(color),
                            stroke: Some(Stroke::new(Color::new(255, 255, 255), 1.0)),
                        },
                    });
                }
                crate::model::Symbol::Triangle => {
                    let mut path = vello_cpu::kurbo::BezPath::new();
                    let sqrt3_2 = 0.86602540378;
                    path.move_to(Point::new(point_center.x, point_center.y - symbol_size));
                    path.line_to(Point::new(
                        point_center.x + symbol_size * sqrt3_2,
                        point_center.y + symbol_size * 0.5,
                    ));
                    path.line_to(Point::new(
                        point_center.x - symbol_size * sqrt3_2,
                        point_center.y + symbol_size * 0.5,
                    ));
                    path.close_path();
                    elements.push(VisualElement::Path {
                        path,
                        style: FillStrokeStyle {
                            fill: Some(color),
                            stroke: Some(Stroke::new(Color::new(255, 255, 255), 1.0)),
                        },
                    });
                }
                _ => {
                    elements.push(VisualElement::Circle {
                        center: point_center,
                        radius: symbol_size,
                        style: FillStrokeStyle {
                            fill: Some(color),
                            stroke: Some(Stroke::new(Color::new(255, 255, 255), 1.0)),
                        },
                    });
                }
            }
        }

        elements
    }
}

impl SeriesComponent for PolarScatterSeriesComponent {
    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 PolarScatterSeriesComponent {
    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(),
        };

        let (center, max_radius) = self.get_center_and_radius(&ctx);

        let mut elements = Vec::new();
        elements.extend(self.build_grid(center, max_radius));
        elements.extend(self.build_scatter_points(center, max_radius));

        elements
    }
}