use super::model::{Chart, Curve, Data, Series, SeriesKind};
use super::palette::deepen;
use super::project::{Dir, Plot};
use super::scale::{Scale, fmt_tick};
use crate::layout::PlacedNode;
use crate::layout::prim;
use crate::resolve::MarkerKind;
pub(super) type Plotted = ((f64, f64), (f64, f64));
pub fn areas(plot: &Plot, chart: &Chart, out: &mut Vec<PlacedNode>) {
for ser in &chart.series {
if matches!(ser.kind, SeriesKind::Area) {
draw_area(plot, chart, ser, out);
}
}
}
pub fn lines(plot: &Plot, chart: &Chart, out: &mut Vec<PlacedNode>) {
for ser in &chart.series {
if matches!(ser.kind, SeriesKind::Line) {
draw_line(plot, chart, ser, out);
}
}
}
pub fn dots(plot: &Plot, chart: &Chart, out: &mut Vec<PlacedNode>) {
for ser in &chart.series {
if matches!(ser.kind, SeriesKind::Dots) {
draw_dots(plot, chart, ser, out);
}
}
}
pub(super) fn samples(plot: &Plot, chart: &Chart, ser: &Series) -> Vec<Plotted> {
let xs = &chart.x.scale;
let vs = &chart.values[ser.axis].scale;
match &ser.data {
Data::Categorical(v) => v
.iter()
.enumerate()
.map(|(i, &y)| ((i as f64, y), plot.project(xs, i as f64, vs, y)))
.collect(),
Data::Points(p) => p
.iter()
.map(|&(x, y)| ((x, y), plot.project(xs, x, vs, y)))
.collect(),
Data::Formula(_) => Vec::new(),
}
}
fn draw_line(plot: &Plot, chart: &Chart, ser: &Series, out: &mut Vec<PlacedNode>) {
let pts = samples(plot, chart, ser);
if pts.len() < 2 {
return;
}
let px: Vec<(f64, f64)> = pts.iter().map(|(_, p)| *p).collect();
for run in line_runs(plot, &px, &ser.curve) {
let mut ln = prim::line(run, ser.color.clone(), ser.thickness);
if let Some(s) = &ser.stroke_style {
ln.attrs.insert("stroke-style", s.clone());
}
out.push(ln);
}
vertex_markers(chart, ser, &pts, out);
}
fn line_runs(plot: &Plot, px: &[(f64, f64)], curve: &Curve) -> Vec<Vec<(f64, f64)>> {
if plot.is_radial() {
let mut loop_ = px.to_vec();
loop_.push(px[0]);
return vec![loop_];
}
plot.clip(&curve_points(px, curve))
}
fn draw_area(plot: &Plot, chart: &Chart, ser: &Series, out: &mut Vec<PlacedNode>) {
let pts = samples(plot, chart, ser);
if pts.len() < 2 {
return;
}
let px: Vec<(f64, f64)> = pts.iter().map(|(_, p)| *p).collect();
let edge = ser
.outline
.as_ref()
.map(|(c, _)| c.clone())
.unwrap_or_else(|| deepen(&ser.color));
if plot.is_radial() {
out.push(prim::poly(px.clone(), ser.color.clone(), 0.82));
for run in line_runs(plot, &px, &ser.curve) {
out.push(prim::line(run, edge.clone(), ser.thickness));
}
vertex_markers(chart, ser, &pts, out);
return;
}
let top = if plot.dir == Dir::Row {
px.clone()
} else {
curve_points(&px, &ser.curve)
};
let scale = &chart.values[ser.axis].scale;
let base = ser.baseline.unwrap_or(0.0);
let mut poly: Vec<(f64, f64)> = top
.iter()
.map(|&(x, y)| (x.clamp(plot.x0, plot.x1), y.clamp(plot.y0, plot.y1)))
.collect();
if let (Some(&(fx, fy)), Some(&(lx, ly))) = (poly.first(), poly.last()) {
match plot.dir {
Dir::Row => {
let bx = plot.x0 + scale.frac(scale.clamp(base)) * plot.w();
poly.push((bx, ly));
poly.push((bx, fy));
}
_ => {
let by = plot.y_at(scale, scale.clamp(base));
poly.push((lx, by));
poly.push((fx, by));
}
}
out.push(prim::poly(poly, ser.color.clone(), 0.82));
}
for run in plot.clip(&top) {
out.push(prim::line(run, edge.clone(), ser.thickness));
}
vertex_markers(chart, ser, &pts, out);
}
pub(super) fn marker_diameter(kind: MarkerKind, thickness: f64) -> f64 {
match kind {
MarkerKind::Circle | MarkerKind::Diamond => (thickness * 4.0).max(11.0),
_ => (thickness * 2.5).max(5.0),
}
}
fn vertex_markers(chart: &Chart, ser: &Series, pts: &[Plotted], out: &mut Vec<PlacedNode>) {
if ser.marker == MarkerKind::None {
return;
}
let d = marker_diameter(ser.marker, ser.thickness);
for ((xd, yd), (xp, yp)) in pts {
if in_domain(chart, ser, *xd, *yd) {
let mut m = prim::marker(ser.marker, *xp, *yp, d, d, ser.color.clone());
prim::set_title(&mut m, dot_title(chart, ser, *xd, *yd));
out.push(m);
}
}
}
fn draw_dots(plot: &Plot, chart: &Chart, ser: &Series, out: &mut Vec<PlacedNode>) {
let (w, h) = ser.dot;
for ((xd, yd), (xp, yp)) in samples(plot, chart, ser) {
if !in_domain(chart, ser, xd, yd) {
continue;
}
let mut dot = prim::marker(ser.marker, xp, yp, w, h, ser.color.clone());
prim::set_title(&mut dot, dot_title(chart, ser, xd, yd));
out.push(dot);
}
}
pub(super) fn in_domain(chart: &Chart, ser: &Series, x: f64, y: f64) -> bool {
chart.x.scale.contains(x) && chart.values[ser.axis].scale.contains(y)
}
fn dot_title(chart: &Chart, ser: &Series, x: f64, y: f64) -> String {
let name = ser.label.as_deref().unwrap_or("");
let value = match &ser.data {
Data::Categorical(_) => match (chart.x.labels.get(x as usize), &chart.x.scale) {
(Some(c), Scale::Band { .. }) => format!("{c}: {}", fmt_tick(y)),
_ => fmt_tick(y),
},
_ => format!("{}, {}", fmt_tick(x), fmt_tick(y)),
};
if name.is_empty() {
value
} else {
format!("{name}: {value}")
}
}
fn curve_points(pts: &[(f64, f64)], curve: &Curve) -> Vec<(f64, f64)> {
match curve {
Curve::Linear => pts.to_vec(),
Curve::Step => {
let mut out = vec![pts[0]];
for w in pts.windows(2) {
out.push((w[1].0, w[0].1));
out.push((w[1].0, w[1].1));
}
out
}
Curve::Smooth => monotone_resample(pts),
}
}
fn monotone_resample(pts: &[(f64, f64)]) -> Vec<(f64, f64)> {
let n = pts.len();
if n < 3 {
return pts.to_vec();
}
let dx: Vec<f64> = (0..n - 1).map(|i| pts[i + 1].0 - pts[i].0).collect();
let s: Vec<f64> = (0..n - 1)
.map(|i| {
if dx[i].abs() < 1e-9 {
0.0
} else {
(pts[i + 1].1 - pts[i].1) / dx[i]
}
})
.collect();
let mut m = vec![0.0; n];
m[0] = s[0];
m[n - 1] = s[n - 2];
for i in 1..n - 1 {
m[i] = if s[i - 1] * s[i] <= 0.0 {
0.0
} else {
(s[i - 1] + s[i]) / 2.0
};
}
for i in 0..n - 1 {
if s[i].abs() < 1e-12 {
m[i] = 0.0;
m[i + 1] = 0.0;
} else {
let (a, b) = (m[i] / s[i], m[i + 1] / s[i]);
let h = a.hypot(b);
if h > 3.0 {
let t = 3.0 / h;
m[i] = t * a * s[i];
m[i + 1] = t * b * s[i];
}
}
}
const K: usize = 16; let mut out = vec![pts[0]];
for i in 0..n - 1 {
let (x0, y0) = pts[i];
let (x1, y1) = pts[i + 1];
let h = x1 - x0;
for k in 1..=K {
let t = k as f64 / K as f64;
let (t2, t3) = (t * t, t * t * t);
let y = (2.0 * t3 - 3.0 * t2 + 1.0) * y0
+ (t3 - 2.0 * t2 + t) * h * m[i]
+ (-2.0 * t3 + 3.0 * t2) * y1
+ (t3 - t2) * h * m[i + 1];
out.push((x0 + t * h, y));
}
}
out
}