work
authorBen Pfaff <blp@cs.stanford.edu>
Wed, 29 Oct 2025 14:57:10 +0000 (07:57 -0700)
committerBen Pfaff <blp@cs.stanford.edu>
Wed, 29 Oct 2025 14:57:10 +0000 (07:57 -0700)
rust/pspp/src/output/pivot.rs
rust/pspp/src/output/pivot/look_xml.rs
rust/pspp/src/output/spv/legacy_xml.rs

index 33820af281bf7610d311d0121cd7904aad838a50..c2a5dc0b87a81181f7d7c2478de63d2a272eae1b 100644 (file)
@@ -156,7 +156,7 @@ impl Serialize for Area {
 }
 
 impl Area {
-    fn default_cell_style(self) -> CellStyle {
+    pub fn default_cell_style(self) -> CellStyle {
         use HorzAlign::*;
         use VertAlign::*;
         let (horz_align, vert_align, hmargins, vmargins) = match self {
@@ -176,11 +176,11 @@ impl Area {
         }
     }
 
-    fn default_font_style(self) -> FontStyle {
+    pub fn default_font_style(self) -> FontStyle {
         FontStyle::default().with_bold(self == Area::Title)
     }
 
-    fn default_area_style(self) -> AreaStyle {
+    pub fn default_area_style(self) -> AreaStyle {
         AreaStyle {
             cell_style: self.default_cell_style(),
             font_style: self.default_font_style(),
@@ -1373,6 +1373,21 @@ impl Not for Axis2 {
     }
 }
 
+/// Can't convert `Axis3::Z` to `Axis2`.
+pub struct ZAxis;
+
+impl TryFrom<Axis3> for Axis2 {
+    type Error = ZAxis;
+
+    fn try_from(value: Axis3) -> Result<Self, Self::Error> {
+        match value {
+            Axis3::X => Ok(Axis2::X),
+            Axis3::Y => Ok(Axis2::Y),
+            Axis3::Z => Err(ZAxis),
+        }
+    }
+}
+
 /// A 2-dimensional `(x,y)` pair.
 #[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
 pub struct Coord2(pub EnumMap<Axis2, usize>);
index 0a15f042e745a8849a42177f5a846707eae9fa55..2d338f6deb81532818558bf551cd79a079fd1c21 100644 (file)
@@ -348,7 +348,7 @@ struct PrintingProperties {
 }
 
 #[derive(Copy, Clone, Default, PartialEq)]
-struct Dimension(
+pub struct Dimension(
     /// In inches.
     f64,
 );
index b8bf8a6b6d9dae1d236ede5318f971b560aafb74..f0ca5803b87706b18a04e189bd9ac25674d6726f 100644 (file)
@@ -23,6 +23,7 @@ use std::{
 };
 
 use enum_map::{Enum, EnumMap};
+use itertools::Itertools;
 use ordered_float::OrderedFloat;
 use serde::{Deserialize, de::Error as _};
 
@@ -31,11 +32,12 @@ use crate::{
     format::{Decimal::Dot, F8_0, Type, UncheckedFormat},
     output::{
         pivot::{
-            Area, AreaStyle, Color, HeadingRegion, HorzAlign, Look, PivotTable, RowParity, Value,
-            VertAlign,
+            self, Area, AreaStyle, Axis2, Axis3, Color, HeadingRegion, HorzAlign, Look, PivotTable,
+            RowParity, Value, VertAlign,
         },
         spv::legacy_bin::DataValue,
     },
+    variable,
 };
 
 #[derive(Debug)]
@@ -44,6 +46,12 @@ struct Ref<T> {
     _phantom: PhantomData<T>,
 }
 
+impl<T> Ref<T> {
+    fn get<'a>(&self, table: &HashMap<&str, &'a T>) -> Option<&'a T> {
+        table.get(self.references.as_str()).map(|v| &**v)
+    }
+}
+
 impl<'de, T> Deserialize<'de> for Ref<T> {
     fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
     where
@@ -224,6 +232,13 @@ impl Visualization {
             todo!()
         };
 
+        let mut axes = HashMap::new();
+        for child in &graph.facet_layout.children {
+            if let FacetLayoutChild::FacetLevel(facet_level) = child {
+                axes.insert(facet_level.level, &facet_level.axis);
+            }
+        }
+
         // Footnotes.
         //
         // Any pivot_value might refer to footnotes, so it's important to
@@ -273,9 +288,7 @@ impl Visualization {
         {
             Style::decode(
                 Some(*style),
-                styles
-                    .get(graph.cell_style.references.as_str())
-                    .map(|v| &**v),
+                graph.cell_style.get(&styles),
                 &mut look.areas[Area::Data(RowParity::Even)],
             );
             look.areas[Area::Data(RowParity::Odd)] =
@@ -332,6 +345,87 @@ impl Visualization {
             }
         }
 
+        fn decode_dimension(
+            variables: &[(&Series, usize)],
+            axes: &HashMap<usize, &Axis>,
+            styles: &HashMap<&str, &Style>,
+            a: Axis3,
+            look: &mut Look,
+        ) {
+            let base_level = variables[0].1;
+            if let Ok(a) = Axis2::try_from(a)
+                && let Some(axis) = axes.get(&(base_level + variables.len()))
+                && let Some(label) = &axis.label
+            {
+                let out = &mut look.areas[Area::Labels(a)];
+                *out = Area::Labels(a).default_area_style();
+                Style::decode(
+                    label.style.get(&styles),
+                    label.text_frame_style.as_ref().and_then(|r| r.get(styles)),
+                    out,
+                );
+            }
+            if a == Axis3::Y
+                && let Some(axis) = axes.get(&(base_level + variables.len() - 1))
+            {}
+
+            todo!()
+        }
+
+        fn decode_dimensions(
+            variables: &[VariableReference],
+            series: &HashMap<&str, Series>,
+            axes: &HashMap<usize, &Axis>,
+            styles: &HashMap<&str, &Style>,
+            a: Axis3,
+            look: &mut Look,
+            level_ofs: usize,
+        ) -> Vec<pivot::Dimension> {
+            let variables = variables
+                .into_iter()
+                .zip(level_ofs..)
+                .map(|(vr, level)| {
+                    series
+                        .get(vr.reference.as_str())
+                        .filter(|s| !s.values.is_empty())
+                        .map(|s| (s, level))
+                })
+                .collect::<Vec<_>>();
+            let mut dim_vars = Vec::new();
+            for var in variables {
+                if let Some((var, level)) = var {
+                    dim_vars.push((var, level));
+                } else if !dim_vars.is_empty() {
+                    decode_dimension(&dim_vars, axes, styles, a, look);
+                    dim_vars.clear();
+                }
+            }
+            if !dim_vars.is_empty() {
+                decode_dimension(&dim_vars, axes, styles, a, look);
+            }
+            todo!()
+        }
+
+        let cross = &graph.faceting.cross.children;
+        let columns = cross
+            .first()
+            .map(|child| child.variables())
+            .unwrap_or_default();
+        decode_dimensions(columns, &series, &axes, &styles, Axis3::X, &mut look, 1);
+        let rows = cross
+            .get(1)
+            .map(|child| child.variables())
+            .unwrap_or_default();
+        decode_dimensions(
+            rows,
+            &series,
+            &axes,
+            &styles,
+            Axis3::Y,
+            &mut look,
+            1 + columns.len(),
+        );
+
         todo!()
     }
 }
@@ -539,7 +633,7 @@ struct CategoricalDomain {
 #[serde(rename_all = "camelCase")]
 struct VariableReference {
     #[serde(rename = "@ref")]
-    reference: Option<String>,
+    reference: String,
 }
 
 #[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
@@ -1422,6 +1516,15 @@ enum CrossChild {
     Nest(Nest),
 }
 
+impl CrossChild {
+    fn variables(&self) -> &[VariableReference] {
+        match self {
+            CrossChild::Unity => &[],
+            CrossChild::Nest(nest) => &nest.variable_references,
+        }
+    }
+}
+
 #[derive(Deserialize, Debug)]
 #[serde(rename_all = "camelCase")]
 struct Nest {
@@ -1766,14 +1869,8 @@ impl Label {
     }
 
     fn decode_style(&self, area_style: &mut AreaStyle, styles: &HashMap<&str, &Style>) {
-        let fg = styles.get(self.style.references.as_str()).map(|v| &**v);
-        let bg = if let Some(text_frame_style) = &self.text_frame_style {
-            styles
-                .get(text_frame_style.references.as_str())
-                .map(|v| &**v)
-        } else {
-            None
-        };
+        let fg = self.style.get(styles);
+        let bg = self.text_frame_style.as_ref().and_then(|r| r.get(styles));
         Style::decode(fg, bg, area_style);
     }
 }
@@ -1864,7 +1961,7 @@ struct LabelFrame {
 }
 
 impl LabelFrame {
-    fn decode(&self, look: &mut Look, styles: &HashMap<&String, &Style>) {
+    fn decode(&self, look: &mut Look, styles: &HashMap<&str, &Style>) {
         let Some(label) = &self.label else { return };
         let Some(purpose) = label.purpose else { return };
         let area = match purpose {
@@ -1874,13 +1971,11 @@ impl LabelFrame {
             Purpose::Layer => Area::Layers,
             Purpose::Footnote => Area::Footer,
         };
-        let fg = styles.get(&label.style.references).map(|v| &**v);
-        let bg = if let Some(text_frame_style) = &label.text_frame_style {
-            styles.get(&&text_frame_style.references).map(|v| &**v)
-        } else {
-            None
-        };
-        Style::decode(fg, bg, &mut look.areas[area]);
+        Style::decode(
+            label.style.get(styles),
+            label.text_frame_style.as_ref().and_then(|r| r.get(styles)),
+            &mut look.areas[area],
+        );
         todo!()
     }
 }