]> pintos-os.org Git - pspp/commitdiff
Work on facet and axis labeling. Doesn't pass tests yet.
authorBen Pfaff <blp@cs.stanford.edu>
Tue, 23 Jun 2026 23:49:50 +0000 (16:49 -0700)
committerBen Pfaff <blp@cs.stanford.edu>
Tue, 23 Jun 2026 23:49:50 +0000 (16:49 -0700)
rust/pspp/src/output/drivers/cairo.rs
rust/pspp/src/spv/read/graph.rs
rust/pspp/src/spv/testdata/graph5.xml

index 67362805db2d8bd1992b4cb5ed94c07b0ee3ebde..7494d1741a562bdd095331cf01e03c02841c31fd 100644 (file)
@@ -30,6 +30,10 @@ fn px_to_xr(x: isize) -> isize {
     x * 3 * (SCALE as isize * 72 / 96) / 3
 }
 
+pub fn pt_to_xr(x: f64) -> isize {
+    (x * SCALE as f64 + 0.5) as isize
+}
+
 pub fn xr_to_pt(x: isize) -> f64 {
     x as f64 / SCALE as f64
 }
index 62b0f4e829d225bdc853b397a14e7535d0eaedbb..a384616edea86aaf6048919ddfd515ee3b6178bf 100644 (file)
@@ -53,7 +53,7 @@ use crate::{
     locale::{Item, Locale, data::EN},
     output::{
         SpvGraphMembers, SpvInfo, SpvMembers,
-        drivers::cairo::xr_to_pt,
+        drivers::cairo::{pt_to_xr, xr_to_pt},
         pivot::{Axis2, Axis3, Length, ParseLengthError, look::Color},
     },
     spv::{
@@ -599,7 +599,7 @@ impl Visualization {
             && let Some(top) = frames.frames[0].position[Axis2::Y].start()
             && let Some(height) = frames.frames[0].position[Axis2::Y].length()
         {
-            document.assign("viewBox", (left, top, left + width, top + height));
+            document.assign("viewBox", (left, top, left + width + 75.0, top + height));
             document.assign("width", width);
             document.assign("height", height);
         }
@@ -710,21 +710,104 @@ impl Visualization {
         if axis_domains[Axis3::X].label.is_some() {
             rect.0[Axis2::Y].1 -= font_height;
         }
+        dbg!(&rect);
 
         let facet_levels = graph.facet_levels();
         let faceting = graph
             .faceting()
             .map(|f| f.decode(&series, &facet_levels))
             .unwrap_or_default();
+        dbg!(&faceting);
 
-        let x_facets = FacetIterator::new(&faceting[Axis2::X]);
+        let x_facets = FacetIterator::new(&faceting[Axis2::X]).into_values();
+        dbg!(AxisFacets::new(&faceting[Axis2::X]));
         let nx_facets = x_facets.len();
-        let y_facets = FacetIterator::new(&faceting[Axis2::Y]);
+        let y_facets = FacetIterator::new(&faceting[Axis2::Y]).into_values();
+        dbg!(AxisFacets::new(&faceting[Axis2::Y]));
         let ny_facets = y_facets.len();
 
+        let mut x_headings = FacetHeadings::new(
+            Side::Bottom,
+            &faceting[Axis2::X],
+            rect.0[Axis2::X].length(),
+            graph.x_dimension(),
+            &mut font_metrics,
+        );
+        let mut y_headings = FacetHeadings::new(
+            Side::Top,
+            &faceting[Axis2::Y],
+            rect.0[Axis2::Y].length() - x_headings.total_height(),
+            graph.y_dimension(),
+            &mut font_metrics,
+        );
+        for _ in 0..5 {
+            println!(
+                "{:.0} x {:.0}",
+                x_headings.total_height(),
+                y_headings.total_height()
+            );
+            x_headings = FacetHeadings::new(
+                Side::Bottom,
+                &faceting[Axis2::X],
+                rect.0[Axis2::X].length() - y_headings.total_height(),
+                graph.x_dimension(),
+                &mut font_metrics,
+            );
+            y_headings = FacetHeadings::new(
+                Side::Top,
+                &faceting[Axis2::Y],
+                rect.0[Axis2::Y].length() - x_headings.total_height(),
+                graph.y_dimension(),
+                &mut font_metrics,
+            );
+        }
+
+        let inner = rect.inset(
+            [
+                y_headings.height(Side::Top),
+                y_headings.height(Side::Bottom),
+            ],
+            [
+                x_headings.height(Side::Top),
+                x_headings.height(Side::Bottom),
+            ],
+        );
+
+        let top = x_headings
+            .render(Side::Top)
+            .set("c:description", "X axis facets above graph")
+            .set(
+                "transform",
+                format!("translate({} {})", rect.0[Axis2::X].0, rect.0[Axis2::Y].0),
+            );
+        let bottom = x_headings
+            .render(Side::Bottom)
+            .set("c:description", "X axis facets below graph")
+            .set(
+                "transform",
+                format!("translate({} {})", inner.0[Axis2::X].0, inner.0[Axis2::Y].1,),
+            );
+
+        let left = y_headings
+            .render(Side::Top)
+            .set("c:description", "Y axis facets on left side of graph")
+            .set("transform", {
+                let x = rect.0[Axis2::X].0;
+                let y = inner.0[Axis2::Y].1;
+                format!("rotate(-90 {x} {y}) translate({x} {y})")
+            });
+        let right = y_headings
+            .render(Side::Bottom)
+            .set("c:description", "Y axis facets on right side of graph")
+            .set("transform", {
+                let x = rect.0[Axis2::X].1;
+                let y = inner.0[Axis2::Y].0;
+                format!("rotate(90 {x} {y}) translate({x} {y})")
+            });
+
         for (x_index, _x_facet) in x_facets.clone().enumerate() {
             {
-                let rect = rect.subrect(x_index, nx_facets, ny_facets - 1, ny_facets);
+                let rect = inner.subrect(x_index, nx_facets, ny_facets - 1, ny_facets);
                 // X axis.
                 let y = rect.0[Axis2::Y].1;
                 let x_span = rect.0[Axis2::X];
@@ -736,46 +819,25 @@ impl Visualization {
                         .set("y2", y)
                         .set("stroke", "black"),
                 );
-                for (frac, label) in axis_domains[Axis3::X].ticks() {
+                for (frac, _label) in axis_domains[Axis3::X].ticks() {
                     let svg_x = rect.0[Axis2::X].frac(frac);
-                    graph_svg = graph_svg
-                        .add(
-                            Line::new()
-                                .set("x1", svg_x)
-                                .set("x2", svg_x)
-                                .set("y1", y)
-                                .set("y2", y + 10.0)
-                                .set("stroke", "black"),
-                        )
-                        .add(
-                            svg::node::element::Text::new(label)
-                                .set("x", svg_x)
-                                .set("y", y + 10.0)
-                                .set("dominant-baseline", "hanging")
-                                .set("text-anchor", "middle"),
-                        );
+                    graph_svg = graph_svg.add(
+                        Line::new()
+                            .set("x1", svg_x)
+                            .set("x2", svg_x)
+                            .set("y1", y)
+                            .set("y2", y + 10.0)
+                            .set("stroke", "black"),
+                    );
                 }
             }
-            if let Some(label) = &axis_domains[Axis3::X].label
-                && let Some(label) = label.text()
-            {
-                let x = rect.0[Axis2::X].midpoint();
-                let y = rect.0[Axis2::Y].1 + 10.0 + font_height;
-                graph_svg.append(
-                    svg::node::element::Text::new(label)
-                        .set("x", x)
-                        .set("y", y)
-                        .set("dominant-baseline", "hanging")
-                        .set("text-anchor", "middle"),
-                );
-            }
         }
 
-        for (y_index, y_facet) in y_facets.clone().enumerate() {
-            let rect = rect.subrect(0, nx_facets, y_index, ny_facets);
+        for (y_index, _y_facet) in y_facets.clone().enumerate() {
+            let subrect = inner.subrect(0, nx_facets, y_index, ny_facets);
             // Y axis.
-            let x = rect.0[Axis2::X].0;
-            let y_span = rect.0[Axis2::Y];
+            let x = subrect.0[Axis2::X].0;
+            let y_span = subrect.0[Axis2::Y];
             graph_svg.append(
                 Line::new()
                     .set("y1", y_span.0)
@@ -785,69 +847,22 @@ impl Visualization {
                     .set("stroke", "black")
                     .set("c:description", format!("Y axis {y_index}")),
             );
-            for (frac, label) in axis_domains[Axis3::Y].ticks() {
-                let svg_y = rect.0[Axis2::Y].frac(1.0 - frac);
-                graph_svg = graph_svg
-                    .add(
-                        Line::new()
-                            .set("y1", svg_y)
-                            .set("y2", svg_y)
-                            .set("x1", x)
-                            .set("x2", x - 10.0)
-                            .set("stroke", "black"),
-                    )
-                    .add(
-                        svg::node::element::Text::new(label)
-                            .set("y", svg_y)
-                            .set("x", x - 10.0)
-                            .set("dominant-baseline", "middle")
-                            .set("text-anchor", "end"),
-                    );
-            }
-
-            let label = zip(&y_facet, &faceting[Axis2::Y])
-                .map(|(value, facet)| {
-                    if let Some(label) = &value.label {
-                        label.clone()
-                    } else {
-                        facet.series.formatter.display(&value.value).to_string()
-                    }
-                })
-                .join(", ");
-            if !label.is_empty() {
-                let x = x - 10.0 - y_axis_text_width - font_height;
-                let y = rect.0[Axis2::Y].midpoint();
-                graph_svg.append(
-                    svg::node::element::Text::new(label)
-                        .set("x", x)
-                        .set("y", y)
-                        .set("dominant-baseline", "hanging")
-                        .set("text-anchor", "middle")
-                        .set("transform", format!("rotate(-90, {x}, {y})"))
-                        .set("c:description", "Row facet label"),
+            for (frac, _label) in axis_domains[Axis3::Y].ticks() {
+                let svg_y = subrect.0[Axis2::Y].frac(1.0 - frac);
+                graph_svg = graph_svg.add(
+                    Line::new()
+                        .set("y1", svg_y)
+                        .set("y2", svg_y)
+                        .set("x1", x)
+                        .set("x2", x - 10.0)
+                        .set("stroke", "black"),
                 );
             }
         }
 
-        if let Some(label) = &axis_domains[Axis3::Y].label
-            && let Some(label) = label.text()
-        {
-            let x = rect.0[Axis2::X].0 - 10.0 - y_axis_text_width - font_height;
-            let y = rect.0[Axis2::Y].midpoint();
-            graph_svg.append(
-                svg::node::element::Text::new(label)
-                    .set("x", x)
-                    .set("y", y)
-                    .set("dominant-baseline", "hanging")
-                    .set("text-anchor", "middle")
-                    .set("transform", format!("rotate(-90, {x}, {y})"))
-                    .set("c:description", "Y axis label"),
-            )
-        }
-
         for (x_index, x_facet) in x_facets.enumerate() {
             for (y_index, y_facet) in y_facets.clone().enumerate() {
-                let rect = rect.subrect(x_index, nx_facets, y_index, ny_facets);
+                let rect = inner.subrect(x_index, nx_facets, y_index, ny_facets);
 
                 if cell_style.color.is_some() {
                     graph_svg.append(
@@ -1208,6 +1223,12 @@ impl Visualization {
         {
             graph_svg.append(defs);
         }
+        graph_svg.append(top);
+        graph_svg.append(bottom);
+        graph_svg.append(left);
+        graph_svg.append(right);
+        //graph_svg.append(rect.as_svg().set("fill", "none").set("stroke", "red"));
+        //graph_svg.append(inner.as_svg().set("fill", "none").set("stroke", "green"));
         Ok(graph_svg)
     }
 }
@@ -1229,6 +1250,12 @@ impl FontMetrics {
         let (w, h) = self.layout.size();
         enum_map! { Axis2::X => xr_to_pt(w as isize), Axis2::Y => xr_to_pt(h as isize) }
     }
+    fn measure_height_for_width(&mut self, s: &str, width: f64) -> f64 {
+        self.layout.set_width(pt_to_xr(width) as i32);
+        self.layout.set_text(s);
+        let (_w, h) = self.layout.size();
+        xr_to_pt(h as isize)
+    }
 }
 
 #[derive(Clone, Debug)]
@@ -1278,10 +1305,11 @@ impl Series {
         categorical: bool,
         formatter: Formatter,
     ) -> Self {
-        let categorical_values = if categorical {
+        let unique_values = if categorical {
             values
                 .iter()
                 .unique_by(|sv| sv.value.clone())
+                .sorted_by(|a, b| a.value.cmp(&b.value))
                 .cloned()
                 .collect()
         } else {
@@ -1295,7 +1323,7 @@ impl Series {
             key: false,
             categorical,
             values,
-            unique_values: categorical_values,
+            unique_values,
             formatter,
         }
     }
@@ -1583,6 +1611,7 @@ impl Variable for SourceVariable {
             // XXX warn about missing variable
             Vec::new()
         };
+        dbg!(&data);
         let formatter = self.format.as_ref().map_or_else(
             || Formatter::default_for_locale(locale),
             |format| Formatter::new(format, locale),
@@ -2947,6 +2976,14 @@ impl Graph {
         })
     }
 
+    fn x_dimension(&self) -> Option<&Dimension> {
+        self.coordinates().next()?.dimensions().next()
+    }
+
+    fn y_dimension(&self) -> Option<&Dimension> {
+        self.coordinates().next()?.dimensions().skip(1).next()
+    }
+
     fn elements(&self) -> impl Iterator<Item = (&RawElement, Graphic)> {
         self.children.iter().filter_map(|child| child.element())
     }
@@ -3416,8 +3453,7 @@ struct Facet<'a> {
 }
 
 impl Faceting {
-    /// Returns the faceting for each axis, as variable names from innermost to
-    /// outermost.
+    /// Returns the faceting for each axis, from innermost to outermost.
     ///
     /// This could decode `nest` and `blend` but those are super rare so we only
     /// decode `cross` instead.
@@ -3443,6 +3479,7 @@ impl Faceting {
 enum FacetingChild {
     Dot(Cross),
     Cross(Cross),
+    Nest(Nest),
     Layer(Layer),
 }
 
@@ -3458,6 +3495,9 @@ impl FacetingChild {
             FacetingChild::Cross(cross) | FacetingChild::Dot(cross) => {
                 cross.decode(series, facet_levels, faceting, 0, variable_index)
             }
+            FacetingChild::Nest(nest) => {
+                nest.decode(series, facet_levels, faceting, 0, variable_index)
+            }
             FacetingChild::Layer(_layer) => { // XXX warn
             }
         }
@@ -4074,6 +4114,20 @@ struct FacetLevel {
 #[derive(Deserialize, Debug)]
 #[serde(rename_all = "camelCase")]
 struct Axis {
+    /// For an X-axis, false means that the axis appears below the graph, true
+    /// means that it appears above the graph.
+    ///
+    /// For a Y-axis, false means that the axis appears to the left of the
+    /// graph, true means that it appears to the right of the graph.
+    #[serde(rename = "@opposite", default)]
+    opposite: bool,
+
+    /// If false, `label` appears once per facet along the axis.
+    ///
+    /// If true, `label` appears once along the axis.
+    #[serde(rename = "@combineAxisLabels", default)]
+    combine_axis_labels: bool,
+
     label: Option<Label>,
     major_ticks: Option<MajorTicks>,
 }
@@ -4491,10 +4545,21 @@ impl Coord {
 struct Rect(EnumMap<Axis2, Span>);
 
 impl Rect {
+    fn new(x: Span, y: Span) -> Self {
+        Self(enum_map! { Axis2::X => x, Axis2::Y => y})
+    }
+
+    fn inset(&self, x: [f64; 2], y: [f64; 2]) -> Self {
+        Self::new(
+            Span(self.0[Axis2::X].0 + x[0], self.0[Axis2::X].1 - x[1]),
+            Span(self.0[Axis2::Y].0 + y[0], self.0[Axis2::Y].1 - y[1]),
+        )
+    }
+
     fn subrect(&self, x: usize, nx: usize, y: usize, ny: usize) -> Self {
         Self(enum_map! {
-            Axis2::X => self.0[Axis2::X].subspan(x, nx, 5.0),
-            Axis2::Y => self.0[Axis2::Y].subspan(y, ny, 5.0),
+            Axis2::X => self.0[Axis2::X].subdivide(nx, 5.0).nth(x),
+            Axis2::Y => self.0[Axis2::Y].subdivide(ny, 5.0).nth(y),
         })
     }
 
@@ -4513,16 +4578,89 @@ impl Rect {
             .map(|(axis, span)| span.overflow_demerits(&limits.0[axis]))
             .sum()
     }
+
+    fn as_svg(&self) -> Rectangle {
+        Rectangle::new()
+            .set("x", self.0[Axis2::X].0)
+            .set("y", self.0[Axis2::Y].0)
+            .set("width", self.0[Axis2::X].length())
+            .set("height", self.0[Axis2::Y].length())
+    }
+}
+
+/// A span divided into a number of equal-length segments separated by some
+/// amount of space.
+#[derive(Clone, Debug)]
+struct DividedSpan {
+    /// The divided span.
+    pub span: Span,
+
+    /// The space between segments.
+    pub spacing: f64,
+
+    /// The length of each segment.
+    pub each: f64,
+}
+
+impl DividedSpan {
+    /// Returns the start of the segment with the given 0-based `index`.
+    fn start(&self, index: usize) -> f64 {
+        self.span.0 + (self.each + self.spacing) * index as f64
+    }
+
+    /// Returns the end of the segment with the given 0-based `index`.
+    fn end(&self, index: usize) -> f64 {
+        self.start(index) + self.each
+    }
+
+    /// Returns the span of the segment with the given 0-based `index`.
+    fn nth(&self, index: usize) -> Span {
+        Span(self.start(index), self.end(index))
+    }
+
+    /// Returns the span for the given `range` of 0-based segments.  The span
+    /// will be empty if `range` is empty.
+    fn range(&self, range: &Range<usize>) -> Span {
+        let start = self.start(range.start);
+        let end = if !range.is_empty() {
+            self.end(range.end - 1)
+        } else {
+            start
+        };
+        Span(start, end)
+    }
 }
 
 #[derive(Copy, Clone, Debug, Default)]
 struct Span(f64, f64);
 
 impl Span {
-    fn subspan(&self, index: usize, count: usize, spacing: f64) -> Self {
-        let each = (self.length() - spacing * (count - 1) as f64) / count as f64;
-        let start = self.0 + (each + spacing) * index as f64;
-        Self(start, start + each)
+    /// Returns this span divided into `count > 0` equally sized segments with
+    /// `spacing` between them.
+    fn subdivide(&self, count: usize, spacing: f64) -> DividedSpan {
+        if count == 0 {
+            DividedSpan {
+                span: *self,
+                spacing: 0.0,
+                each: 0.0,
+            }
+        } else {
+            let each = (self.length() - spacing * (count - 1) as f64) / count as f64;
+            if each > 0.0 {
+                DividedSpan {
+                    span: *self,
+                    spacing,
+                    each,
+                }
+            } else {
+                // No room for the spacing, so drop it.
+                DividedSpan {
+                    span: *self,
+                    spacing: 0.0,
+                    each: self.length() / count as f64,
+                }
+            }
+        }
     }
 
     /// Returns a demerits score based on how much this span overflows the
@@ -4760,6 +4898,370 @@ fn diaconis_rule(iqr: f64, n: f64) -> Option<usize> {
     NumCast::from((2.0 * iqr / n.cbrt()).ceil())
 }
 
+#[derive(Debug)]
+struct FacetColumn<'a> {
+    columns: Range<usize>,
+    value: &'a SeriesValue,
+}
+
+impl<'a> FacetColumn<'a> {
+    fn label(&self, series: &Series) -> String {
+        if let Some(label) = &self.value.label {
+            label.clone()
+        } else {
+            series.formatter.display(&self.value.value).to_string()
+        }
+    }
+}
+
+#[derive(Debug)]
+struct FacetRow<'a> {
+    series: &'a Series,
+    facet_level: Option<&'a FacetLevel>,
+    columns: Vec<FacetColumn<'a>>,
+}
+
+impl<'a> FacetRow<'a> {
+    fn axis_label(&self) -> Option<String> {
+        self.facet_level?.axis.label.as_ref()?.text()
+    }
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, Enum)]
+enum Side {
+    /// Top or left.
+    Top,
+    /// Bottom or right.
+    Bottom,
+}
+
+impl Not for Side {
+    type Output = Side;
+
+    fn not(self) -> Self::Output {
+        match self {
+            Side::Top => Side::Bottom,
+            Side::Bottom => Side::Top,
+        }
+    }
+}
+
+/// A set of headings for the facets along the X or Y axis.
+///
+/// This structure applies to either axis, but the fields and code are written
+/// for X-axis facets.  It is rotated 90 degrees for Y axis display.
+struct FacetHeadings<'a> {
+    /// Width, fixed by the caller.
+    width: f64,
+
+    /// Axes and their labels.
+    ///
+    /// Ordinarily there is at most one axis on each side; if there happens to
+    /// be more than one then these go from innermost to outermost.  `axes`
+    /// appear inside `facets` if both are present on the same side.
+    axes: Vec<FacetAxisHeight<'a>>,
+
+    /// Per-row heights on the default (left or top) side and the opposite side,
+    /// calculated based on `axis_facets` and `width`.
+    ///
+    /// From innermost to outermost.
+    facets: Vec<FacetRowHeight<'a>>,
+
+    /// Number of facets.
+    n_facets: usize,
+}
+
+struct FacetAxisHeight<'a> {
+    axis: &'a Axis,
+    text: String,
+    side: Side,
+    height: f64,
+}
+
+struct FacetRowHeight<'a> {
+    row: FacetRow<'a>,
+    side: Side,
+    axis_label_height: f64,
+    facet_label_height: f64,
+}
+
+impl<'a> FacetRowHeight<'a> {
+    fn height(&self) -> f64 {
+        self.axis_label_height + self.facet_label_height
+    }
+}
+
+impl<'a> FacetHeadings<'a> {
+    fn total_height(&self) -> f64 {
+        self.height(Side::Top) + self.height(Side::Bottom)
+    }
+
+    fn height(&self, side: Side) -> f64 {
+        self.axes
+            .iter()
+            .filter(|axis| axis.side == side)
+            .map(|axis| axis.height)
+            .sum::<f64>()
+            + self
+                .facets
+                .iter()
+                .filter(|row_height| row_height.side == side)
+                .map(|row_height| row_height.height())
+                .sum::<f64>()
+    }
+
+    fn new(
+        default_side: Side,
+        facets: &'a [Facet<'a>],
+        width: f64,
+        dimension: Option<&'a Dimension>,
+        font_metrics: &mut FontMetrics,
+    ) -> Self {
+        let axis_facets = AxisFacets::new(facets);
+        let n_facets = axis_facets.len();
+        let span = Span(0.0, width).subdivide(n_facets, 5.0);
+        println!("span: {span:?}");
+        let facets = axis_facets
+            .rows
+            .into_iter()
+            .map(|row| {
+                let side = if let Some(facet_level) = row.facet_level
+                    && facet_level.axis.opposite
+                {
+                    !default_side
+                } else {
+                    default_side
+                };
+                let axis_label_height = row.axis_label().map_or(0.0, |label| {
+                    font_metrics.measure_height_for_width(&label, width)
+                });
+
+                let mut facet_label_height = 0.0f64;
+                for column in &row.columns {
+                    let label = column.label(&row.series);
+                    if !label.is_empty() {
+                        let column_width = span.range(&column.columns).length();
+                        let tmp = font_metrics.measure_height_for_width(&label, column_width);
+                        println!("height of {label} with width {column_width:.0} is {tmp:.0}");
+                        facet_label_height = facet_label_height.max(tmp);
+                    }
+                }
+
+                FacetRowHeight {
+                    row,
+                    side,
+                    axis_label_height,
+                    facet_label_height,
+                }
+            })
+            .collect();
+        let axes = dimension
+            .iter()
+            .flat_map(|d| &d.axis)
+            .filter_map(|axis| {
+                if let Some(label) = &axis.label
+                    && let Some(text) = label.text()
+                {
+                    let width = if axis.combine_axis_labels {
+                        width
+                    } else {
+                        span.each
+                    };
+                    let height = font_metrics.measure_height_for_width(&text, width);
+                    Some(FacetAxisHeight {
+                        axis,
+                        text,
+                        side: if axis.opposite {
+                            !default_side
+                        } else {
+                            default_side
+                        },
+                        height,
+                    })
+                } else {
+                    None
+                }
+            })
+            .collect();
+        Self {
+            width,
+            axes,
+            facets,
+            n_facets,
+        }
+    }
+
+    fn render(&self, side: Side) -> Group {
+        let span = Span(0.0, self.width).subdivide(self.n_facets, 5.0);
+        let mut group = Group::new();
+        /*
+        group.append(
+                    Rect(enum_map! {
+                        Axis2::X => Span(0.0, self.width),
+                        Axis2::Y => Span(0.0, self.height(side))
+                    })
+                    .as_svg()
+                    .set("fill", "blue"),
+                );*/
+        let mut y = 0.0;
+        for axis in self.axes.iter().filter(|axis| axis.side == side) {
+            if dbg!(axis.axis.combine_axis_labels) {
+                group.append(
+                    svg::node::element::Text::new(&axis.text)
+                        .set("x", self.width / 2.0)
+                        .set("y", y)
+                        .set("dominant-baseline", "hanging")
+                        .set("text-anchor", "middle"),
+                );
+            } else {
+                for index in dbg!(0..self.n_facets) {
+                    group.append(
+                        svg::node::element::Text::new(&axis.text)
+                            .set("x", span.nth(index).midpoint())
+                            .set("y", y)
+                            .set("dominant-baseline", "hanging")
+                            .set("text-anchor", "middle"),
+                    );
+                }
+            };
+            y += axis.height;
+        }
+        for height in self.facets.iter().filter(|height| height.side == side) {
+            if let Some(axis_label) = height.row.axis_label() {
+                dbg!(&axis_label);
+                group.append(
+                    svg::node::element::Text::new(axis_label)
+                        .set("x", self.width / 2.0)
+                        .set("y", y)
+                        .set("dominant-baseline", "hanging")
+                        .set("text-anchor", "middle"),
+                );
+            }
+            y += height.axis_label_height;
+
+            for column in &height.row.columns {
+                let label = column.label(&height.row.series);
+                if !label.is_empty() {
+                    dbg!(&label);
+                    let span = span.range(&column.columns);
+                    group.append(
+                        svg::node::element::Text::new(label)
+                            .set("x", span.midpoint())
+                            .set("y", y)
+                            .set("dominant-baseline", "hanging")
+                            .set("text-anchor", "middle"),
+                    );
+                }
+            }
+            y += height.facet_label_height;
+        }
+        group
+    }
+}
+
+#[derive(Debug)]
+struct AxisFacets<'a> {
+    /// From innermost to outermost.
+    ///
+    /// If there are no rows, then there is a single facet.
+    /// If rows exist but they have zero columns, then there are no facets.
+    rows: Vec<FacetRow<'a>>,
+}
+
+impl<'a> AxisFacets<'a> {
+    /// `facets` goes from innermost to outermost.
+    fn new(facets: &'a [Facet<'a>]) -> Self {
+        let mut rows = facets
+            .iter()
+            .map(|facet| FacetRow {
+                series: facet.series,
+                facet_level: facet.facet_level,
+                columns: Vec::new(),
+            })
+            .collect_vec();
+        for (column_index, (increment, values)) in
+            LexIter::new(facets.iter().map(|facet| facet.series.unique_values.len())).enumerate()
+        {
+            for (row_index, (facet, row)) in zip(facets, &mut rows).enumerate() {
+                if row_index <= increment {
+                    row.columns.push(FacetColumn {
+                        columns: column_index..column_index + 1,
+                        value: &facet.series.unique_values[values[row_index]],
+                    });
+                } else {
+                    row.columns.last_mut().unwrap().columns.end += 1;
+                }
+            }
+        }
+        Self { rows }
+    }
+
+    /// The number of facets.
+    ///
+    /// If there are no rows, then there is a single facet.
+    /// If rows exist but they have zero columns, then there are no facets.
+    fn len(&self) -> usize {
+        self.rows.first().map_or(1, |row| row.columns.len())
+    }
+}
+
+struct LexIter<C> {
+    counts: C,
+    next: SmallVec<[usize; 2]>,
+    zeroed: usize,
+    remaining: usize,
+}
+
+impl<C> LexIter<C>
+where
+    C: ExactSizeIterator<Item = usize> + Clone,
+{
+    fn new<I>(counts: I) -> Self
+    where
+        I: IntoIterator<IntoIter = C>,
+    {
+        let counts = counts.into_iter();
+        Self {
+            next: smallvec![0; counts.len()],
+            zeroed: counts.len(),
+            remaining: counts.clone().product(),
+            counts,
+        }
+    }
+}
+
+impl<C> Iterator for LexIter<C>
+where
+    C: ExactSizeIterator<Item = usize> + Clone,
+{
+    type Item = (usize, SmallVec<[usize; 2]>);
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if self.remaining == 0 {
+            None
+        } else {
+            let result = (self.zeroed, self.next.clone());
+            self.zeroed = self.counts.len();
+            for (index, (next, count)) in zip(&mut self.next, self.counts.clone()).enumerate() {
+                *next += 1;
+                if *next < count {
+                    self.zeroed = index;
+                    break;
+                }
+                *next = 0;
+            }
+            self.remaining -= 1;
+            Some(result)
+        }
+    }
+
+    fn size_hint(&self) -> (usize, Option<usize>) {
+        (self.remaining, Some(self.remaining))
+    }
+}
+
+impl<C> ExactSizeIterator for LexIter<C> where C: ExactSizeIterator<Item = usize> + Clone {}
+
 #[derive(Clone, Debug)]
 struct FacetIterator<'a> {
     facets: &'a [Facet<'a>],
@@ -4782,22 +5284,29 @@ impl<'a> FacetIterator<'a> {
             },
         }
     }
+
+    fn into_values(
+        self,
+    ) -> impl Iterator<Item = SmallVec<[&'a SeriesValue; 2]>> + ExactSizeIterator + Clone {
+        let facets = self.facets;
+        self.map(move |indexes| {
+            indexes
+                .into_iter()
+                .zip(facets)
+                .map(|(index, facet)| &facet.series.unique_values[index])
+                .collect()
+        })
+    }
 }
 
 impl<'a> Iterator for FacetIterator<'a> {
-    type Item = SmallVec<[&'a SeriesValue; 2]>;
+    type Item = SmallVec<[usize; 2]>;
 
     fn next(&mut self) -> Option<Self::Item> {
         if self.remaining == 0 {
             None
         } else {
-            let result = self
-                .next
-                .iter()
-                .copied()
-                .zip(self.facets)
-                .map(|(index, facet)| &facet.series.unique_values[index])
-                .collect();
+            let result = self.next.iter().copied().collect();
             for (index, facet) in self.next.iter_mut().zip(self.facets) {
                 *index += 1;
                 if *index < facet.series.unique_values.len() {
@@ -5122,14 +5631,53 @@ mod tests {
 
     use approx::assert_relative_eq;
     use itertools::Itertools;
+    use smallvec::smallvec;
 
     use crate::{
         output::pivot::{Length, ParseLengthError, tests::assert_lines_eq},
         spv::read::graph::{
-            Binning, LocationLength, LocationLengthRange, Scaling, SemicolonSplit, sturges_rule,
+            Binning, LexIter, LocationLength, LocationLengthRange, Scaling, SemicolonSplit,
+            sturges_rule,
         },
     };
 
+    #[test]
+    fn lex_iter() {
+        // Normal lexicographical iteration.
+        assert_eq!(
+            LexIter::new([2, 2, 2]).collect_vec(),
+            vec![
+                (3, smallvec![0, 0, 0]),
+                (0, smallvec![1, 0, 0]),
+                (1, smallvec![0, 1, 0]),
+                (0, smallvec![1, 1, 0]),
+                (2, smallvec![0, 0, 1]),
+                (0, smallvec![1, 0, 1]),
+                (1, smallvec![0, 1, 1]),
+                (0, smallvec![1, 1, 1]),
+            ]
+        );
+
+        // Normal lexicographical iteration.
+        assert_eq!(
+            LexIter::new([1, 2, 3]).collect_vec(),
+            vec![
+                (3, smallvec![0, 0, 0]),
+                (1, smallvec![0, 1, 0]),
+                (2, smallvec![0, 0, 1]),
+                (1, smallvec![0, 1, 1]),
+                (2, smallvec![0, 0, 2]),
+                (1, smallvec![0, 1, 2]),
+            ]
+        );
+
+        // An empty set of counts yields a single empty element.
+        assert_eq!(LexIter::new([]).collect_vec(), vec![(0, smallvec![])]);
+
+        // But a count with value 0 yields no elements.
+        assert_eq!(LexIter::new([2, 0, 2]).collect_vec(), vec![]);
+    }
+
     #[test]
     fn length() {
         // Individual lengths.
index 4f06ca6e33dafb770fd5b0ba0b37ca44fc374d7a..b10d5612907fbd50b401bf8ec53b02f353763395 100644 (file)
@@ -46,7 +46,7 @@
     <location method="fixed" part="right" value="99%" />
     <coordinates id="coordinates_1305">
       <dimension id="dimension_1306" lowerMargin="5%" upperMargin="5%">
-        <axis combineAxisLabels="true" id="axis_axisx_1" repeat="never" style="axis_axisx_1s">
+        <axis combineAxisLabels="false" id="axis_axisx_1" repeat="never" style="axis_axisx_1s">
           <label id="label_axisx_1" style="style_1312" textFrameStyle="style_1310">
             <text id="text_1309">Variable 4</text>
           </label>
@@ -54,7 +54,7 @@
         </axis>
       </dimension>
       <dimension id="dimension_1319" upperMargin="5%">
-        <axis combineAxisLabels="true" id="axis_axisy_1" repeat="never" style="axis_axisy_1s">
+        <axis combineAxisLabels="false" id="axis_axisy_1" repeat="never" style="axis_axisy_1s">
           <label id="label_axisy_1" style="style_1325" textFrameStyle="style_1323">
             <text id="text_1322">Frequency</text>
           </label>