From cdd3a59dbc868cd4f8d3d3688fe21a485034482f Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Tue, 3 Nov 2026 22:55:35 -0500 Subject: [PATCH] Treemap rows keep running aggregates; General-fit width is clamped (#3290) --- crates/chart/src/render/kinds.rs | 17 +++++++++-------- crates/numfmt/src/decimal.rs | 5 +++++ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/crates/chart/src/render/kinds.rs b/crates/chart/src/render/kinds.rs index 54ce6bec8..10286107b 100644 --- a/crates/chart/src/render/kinds.rs +++ b/crates/chart/src/render/kinds.rs @@ -203,12 +203,11 @@ pub(super) fn pie(ctx: &mut Ctx, chart: &Chart, data: &ChartData, area: Bx, inne // ---------------------------------------------------------------- treemap -fn worst(row: &[f64], side: f64) -> f64 { - let s: f64 = row.iter().sum(); +/// Worst aspect ratio of a row with the given sum, smallest and largest area. +fn worst(s: f64, mn: f64, mx: f64, side: f64) -> f64 { if s <= 0.0 || side <= 0.0 { return f64::INFINITY; } - let (mn, mx) = row.iter().fold((f64::INFINITY, 0.0f64), |(a, b), v| (a.min(*v), b.max(*v))); let s2 = s * s; let w2 = side * side; (w2 * mx / s2).max(s2 / (w2 * mn)) @@ -225,6 +224,8 @@ fn squarify(items: &[(usize, f64)], r: Bx) -> Vec<(usize, Bx)> { let mut out = Vec::with_capacity(items.len()); let mut rem = r; let mut row: Vec<(usize, f64)> = Vec::new(); + // The row's sum, smallest and largest area, kept as items join so each step is O(1). + let (mut sum, mut mn, mut mx) = (0.0f64, f64::INFINITY, 0.0f64); let mut i = 0; let layout = |row: &[(usize, f64)], rem: &mut Bx, out: &mut Vec<(usize, Bx)>| { let s: f64 = row.iter().map(|x| x.1).sum(); @@ -256,15 +257,14 @@ fn squarify(items: &[(usize, f64)], r: Bx) -> Vec<(usize, Bx)> { while let Some(&(j, v)) = items.get(i) { let a = v * scale; let side = rem.w.min(rem.h) as f64; - let cur: Vec = row.iter().map(|x| x.1).collect(); - let mut with = cur.clone(); - with.push(a); - if row.is_empty() || worst(&with, side) <= worst(&cur, side) { + if row.is_empty() || worst(sum + a, mn.min(a), mx.max(a), side) <= worst(sum, mn, mx, side) { row.push((j, a)); + (sum, mn, mx) = (sum + a, mn.min(a), mx.max(a)); i += 1; } else { layout(&row, &mut rem, &mut out); row.clear(); + (sum, mn, mx) = (0.0, f64::INFINITY, 0.0); } } if !row.is_empty() { @@ -279,6 +279,7 @@ pub(super) fn treemap(ctx: &mut Ctx, chart: &Chart, data: &ChartData, area: Bx) s.values.iter().take(MAX_MARKERS).enumerate().filter_map(|(j, v)| v.filter(|v| *v > 0.0).map(|v| (j, v))).collect(); items.sort_by(|a, b| b.1.total_cmp(&a.1)); let f = Fmt::new(&s.number_format); + let value_of: std::collections::HashMap = items.iter().copied().collect(); for (j, b) in squarify(&items, area) { let fill = point_color(data, j); ctx.rect(b.x, b.y, b.w, b.h, fill, Some((WHITE, 1.5))); @@ -286,7 +287,7 @@ pub(super) fn treemap(ctx: &mut Ctx, chart: &Chart, data: &ChartData, area: Bx) let name = ctx.fit(&cat_label(data, j), LABEL_SIZE, b.w - 8.0); ctx.text(b.x + 4.0, b.y + 4.0, &name, LABEL_SIZE, on(fill), (HAlign::Left, VAlign::Top)); if chart.data_labels && b.h > LABEL_SIZE * 3.2 { - let v = items.iter().find(|x| x.0 == j).map(|x| x.1).unwrap_or(0.0); + let v = value_of.get(&j).copied().unwrap_or(0.0); let t = ctx.fit(&f.format(v, None), LABEL_SIZE, b.w - 8.0); ctx.text(b.x + 4.0, b.y + 4.0 + LABEL_SIZE * 1.4, &t, LABEL_SIZE, on(fill), (HAlign::Left, VAlign::Top)); } diff --git a/crates/numfmt/src/decimal.rs b/crates/numfmt/src/decimal.rs index 67071d4e0..b6c831f71 100644 --- a/crates/numfmt/src/decimal.rs +++ b/crates/numfmt/src/decimal.rs @@ -136,6 +136,9 @@ pub(crate) fn fixed(a: f64, decimals: usize) -> (String, String) { /// General format squeezed into `max_chars` characters. Excel shows fewer decimals, then /// scientific notation, then "####" when a column is narrow; `None` means the caller paints "####". pub fn format_general_fit(n: f64, max_chars: usize) -> Option { + // The widest fixed rendering of a double is about 330 characters; a larger request only + // pads zero decimals the trim below throws away. + let max_chars = max_chars.min(1024); if !n.is_finite() { return None; } @@ -214,6 +217,8 @@ mod tests { fn general_fit() { assert_eq!(format_general_fit(1e11, 11).as_deref(), Some("1E+11")); assert_eq!(format_general_fit(12345678901.0, 11).as_deref(), Some("12345678901")); + // A huge width neither allocates width-sized padding nor changes the text. + assert_eq!(format_general_fit(1.5, usize::MAX).as_deref(), Some("1.5")); assert_eq!(format_general_fit(123456789012.0, 11).as_deref(), Some("1.23457E+11")); assert_eq!(format_general_fit(0.1 + 0.2, 11).as_deref(), Some("0.3")); assert_eq!(format_general_fit(1.0 / 3.0, 11).as_deref(), Some("0.333333333"));