use super::model::{BarMode, Chart, Data, Series, SeriesKind};
use super::project::{Dir, Plot};
use super::scale::{Scale, fmt_tick};
use crate::layout::PlacedNode;
use crate::layout::prim;
use std::f64::consts::TAU;
const GROUP: f64 = 0.72;
const OVERLAY: f64 = 0.6;
pub fn lay_out(plot: &Plot, chart: &Chart, out: &mut Vec<PlacedNode>) {
let bars: Vec<&Series> = chart
.series
.iter()
.filter(|s| matches!(s.kind, SeriesKind::Bars))
.collect();
if bars.is_empty() {
return;
}
let Scale::Band { n } = chart.x.scale else {
return; };
match plot.dir {
Dir::Column => column_bars(plot, chart, &bars, n, out),
Dir::Row => row_bars(plot, chart, &bars, n, out),
Dir::Radial => radial_bars(plot, chart, &bars, n, out),
}
}
fn visit_bars(
bars: &[&Series],
i: usize,
mode: &BarMode,
mut emit: impl FnMut(&Series, usize, usize, f64, f64, f64, f64),
) {
match mode {
BarMode::Grouped => {
let s = bars.len();
for (k, ser) in bars.iter().copied().enumerate() {
if let Some(v) = datum(ser, i) {
emit(ser, k, s, 0.0, v, v, 1.0);
}
}
}
BarMode::Stacked => {
let mut cum = 0.0;
for ser in bars.iter().copied() {
if let Some(v) = datum(ser, i) {
emit(ser, 0, 1, cum, cum + v, v, 1.0);
cum += v;
}
}
}
BarMode::Overlay => {
for ser in bars.iter().copied() {
if let Some(v) = datum(ser, i) {
emit(ser, 0, 1, 0.0, v, v, OVERLAY);
}
}
}
}
}
fn column_bars(plot: &Plot, chart: &Chart, bars: &[&Series], n: usize, out: &mut Vec<PlacedNode>) {
for i in 0..n {
let (sx0, sx1) = plot.slot_px(&chart.x.scale, i);
let group_w = (sx1 - sx0) * GROUP;
let group0 = sx0 + (sx1 - sx0 - group_w) / 2.0;
let cat = chart.x.labels.get(i);
visit_bars(bars, i, &chart.bars, |ser, k, s, lo, hi, value, op| {
let bar_w = group_w / s as f64;
let bx = group0 + (k as f64 + 0.5) * bar_w;
let scale = &chart.values[ser.axis].scale;
let y0 = plot.y_at(scale, scale.clamp(lo));
let y1 = plot.y_at(scale, scale.clamp(hi));
emit_rect(
bx,
(y0 + y1) / 2.0,
bar_w * 0.9,
(y0 - y1).abs(),
ser,
value,
cat,
op,
out,
);
});
}
}
fn row_bars(plot: &Plot, chart: &Chart, bars: &[&Series], n: usize, out: &mut Vec<PlacedNode>) {
for i in 0..n {
let sy0 = plot.y0 + (i as f64 / n as f64) * plot.h();
let slot_h = plot.h() / n as f64;
let group_h = slot_h * GROUP;
let group0 = sy0 + (slot_h - group_h) / 2.0;
let cat = chart.x.labels.get(i);
visit_bars(bars, i, &chart.bars, |ser, k, s, lo, hi, value, op| {
let bar_h = group_h / s as f64;
let by = group0 + (k as f64 + 0.5) * bar_h;
let scale = &chart.values[ser.axis].scale;
let x0 = plot.x0 + scale.frac(scale.clamp(lo)) * plot.w();
let x1 = plot.x0 + scale.frac(scale.clamp(hi)) * plot.w();
emit_rect(
(x0 + x1) / 2.0,
by,
(x1 - x0).abs(),
bar_h * 0.9,
ser,
value,
cat,
op,
out,
);
});
}
}
fn radial_bars(plot: &Plot, chart: &Chart, bars: &[&Series], n: usize, out: &mut Vec<PlacedNode>) {
let xs = &chart.x.scale;
let slot = TAU / n as f64;
let pad = slot * 0.14;
for i in 0..n {
let center = plot.spoke_angle(xs, i as f64);
let (a_lo0, a_hi0) = (center - slot / 2.0 + pad, center + slot / 2.0 - pad);
let cat = chart.x.labels.get(i);
visit_bars(bars, i, &chart.bars, |ser, k, s, lo, hi, value, op| {
let step = (a_hi0 - a_lo0) / s as f64;
let a_lo = a_lo0 + step * k as f64;
emit_wedge(
plot,
chart,
ser,
lo,
hi,
a_lo,
a_lo + step,
value,
cat,
op,
out,
);
});
}
}
fn datum(ser: &Series, i: usize) -> Option<f64> {
match &ser.data {
Data::Categorical(v) => v.get(i).copied(),
_ => None,
}
}
#[allow(clippy::too_many_arguments)]
fn emit_rect(
cx: f64,
cy: f64,
w: f64,
h: f64,
ser: &Series,
value: f64,
category: Option<&String>,
opacity: f64,
out: &mut Vec<PlacedNode>,
) {
if w.min(h) <= 0.0 {
return;
}
let mut bar = prim::rect(cx, cy, w, h, ser.color.clone(), opacity);
prim::round(&mut bar, ser.radius);
if let Some((color, width)) = &ser.outline {
prim::outline(&mut bar, color.clone(), *width);
}
prim::set_title(&mut bar, title(category, ser.label.as_deref(), value));
out.push(bar);
}
#[allow(clippy::too_many_arguments)]
fn emit_wedge(
plot: &Plot,
chart: &Chart,
ser: &Series,
lo: f64,
hi: f64,
a_lo: f64,
a_hi: f64,
value: f64,
category: Option<&String>,
opacity: f64,
out: &mut Vec<PlacedNode>,
) {
let vs = &chart.values[ser.axis].scale;
let (cx, cy) = plot.center();
let r0 = vs.frac(lo) * plot.radius();
let r1 = vs.frac(hi) * plot.radius();
if (r1 - r0).abs() < 0.5 {
return;
}
let mut wedge = prim::wedge(cx, cy, r0, r1, a_lo, a_hi, ser.color.clone(), opacity);
if let Some((color, width)) = &ser.outline {
prim::outline(&mut wedge, color.clone(), *width);
}
prim::set_title(&mut wedge, title(category, ser.label.as_deref(), value));
out.push(wedge);
}
fn title(category: Option<&String>, name: Option<&str>, value: f64) -> String {
let v = fmt_tick(value);
match (category.map(String::as_str), name) {
(Some(c), Some(n)) => format!("{c} · {n}: {v}"),
(Some(c), None) => format!("{c}: {v}"),
(None, Some(n)) => format!("{n}: {v}"),
(None, None) => v,
}
}