use super::common;
use crate::ir::{ChartSpec, SeriesType};
use crate::num::fmt_num;
use crate::scene::{Anchor, Prim, Scene};
use crate::text::TextMeasurer;
use std::fmt::Write;
const GROUP_RATIO: f64 = 0.8;
const BAND_PAD_RATIO: f64 = 0.1;
const BAR_FILL_RATIO: f64 = 0.9;
const MARKER_R: f64 = 3.0;
pub fn build(spec: &ChartSpec, m: &TextMeasurer) -> Scene {
let ink = spec.theme.text_color;
let label_font = spec.theme.font_size;
let frame = common::compute(spec, m);
let mut items: Vec<Prim> = Vec::new();
common::draw_frame(&mut items, spec, &frame, m);
let n = spec.categories.len().max(1);
let bar_count = spec
.series
.iter()
.filter(|s| s.series_type == SeriesType::Bar)
.count();
if bar_count > 0 {
let band_w = common::band_width(&frame, n);
let group_w = band_w * GROUP_RATIO;
let bar_w = group_w / bar_count as f64;
let base_v = 0.0_f64.clamp(frame.ticks.min, frame.ticks.max);
let baseline_y = frame.ys.map(base_v);
for i in 0..spec.categories.len() {
let band_left = common::category_center(&frame, i, n) - band_w / 2.0;
let mut bar_slot = 0_usize;
for ser in &spec.series {
if ser.series_type != SeriesType::Bar {
continue;
}
let bx = band_left + band_w * BAND_PAD_RATIO + bar_slot as f64 * bar_w;
let v = ser.values.get(i).copied().unwrap_or(0.0);
let vy = frame.ys.map(v);
let y_top = vy.min(baseline_y);
let h = (vy - baseline_y).abs();
items.push(Prim::Rect {
x: bx,
y: y_top,
w: (bar_w * BAR_FILL_RATIO).max(0.0),
h,
fill: ser.fill_at(i),
});
if spec.data_labels && ser.values.get(i).is_some() && v.is_finite() {
let cx = bx + (bar_w * BAR_FILL_RATIO) / 2.0;
let label_y = if v >= base_v {
y_top - common::LABEL_GAP
} else {
y_top + h + label_font
};
items.push(common::value_label(
cx,
label_y,
label_font,
Anchor::Middle,
ink,
v,
));
}
bar_slot += 1;
}
}
}
for ser in &spec.series {
if ser.series_type != SeriesType::Line {
continue;
}
let pts: Vec<(f64, f64)> = (0..spec.categories.len())
.map(|i| {
let x = common::category_center(&frame, i, n);
let v = ser.values.get(i).copied().unwrap_or(0.0);
(x, frame.ys.map(v))
})
.collect();
if ser.area && !pts.is_empty() {
let baseline_y = frame
.ys
.map(0.0_f64.clamp(frame.ticks.min, frame.ticks.max));
let mut d = String::new();
for (k, (x, y)) in pts.iter().enumerate() {
let cmd = if k == 0 { 'M' } else { 'L' };
write!(d, "{} {} {} ", cmd, fmt_num(*x), fmt_num(*y)).unwrap();
}
let (last_x, _) = pts[pts.len() - 1];
let (first_x, _) = pts[0];
write!(
d,
"L {} {} L {} {} Z",
fmt_num(last_x),
fmt_num(baseline_y),
fmt_num(first_x),
fmt_num(baseline_y)
)
.unwrap();
items.push(Prim::Path {
d,
fill: Some(ser.fill_at(0)),
stroke: None,
stroke_width: 0.0,
});
}
if pts.len() >= 2 {
if ser.tension <= 0.0 {
items.push(Prim::Polyline {
points: pts.clone(),
stroke: ser.stroke_at(0),
stroke_width: ser.stroke_width,
});
} else {
let d = catmull_rom_path(&pts, ser.tension);
items.push(Prim::Path {
d,
fill: None,
stroke: Some(ser.stroke_at(0)),
stroke_width: ser.stroke_width,
});
}
}
for (cx, cy) in &pts {
items.push(Prim::Circle {
cx: *cx,
cy: *cy,
r: MARKER_R,
fill: ser.stroke_at(0),
stroke: ser.stroke_at(0),
stroke_width: 0.0,
});
}
if spec.data_labels {
for (i, (x, y)) in pts.iter().enumerate() {
if let Some(&v) = ser.values.get(i)
&& v.is_finite()
{
items.push(common::value_label(
*x,
*y - MARKER_R - common::LABEL_GAP,
label_font,
Anchor::Middle,
spec.theme.text_color,
v,
));
}
}
}
}
Scene {
width: spec.width,
height: spec.height,
items,
}
}
fn catmull_rom_path(pts: &[(f64, f64)], tension: f64) -> String {
let k = pts.len();
let mut d = String::new();
write!(d, "M {} {} ", fmt_num(pts[0].0), fmt_num(pts[0].1)).unwrap();
for i in 0..k - 1 {
let p0 = pts[i.saturating_sub(1)];
let p1 = pts[i];
let p2 = pts[i + 1];
let p3 = pts[(i + 2).min(k - 1)];
let cp1 = (
p1.0 + (p2.0 - p0.0) / 6.0 * tension,
p1.1 + (p2.1 - p0.1) / 6.0 * tension,
);
let cp2 = (
p2.0 - (p3.0 - p1.0) / 6.0 * tension,
p2.1 - (p3.1 - p1.1) / 6.0 * tension,
);
write!(
d,
"C {} {} {} {} {} {} ",
fmt_num(cp1.0),
fmt_num(cp1.1),
fmt_num(cp2.0),
fmt_num(cp2.1),
fmt_num(p2.0),
fmt_num(p2.1)
)
.unwrap();
}
d
}