use super::common;
use crate::ir::{ChartKind, ChartSpec, Color, LegendPos};
use crate::num::fmt_num;
use crate::scene::{Anchor, Prim, Scene};
use crate::text::TextMeasurer;
use std::f64::consts::PI;
use std::fmt::Write;
pub(crate) const SLICE_STROKE: Color = Color {
r: 255,
g: 255,
b: 255,
a: 1.0,
};
pub(crate) const LABEL_COLOR: Color = Color {
r: 255,
g: 255,
b: 255,
a: 1.0,
};
pub fn build(spec: &ChartSpec, m: &TextMeasurer) -> Scene {
let mut items: Vec<Prim> = Vec::new();
let ink = spec.theme.text_color;
let label_font = spec.theme.font_size;
let donut_ratio = match spec.kind {
ChartKind::Pie { donut_ratio } => donut_ratio,
_ => 0.0,
};
let series = spec.series.first();
let empty: Vec<f64> = Vec::new();
let values = series.map(|s| &s.values).unwrap_or(&empty);
let title_band = if spec.title.is_some() {
common::TITLE_BAND
} else {
0.0
};
if let Some(title) = &spec.title {
items.push(Prim::Text {
x: spec.width / 2.0,
y: common::OUTER_PAD + common::TITLE_FONT,
size: common::TITLE_FONT,
anchor: Anchor::Middle,
fill: ink,
content: title.clone(),
});
}
let has_legend = matches!(
spec.legend,
LegendPos::Top | LegendPos::Bottom | LegendPos::Left | LegendPos::Right
) && spec.categories.iter().any(|c| !c.is_empty());
let legend_top = if has_legend && spec.legend == LegendPos::Top {
common::LEGEND_BAND
} else {
0.0
};
let legend_bottom = if has_legend && spec.legend == LegendPos::Bottom {
common::LEGEND_BAND
} else {
0.0
};
let legend_left = if has_legend && spec.legend == LegendPos::Left {
common::legend_band_width_vertical(m, &spec.categories, label_font)
} else {
0.0
};
let legend_right = if has_legend && spec.legend == LegendPos::Right {
common::legend_band_width_vertical(m, &spec.categories, label_font)
} else {
0.0
};
if has_legend && matches!(spec.legend, LegendPos::Top | LegendPos::Bottom) {
let mut total = 0.0_f64;
let n = spec.categories.len();
for (k, cat) in spec.categories.iter().enumerate() {
total += common::legend_entry_width(m, cat, label_font);
if k == n - 1 {
total -= 16.0;
}
}
let start_x = (spec.width - total) / 2.0;
let legend_cy = if spec.legend == LegendPos::Top {
common::OUTER_PAD + title_band + common::LEGEND_BAND / 2.0
} else {
spec.height - common::OUTER_PAD - common::LEGEND_BAND / 2.0
};
let mut cursor = start_x;
for (i, cat) in spec.categories.iter().enumerate() {
let swatch = series.map(|s| s.fill_at(i)).unwrap_or(ink);
items.push(Prim::Rect {
x: cursor,
y: legend_cy - 6.0,
w: 12.0,
h: 12.0,
fill: swatch,
});
items.push(Prim::Text {
x: cursor + 16.0,
y: legend_cy + label_font * common::TEXT_BASELINE_RATIO,
size: label_font,
anchor: Anchor::Start,
fill: ink,
content: cat.clone(),
});
cursor += common::legend_entry_width(m, cat, label_font);
}
}
if has_legend && matches!(spec.legend, LegendPos::Left | LegendPos::Right) {
let entries: Vec<(String, Color)> = spec
.categories
.iter()
.enumerate()
.map(|(i, cat)| {
let swatch = series.map(|s| s.fill_at(i)).unwrap_or(ink);
(cat.clone(), swatch)
})
.collect();
let band_w = if spec.legend == LegendPos::Left {
legend_left
} else {
legend_right
};
let band_x = if spec.legend == LegendPos::Left {
common::OUTER_PAD
} else {
spec.width - common::OUTER_PAD - band_w
};
let area_top = common::OUTER_PAD + title_band + legend_top;
let area_bottom = spec.height - common::OUTER_PAD - legend_bottom;
common::draw_vertical_legend(
&mut items,
&entries,
band_x,
area_top,
area_bottom,
ink,
label_font,
);
}
let area_top = common::OUTER_PAD + title_band + legend_top;
let area_bottom = spec.height - common::OUTER_PAD - legend_bottom;
let area_left = common::OUTER_PAD + legend_left;
let area_right = spec.width - common::OUTER_PAD - legend_right;
let cx = (area_left + area_right) / 2.0;
let cy = (area_top + area_bottom) / 2.0;
let radius = ((area_right - area_left).min(area_bottom - area_top) / 2.0 * 0.9).max(0.0);
let inner = radius * donut_ratio;
let total: f64 = values.iter().filter(|v| v.is_finite() && **v > 0.0).sum();
let mut labels: Vec<Prim> = Vec::new();
if total > 0.0 && radius > 0.0 {
let mut a0 = -PI / 2.0; for (i, &v) in values.iter().enumerate() {
if !(v.is_finite() && v > 0.0) {
continue; }
let frac = v / total;
let a1 = a0 + frac * 2.0 * PI;
let fill = series.map(|s| s.fill_at(i)).unwrap_or(ink);
let g = Geom {
cx,
cy,
r_outer: radius,
r_inner: inner,
};
if a1 - a0 >= 2.0 * PI - 1e-9 {
let amid = a0 + (a1 - a0) / 2.0;
items.push(make_slice(&g, a0, amid, fill));
items.push(make_slice(&g, amid, a1, fill));
} else {
items.push(make_slice(&g, a0, a1, fill));
}
if spec.data_labels {
let amid = (a0 + a1) / 2.0;
let label_r = if inner > 0.0 {
(inner + radius) / 2.0
} else {
radius * 0.6
};
labels.push(common::value_label(
cx + label_r * amid.cos(),
cy + label_r * amid.sin() + label_font * common::TEXT_BASELINE_RATIO,
label_font,
Anchor::Middle,
LABEL_COLOR,
v,
));
}
a0 = a1;
}
}
items.extend(labels);
Scene {
width: spec.width,
height: spec.height,
items,
}
}
pub(crate) struct Geom {
pub(crate) cx: f64,
pub(crate) cy: f64,
pub(crate) r_outer: f64,
pub(crate) r_inner: f64,
}
pub(crate) fn make_slice(g: &Geom, a0: f64, a1: f64, fill: Color) -> Prim {
Prim::Path {
d: slice_path(g, a0, a1),
fill: Some(fill),
stroke: Some(SLICE_STROKE),
stroke_width: 1.0,
}
}
fn slice_path(g: &Geom, a0: f64, a1: f64) -> String {
let Geom {
cx,
cy,
r_outer,
r_inner,
} = *g;
let laf = if (a1 - a0) > PI { 1 } else { 0 };
let o0 = (cx + r_outer * a0.cos(), cy + r_outer * a0.sin());
let o1 = (cx + r_outer * a1.cos(), cy + r_outer * a1.sin());
let mut d = String::new();
if r_inner > 0.0 {
let i0 = (cx + r_inner * a0.cos(), cy + r_inner * a0.sin());
let i1 = (cx + r_inner * a1.cos(), cy + r_inner * a1.sin());
write!(
d,
"M {} {} A {} {} 0 {} 1 {} {} L {} {} A {} {} 0 {} 0 {} {} Z",
fmt_num(o0.0),
fmt_num(o0.1),
fmt_num(r_outer),
fmt_num(r_outer),
laf,
fmt_num(o1.0),
fmt_num(o1.1),
fmt_num(i1.0),
fmt_num(i1.1),
fmt_num(r_inner),
fmt_num(r_inner),
laf,
fmt_num(i0.0),
fmt_num(i0.1),
)
.unwrap();
} else {
write!(
d,
"M {} {} L {} {} A {} {} 0 {} 1 {} {} Z",
fmt_num(cx),
fmt_num(cy),
fmt_num(o0.0),
fmt_num(o0.1),
fmt_num(r_outer),
fmt_num(r_outer),
laf,
fmt_num(o1.0),
fmt_num(o1.1),
)
.unwrap();
}
d
}