Refactor how label positions work.
authorBen Pfaff <blp@cs.stanford.edu>
Sat, 5 Apr 2025 20:05:09 +0000 (13:05 -0700)
committerBen Pfaff <blp@cs.stanford.edu>
Sat, 5 Apr 2025 20:05:09 +0000 (13:05 -0700)
rust/pspp/src/output/pivot/look_xml.rs
rust/pspp/src/output/pivot/mod.rs
rust/pspp/src/output/pivot/output.rs
rust/pspp/src/output/pivot/test.rs
rust/pspp/src/output/pivot/tlo.rs

index 7175e03967cab4c91b1723711acf128bb70e8120..72d3f8dfde1bff7edf559c3e021b3bf8c007cb1f 100644 (file)
@@ -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)]
index 98795c4a26c38f12866344ed055a72385ec7e307..3061c85448a3d4d4adac33ed7a6e2fd92368def0 100644 (file)
@@ -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<Category>,
 
-    /// 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<LabelPosition>,
 }
 
 #[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<LabelPosition>,
 }
 
 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<T>(&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<Weak<Group>>,
         leaves: &mut Vec<Arc<Leaf>>,
     ) -> Arc<Group> {
@@ -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>,
         group_index: usize,
         leaves: &mut Vec<Arc<Leaf>>,
     ) -> 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<GroupBuilder> for CategoryBuilder {
+    fn from(value: GroupBuilder) -> Self {
+        Self::Group(Box::new(value))
+    }
+}
+
 impl From<Value> 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<HeadingRegion, RangeInclusive<usize>>,
@@ -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,
index 8b9ad9c07c1ba446cc2337020c9157790b231456..d9726ba80699b33918f99bc5d46e894a4ad9c3ae 100644 (file)
@@ -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()),
index 41cb890c2f8d4013b50c849daf5da6c85409f8d3..2fbd33e51695acba9df8193f79abb54da5db77a6 100644 (file)
@@ -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());
index ece35c3c8480e98228a2d97b8532b6e98f257d3f..74410a2b026c3c289d286535b36940fc51ea1ebf 100644 (file)
@@ -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<TableLook> 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,