From aed386e26936970fd3d19ccb7483f4401d281a0d Mon Sep 17 00:00:00 2001 From: Nikita Rakitin Date: Wed, 14 Oct 2026 04:39:00 +0300 Subject: [PATCH] Charts: draw, read and write value-axis minor gridlines --- crates/model/src/chart.rs | 3 +++ crates/pptx/src/read/chart.rs | 11 +++++++++++ crates/pptx/src/write/chart.rs | 3 +++ crates/pptx/src/write/mod.rs | 2 +- crates/render/src/chart.rs | 20 ++++++++++++++++++++ 5 files changed, 38 insertions(+), 1 deletion(-) diff --git a/crates/model/src/chart.rs b/crates/model/src/chart.rs index aebee497..936d0c9e 100644 --- a/crates/model/src/chart.rs +++ b/crates/model/src/chart.rs @@ -159,6 +159,8 @@ pub struct Chart { pub title_size: Option, pub data_labels: bool, pub gridlines: bool, + /// `c:minorGridlines` on the value axis: five to a major unit, between the major ones. + pub minor_gridlines: bool, pub axis_titles: (Option, Option), /// Chart style number (1–48) and colour palette (`colorful1`, `monochrome3`…). pub style: u32, @@ -204,6 +206,7 @@ impl Default for Chart { title_size: None, data_labels: false, gridlines: true, + minor_gridlines: false, axis_titles: (None, None), style: 2, palette: "colorful1".into(), diff --git a/crates/pptx/src/read/chart.rs b/crates/pptx/src/read/chart.rs index 6d7541ff..a3fe4e86 100644 --- a/crates/pptx/src/read/chart.rs +++ b/crates/pptx/src/read/chart.rs @@ -266,6 +266,7 @@ pub(crate) fn chart_from(root: &El) -> Option { let cat_ax = pa.child("catAx").or_else(|| pa.child("dateAx")).or_else(|| pa.children_named("valAx").find(|a| !vertical(a))); if let Some(v) = val_ax { c.gridlines = v.child("majorGridlines").is_some(); + c.minor_gridlines = v.child("minorGridlines").is_some(); let sc = v.child("scaling"); c.y_min = sc.and_then(|s| s.child_val("min")).and_then(|v| v.parse().ok()).filter(|v: &f64| v.is_finite()); c.y_max = sc.and_then(|s| s.child_val("max")).and_then(|v| v.parse().ok()).filter(|v: &f64| v.is_finite()); @@ -310,6 +311,16 @@ fn text_size(e: Option<&El>) -> Option { mod tests { use super::*; + #[test] + fn minor_gridlines_are_read_and_written_back() { + let mut c = Chart::sample(ChartType::Line); + c.minor_gridlines = true; + let back = read_chart_xml(&crate::write::chart::chart_xml(&c)).unwrap(); + assert!(back.gridlines && back.minor_gridlines); + c.minor_gridlines = false; + assert!(!read_chart_xml(&crate::write::chart::chart_xml(&c)).unwrap().minor_gridlines); + } + #[test] fn legend_position_and_overlay_are_read() { let legend = r#""#; diff --git a/crates/pptx/src/write/chart.rs b/crates/pptx/src/write/chart.rs index 76567c9e..fc7406fe 100644 --- a/crates/pptx/src/write/chart.rs +++ b/crates/pptx/src/write/chart.rs @@ -286,6 +286,9 @@ fn axes(w: &mut W, c: &Chart, g: Group) { if c.gridlines { w.empty0("c:majorGridlines"); } + if c.minor_gridlines { + w.empty0("c:minorGridlines"); + } if let Some(t) = &c.axis_titles.1 { title(w, t); } diff --git a/crates/pptx/src/write/mod.rs b/crates/pptx/src/write/mod.rs index 7fb6a713..a5ebcf95 100644 --- a/crates/pptx/src/write/mod.rs +++ b/crates/pptx/src/write/mod.rs @@ -1,6 +1,6 @@ //! PPTX export: [`Presentation`] → package. -mod chart; +pub(crate) mod chart; pub(crate) mod dml; mod shapes; mod theme; diff --git a/crates/render/src/chart.rs b/crates/render/src/chart.rs index eb2b0a96..cf71fe0b 100644 --- a/crates/render/src/chart.rs +++ b/crates/render/src/chart.rs @@ -282,6 +282,26 @@ pub fn draw(ctx: &mut RenderContext, rctx: &Ctx, c: &Chart, m: Affine, w: f64, h (true, Some((min2, max2, _))) => plot.y1 - (v - min2) / (max2 - min2).max(1e-9) * plot.height(), _ => to_v(v), }; + // Minor gridlines, under the major ones: five to a major unit. + if c.minor_gridlines { + let minor = step / 5.0; + let n = (((max - min) / minor.max(1e-12)).round() as i64).clamp(1, 500); + let mut g = BezPath::new(); + for k in (0..=n).filter(|k| k % 5 != 0) { + let pos = to_v(min + k as f64 * minor); + if horiz { + g.move_to((pos, plot.y0)); + g.line_to((pos, plot.y1)); + } else { + g.move_to((plot.x0, pos)); + g.line_to((plot.x1, pos)); + } + } + ctx.set_transform(m); + ctx.set_paint(color(Rgba::rgb(0xF2, 0xF2, 0xF2), 1.0)); + ctx.set_stroke(kurbo::Stroke::new(0.75)); + ctx.stroke_path(&g); + } // Gridlines and value labels. for v in ticks((min, max, step)) { let pos = to_v(v);