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>, pub size: WidgetSizeVars,
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() {
}
}
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),
};
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) {
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()),
)
};
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 {
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),
}
}
}
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| {
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,
) {
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,
);
let label_coordinates = self.y_axis.labeling_algorithm.locate(range.clone(), 5);
let theme = &MAKIE.cartesian2.y_axis;
ctx.with_save(|ctx: &mut C| {
for coord in &label_coordinates {
let y = size.main_height - camera.map((0.0, *coord)).y;
if theme.mark_length > 0.0 {
ctx.stroke(
Line::new((0.0, y), (-theme.mark_length, y)),
&theme.mark_color,
theme.mark_thickness,
);
}
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,
),
);
}
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();
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();
}
}
fn render_x_axis<C: RenderContext>(
&self,
ctx: &mut C,
size: &WidgetSize,
range: std::ops::Range<f64>,
camera: &Camera2,
) {
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| {
for coord in &label_coordinates {
let x = camera.map((*coord, 0.0)).x;
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,
);
}
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();
}
}