From fd1b0a1965c52ac31f31fbed7a5a380e1525942a Mon Sep 17 00:00:00 2001 From: Nikita Rakitin Date: Wed, 14 Oct 2026 04:12:18 +0300 Subject: [PATCH] Charts: draw the axes' titles --- crates/render/src/chart.rs | 17 +++++++++++++++++ crates/render/src/tests.rs | 22 ++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/crates/render/src/chart.rs b/crates/render/src/chart.rs index eb2b0a96..d9181472 100644 --- a/crates/render/src/chart.rs +++ b/crates/render/src/chart.rs @@ -246,6 +246,23 @@ pub fn draw(ctx: &mut RenderContext, rctx: &Ctx, c: &Chart, m: Affine, w: f64, h // Bars are drawn first and lines over them; a combo chart has both. let bar_kind = c.series.iter().map(|s| c.series_kind(s)).find(|k| k.is_bar_like()); let horiz = if combo { bar_kind.is_some_and(ChartType::horizontal) } else { c.kind.horizontal() }; + // Axis titles: the left one turned to read upwards, the bottom one centred under the axis. + // On a horizontal bar chart the category axis is the left one. + let (left_title, bottom_title) = + if horiz { (c.axis_titles.0.as_deref(), c.axis_titles.1.as_deref()) } else { (c.axis_titles.1.as_deref(), c.axis_titles.0.as_deref()) }; + let band_h = base * 1.6; + if let Some(t) = bottom_title.filter(|t| !t.is_empty()) { + let r = Rect::new(area.x0, area.y1 - band_h, area.x1, area.y1); + label(ctx, rctx, m, t, r, base, Align::Center, Anchor::Bottom, text_col, true); + area.y1 -= band_h + base * 0.3; + } + if let Some(t) = left_title.filter(|t| !t.is_empty()) { + let (cx, cy) = (area.x0 + band_h / 2.0, (area.y0 + area.y1) / 2.0); + let turned = m * Affine::translate((cx, cy)) * Affine::rotate(-std::f64::consts::FRAC_PI_2); + let half = area.height() / 2.0; + label(ctx, rctx, turned, t, Rect::new(-half, -band_h / 2.0, half, band_h / 2.0), base, Align::Center, Anchor::Middle, text_col, true); + area.x0 += band_h + base * 0.3; + } // As many major units as the value axis has room for labels (PowerPoint's automatic unit). let axis_len = if horiz { area.width() - base * 4.5 } else { area.height() - base * 1.8 }; let max_intervals = (axis_len / if horiz { base * 6.0 } else { base * 1.9 }).floor().clamp(2.0, 12.0) as usize; diff --git a/crates/render/src/tests.rs b/crates/render/src/tests.rs index 0de3d695..9cf606af 100644 --- a/crates/render/src/tests.rs +++ b/crates/render/src/tests.rs @@ -155,6 +155,28 @@ fn combo_chart_draws_columns_and_a_line_on_its_own_axis() { assert!(col[0] < 250 || col[1] < 250 || col[2] < 250, "no column: {col:?}"); } +#[test] +fn chart_axis_titles_are_drawn() { + let draw = |titles: (Option, Option)| { + let mut c = Chart::sample(ChartType::Column); + c.title = None; + c.legend = None; + c.axis_titles = titles; + let sh = Shape { id: ShapeId(624), xfrm: Some(Xfrm::new(0.0, 0.0, 960.0, 540.0)), kind: ShapeKind::Chart(Box::new(c)), ..Default::default() }; + render_slide(&deck_with(vec![sh]), 0, &RenderOpts { scale: 1.0, ..Default::default() }) + }; + let ink = |img: &Image, r: (u32, u32, u32, u32)| { + (r.0..r.2).flat_map(|x| (r.1..r.3).map(move |y| (x, y))).filter(|(x, y)| img.pixel(*x, *y)[0] < 160).count() + }; + let bare = draw((None, None)); + let titled = draw((Some("Month".into()), Some("Revenue".into()))); + // A turned "Revenue" down the left edge, "Month" along the bottom. + let left = (38, 150, 62, 390); + let bottom = (380, 490, 580, 520); + assert!(ink(&titled, left) > ink(&bare, left) + 30, "value axis title"); + assert!(ink(&titled, bottom) > ink(&bare, bottom) + 30, "category axis title"); +} + #[test] fn placeholder_prompts_only_in_edit_view() { let p = Presentation::default();