From 91873e03eb82d723b83d692e42d350490142e773 Mon Sep 17 00:00:00 2001 From: MarekSpodBiedry <73122968+MarekSpodBiedry@users.noreply.github.com> Date: Mon, 12 Oct 2026 08:31:20 +0200 Subject: [PATCH] Draw data labels on area and stacked area charts (#545) --- crates/chart/src/render.rs | 9 +++++++++ crates/chart/src/tests.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/crates/chart/src/render.rs b/crates/chart/src/render.rs index 63d30d982..f1b1b3eac 100644 --- a/crates/chart/src/render.rs +++ b/crates/chart/src/render.rs @@ -970,6 +970,8 @@ fn cartesian(ctx: &mut Ctx, chart: &Chart, data: &ChartData, area: Bx, horizonta let nslots = slot_keys.len().max(1) as f32; let bw = cw / (nslots + 1.5); let labels_on = chart.data_labels && ncat <= MAX_LABELS; + // Area labels wait until every area is filled, so a later (stacked) area can't paint over them. + let mut area_labels = Vec::new(); // Areas first, then columns, then lines on top. for pass in 0..3 { @@ -998,6 +1000,10 @@ fn cartesian(ctx: &mut Ctx, chart: &Chart, data: &ChartData, area: Bx, horizonta let c = center(j); top.push([c, vpos(t, &sc)]); base.push([c, vpos(b, &sc)]); + // Source values (not stack totals), just above the top edge like the line labels. + if labels_on && sp.is_some() && let Some(v) = s.values.get(j).copied().flatten() { + area_labels.push((c, vpos(t, &sc) - 5.0, fmt.format(v, None))); + } } if top.len() == 1 { // A single point: give it some width. @@ -1072,6 +1078,9 @@ fn cartesian(ctx: &mut Ctx, chart: &Chart, data: &ChartData, area: Bx, horizonta } } } + for (x, y, t) in area_labels.drain(..) { + ctx.text(x, y, &t, LABEL_SIZE, TEXT, HAlign::Center, VAlign::Bottom); + } } // Trendlines on top: x is the category number 1..n. let mut trend_labels = Vec::new(); diff --git a/crates/chart/src/tests.rs b/crates/chart/src/tests.rs index a94267713..7fb830184 100644 --- a/crates/chart/src/tests.rs +++ b/crates/chart/src/tests.rs @@ -902,3 +902,29 @@ fn union_series_references() { c.series[0].values = format!("{}Sheet1!$B$1,Sheet1!$B$2{}", "(".repeat(5000), ")".repeat(5000)); let _ = resolve(&wb, 0, &c); } + +#[test] +fn area_charts_draw_source_value_labels_only_when_enabled() { + for kind in [ChartKind::Area, ChartKind::AreaStacked] { + let d = sample(kind); + let mut c = chart(kind); + c.data_labels = false; + let off = render(&c, &d, 640.0, 400.0, &ApproxMeasure); + c.data_labels = true; + let on = render(&c, &d, 640.0, 400.0, &ApproxMeasure); + let count = |p: &[Prim], v: &str| texts(p).iter().filter(|t| **t == v).count(); + // One label per point: 3 series x 6 categories. + assert_eq!(texts(&on).len(), texts(&off).len() + 18, "{kind:?}"); + for v in ["$12,000", "$38,000", "$9,000", "$27,000"] { + assert_eq!(count(&on, v), count(&off, v) + 1, "{kind:?}: {v}"); + } + // Stacked areas label the source values, not cumulative totals (12,000 + 38,000). + assert_eq!(count(&on, "$50,000"), count(&off, "$50,000"), "{kind:?}: cumulative total shown"); + let polys = |p: &[Prim]| p.iter().filter(|p| matches!(p, Prim::Polygon { .. })).cloned().collect::>(); + assert_eq!(polys(&on), polys(&off)); + // The first series' labels are drawn after every area, so the next area can't cover them. + let last_poly = on.iter().rposition(|p| matches!(p, Prim::Polygon { .. })); + let first_label = on.iter().position(|p| matches!(p, Prim::Text { text, .. } if text == "$12,000")); + assert!(first_label > last_poly, "{kind:?}: label drawn under a later area"); + } +}