mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 17:44:02 +00:00
Graphs: centered labels with kept leading stay centered
Category labels beside an axis and labels in pie wedges are centered on their place by the default leading. A multi-line label that keeps more leading or paragraph spacing ran down from that place instead. The graph now puts the middle of such a label's first and last baselines on its place.
This commit is contained in:
@@ -48,17 +48,21 @@ pub(super) fn label_style(styles: &vectorcraft_doc::LabelStyles, role: Role) ->
|
||||
/// The paragraph attributes labels of `role` are set with: the kept ones, else the defaults. The caller sets the
|
||||
/// alignment.
|
||||
pub(super) fn label_para(styles: &vectorcraft_doc::LabelStyles, role: Role) -> vectorcraft_doc::ParaStyle {
|
||||
let kept = match role {
|
||||
Role::Value => &styles.values_para,
|
||||
Role::Category => &styles.categories_para,
|
||||
Role::Legend => &styles.legend_para,
|
||||
};
|
||||
let mut para = kept.clone().unwrap_or_default();
|
||||
let mut para = kept_para(styles, role).cloned().unwrap_or_default();
|
||||
// A file's attributes within the limits the Paragraph controls keep to (they are copied onto every label).
|
||||
para.keep_spacing_in_range();
|
||||
para
|
||||
}
|
||||
|
||||
/// The paragraph attributes labels of `role` keep, if any.
|
||||
fn kept_para(styles: &vectorcraft_doc::LabelStyles, role: Role) -> Option<&vectorcraft_doc::ParaStyle> {
|
||||
match role {
|
||||
Role::Value => styles.values_para.as_ref(),
|
||||
Role::Category => styles.categories_para.as_ref(),
|
||||
Role::Legend => styles.legend_para.as_ref(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The anti-aliasing method labels of `role` are drawn with: the kept one, else Sharp.
|
||||
pub(super) fn label_anti_alias(styles: &vectorcraft_doc::LabelStyles, role: Role) -> vectorcraft_doc::text::TypeAntiAlias {
|
||||
let kept = match role {
|
||||
@@ -85,6 +89,11 @@ impl Gen<'_> {
|
||||
self.path(PathData::from_bezpath(&bp), Paint::None, Paint::solid(Color::BLACK), 0.5)
|
||||
}
|
||||
pub(super) fn text(&mut self, role: Role, at: Point, s: &str, justify: Justify) -> Arc<Node> {
|
||||
let t = self.text_object(role, at, s, justify);
|
||||
Arc::new(Node::new(self.d.alloc_id(), NodeKind::Text(Box::new(t))))
|
||||
}
|
||||
/// Label `s` of `role` set at `at` and laid out, not yet in the document.
|
||||
fn text_object(&self, role: Role, at: Point, s: &str, justify: Justify) -> TextObject {
|
||||
let style = label_style(self.styles, role);
|
||||
let mut t = TextObject::point(at, s, style);
|
||||
t.para = label_para(self.styles, role);
|
||||
@@ -92,14 +101,25 @@ impl Gen<'_> {
|
||||
t.para.justify = justify;
|
||||
t.system_layout = self.d.setup.system_layout;
|
||||
refresh_bounds(&mut t);
|
||||
Arc::new(Node::new(self.d.alloc_id(), NodeKind::Text(Box::new(t))))
|
||||
t
|
||||
}
|
||||
/// A category or series label: a `|` in it starts a new line (Add graph labels). `middle`: the lines are centred
|
||||
/// on `at` (labels beside an axis or in a wedge) instead of running down from it.
|
||||
pub(super) fn label(&mut self, role: Role, at: Point, s: &str, justify: Justify, middle: bool) -> Arc<Node> {
|
||||
let lines = s.split('|').count();
|
||||
let at = if middle { Point::new(at.x, at.y - (lines - 1) as f64 * label_size(self.styles, role) * 1.2 / 2.0) } else { at };
|
||||
self.text(role, at, &s.replace('|', "\n"), justify)
|
||||
// Default leading puts the first and last baselines this far above and below `at`.
|
||||
let half = (lines - 1) as f64 * label_size(self.styles, role) * 1.2 / 2.0;
|
||||
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.
|
||||
if middle && kept_para(self.styles, role).is_some() {
|
||||
let ys: Vec<f64> = t.baselines().map(|(a, _)| a.y).collect();
|
||||
if let (Some(first), Some(last), true) = (ys.first(), ys.last(), ys.len() == lines) {
|
||||
t.xf = Affine::translate((0.0, half - (first + last) / 2.0)) * t.xf;
|
||||
}
|
||||
}
|
||||
Arc::new(Node::new(self.d.alloc_id(), NodeKind::Text(Box::new(t))))
|
||||
}
|
||||
pub(super) fn group(&mut self, name: &str, children: Vec<Arc<Node>>) -> Arc<Node> {
|
||||
let mut g = Node::group(self.d.alloc_id(), children);
|
||||
|
||||
@@ -4344,6 +4344,32 @@ fn a_legend_across_top_spaces_its_labels_by_their_kept_paragraph_settings() {
|
||||
assert!(next.x0 >= label.x1, "the next swatch starts after the indented label: {label:?} {next:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_centered_label_with_kept_leading_stays_centered_when_the_graph_is_drawn_again() {
|
||||
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\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\n1");
|
||||
s.execute("select.set", &json!({"ids": [l.0]})).unwrap();
|
||||
s.execute("text.setFormat", &json!({"autoLeading": 400, "spaceBefore": 6})).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 two-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();
|
||||
|
||||
Reference in New Issue
Block a user