work on footnotes
authorBen Pfaff <blp@cs.stanford.edu>
Fri, 11 Apr 2025 20:53:25 +0000 (13:53 -0700)
committerBen Pfaff <blp@cs.stanford.edu>
Fri, 11 Apr 2025 20:53:25 +0000 (13:53 -0700)
rust/pspp/src/output/pivot/mod.rs
rust/pspp/src/output/pivot/test.rs
rust/pspp/src/output/render.rs

index ecf71afeab1410d5869e2c66f8d051deb47e3c46..e53858346fefee9ee3cc2067e71be27303e617ce 100644 (file)
@@ -516,12 +516,33 @@ impl From<&str> for CategoryBuilder {
         }
     }
 }
+
+#[derive(Default)]
+pub struct Footnotes(Vec<Arc<Footnote>>);
+
+impl Footnotes {
+    pub fn new() -> Self {
+        Self::default()
+    }
+
+    pub fn push(&mut self, footnote: Footnote) -> Arc<Footnote> {
+        let footnote = Arc::new(footnote.with_index(self.0.len()));
+        self.0.push(footnote.clone());
+        footnote
+    }
+
+    pub fn is_empty(&self) -> bool {
+        self.0.is_empty()
+    }
+}
+
 pub struct PivotTableBuilder {
     look: Arc<Look>,
     title: Box<Value>,
     axes: EnumMap<Axis3, Axis>,
     dimensions: Vec<Dimension>,
     cells: HashMap<usize, Value>,
+    footnotes: Footnotes,
 }
 
 impl PivotTableBuilder {
@@ -540,6 +561,7 @@ impl PivotTableBuilder {
             axes,
             dimensions,
             cells: HashMap::new(),
+            footnotes: Footnotes::new(),
         }
     }
     pub fn with_look(mut self, look: Arc<Look>) -> Self {
@@ -572,6 +594,11 @@ impl PivotTableBuilder {
             value,
         );
     }
+    pub fn with_footnotes(mut self, footnotes: Footnotes) -> Self {
+        debug_assert!(self.footnotes.is_empty());
+        self.footnotes = footnotes;
+        self
+    }
     pub fn build(self) -> PivotTable {
         let mut table = PivotTable::new(self.title, self.look.clone());
         table.dimensions = self.dimensions;
@@ -1417,6 +1444,11 @@ impl PivotTable {
         self
     }
 
+    fn with_corner_text(mut self, corner_text: Value) -> Self {
+        self.corner_text = Some(Box::new(corner_text));
+        self
+    }
+
     fn with_show_title(mut self, show_title: bool) -> Self {
         self.show_title = show_title;
         self
@@ -1624,17 +1656,35 @@ impl PivotTable {
     }
 }
 
-pub struct Layers {}
-
 #[derive(Clone, Debug)]
 pub struct Footnote {
     index: usize,
-    content: Value,
-    marker: Option<Value>,
+    content: Box<Value>,
+    marker: Option<Box<Value>>,
     show: bool,
 }
 
 impl Footnote {
+    pub fn new(content: impl Into<Value>) -> Self {
+        Self {
+            index: 0,
+            content: Box::new(content.into()),
+            marker: None,
+            show: true,
+        }
+    }
+    pub fn with_marker(mut self, marker: impl Into<Value>) -> Self {
+        self.marker = Some(Box::new(marker.into()));
+        self
+    }
+    pub fn with_show(mut self, show: bool) -> Self {
+        self.show = show;
+        self
+    }
+    pub fn with_index(mut self, index: usize) -> Self {
+        self.index = index;
+        self
+    }
     pub fn display_marker(&self, options: impl IntoValueOptions) -> DisplayMarker<'_> {
         DisplayMarker {
             footnote: self,
@@ -1763,6 +1813,12 @@ impl Value {
             id: s.clone(),
         })
     }
+    pub fn with_footnote(mut self, footnote: &Arc<Footnote>) -> Self {
+        let footnotes = &mut self.styling.get_or_insert_default().footnotes;
+        footnotes.push(footnote.clone());
+        footnotes.sort_by_key(|f| f.index);
+        self
+    }
 }
 
 impl From<&str> for Value {
@@ -1790,13 +1846,13 @@ impl<'a> DisplayValue<'a> {
         }
     }
 
-    pub fn with_styling(self, styling: &'a ValueStyle) -> Self {
-        Self {
-            markup: styling.style.font_style.markup,
-            subscripts: styling.subscripts.as_slice(),
-            footnotes: styling.footnotes.as_slice(),
-            ..self
+    pub fn with_styling(mut self, styling: &'a ValueStyle) -> Self {
+        if let Some(area_style) = &styling.style {
+            self.markup = area_style.font_style.markup;
         }
+        self.subscripts = styling.subscripts.as_slice();
+        self.footnotes = styling.footnotes.as_slice();
+        self
     }
 
     pub fn with_font_style(self, font_style: &FontStyle) -> Self {
@@ -2118,9 +2174,9 @@ impl ValueInner {
     }
 }
 
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, Default)]
 pub struct ValueStyle {
-    pub style: AreaStyle,
+    pub style: Option<AreaStyle>,
     pub subscripts: Vec<String>,
     pub footnotes: Vec<Arc<Footnote>>,
 }
index b4748c007c5d7a90bde12b5ebe3d5001abd61a2f..8c44dc80247c6b128d65a3e1b001ca8c7d559c91 100644 (file)
@@ -3,8 +3,8 @@ use std::sync::Arc;
 use enum_map::EnumMap;
 
 use crate::output::pivot::{
-    Area, Axis2, Border, BorderStyle, Class, Color, Dimension, Group, HeadingRegion, LabelPosition,
-    Look, PivotTable, RowColBorder, Stroke,
+    Area, Axis2, Border, BorderStyle, Class, Color, Dimension, Footnote, Footnotes, Group,
+    HeadingRegion, LabelPosition, Look, PivotTable, RowColBorder, Stroke,
 };
 
 use super::{Axis3, DimensionBuilder, GroupBuilder, PivotTableBuilder, Value};
@@ -542,6 +542,67 @@ Caption
     );
 }
 
+#[test]
+fn footnotes() {
+    let mut footnotes = Footnotes::new();
+    let f0 = footnotes.push(Footnote::new("First footnote").with_marker("*"));
+    let f1 = footnotes.push(Footnote::new("Second footnote"));
+    let a = Dimension::builder(
+        Axis3::X,
+        Group::builder(Value::new_text("A").with_footnote(&f0))
+            .with_label_shown()
+            .with(Value::new_text("B").with_footnote(&f1))
+            .with(Value::new_text("C").with_footnote(&f0).with_footnote(&f1)),
+    );
+    let d = Dimension::builder(
+        Axis3::Y,
+        Group::builder(Value::new_text("D").with_footnote(&f1))
+            .with_label_shown()
+            .with(Value::new_text("E").with_footnote(&f0))
+            .with(Value::new_text("F").with_footnote(&f1).with_footnote(&f0)),
+    );
+    let look = test_look().with_row_label_position(LabelPosition::Nested);
+    let mut pt = PivotTable::builder(
+        Value::new_text("Pivot Table with Alphabetic Subscript Footnotes").with_footnote(&f0),
+        vec![a, d],
+    );
+    pt.insert(&[0, 0], Value::new_number(Some(0.0)));
+    pt.insert(&[1, 0], Value::new_number(Some(1.0)).with_footnote(&f0));
+    pt.insert(&[0, 1], Value::new_number(Some(2.0)).with_footnote(&f1));
+    pt.insert(
+        &[1, 1],
+        Value::new_number(Some(3.0))
+            .with_footnote(&f0)
+            .with_footnote(&f1),
+    );
+    let pt = pt
+        .with_look(Arc::new(look))
+        .build()
+        .with_caption(Value::new_text("Caption").with_footnote(&f0))
+        .with_corner_text(
+            Value::new_text("Corner")
+                .with_footnote(&f0)
+                .with_footnote(&f1),
+        );
+    assert_rendering(
+        &pt,
+        "\
+Pivot Table with Alphabetic Subscript Footnotes[*]
+╭────────────┬──────────────────╮
+│            │       A[*]       │
+│            ├───────┬──────────┤
+│Corner[*][b]│  B[b] │  C[*][b] │
+├────────────┼───────┼──────────┤
+│D[b] E[*]   │    .00│   1.00[*]│
+│     F[*][b]│2.00[b]│3.00[*][b]│
+╰────────────┴───────┴──────────╯
+Caption[*]
+*. First footnote
+b. Second footnote
+",
+    );
+}
+
 #[test]
 fn no_dimension() {
     let pivot_table = PivotTableBuilder::new(Value::new_text("No Dimensions"), vec![])
index 8d2e518c7e441a02d0b96442bd79d5c1d3363834..d5caf49965f5dc37c2386d6523d520025563da32 100644 (file)
@@ -155,14 +155,15 @@ pub struct DrawCell<'a> {
 
 impl<'a> DrawCell<'a> {
     fn new(inner: &'a CellInner, table: &'a Table) -> Self {
-        let (style, subscripts, footnotes) = if let Some(styling) = inner.value.styling.as_ref() {
+        let default_area_style = &table.areas[inner.area];
+        let (style, subscripts, footnotes) = if let Some(styling) = &inner.value.styling {
             (
-                &styling.style,
+                styling.style.as_ref().unwrap_or(default_area_style),
                 styling.subscripts.as_slice(),
                 styling.footnotes.as_slice(),
             )
         } else {
-            (&table.areas[inner.area], [].as_slice(), [].as_slice())
+            (default_area_style, [].as_slice(), [].as_slice())
         };
         Self {
             rotate: inner.rotate,
@@ -422,6 +423,7 @@ impl Page {
         }
 
         // Distribute widths of spanned columns.
+        dbg!(&unspanned_columns);
         let mut columns = unspanned_columns.clone();
         for cell in table.cells().filter(|cell| cell.col_span() > 1) {
             let rect = cell.rect();
@@ -446,6 +448,7 @@ impl Page {
                 );
             }
         }
+        dbg!(&columns);
 
         // In pathological cases, spans can cause the minimum width of a column
         // to exceed the maximum width.  This bollixes our interpolation