Compare commits

...
5 changed files with 90 additions and 11 deletions
+25 -8
View File
@@ -1,5 +1,7 @@
//! Charts: title, plot area with axes and gridlines, series marks, legend.
use std::collections::HashMap;
use deckcraft_color::{ColorTransform, Rgba, SchemeSlot};
use deckcraft_model::Shape;
use deckcraft_model::chart::{Chart, ChartType, TrendlineType, axis_scale};
@@ -85,9 +87,9 @@ fn bar_outline(ctx: &Ctx, s: &deckcraft_model::chart::Series) -> Option<(Rgba, f
/// Paint of one bar: a `<c:dPt>` fill wins, and a single-series chart that varies colours by point uses one accent per point
/// (not waterfalls, which are never read from a `<c:barChart>`). Pattern fills draw as patterns and
/// `a:noFill` leaves the bar empty, as in PowerPoint.
fn bar_paint(ctx: &Ctx, c: &Chart, si: usize, pi: usize, s: &deckcraft_model::chart::Series) -> Paint {
if let Some((_, f)) = s.point_fills.iter().find(|(k, _)| *k == pi) {
return paint_of(ctx, Some(f), series_fill(ctx, c, si, Some(s)));
fn bar_paint(ctx: &Ctx, c: &Chart, si: usize, pi: usize, s: &deckcraft_model::chart::Series, fills: Option<&HashMap<usize, &Fill>>) -> Paint {
if let Some(f) = fills.and_then(|m| m.get(&pi)) {
return paint_of(ctx, Some(*f), series_fill(ctx, c, si, Some(s)));
}
if c.vary_colors && c.series.len() == 1 && s.fill.is_none() && c.kind != ChartType::Waterfall {
return Paint::Color(series_color(ctx, c, pi));
@@ -883,6 +885,8 @@ fn draw_lines(ctx: &mut RenderContext, draw: &mut LineDrawing<'_, '_>) {
let col = if area_kind { series_fill(rctx, c, si, Some(s)) } else { series_line(rctx, c, si, s) };
let series_stats = s.error_bars.as_ref().map(|_| deckcraft_model::chart::SeriesStats::of(&s.values));
let mut pts = vec![];
// Whether the series has any x value, found once rather than per point.
let any_x = s.x_values.iter().flatten().next().is_some();
for i in 0..ncat {
let Some(v) = s.values.get(i).copied().flatten() else { continue };
let v = if stacked {
@@ -896,11 +900,11 @@ fn draw_lines(ctx: &mut RenderContext, draw: &mut LineDrawing<'_, '_>) {
};
let x = match kind {
ChartType::Scatter | ChartType::Bubble => {
match (s.x_values.get(i).copied().flatten().and_then(to_x), s.x_values.iter().flatten().next()) {
match (s.x_values.get(i).copied().flatten().and_then(to_x), any_x) {
(Some(x), _) => x,
// No x values at all: the points are numbered 1, 2, 3...
(None, None) => to_x(i as f64 + 1.0).unwrap_or(plot.x0 + band * (slot(i) + 0.5)),
(None, Some(_)) => continue,
(None, false) => to_x(i as f64 + 1.0).unwrap_or(plot.x0 + band * (slot(i) + 0.5)),
(None, true) => continue,
}
}
_ => plot.x0 + band * (slot(i) + 0.5),
@@ -970,6 +974,19 @@ fn draw_bars(ctx: &mut RenderContext, draw: &BarDrawing<'_, '_>) -> Vec<(f64, f6
let group = bar + step * (n - 1.0);
// Error bars need each series' mean and spread: worked out once, not per point.
let stats: Vec<deckcraft_model::chart::SeriesStats> = bars.iter().map(|(_, s)| deckcraft_model::chart::SeriesStats::of(&s.values)).collect();
// Each series' per-point fills by point (the first for a point wins), not searched per bar.
let point_fills: Vec<HashMap<usize, &Fill>> = bars
.iter()
.map(|(_, s)| {
let mut m = HashMap::new();
for (k, f) in &s.point_fills {
m.entry(*k).or_insert(f);
}
m
})
.collect();
// A waterfall's running bars, worked out once for all its categories.
let waterfall_bars = if waterfall { c.waterfall_bars() } else { Vec::new() };
for i in 0..ncat {
let mut pos_acc = 0.0;
let mut neg_acc = 0.0;
@@ -983,7 +1000,7 @@ fn draw_bars(ctx: &mut RenderContext, draw: &BarDrawing<'_, '_>) -> Vec<(f64, f6
let v = s.values.get(i).copied().flatten().unwrap_or(0.0) / total;
let start = band * slot(i) + (band - group) / 2.0 + if stacked { 0.0 } else { step * bi as f64 };
let (a, b) = if waterfall {
c.waterfall_bars().get(i).copied().unwrap_or_default()
waterfall_bars.get(i).copied().unwrap_or_default()
} else if stacked {
if v >= 0.0 {
let r = (pos_acc, pos_acc + v);
@@ -1015,7 +1032,7 @@ fn draw_bars(ctx: &mut RenderContext, draw: &BarDrawing<'_, '_>) -> Vec<(f64, f6
};
Paint::Color(series_color(rctx, c, slot))
} else {
bar_paint(rctx, c, *si, i, s)
bar_paint(rctx, c, *si, i, s, point_fills.get(bi))
};
fill_with(ctx, &paint, r.abs(), m);
if !waterfall && let Some((oc, ow)) = bar_outline(rctx, s).filter(|o| o.1 > 0.0) {
+3 -1
View File
@@ -131,9 +131,11 @@ fn place(out: &mut Placed, rctx: &Ctx, fields: &SlideFields, s: &Shape, parent:
}
ShapeKind::Table(t) => {
// Cell text, laid out exactly as the table draws it.
// The table's style is looked up once, not once per cell.
let def = table::resolve_style(rctx, t);
for (r, c, rect) in table::cell_rects(t, &table::row_heights(rctx, t, fields)) {
let Some(cell) = t.cell(r, c).filter(|cell| !cell.text.is_empty()) else { continue };
let body = table::cell_body(rctx, t, r, c, cell);
let body = table::cell_body_with_def(rctx, def, t, r, c, cell);
let tmp = Shape::table_cell(body.clone());
place_text(out, rctx, fields, s.id, &tmp, &body, rect, m);
}
+27 -1
View File
@@ -33,6 +33,10 @@ pub struct Renderer {
/// Visits spent on outer-shadow silhouette passes, kept apart so they cannot use up the budget
/// of the normal drawing that follows.
shadow_work: usize,
/// `RenderOpts::skip` as a set, with the slice (address, length) it was built from: the
/// slice can be as long as the slide, and is tested for every shape drawn. Forgotten when a
/// frame starts, so another frame's slice at the same address is never mistaken for it.
skip: Option<(usize, usize, std::collections::HashSet<ShapeId>)>,
}
impl Default for Renderer {
@@ -48,6 +52,7 @@ impl Default for Renderer {
in_shadow_pass: false,
render_work: 0,
shadow_work: 0,
skip: None,
}
}
}
@@ -163,6 +168,7 @@ impl Renderer {
pub(crate) fn slide(&mut self, pres: &Presentation, slide: &Slide, index: usize, opts: &RenderOpts) -> Image {
self.render_work = 0;
self.shadow_work = 0;
self.skip = None;
self.in_shadow_pass = false;
let (mut ctx, w, h, view) = Self::canvas(pres, opts, needs_filters(pres, slide));
let f = Frame { pres, opts, fields: Self::fields(pres, index, opts) };
@@ -178,6 +184,7 @@ impl Renderer {
pub(crate) fn slide_part(&mut self, pres: &Presentation, slide: &Slide, index: usize, part: SlidePart, opts: &RenderOpts) -> Image {
self.render_work = 0;
self.shadow_work = 0;
self.skip = None;
self.in_shadow_pass = false;
let (mut ctx, w, h, view) = Self::canvas(pres, opts, needs_filters(pres, slide));
let f = Frame { pres, opts, fields: Self::fields(pres, index, opts) };
@@ -212,6 +219,7 @@ impl Renderer {
let mix = if mix.is_finite() { mix.clamp(0.0, 1.0) } else { 1.0 };
self.render_work = 0;
self.shadow_work = 0;
self.skip = None;
self.in_shadow_pass = false;
let (mut ctx, w, h, view) = Self::canvas(pres, opts, needs_filters(pres, old.0) || needs_filters(pres, new.0));
let (Some(co), Some(cn)) = (Ctx::for_slide(pres, old.0), Ctx::for_slide(pres, new.0)) else { return self.finish(ctx, w, h) };
@@ -260,6 +268,7 @@ impl Renderer {
pub(crate) fn layout(&mut self, pres: &Presentation, mi: usize, li: Option<usize>, opts: &RenderOpts) -> Image {
self.render_work = 0;
self.shadow_work = 0;
self.skip = None;
self.in_shadow_pass = false;
let (w, h) = output_size(pres, opts);
let mut ctx = RenderContext::new_with(w, h, vello_cpu::RenderSettings { num_threads: 0, ..Default::default() });
@@ -385,8 +394,25 @@ impl Renderer {
geo
}
/// Whether `id` is in `skip`, looked up in a set built once per frame and slice.
fn skipped(&mut self, skip: &[ShapeId], id: ShapeId) -> bool {
if skip.is_empty() {
return false;
}
let key = (skip.as_ptr() as usize, skip.len());
match &self.skip {
Some((p, n, set)) if (*p, *n) == key => set.contains(&id),
_ => {
let set: std::collections::HashSet<ShapeId> = skip.iter().copied().collect();
let hit = set.contains(&id);
self.skip = Some((key.0, key.1, set));
hit
}
}
}
fn shape_at(&mut self, ctx: &mut RenderContext, f: &Frame, rctx: &Ctx, s: &Shape, parent: Affine, depth: usize) {
if depth > 32 || (s.hidden && !f.opts.show_hidden) || f.opts.skip.contains(&s.id) {
if depth > 32 || (s.hidden && !f.opts.show_hidden) || self.skipped(f.opts.skip, s.id) {
return;
}
let work = if self.in_shadow_pass { &mut self.shadow_work } else { &mut self.render_work };
+2 -1
View File
@@ -191,7 +191,8 @@ pub(crate) fn limit_dash_count(path: &BezPath, pat: Vec<f64>) -> Vec<f64> {
return pat;
}
let len: f64 = path.segments().map(|s| s.arclen(1e-2)).sum();
let dashes = len / period;
// Every dash of the pattern counts, not each repeat of it: a custom pattern can hold many.
let dashes = len / period * pat.len().div_ceil(2) as f64;
if !len.is_finite() {
return vec![];
}
+33
View File
@@ -798,4 +798,37 @@ fn a_tiny_dash_on_a_huge_shape_draws_without_running_out_of_memory() {
p.line_to((4.0e6, 0.0));
let pat = limit_dash_count(&p, vec![1.0, 1.0]);
assert!((4.0e6 / (pat[0] + pat[1]) - MAX_DASHES).abs() < 1.0, "{pat:?}");
// A custom pattern of 1,000 dashes counts each of them: the limit is on dashes drawn.
let pat = limit_dash_count(&p, vec![1.0; 2000]);
let period: f64 = pat.iter().sum();
assert!((4.0e6 / period * 1000.0 - MAX_DASHES).abs() < 1.0, "{}", period);
}
/// A bar chart with 20,000 points, each with its own fill, and a sparse scatter of the same size:
/// the per-point fills and the scatter's "has any x" are looked up, not searched per point.
#[test]
fn many_point_fills_and_sparse_scatter_x_render() {
use deckcraft_model::ShapeKind;
use deckcraft_model::chart::{Chart, ChartType};
use deckcraft_model::style::Fill;
let n = 20_000;
let chart = |kind: ChartType| {
let mut c = Chart::sample(kind);
c.series.truncate(1);
c.categories = (0..n).map(|i| i.to_string()).collect();
c.series[0].values = (0..n).map(|i| Some(i as f64)).collect();
c
};
let mut bars = chart(ChartType::Column);
bars.series[0].point_fills = (0..n).rev().map(|i| (i, Fill::solid(ColorRef::rgb(Rgba::rgb(200, 0, 0))))).collect();
let mut scatter = chart(ChartType::Scatter);
scatter.series[0].x_values = (0..n).map(|i| (i + 1 == n).then_some(1.0)).collect();
let shape = |id: u32, c: Chart| Shape {
id: ShapeId(id),
xfrm: Some(Xfrm::new(10.0, 10.0, 400.0, 300.0)),
kind: ShapeKind::Chart(Box::new(c)),
..Default::default()
};
let img = render(vec![shape(2, bars), shape(3, scatter)]);
assert!(img.width > 0);
}