diff --git a/crates/engine/src/cmd/graph/labels.rs b/crates/engine/src/cmd/graph/labels.rs index 9d521c5d9..b11571425 100644 --- a/crates/engine/src/cmd/graph/labels.rs +++ b/crates/engine/src/cmd/graph/labels.rs @@ -112,10 +112,12 @@ impl Gen<'_> { let start = if middle { Point::new(at.x, at.y - half) } else { at }; let mut t = self.text_object(role, start, &s.replace('|', "\n"), justify); // Kept leading or paragraph spacing moves the lines: put the middle of the first and last baselines on `at`. An - // empty first or last line has no baseline, and such a label stays where the default leading puts it. + // empty line has no baseline: an empty first or last line leaves the label where the default leading puts it. if middle && kept_para(self.styles, role).is_some() { let ys: Vec = t.baselines().map(|(a, _)| a.y).collect(); - if let (Some(first), Some(last), true) = (ys.first(), ys.last(), ys.len() == lines) { + let ends_set = s.split('|').next().is_some_and(|l| !l.is_empty()) && s.split('|').next_back().is_some_and(|l| !l.is_empty()); + let set = s.split('|').filter(|l| !l.is_empty()).count(); + if let (Some(first), Some(last), true) = (ys.first(), ys.last(), ends_set && ys.len() == set) { t.xf = Affine::translate((0.0, half - (first + last) / 2.0)) * t.xf; } } diff --git a/crates/engine/src/cmd/graph/tests.rs b/crates/engine/src/cmd/graph/tests.rs index 7f85763a7..efcd04320 100644 --- a/crates/engine/src/cmd/graph/tests.rs +++ b/crates/engine/src/cmd/graph/tests.rs @@ -4370,6 +4370,33 @@ fn a_centered_label_with_kept_leading_stays_centered_when_the_graph_is_drawn_aga assert!((after - before).abs() < 0.01, "the two-line category label keeps its middle: {before} {after}"); } +#[test] +fn a_centered_label_with_an_empty_middle_line_and_kept_leading_stays_centered() { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 800, "height": 800})).unwrap(); + let csv = ",2024,2025\nQ||1,3,2\nQ2,5,4\nQ3,-1,6"; + let id = NodeId( + s.execute("graph.create", &json!({"type": "bar", "x": 100, "y": 100, "width": 300, "height": 200, "csv": csv})).unwrap()["id"] + .as_u64() + .unwrap(), + ); + let middle = |s: &Session| { + let n = s.doc().unwrap().doc.node(id).unwrap().clone(); + let label = + group(&n, "Axes").children().unwrap().iter().find(|c| matches!(&c.kind, NodeKind::Text(t) if t.plain_text() == "Q\n\n1")).unwrap(); + let b = label.geometric_bounds().unwrap(); + (b.y0 + b.y1) / 2.0 + }; + let before = middle(&s); + let l = text_id(&s, id, "Axes", "Q\n\n1"); + s.execute("select.set", &json!({"ids": [l.0]})).unwrap(); + s.execute("text.setFormat", &json!({"autoLeading": 400})).unwrap(); + s.execute("select.set", &json!({"ids": [id.0]})).unwrap(); + s.execute("graph.setData", &json!({"id": id.0, "csv": csv})).unwrap(); + let after = middle(&s); + assert!((after - before).abs() < 0.01, "the three-line category label keeps its middle: {before} {after}"); +} + #[test] fn a_legend_across_top_measures_its_labels_with_system_layout() { let mut s = Session::new();