ezel 0.0.1

A good plotting library
Documentation
/// grid cell padding
///
/// vs
///
/// each widget's padding
use crate::cartesian::geometry::{scatter::Scatter, Geometry};
use crate::data::ConstOrColumn;
use crate::theme::MAKIE;
use crate::widget_size::WidgetSize;
use crate::{widget_size::WidgetSizeVars, Renderable};
use cassowary::strength::WEAK;
use cassowary::WeightedRelation::EQ;
use cassowary::{strength::REQUIRED, Solver};
use piet::kurbo::Affine;
use piet::{
    kurbo::{Line, Point, Rect},
    RenderContext,
};
use piet::{Text, TextLayout, TextLayoutBuilder};
use polars::prelude::DataFrame;

use super::axis::Axis;
use super::camera2::Camera2;
use super::geometry::scatter::MarkerOpts;
use super::geometry::BoundingBox;

pub struct Cartesian2 {
    items: Vec<Geometry>, // scatter, band, ..
    pub size: WidgetSizeVars,
    /*
    x_scale: linear, log
    x_tick_labeler
    x_label
    */
    pub x_axis: Axis,
    pub y_axis: Axis,
}

impl Default for Cartesian2 {
    fn default() -> Self {
        Self {
            items: vec![],
            size: WidgetSizeVars::new(),
            x_axis: Default::default(),
            y_axis: Default::default(),
        }
    }
}

impl Cartesian2 {
    pub fn scatter(&mut self, df: DataFrame, x: ConstOrColumn<f64>, y: ConstOrColumn<f64>) {
        self.items.push(Geometry::Scatter(Scatter {
            df,
            x,
            y,
            marker: MarkerOpts::default(),
        }));
    }

    pub fn line(&mut self, df: DataFrame, x: ConstOrColumn<f64>, y: ConstOrColumn<f64>) {
        self.items
            .push(Geometry::Line(crate::cartesian::geometry::line::Line {
                df,
                x,
                y,
            }));
    }
}

pub fn render_line_segments<C>(_ctx: &mut C, positions: &[(f64, f64)]) {
    for _i in 1..positions.len() {
        // ctx.stroke(
        //     Line::new(&positions[i - 1], &positions[i]),
        //     brush,
        //     thickness,
        //     stroke_style,
        // )
    }
}

impl Renderable for Cartesian2 {
    fn render<C: RenderContext>(&self, ctx: &mut C, layout: &Solver) {
        let size = self.size.read(layout);

        if let Some(color) = &MAKIE.cartesian2.background {
            ctx.fill(
                Rect {
                    x0: 0.0,
                    y0: 0.0,
                    x1: size.main_width,
                    y1: size.main_height,
                },
                color,
            );
        }

        let x_auto_limit_margin = MAKIE.cartesian2.x_axis.auto_limit_margin;
        let y_auto_limit_margin = MAKIE.cartesian2.y_axis.auto_limit_margin;

        let data_bbox = self.data_bbox();
        let display_bbox = BoundingBox {
            min_x: size.main_width * x_auto_limit_margin,
            max_x: size.main_width * (1.0 - x_auto_limit_margin),
            min_y: size.main_height * y_auto_limit_margin,
            max_y: size.main_height * (1.0 - y_auto_limit_margin),
        };
        // camera only applies to coordinates
        let camera = Camera2::new(&data_bbox, &display_bbox);

        let origin = (0.0, size.main_height);
        self.render_x_axis(ctx, &size, data_bbox.x_range(), &camera);
        self.render_y_axis(ctx, &size, data_bbox.y_range(), &camera);
        self.render_main(ctx, origin, &camera);
    }

    fn layout<C: RenderContext>(&self, ctx: &mut C, solver: &mut Solver) {
        // layout: inner box, labels, padding

        let x_label_area_height = {
            let theme = MAKIE.cartesian2.x_axis;
            theme.x_total_offset(self.x_axis.label.is_some())
        };
        let y_label_area_width = {
            let theme = MAKIE.cartesian2.y_axis;
            let data_bbox = self.data_bbox();
            theme.y_total_offset(
                self.y_axis.label.is_some(),
                self.y_tick_label_width(ctx, data_bbox.y_range()),
            )
        };

        // bottom height >= mark_length + tick_label_padding + tick_label_max_height + label_padding + label_height

        solver
            .add_constraints(&[
                self.size.bottom | EQ(REQUIRED) | x_label_area_height,
                self.size.left | EQ(REQUIRED) | y_label_area_width,
                self.size.top | EQ(WEAK) | 0.0,
                self.size.right | EQ(WEAK) | 0.0,
            ])
            .unwrap();
    }

    fn size(&self) -> &WidgetSizeVars {
        &self.size
    }
}

impl Cartesian2 {
    // draw dimmed grid lines
    fn _render_grid<C: RenderContext>(&self, _ctx: &mut C, _layout: &Solver) {}

    fn data_bbox(&self) -> BoundingBox {
        let mut bbox: Option<BoundingBox> = None;
        for item in &self.items {
            if let Some(b) = item.bounding_box() {
                match &mut bbox {
                    None => bbox = Some(b),
                    Some(bbox) => bbox.merge(&b),
                }
            }
        }

        // empty plot's default xy range
        bbox.unwrap_or(BoundingBox {
            min_x: 0.0,
            max_x: 5.0,
            min_y: 0.0,
            max_y: 5.0,
        })
    }

    fn y_tick_label_width<C: RenderContext>(
        &self,
        ctx: &mut C,
        range: std::ops::Range<f64>,
    ) -> f64 {
        let label_coordinates = self.y_axis.labeling_algorithm.locate(range, 5);
        let theme = &MAKIE.cartesian2.y_axis;
        label_coordinates
            .into_iter()
            .map(|coord| {
                // tick labels
                let text_layout = ctx
                    .text()
                    .new_text_layout(coord.to_string())
                    .font(
                        ctx.text()
                            .font_family(theme.tick_label.family)
                            .unwrap_or_default(),
                        theme.tick_label.size,
                    )
                    .build()
                    .unwrap();
                let text_size = text_layout.image_bounds().size();
                text_size.width
            })
            .fold(0.0, f64::max)
    }

    fn render_y_axis<C: RenderContext>(
        &self,
        ctx: &mut C,
        size: &WidgetSize,
        range: std::ops::Range<f64>,
        camera: &Camera2,
    ) {
        // spine
        ctx.stroke(
            Line {
                p0: Point { x: 0.0, y: 0.0 },
                p1: Point {
                    x: 0.0,
                    y: size.main_height,
                },
            },
            &MAKIE.cartesian2.x_axis.spine_color,
            2.0,
        );

        // draw ticks
        let label_coordinates = self.y_axis.labeling_algorithm.locate(range.clone(), 5);
        let theme = &MAKIE.cartesian2.y_axis;
        ctx.with_save(|ctx: &mut C| {
            // ctx.transform(camera.transform);
            for coord in &label_coordinates {
                let y = size.main_height - camera.map((0.0, *coord)).y;
                // tick mark
                if theme.mark_length > 0.0 {
                    ctx.stroke(
                        Line::new((0.0, y), (-theme.mark_length, y)),
                        &theme.mark_color,
                        theme.mark_thickness,
                    );
                }

                // tick labels
                let text_layout = ctx
                    .text()
                    .new_text_layout(coord.to_string())
                    .font(
                        ctx.text()
                            .font_family(theme.tick_label.family)
                            .unwrap_or_default(),
                        theme.tick_label.size,
                    )
                    .build()
                    .unwrap();
                let text_size = text_layout.image_bounds().size();

                ctx.draw_text(
                    &text_layout,
                    (
                        -theme.tick_label_offset() - text_size.width,
                        y - text_size.height / 2.0,
                    ),
                );
                // ctx.stroke(text_layout.image_bounds(), &Color::BLACK, 1.0);
            }
            if let Some(grid_line) = &MAKIE.cartesian2.grid_line.y {
                for coord in label_coordinates {
                    let y = camera.map((0.0, coord)).y;
                    ctx.stroke(
                        Line::new((0.0, y), (size.main_width, y)),
                        &grid_line.color,
                        grid_line.width,
                    );
                }
            }
            Ok(())
        })
        .unwrap();

        // label
        if let Some(text_layout) = self.y_axis.build_text_layout(ctx) {
            let text_size = text_layout.image_bounds().size();

            ctx.with_save(|ctx: &mut C| {
                ctx.transform(Affine::rotate(-std::f64::consts::PI / 2.0));

                let y_tick_label_width = self.y_tick_label_width(ctx, range);
                ctx.draw_text(
                    &text_layout,
                    (
                        -((size.main_height + text_size.width) / 2.0),
                        -text_size.height - theme.y_label_offset(y_tick_label_width),
                    ),
                );
                Ok(())
            })
            .unwrap();
        }
    }

    /// draw x axis in the bottom protrusion area
    fn render_x_axis<C: RenderContext>(
        &self,
        ctx: &mut C,
        size: &WidgetSize,
        range: std::ops::Range<f64>,
        camera: &Camera2,
    ) {
        // spine
        ctx.stroke(
            Line {
                p0: Point {
                    x: 0.0,
                    y: size.main_height,
                },
                p1: Point {
                    x: size.main_width,
                    y: size.main_height,
                },
            },
            &MAKIE.cartesian2.x_axis.spine_color,
            2.0,
        );

        let label_coordinates = self.x_axis.labeling_algorithm.locate(range, 5);
        let theme = &MAKIE.cartesian2.x_axis;
        ctx.with_save(|ctx: &mut C| {
            // ctx.transform(camera.transform);
            for coord in &label_coordinates {
                let x = camera.map((*coord, 0.0)).x;
                // tick mark
                if theme.mark_length > 0.0 {
                    ctx.stroke(
                        Line::new(
                            (x, size.main_height),
                            (x, size.main_height + theme.mark_length),
                        ),
                        &theme.mark_color,
                        theme.mark_thickness,
                    );
                }

                // tick labels
                let text_layout = ctx
                    .text()
                    .new_text_layout(coord.to_string())
                    .font(
                        ctx.text()
                            .font_family(crate::cartesian::axis::DEFAULT_LABEL_FONT_FAMILY)
                            .unwrap_or_default(),
                        crate::cartesian::axis::DEFAULT_LABEL_FONT_SIZE,
                    )
                    .build()
                    .unwrap();
                let text_size = text_layout.image_bounds().size();

                ctx.draw_text(
                    &text_layout,
                    (
                        x - text_size.width / 2.0,
                        size.main_height + theme.tick_label_offset(),
                    ),
                );
            }

            if let Some(grid_line) = &MAKIE.cartesian2.grid_line.x {
                for coord in label_coordinates {
                    let x = camera.map((coord, 0.0)).x;
                    ctx.stroke(
                        Line::new((x, 0.0), (x, size.main_height)),
                        &grid_line.color,
                        grid_line.width,
                    );
                }
            }
            Ok(())
        })
        .unwrap();

        if let Some(text_layout) = self.x_axis.build_text_layout(ctx) {
            let text_size = text_layout.image_bounds().size();
            ctx.draw_text(
                &text_layout,
                (
                    (size.main_width - text_size.width) / 2.0,
                    size.main_height + theme.x_label_offset(),
                ),
            );
        }
    }
    fn render_main<C: RenderContext>(&self, ctx: &mut C, origin: (f64, f64), camera: &Camera2) {
        ctx.with_save(|ctx: &mut C| {
            ctx.transform(Affine::translate(origin) * Affine::FLIP_Y);
            for item in &self.items {
                item.render(ctx, camera);
            }
            Ok(())
        })
        .unwrap();
    }
}