From 21b7d52ca6a8b53b6e442a388140d286aa681f51 Mon Sep 17 00:00:00 2001 From: Ben Pfaff Date: Sat, 5 Apr 2025 13:05:09 -0700 Subject: [PATCH] Refactor how label positions work. --- rust/pspp/src/output/pivot/look_xml.rs | 4 +- rust/pspp/src/output/pivot/mod.rs | 116 ++++++++++++++----------- rust/pspp/src/output/pivot/output.rs | 8 +- rust/pspp/src/output/pivot/test.rs | 11 ++- rust/pspp/src/output/pivot/tlo.rs | 6 +- 5 files changed, 84 insertions(+), 61 deletions(-) diff --git a/rust/pspp/src/output/pivot/look_xml.rs b/rust/pspp/src/output/pivot/look_xml.rs index 7175e03967..72d3f8dfde 100644 --- a/rust/pspp/src/output/pivot/look_xml.rs +++ b/rust/pspp/src/output/pivot/look_xml.rs @@ -5,7 +5,7 @@ use serde::{de::Visitor, Deserialize}; use crate::output::pivot::{ Area, AreaStyle, Axis2, Border, BorderStyle, BoxBorder, Color, FootnoteMarkerPosition, - FootnoteMarkerType, HeadingRegion, HorzAlign, Look, RowColBorder, RowLabelPosition, VertAlign, + FootnoteMarkerType, HeadingRegion, HorzAlign, LabelPosition, Look, RowColBorder, VertAlign, }; use thiserror::Error as ThisError; @@ -113,7 +113,7 @@ struct GeneralProperties { minimum_row_width: i64, #[serde(rename = "@rowDimensionLabels")] - row_label_position: RowLabelPosition, + row_label_position: LabelPosition, } #[derive(Deserialize, Debug)] diff --git a/rust/pspp/src/output/pivot/mod.rs b/rust/pspp/src/output/pivot/mod.rs index 98795c4a26..3061c85448 100644 --- a/rust/pspp/src/output/pivot/mod.rs +++ b/rust/pspp/src/output/pivot/mod.rs @@ -59,7 +59,7 @@ use std::{ collections::HashMap, fmt::{Debug, Display, Write}, io::Read, - iter::{once, repeat}, + iter::{once, repeat, repeat_n}, ops::{Index, IndexMut, Not, Range, RangeInclusive}, str::{from_utf8, FromStr, Utf8Error}, sync::{Arc, OnceLock, Weak}, @@ -390,10 +390,8 @@ pub struct Group { /// only one or even (pathologically) none. children: Vec, - /// Display a label for the group itself? - pub show_label: bool, - - show_label_in_corner: bool, + /// Position for the group's own label, if any. + pub show_label: Option, } #[derive(Clone)] @@ -418,17 +416,10 @@ impl DimensionBuilder { self.hide_all_labels = true; self } - fn build( - mut self, - level: usize, - top_index: usize, - dimension_labels_in_corner: bool, - ) -> Dimension { + fn build(mut self, level: usize, top_index: usize, label_position: LabelPosition) -> Dimension { let mut leaves = Vec::with_capacity(self.len); - self.root.assign_label_depth(dimension_labels_in_corner); - let root = self - .root - .build(dimension_labels_in_corner, None, &mut leaves); + self.root.assign_label_depth(label_position); + let root = self.root.build(label_position, None, &mut leaves); Dimension { axis: self.axis, level, @@ -448,7 +439,7 @@ pub struct GroupBuilder { show_label: bool, label_depth: usize, extra_depth: usize, - show_label_in_corner: bool, + label_position: Option, } impl GroupBuilder { @@ -459,7 +450,7 @@ impl GroupBuilder { show_label: true, label_depth: 0, extra_depth: 0, - show_label_in_corner: false, + label_position: None, } } pub fn push(&mut self, value: T) @@ -482,9 +473,9 @@ impl GroupBuilder { fn len(&self) -> usize { self.children.iter().map(|category| category.len()).sum() } - fn assign_label_depth(&mut self, dimension_labels_in_corner: bool) { + fn assign_label_depth(&mut self, label_position: LabelPosition) { for child in self.children.iter_mut() { - child.assign_label_depth(false); + child.assign_label_depth(label_position); } let depth = self .children @@ -499,12 +490,8 @@ impl GroupBuilder { } child.set_label_depth(depth); } - self.show_label_in_corner = self.show_label && dimension_labels_in_corner; - self.label_depth = if self.show_label && !self.show_label_in_corner { - depth + 1 - } else { - depth - }; + self.label_position = self.show_label.then_some(label_position); + self.label_depth = depth + (self.label_position == Some(LabelPosition::Nested)) as usize; } fn distribute_extra_depth(&mut self, extra_depth: usize) { if self.children.is_empty() { @@ -517,7 +504,7 @@ impl GroupBuilder { } fn build( self, - dimension_labels_in_corner: bool, + label_position: LabelPosition, parent: Option>, leaves: &mut Vec>, ) -> Arc { @@ -530,17 +517,9 @@ impl GroupBuilder { .children .into_iter() .enumerate() - .map(|(group_index, c)| { - c.build( - dimension_labels_in_corner, - weak.clone(), - group_index, - leaves, - ) - }) + .map(|(group_index, c)| c.build(label_position, weak.clone(), group_index, leaves)) .collect(), - show_label: self.show_label, - show_label_in_corner: self.show_label && dimension_labels_in_corner, + show_label: self.show_label.then_some(label_position), }) } } @@ -565,14 +544,14 @@ impl CategoryBuilder { } fn build( self, - dimension_labels_in_corner: bool, + label_position: LabelPosition, parent: Weak, group_index: usize, leaves: &mut Vec>, ) -> Category { match self { Self::Group(group) => { - Category::Group(group.build(dimension_labels_in_corner, Some(parent), leaves)) + Category::Group(group.build(label_position, Some(parent), leaves)) } Self::Leaf { name, @@ -595,9 +574,9 @@ impl CategoryBuilder { } } } - fn assign_label_depth(&mut self, dimension_labels_in_corner: bool) { + fn assign_label_depth(&mut self, label_position: LabelPosition) { match self { - CategoryBuilder::Group(group) => group.assign_label_depth(dimension_labels_in_corner), + CategoryBuilder::Group(group) => group.assign_label_depth(label_position), CategoryBuilder::Leaf { label_depth, .. } => { *label_depth = 1; } @@ -623,6 +602,12 @@ impl CategoryBuilder { } } +impl From for CategoryBuilder { + fn from(value: GroupBuilder) -> Self { + Self::Group(Box::new(value)) + } +} + impl From for CategoryBuilder { fn from(name: Value) -> Self { Self::Leaf { @@ -679,11 +664,12 @@ impl PivotTableBuilder { let mut axes = EnumMap::from_fn(|_key| Vec::with_capacity(self.dimensions.len())); for (top_index, d) in self.dimensions.into_iter().enumerate() { let axis = d.axis; - let d = Arc::new(d.build( - axes[axis].len(), - top_index, - axis == Axis3::Y && row_label_position == RowLabelPosition::Corner && !corner_text, - )); + let label_position = if axis == Axis3::Y && !corner_text { + self.look.row_label_position + } else { + LabelPosition::Nested + }; + let d = Arc::new(d.build(axes[axis].len(), top_index, label_position)); axes[d.axis].push(d.clone()); dimensions.push(d); } @@ -699,6 +685,7 @@ impl PivotTableBuilder { } }); table.cells = self.cells; + table.current_layer = repeat_n(0, table.axes[Axis3::Z].dimensions.len()).collect(); table } } @@ -779,7 +766,7 @@ impl Category { fn show_label(&self) -> bool { match self { - Category::Group(group) => group.show_label, + Category::Group(group) => group.show_label.is_some(), Category::Leaf(_) => true, } } @@ -878,7 +865,7 @@ pub struct Look { /// Whether to hide rows or columns whose cells are all empty. pub omit_empty: bool, - pub row_label_position: RowLabelPosition, + pub row_label_position: LabelPosition, /// Ranges of column widths in the two heading regions, in 1/96" units. pub heading_widths: EnumMap>, @@ -915,7 +902,7 @@ impl Default for Look { Self { name: None, omit_empty: true, - row_label_position: RowLabelPosition::default(), + row_label_position: LabelPosition::default(), heading_widths: EnumMap::from_fn(|region| match region { HeadingRegion::RowHeadings => 36..=72, HeadingRegion::ColumnHeadings => 36..=120, @@ -986,11 +973,42 @@ impl Look { } } +/// Position for group labels. #[derive(Copy, Clone, Debug, Default, Deserialize, PartialEq, Eq)] -pub enum RowLabelPosition { +pub enum LabelPosition { + /// Hierarachically enclosing the categories. + /// + /// For column labels, group labels appear above the categories. For row + /// labels, group labels appear to the left of the categories. + /// + /// ```text + /// +---------+----------+ + /// | | columns | + /// +----+----+----+----+ +------+--+----------+ + /// | | nested | | |a1|...data...| + /// | +----+----+----+ |nested|a2|...data...| + /// | | a1 | a2 | a3 | | |a3|...data...| + /// +----+----+----+----+ +------+--+----------+ + /// | |data|data|data| + /// | | . | . | . | + /// |rows| . | . | . | + /// | | . | . | . | + /// +----+----+----+----+ + /// ``` #[serde(rename = "nested")] Nested, + /// In the corner (row labels only). + /// + /// ```text + /// +------+----------+ + /// |corner| columns | + /// +------+----------+ + /// | a1|...data...| + /// | a2|...data...| + /// | a3|...data...| + /// +------+----------+ + /// ``` #[default] #[serde(rename = "inCorner")] Corner, diff --git a/rust/pspp/src/output/pivot/output.rs b/rust/pspp/src/output/pivot/output.rs index 8b9ad9c07c..d9726ba806 100644 --- a/rust/pspp/src/output/pivot/output.rs +++ b/rust/pspp/src/output/pivot/output.rs @@ -5,7 +5,7 @@ use itertools::Itertools; use smallvec::{SmallVec, ToSmallVec}; use crate::output::{ - pivot::RowLabelPosition, + pivot::LabelPosition, table::{CellInner, Table}, }; @@ -220,7 +220,7 @@ impl PivotTable { ); } } - if (self.corner_text.is_some() || self.look.row_label_position == RowLabelPosition::Corner) + if (self.corner_text.is_some() || self.look.row_label_position == LabelPosition::Corner) && stub.x() > 0 && stub.y() > 0 { @@ -538,7 +538,7 @@ fn compose_headings( // |aaaa1|aaaa2|aaaa3|aaaa1|aaaa2|aaaa3|aaaa1|aaaa2|aaaa3| // +-----+-----+-----+-----+-----+-----+-----+-----+-----+ // ``` - if c.parent().is_some_and(|parent| parent.show_label) { + if c.parent().is_some_and(|parent| parent.show_label.is_some()) { table.draw_line( Border::Categories(col_horz), (h, top_row), @@ -548,7 +548,7 @@ fn compose_headings( } } - if d.root.show_label_in_corner && h_ofs > 0 { + if d.root.show_label == Some(LabelPosition::Corner) && h_ofs > 0 { table.put( Rect2::for_ranges((h, 0..h_ofs), top_row..top_row + d.label_depth()), CellInner::new(Area::Corner, d.root.name.clone()), diff --git a/rust/pspp/src/output/pivot/test.rs b/rust/pspp/src/output/pivot/test.rs index 41cb890c2f..2fbd33e516 100644 --- a/rust/pspp/src/output/pivot/test.rs +++ b/rust/pspp/src/output/pivot/test.rs @@ -19,13 +19,18 @@ fn color() { #[test] fn pivot_table_1d() { - let mut group = GroupBuilder::new(Value::new_text("a")); + let mut group = GroupBuilder::new(Value::new_text("smaller")); for name in ["a1", "a2", "a3"] { group.push(Value::new_text(name)); } - let dimension = DimensionBuilder::new(Axis3::X, group); + let mut bigger_group = GroupBuilder::new(Value::new_text("bigger")); + bigger_group.push(group); + for name in ["b1", "b2", "b3"] { + bigger_group.push(Value::new_text(name)); + } + let dimension = DimensionBuilder::new(Axis3::X, bigger_group); let mut pt = PivotTableBuilder::new(Value::new_text("Columns"), &[dimension]); - for i in 0..3 { + for i in 0..6 { pt.insert(&[i], Value::new_number(Some(i as f64))); } let mut driver = TextDriver::new(File::create("/dev/stdout").unwrap()); diff --git a/rust/pspp/src/output/pivot/tlo.rs b/rust/pspp/src/output/pivot/tlo.rs index ece35c3c84..74410a2b02 100644 --- a/rust/pspp/src/output/pivot/tlo.rs +++ b/rust/pspp/src/output/pivot/tlo.rs @@ -2,7 +2,7 @@ use std::{fmt::Debug, io::Cursor}; use crate::output::pivot::{ Axis2, Border, BoxBorder, FootnoteMarkerPosition, FootnoteMarkerType, HeadingRegion, - RowColBorder, RowLabelPosition, + LabelPosition, RowColBorder, }; use super::{Area, BorderStyle, Color, HorzAlign, Look, Stroke, VertAlign}; @@ -59,9 +59,9 @@ impl From for Look { name: None, omit_empty: (flags & 2) != 0, row_label_position: if look.pt_table_look.nested_row_labels { - RowLabelPosition::Nested + LabelPosition::Nested } else { - RowLabelPosition::Corner + LabelPosition::Corner }, heading_widths: enum_map! { HeadingRegion::ColumnHeadings => look.v2_styles.min_column_width..=look.v2_styles.max_column_width, -- 2.30.2