diff --git a/crates/pptx/src/read/chart.rs b/crates/pptx/src/read/chart.rs index ecd43cba..49114976 100644 --- a/crates/pptx/src/read/chart.rs +++ b/crates/pptx/src/read/chart.rs @@ -195,9 +195,8 @@ fn chart_within(root: &El, mut budget: usize) -> Option { "surfaceChart" | "surface3DChart" => ChartType::Surface, _ => continue, }; - if let Some(v) = g.child_val("varyColors") { - c.vary_colors = v == "1" || v == "true"; - } + // A missing means "vary by point" (the schema default is true), as PowerPoint draws it. + c.vary_colors = g.child_val("varyColors").is_none_or(|v| v == "1" || v == "true"); // A bar group without c:gapWidth / c:overlap takes the schema's defaults (150 %, 0 %), // not the 219 % / -27 % PowerPoint writes into its new charts. let bar_group = g.is("barChart") || g.is("bar3DChart"); @@ -618,4 +617,23 @@ mod tests { let t = "Mine"; assert_eq!(chart(r#""#, t, 1).title.as_deref(), Some("Mine")); } + + const NS_CHART: &str = "http://schemas.openxmlformats.org/drawingml/2006/chart"; + + fn vary_xml(vary: &str) -> String { + let ser = r#""#; + let bar = format!(r#"{vary}{ser}"#); + format!(r#"{bar}"#) + } + + #[test] + fn missing_vary_colors_means_vary_by_point() { + assert!(read_chart_xml(vary_xml("").as_bytes()).is_some_and(|c| c.vary_colors)); + } + + #[test] + fn explicit_vary_colors_is_respected() { + assert!(read_chart_xml(vary_xml(r#""#).as_bytes()).is_some_and(|c| !c.vary_colors)); + assert!(read_chart_xml(vary_xml(r#""#).as_bytes()).is_some_and(|c| c.vary_colors)); + } } diff --git a/crates/render/src/chart.rs b/crates/render/src/chart.rs index c6f35fdc..1f8730d6 100644 --- a/crates/render/src/chart.rs +++ b/crates/render/src/chart.rs @@ -44,6 +44,18 @@ fn point_fill(ctx: &Ctx, c: &Chart, i: usize, s: &deckcraft_model::chart::Series } } +/// Fill 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 ``). +fn bar_fill(ctx: &Ctx, c: &Chart, si: usize, pi: usize, s: &deckcraft_model::chart::Series) -> Rgba { + if s.point_fills.iter().any(|(k, _)| *k == pi) { + return point_fill(ctx, c, si, s, pi); + } + if c.vary_colors && c.series.len() == 1 && s.fill.is_none() && c.kind != ChartType::Waterfall { + return series_color(ctx, c, pi); + } + series_fill(ctx, c, si, Some(s)) +} + fn polyline(pts: &[Point]) -> BezPath { let mut p = BezPath::new(); for (k, q) in pts.iter().enumerate() { @@ -619,7 +631,7 @@ fn draw_inner(ctx: &mut RenderContext, rctx: &Ctx, c: &Chart, m: Affine, w: f64, }; series_color(rctx, c, slot) } else { - point_fill(rctx, c, *si, s, i) + bar_fill(rctx, c, *si, i, s) }; ctx.set_transform(m); ctx.set_paint(color(fill, 1.0)); diff --git a/crates/render/src/tests.rs b/crates/render/src/tests.rs index 84754b18..7b700341 100644 --- a/crates/render/src/tests.rs +++ b/crates/render/src/tests.rs @@ -2400,3 +2400,20 @@ fn render_an_emf_file_to_look_at() { let img = render_slide(&p, 0, &RenderOpts { scale: 1.0, ..Default::default() }); let _ = std::fs::write(out, img.to_png()); } + +#[test] +fn single_series_column_chart_varies_colours_by_point() { + let render = |vary: bool| { + let mut c = Chart::sample(ChartType::Column); + c.series.truncate(1); + c.vary_colors = vary; + let sh = Shape { + id: ShapeId(800), + xfrm: Some(Xfrm::new(50.0, 50.0, 400.0, 300.0)), + kind: ShapeKind::Chart(Box::new(c)), + ..Default::default() + }; + render_slide(&deck_with(vec![sh]), 0, &RenderOpts { scale: 1.0, ..Default::default() }).pixels + }; + assert_ne!(render(true), render(false)); +}