use enum_map::{enum_map, EnumMap};
use itertools::Itertools;
-use smallvec::{SmallVec, ToSmallVec};
+use smallvec::SmallVec;
use crate::output::{
- pivot::LabelPosition,
+ pivot::{Group, LabelPosition, Leaf},
table::{CellInner, Table},
};
}
impl<'a> Iterator for AxisEnumerationIter<'a> {
- type Item = SmallVec<[usize; 4]>;
+ type Item = &'a [usize];
fn next(&mut self) -> Option<Self::Item> {
if self.position < self.enumeration.indexes.len() {
- let item = (&self.enumeration.indexes
- [self.position..self.position + self.enumeration.stride])
- .to_smallvec();
+ let item =
+ &self.enumeration.indexes[self.position..self.position + self.enumeration.stride];
self.position += self.enumeration.stride;
Some(item)
} else {
pub fn output_body(&self, layer_indexes: &[usize], printing: bool) -> Table {
let column_enumeration = self.enumerate_axis(Axis3::X, layer_indexes, self.look.omit_empty);
+ let column_headings = Headings::new(self, Axis2::X, &column_enumeration);
+
let row_enumeration = self.enumerate_axis(Axis3::Y, layer_indexes, self.look.omit_empty);
+ let row_headings = Headings::new(self, Axis2::Y, &row_enumeration);
+
let data = Coord2::new(column_enumeration.len(), row_enumeration.len());
- let stub = Coord2::new(
- self.axis_label_depth(Axis3::Y),
- self.axis_label_depth(Axis3::X),
- );
+ let stub = Coord2::new(row_headings.height(), column_headings.height());
let n = EnumMap::from_fn(|axis| data[axis] + stub[axis]).into();
let mut body = Table::new(
n,
self.borders(printing),
self.as_value_options(),
);
- compose_headings(
- self,
+
+ column_headings.render(
&mut body,
- Axis2::X,
- &column_enumeration,
+ row_headings.height(),
RowColBorder::ColHorz,
RowColBorder::ColVert,
self.rotate_outer_row_labels,
false,
Area::ColumnLabels,
);
- compose_headings(
- self,
+ row_headings.render(
&mut body,
- Axis2::Y,
- &row_enumeration,
+ column_headings.height(),
RowColBorder::RowVert,
RowColBorder::RowHorz,
false,
Some(c)
}
-/// Fills row or column headings into `table`.
-///
-/// This function uses terminology and variable names for column headings, but
-/// it also applies to row headings because it uses variables for the
-/// differences, e.g. when for column headings it would use the H axis, it
-/// instead uses 'h', which is set to H for column headings and V for row
-/// headings.
-fn compose_headings(
- pt: &PivotTable,
- table: &mut Table,
- h: Axis2,
- column_enumeration: &AxisEnumeration,
- col_horz: RowColBorder,
- col_vert: RowColBorder,
- rotate_inner_labels: bool,
- rotate_outer_labels: bool,
- area: Area,
-) {
- let v = !h;
- let h_axis = &pt.axes[h.into()];
- let v_size = pt.axis_label_depth(h.into());
- let h_ofs = pt.axis_label_depth(v.into());
- let n_columns = column_enumeration.len();
-
- if h_axis.dimensions.is_empty() || n_columns == 0 || v_size == 0 {
- return;
+struct HeadingColumn<'a> {
+ leaf: &'a Leaf,
+ groups: SmallVec<[Arc<Group>; 4]>,
+}
+
+impl<'a> HeadingColumn<'a> {
+ pub fn get(&self, y: usize, height: usize) -> Option<&Value> {
+ if y + 1 == height {
+ Some(&self.leaf.name)
+ } else {
+ self.groups.get(y).map(|group| &*group.name)
+ }
}
+}
- // Below, we're going to iterate through the dimensions. Each dimension
- // occupies one or more rows in the heading. `top_row` is the top row of
- // these (and `top_row + d->label_depth - 1` is the bottom row).
- let mut top_row = 0;
-
- // We're going to iterate through dimensions and the rows that label them
- // from top to bottom (from outer to inner dimensions). As we move
- // downward, we start drawing vertical rules to separate categories and
- // groups. After we start drawing a vertical rule in a particular
- // horizontal position, it continues until the bottom of the heading.
- // vrules[pos] indicates whether, in our current row, we have already
- // started drawing a vertical rule in horizontal position `pos`. (There are
- // n_columns + 1 horizontal positions. We allocate all of them for
- // convenience below but only the inner `n_columns - 1` of them really
- // matter.)
- //
- // Here's an example that shows how vertical rules continue all the way
- // downward:
- //
- // ```text
- // +-----------------------------------------------------+ __
- // | bbbb | |
- // +-----------------+-----------------+-----------------+ |dimension "bbbb"
- // | bbbb1 | bbbb2 | bbbb3 | _|
- // +-----------------+-----------------+-----------------+ __
- // | aaaa | aaaa | aaaa | |
- // +-----+-----+-----+-----+-----+-----+-----+-----+-----+ |dimension "aaaa"
- // |aaaa1|aaaa2|aaaa3|aaaa1|aaaa2|aaaa3|aaaa1|aaaa2|aaaa3| _|
- // +-----+-----+-----+-----+-----+-----+-----+-----+-----+
- //
- // ^ ^ ^ ^ ^ ^ ^ ^ ^ ^
- // | | | | | | | | | |
- // 0 1 2 3 4 5 6 7 8 9
- // |___________________vrules[] indexes__________________|
- // ```
- //
- // Our data structures are more naturally iterated from bottom to top (inner
- // to outer dimensions). A previous version of this code actually worked
- // like that, but it didn't draw all of the vertical lines correctly as
- // shown above. It ended up rendering the above heading much like shown
- // below, which isn't what users expect. The "aaaa" label really needs to
- // be shown three times for clarity:
- //
- // ```text
- // +-----------------------------------------------------+
- // | bbbb |
- // +-----------------+-----------------+-----------------+
- // | bbbb1 | bbbb2 | bbbb3 |
- // +-----------------+-----------------+-----------------+
- // | | aaaa | |
- // +-----+-----+-----+-----+-----+-----+-----+-----+-----+
- // |aaaa1|aaaa2|aaaa3|aaaa1|aaaa2|aaaa3|aaaa1|aaaa2|aaaa3|
- // +-----+-----+-----+-----+-----+-----+-----+-----+-----+
- // ```
- let mut vrules = vec![false; n_columns + 1];
- vrules[0] = true;
- vrules[n_columns] = true;
-
- for (dim_index, d) in pt
- .axis_dimensions(h.into())
- .enumerate()
- .rev()
- .filter(|(_, d)| !d.hide_all_labels)
- {
- for row_ofs in 0..d.label_depth() {
+struct Heading<'a> {
+ dimension: &'a Dimension,
+ height: usize,
+ columns: Vec<HeadingColumn<'a>>,
+}
+
+impl<'a> Heading<'a> {
+ fn new(
+ dimension: &'a Dimension,
+ dim_index: usize,
+ column_enumeration: &AxisEnumeration,
+ ) -> Option<Self> {
+ if dimension.hide_all_labels {
+ return None;
+ }
+
+ let mut columns = Vec::new();
+ let mut height = 0;
+ for indexes in column_enumeration.iter() {
+ let leaf = &*dimension.data_leaves[dimension.presentation_order[indexes[dim_index]]];
+ let mut groups = leaf
+ .ancestors()
+ .filter(|group| group.show_label.is_some())
+ .collect::<SmallVec<_>>();
+ groups.reverse();
+ height = height.max(1 + groups.len());
+ columns.push(HeadingColumn { leaf, groups });
+ }
+
+ Some(Self {
+ dimension,
+ height,
+ columns,
+ })
+ }
+
+ fn width(&self) -> usize {
+ self.columns.len()
+ }
+
+ fn render(
+ &self,
+ table: &mut Table,
+ vrules: &mut [bool],
+ h: Axis2,
+ h_ofs: usize,
+ v_ofs: usize,
+ column_enumeration: &AxisEnumeration,
+ col_horz: RowColBorder,
+ col_vert: RowColBorder,
+ rotate_inner_labels: bool,
+ rotate_outer_labels: bool,
+ area: Area,
+ ) {
+ let v = !h;
+
+ for row in 0..self.height {
// Find all the categories, dropping columns without a category.
- let categories = (0..n_columns).filter_map(|x| {
- find_category(
- d,
- dim_index,
- column_enumeration.get(x),
- d.label_depth() - row_ofs - 1,
- )
- .map(|c| (x..x + 1, c))
+ let categories = self.columns.iter().enumerate().filter_map(|(x, column)| {
+ column.get(row, self.height).map(|name| (x..x + 1, name))
});
// Merge adjacent identical categories (but don't merge across a vertical rule).
let categories = categories
.coalesce(|(a_r, a), (b_r, b)| {
- if !vrules[b_r.start] && a.ptr_eq(&b) {
+ if !vrules[b_r.start] && std::ptr::eq(a, b) {
Ok((a_r.start..b_r.end, a))
} else {
Err(((a_r, a), (b_r, b)))
})
.collect::<Vec<_>>();
- for (Range { start: x1, end: x2 }, c) in categories {
- let y1 = top_row + row_ofs;
- let y2 = y1 + c.extra_depth() + 1;
- let is_outer_row = y1 == 0;
- let is_inner_row = y2 == v_size;
- if c.show_label() {
- table.put(
- Rect2::for_ranges((h, x1 + h_ofs..x2 + h_ofs), y1..y2),
- CellInner {
- rotate: (rotate_inner_labels && is_inner_row)
- || (rotate_outer_labels && is_outer_row),
- area,
- value: Box::new(c.name().clone()),
+ for (Range { start: x1, end: x2 }, name) in categories {
+ let y1 = v_ofs + row;
+ let y2 = y1 + 1;
+ table.put(
+ Rect2::for_ranges((h, x1 + h_ofs..x2 + h_ofs), y1..y2),
+ CellInner {
+ rotate: {
+ let is_outer_row = y1 == 0;
+ let is_inner_row = y2 == self.height;
+ (rotate_inner_labels && is_inner_row)
+ || (rotate_outer_labels && is_outer_row)
},
- );
-
- // Draw all the vertical lines in our running example, other
- // than the far left and far right ones. Only the ones that
- // start in the last row of the heading are drawn with the
- // "category" style, the rest with the "dimension" style,
- // e.g. only the # below are category style:
- //
- // ```text
- // +-----------------------------------------------------+
- // | bbbb |
- // +-----------------+-----------------+-----------------+
- // | bbbb1 | bbbb2 | bbbb3 |
- // +-----------------+-----------------+-----------------+
- // | aaaa | aaaa | aaaa |
- // +-----+-----+-----+-----+-----+-----+-----+-----+-----+
- // |aaaa1#aaaa2#aaaa3|aaaa1#aaaa2#aaaa3|aaaa1#aaaa2#aaaa3|
- // +-----+-----+-----+-----+-----+-----+-----+-----+-----+
- // ```
- let border = if y1 == v_size - 1 {
- Border::Categories(col_vert)
- } else {
- Border::Dimensions(col_vert)
- };
- if !vrules[x2] {
- table.draw_line(border, (v, x2 + h_ofs), y1..table.n[v]);
- vrules[x2] = true;
- }
- if !vrules[x1] {
- table.draw_line(border, (v, x1 + h_ofs), y1..table.n[v]);
- vrules[x1] = true;
- }
+ area,
+ value: Box::new(name.clone()),
+ },
+ );
- // Draws the horizontal lines within a dimension, that is,
- // those that separate a category (or group) from its parent
- // group or dimension's label. Our running example doesn't
- // have groups but the `====` lines below show the
- // separators between categories and their dimension label:
- //
- // ```text
- // +-----------------------------------------------------+
- // | bbbb |
- // +=================+=================+=================+
- // | bbbb1 | bbbb2 | bbbb3 |
- // +-----------------+-----------------+-----------------+
- // | aaaa | aaaa | aaaa |
- // +=====+=====+=====+=====+=====+=====+=====+=====+=====+
- // |aaaa1|aaaa2|aaaa3|aaaa1|aaaa2|aaaa3|aaaa1|aaaa2|aaaa3|
- // +-----+-----+-----+-----+-----+-----+-----+-----+-----+
- // ```
- if c.parent().is_some_and(|parent| parent.show_label.is_some()) {
- table.draw_line(Border::Categories(col_horz), (h, y1), h_ofs..table.n[h]);
- }
+ // Draw all the vertical lines in our running example, other
+ // than the far left and far right ones. Only the ones that
+ // start in the last row of the heading are drawn with the
+ // "category" style, the rest with the "dimension" style,
+ // e.g. only the # below are category style:
+ //
+ // ```text
+ // +-----------------------------------------------------+
+ // | bbbb |
+ // +-----------------+-----------------+-----------------+
+ // | bbbb1 | bbbb2 | bbbb3 |
+ // +-----------------+-----------------+-----------------+
+ // | aaaa | aaaa | aaaa |
+ // +-----+-----+-----+-----+-----+-----+-----+-----+-----+
+ // |aaaa1#aaaa2#aaaa3|aaaa1#aaaa2#aaaa3|aaaa1#aaaa2#aaaa3|
+ // +-----+-----+-----+-----+-----+-----+-----+-----+-----+
+ // ```
+ let border = if y1 == self.height - 1 {
+ Border::Categories(col_vert)
+ } else {
+ Border::Dimensions(col_vert)
+ };
+ if !vrules[x2] {
+ table.draw_line(border, (v, x2 + h_ofs), y1..table.n[v]);
+ vrules[x2] = true;
+ }
+ if !vrules[x1] {
+ table.draw_line(border, (v, x1 + h_ofs), y1..table.n[v]);
+ vrules[x1] = true;
}
- }
- 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()),
- );
+ // Draws the horizontal lines within a dimension, that is,
+ // those that separate a category (or group) from its parent
+ // group or dimension's label. Our running example doesn't
+ // have groups but the `====` lines below show the
+ // separators between categories and their dimension label:
+ //
+ // ```text
+ // +-----------------------------------------------------+
+ // | bbbb |
+ // +=================+=================+=================+
+ // | bbbb1 | bbbb2 | bbbb3 |
+ // +-----------------+-----------------+-----------------+
+ // | aaaa | aaaa | aaaa |
+ // +=====+=====+=====+=====+=====+=====+=====+=====+=====+
+ // |aaaa1|aaaa2|aaaa3|aaaa1|aaaa2|aaaa3|aaaa1|aaaa2|aaaa3|
+ // +-----+-----+-----+-----+-----+-----+-----+-----+-----+
+ // ```
+ if row > 0 {
+ table.draw_line(Border::Categories(col_horz), (h, y1), h_ofs..table.n[h]);
+ }
}
+ }
+ }
+}
- // Draw the horizontal line between dimensions, e.g. the `=====`
- // line here:
- //
- // ```text
- // +-----------------------------------------------------+ __
- // | bbbb | |
- // +-----------------+-----------------+-----------------+ |dim "bbbb"
- // | bbbb1 | bbbb2 | bbbb3 | _|
- // +=================+=================+=================+ __
- // | aaaa | aaaa | aaaa | |
- // +-----+-----+-----+-----+-----+-----+-----+-----+-----+ |dim "aaaa"
- // |aaaa1|aaaa2|aaaa3|aaaa1|aaaa2|aaaa3|aaaa1|aaaa2|aaaa3| _|
- // +-----+-----+-----+-----+-----+-----+-----+-----+-----+
- // ```
- if dim_index != h_axis.dimensions.len() - 1 {
- table.draw_line(
- Border::Dimensions(col_horz),
- (h, top_row),
- h_ofs..table.n[h],
- );
+struct Headings<'a> {
+ headings: Vec<Heading<'a>>,
+ h: Axis2,
+ column_enumeration: &'a AxisEnumeration,
+}
+
+impl<'a> Headings<'a> {
+ fn new(pt: &'a PivotTable, h: Axis2, column_enumeration: &'a AxisEnumeration) -> Self {
+ Self {
+ headings: pt.axes[h.into()]
+ .dimensions
+ .iter()
+ .copied()
+ .enumerate()
+ .rev()
+ .filter_map(|(axis_index, dim_index)| {
+ Heading::new(&pt.dimensions[dim_index], axis_index, column_enumeration)
+ })
+ .collect(),
+ h,
+ column_enumeration,
+ }
+ }
+
+ fn height(&self) -> usize {
+ self.headings.iter().map(|h| h.height).sum()
+ }
+
+ fn width(&self) -> usize {
+ self.headings.first().map_or(0, |h| h.width())
+ }
+
+ fn render(
+ &self,
+ table: &mut Table,
+ h_ofs: usize,
+ col_horz: RowColBorder,
+ col_vert: RowColBorder,
+ rotate_inner_labels: bool,
+ rotate_outer_labels: bool,
+ area: Area,
+ ) {
+ if self.headings.is_empty() {
+ return;
+ }
+
+ let h = self.h;
+ let n_columns = self.width();
+ let n_rows = self.height();
+ let mut vrules = vec![false; n_columns + 1];
+ vrules[0] = true;
+ vrules[n_columns] = true;
+
+ let mut v_ofs = 0;
+ for (index, heading) in self.headings.iter().enumerate() {
+ heading.render(
+ table,
+ &mut vrules,
+ h,
+ h_ofs,
+ v_ofs,
+ self.column_enumeration,
+ col_horz,
+ col_vert,
+ rotate_inner_labels,
+ rotate_outer_labels,
+ area,
+ );
+ v_ofs += heading.height;
+ if index != self.headings.len() - 1 {
+ // Draw the horizontal line between dimensions, e.g. the `=====`
+ // line here:
+ //
+ // ```text
+ // +-----------------------------------------------------+ __
+ // | bbbb | |
+ // +-----------------+-----------------+-----------------+ |dim "bbbb"
+ // | bbbb1 | bbbb2 | bbbb3 | _|
+ // +=================+=================+=================+ __
+ // | aaaa | aaaa | aaaa | |
+ // +-----+-----+-----+-----+-----+-----+-----+-----+-----+ |dim "aaaa"
+ // |aaaa1|aaaa2|aaaa3|aaaa1|aaaa2|aaaa3|aaaa1|aaaa2|aaaa3| _|
+ // +-----+-----+-----+-----+-----+-----+-----+-----+-----+
+ // ```
+ table.draw_line(Border::Dimensions(col_horz), (h, v_ofs), h_ofs..table.n[h]);
}
}
- top_row += d.label_depth();
+ }
+}
+
+pub fn try_range<R>(range: R, bounds: std::ops::RangeTo<usize>) -> Option<std::ops::Range<usize>>
+where
+ R: std::ops::RangeBounds<usize>,
+{
+ let len = bounds.end;
+
+ let start = match range.start_bound() {
+ std::ops::Bound::Included(&start) => start,
+ std::ops::Bound::Excluded(start) => start.checked_add(1)?,
+ std::ops::Bound::Unbounded => 0,
+ };
+
+ let end = match range.end_bound() {
+ std::ops::Bound::Included(end) => end.checked_add(1)?,
+ std::ops::Bound::Excluded(&end) => end,
+ std::ops::Bound::Unbounded => len,
+ };
+
+ if start > end || end > len {
+ None
+ } else {
+ Some(std::ops::Range { start, end })
}
}