From c4f41b84531e8213e42e6eb66e235b4a33fc5287 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Tue, 3 Nov 2026 11:34:45 +0000 Subject: [PATCH] render: index chart point fills, scatter x presence and waterfall bars once per chart --- crates/render/src/chart.rs | 33 +++++++++++++++++++++++++-------- crates/render/src/tests_wild.rs | 29 +++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 8 deletions(-) diff --git a/crates/render/src/chart.rs b/crates/render/src/chart.rs index d13f65223..53794987a 100644 --- a/crates/render/src/chart.rs +++ b/crates/render/src/chart.rs @@ -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 `` 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 ``). 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>) -> 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 = 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> = 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) { diff --git a/crates/render/src/tests_wild.rs b/crates/render/src/tests_wild.rs index f4cfbbe40..227ffc791 100644 --- a/crates/render/src/tests_wild.rs +++ b/crates/render/src/tests_wild.rs @@ -803,3 +803,32 @@ fn a_tiny_dash_on_a_huge_shape_draws_without_running_out_of_memory() { 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); +}