use super::model::{AxisRef, Chart, Mark, MarkAt};
use super::prim;
use super::project::Plot;
use crate::layout::PlacedNode;
use crate::resolve::ResolvedValue;
const LABEL_SIZE: f64 = 11.0;
const SHADE: f64 = 0.15;
const LINE_W: f64 = 1.5;
fn axis_px(plot: &Plot, chart: &Chart, axis: &AxisRef, v: f64) -> f64 {
match axis {
AxisRef::X => plot.x_at(&chart.x.scale, chart.x.scale.clamp(v)),
AxisRef::Value(i) => {
let s = &chart.values[*i].scale;
plot.y_at(s, s.clamp(v))
}
}
}
fn contains(chart: &Chart, axis: &AxisRef, v: f64) -> bool {
match axis {
AxisRef::X => chart.x.scale.contains(v),
AxisRef::Value(i) => chart.values[*i].scale.contains(v),
}
}
pub fn band_shades(plot: &Plot, chart: &Chart, out: &mut Vec<PlacedNode>) {
let mut drawn: Vec<f64> = Vec::new(); for b in &chart.bands {
let p0 = axis_px(plot, chart, &b.axis, b.span.0);
let p1 = axis_px(plot, chart, &b.axis, b.span.1);
if (p1 - p0).abs() < 0.5 {
continue; }
match &b.fill {
Some(fill) => out.push(shade(plot, &b.axis, p0, p1, fill.clone())),
None => dividers(plot, &b.axis, &b.tick, p0, p1, &mut drawn, out),
}
}
}
fn shade(plot: &Plot, axis: &AxisRef, p0: f64, p1: f64, fill: ResolvedValue) -> PlacedNode {
match axis {
AxisRef::X => prim::rect(
(p0 + p1) / 2.0,
(plot.y0 + plot.y1) / 2.0,
(p1 - p0).abs(),
plot.h(),
fill,
SHADE,
),
AxisRef::Value(_) => prim::rect(
(plot.x0 + plot.x1) / 2.0,
(p0 + p1) / 2.0,
plot.w(),
(p1 - p0).abs(),
fill,
SHADE,
),
}
}
fn dividers(
plot: &Plot,
axis: &AxisRef,
color: &ResolvedValue,
p0: f64,
p1: f64,
drawn: &mut Vec<f64>,
out: &mut Vec<PlacedNode>,
) {
for p in [p0, p1] {
let edge = match axis {
AxisRef::X => near(p, plot.x0) || near(p, plot.x1),
AxisRef::Value(_) => near(p, plot.y0) || near(p, plot.y1),
};
if edge || drawn.iter().any(|&q| near(q, p)) {
continue;
}
drawn.push(p);
let pts = match axis {
AxisRef::X => vec![(p, plot.y0), (p, plot.y1)],
AxisRef::Value(_) => vec![(plot.x0, p), (plot.x1, p)],
};
out.push(prim::line(pts, color.clone(), 1.0));
}
}
pub fn band_ticks(plot: &Plot, chart: &Chart, out: &mut Vec<PlacedNode>) {
for b in &chart.bands {
let Some(label) = &b.label else { continue };
let mid = axis_px(plot, chart, &b.axis, (b.span.0 + b.span.1) / 2.0);
let color = Some(b.tick.clone());
let node = match &b.axis {
AxisRef::X => prim::text(
label,
mid,
plot.y1 + 4.0 + LABEL_SIZE * 1.7,
LABEL_SIZE,
color,
false,
),
AxisRef::Value(_) => prim::text_right(label, plot.x0 - 6.0, mid, LABEL_SIZE, color),
};
out.push(node);
}
}
pub fn x_band_row(chart: &Chart) -> f64 {
let labelled = chart
.bands
.iter()
.any(|b| matches!(b.axis, AxisRef::X) && b.label.is_some());
if labelled { LABEL_SIZE } else { 0.0 }
}
pub fn marks(plot: &Plot, chart: &Chart, out: &mut Vec<PlacedNode>) {
for m in &chart.marks {
match m.at {
MarkAt::Line(v) => ref_line(plot, chart, m, v, out),
MarkAt::Point(x, y) => point(plot, chart, m, x, y, out),
}
}
}
fn ref_line(plot: &Plot, chart: &Chart, m: &Mark, v: f64, out: &mut Vec<PlacedNode>) {
if !contains(chart, &m.axis, v) {
return; }
let p = axis_px(plot, chart, &m.axis, v);
let pts = match &m.axis {
AxisRef::X => vec![(p, plot.y0), (p, plot.y1)],
AxisRef::Value(_) => vec![(plot.x0, p), (plot.x1, p)],
};
let mut ln = prim::line(pts, m.color.clone(), LINE_W);
if let Some(ss) = &m.stroke_style {
ln.attrs.insert("stroke-style", ss.clone());
}
out.push(ln);
if let Some(text) = &m.label {
let color = Some(m.color.clone());
let node = match &m.axis {
AxisRef::X => prim::text(
text,
p,
plot.y0 + LABEL_SIZE * 0.9,
LABEL_SIZE,
color,
false,
),
AxisRef::Value(_) => {
prim::text_left(text, plot.x0 + 3.0, p - LABEL_SIZE * 0.6, LABEL_SIZE, color)
}
};
out.push(node);
}
}
fn point(plot: &Plot, chart: &Chart, m: &Mark, x: f64, y: f64, out: &mut Vec<PlacedNode>) {
let vi = match &m.axis {
AxisRef::Value(i) => *i,
AxisRef::X => 0,
};
if !chart.x.scale.contains(x) || !chart.values[vi].scale.contains(y) {
return;
}
let xp = plot.x_at(&chart.x.scale, x);
let yp = plot.y_at(&chart.values[vi].scale, y);
if m.dot {
out.push(prim::oval(xp, yp, 7.0, 7.0, m.color.clone()));
}
if let Some(text) = &m.label {
let ly = if m.dot { yp - LABEL_SIZE } else { yp };
out.push(prim::text(
text,
xp,
ly,
LABEL_SIZE,
Some(m.color.clone()),
false,
));
}
}
fn near(a: f64, b: f64) -> bool {
(a - b).abs() < 0.5
}