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::{
&& 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);
}
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];
.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)
.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(
{
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)
}
}
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)]
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 {
key: false,
categorical,
values,
- unique_values: categorical_values,
+ unique_values,
formatter,
}
}
// 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),
})
}
+ 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())
}
}
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.
enum FacetingChild {
Dot(Cross),
Cross(Cross),
+ Nest(Nest),
Layer(Layer),
}
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
}
}
#[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>,
}
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),
})
}
.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
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>],
},
}
}
+
+ 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() {
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.