Skip to main content

ravenrs_sheet/engine/pivot/
output.rs

1use super::items::{Acc, FieldItems, Node, is_collapsed};
2use super::{
3    BorderLine, BorderStyle, Color, Fill, HAlign, HashMap, MAX_COLS, MAX_OUTPUT_CELLS, MAX_ROWS, NumFmt, PivotField, PivotLayout, PivotTable, Style,
4    Value,
5};
6
7// ---------------------------------------------------------------- output
8
9/// What a report cell is.
10#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
11pub enum CellKind {
12    Empty,
13    Header,
14    RowLabel,
15    ColLabel,
16    Data,
17    Subtotal,
18    GrandTotal,
19    Filter,
20    Placeholder,
21}
22
23/// What a report row is (for row-wide styling).
24#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize)]
25pub enum RowKind {
26    Filter,
27    Blank,
28    Header,
29    /// Item row; `true` for a parent item (bold).
30    Item(bool),
31    Subtotal,
32    Grand,
33    Placeholder,
34}
35
36#[derive(Clone, Debug, PartialEq)]
37pub struct OutCell {
38    pub value: Value,
39    pub kind: CellKind,
40    pub indent: u8,
41    pub num_fmt: Option<String>,
42}
43
44impl OutCell {
45    fn empty() -> OutCell {
46        OutCell { value: Value::Empty, kind: CellKind::Empty, indent: 0, num_fmt: None }
47    }
48}
49
50/// A computed report: a dense grid relative to its top-left, with row kinds.
51#[derive(Clone, Debug)]
52pub struct Output {
53    pub grid: Vec<Vec<OutCell>>,
54    pub row_kinds: Vec<RowKind>,
55    /// Rows above the body (report filters plus a blank row).
56    pub body_offset: u32,
57    pub width: u32,
58    /// Grid rows of the innermost (or collapsed) items: a PivotChart's categories.
59    pub chart_rows: Vec<u32>,
60    /// Grid columns of item (not total) values with their series names: a PivotChart's series.
61    pub chart_cols: Vec<(u32, String)>,
62    /// Grid column holding the innermost row labels (`None` without row fields).
63    pub label_col: Option<u32>,
64}
65
66impl Output {
67    pub fn height(&self) -> u32 {
68        self.grid.len() as u32
69    }
70    /// Value at (row, col) relative to the top-left.
71    pub fn value(&self, r: usize, c: usize) -> Value {
72        self.grid.get(r).and_then(|row| row.get(c)).map(|x| x.value.clone()).unwrap_or_default()
73    }
74}
75
76pub(super) struct Grid {
77    pub(super) rows: Vec<Vec<OutCell>>,
78    pub(super) kinds: Vec<RowKind>,
79    pub(super) width: usize,
80}
81
82impl Grid {
83    pub(super) fn push_row(&mut self, kind: RowKind) -> Result<usize, String> {
84        if (self.rows.len() as u64 + 1) * self.width.max(1) as u64 > MAX_OUTPUT_CELLS || self.rows.len() as u64 >= MAX_ROWS as u64 {
85            return Err("The PivotTable report is too large.".into());
86        }
87        self.rows.push(vec![OutCell::empty(); self.width]);
88        self.kinds.push(kind);
89        Ok(self.rows.len() - 1)
90    }
91    pub(super) fn set(&mut self, r: usize, c: usize, value: Value, kind: CellKind, indent: u8, fmt: Option<String>) {
92        if let Some(cell) = self.rows.get_mut(r).and_then(|row| row.get_mut(c)) {
93            *cell = OutCell { value, kind, indent, num_fmt: fmt };
94        }
95    }
96}
97
98#[derive(Clone, Copy, PartialEq, Eq)]
99pub(super) enum LineKind {
100    Item(bool),
101    Subtotal,
102    Grand,
103}
104
105pub(super) struct Line {
106    pub(super) kind: LineKind,
107    /// A category of a PivotChart: an innermost (or collapsed) item.
108    pub(super) leaf: bool,
109    pub(super) path: Vec<u32>,
110    pub(super) values: bool,
111    /// (column, value, indent, number format)
112    pub(super) labels: Vec<(usize, Value, u8, Option<String>)>,
113    /// The value field of the line when Σ Values sits in Rows (`None`: one per data column).
114    pub(super) vi: Option<usize>,
115}
116
117#[derive(Clone, Copy, PartialEq, Eq)]
118pub(super) enum ColKind {
119    Leaf,
120    Subtotal,
121    Grand,
122}
123
124pub(super) struct ColSpec {
125    pub(super) kind: ColKind,
126    pub(super) path: Vec<u32>,
127}
128
129pub(super) struct Ctx<'a> {
130    pub(super) pt: &'a PivotTable,
131    pub(super) rows: Vec<&'a PivotField>,
132    pub(super) row_items: Vec<FieldItems>,
133    pub(super) row_fmts: Vec<String>,
134    /// Depth of Σ Values among the row fields when it sits in Rows (two or more value fields).
135    pub(super) vpos: Option<usize>,
136    /// Value field captions.
137    pub(super) val_names: Vec<String>,
138}
139
140impl Ctx<'_> {
141    fn label_cell(&self, depth: usize, item: u32) -> (Value, Option<String>, String) {
142        let Some(it) = self.row_items.get(depth).and_then(|fi| fi.get(item)) else { return (Value::Empty, None, String::new()) };
143        let fmt = if it.formatted { self.row_fmts.get(depth).cloned() } else { None };
144        (it.value.clone(), fmt, it.label.clone())
145    }
146
147    /// (column, indent) of a row level (Σ Values counts as a level) in Compact/Outline form.
148    fn level_cell(&self, level: usize) -> (usize, u8) {
149        if self.pt.layout == PivotLayout::Outline { (level, 0) } else { (0, level.min(250) as u8) }
150    }
151
152    /// Σ Values comes right here, at row field depth `depth` (`vi`: the value field already chosen above).
153    fn values_here(&self, depth: usize, vi: Option<usize>) -> bool {
154        vi.is_none() && self.vpos == Some(depth)
155    }
156
157    /// Per-value subtotal lines of an item with Σ Values below it ("East Sum of Sales").
158    fn value_subtotals(&self, col: usize, indent: u8, label: &str, prefix: &[u32], out: &mut Vec<Line>) {
159        for (vi, name) in self.val_names.iter().enumerate() {
160            out.push(Line {
161                kind: LineKind::Subtotal,
162                leaf: false,
163                path: prefix.to_vec(),
164                values: true,
165                labels: vec![(col, Value::text(format!("{label} {name}")), indent, None)],
166                vi: Some(vi),
167            });
168        }
169    }
170
171    /// The Σ Values level at row field depth `depth`: one item per value field, each followed by
172    /// the row fields below it (`nodes`) or, innermost, holding that value.
173    fn flatten_values(&self, nodes: &[Node], depth: usize, prefix: &mut Vec<u32>, out: &mut Vec<Line>) {
174        let (col, indent) = self.level_cell(depth);
175        for (vi, name) in self.val_names.iter().enumerate() {
176            let labels = vec![(col, Value::text(name.as_str()), indent, None)];
177            let leaf = nodes.is_empty();
178            out.push(Line { kind: LineKind::Item(!leaf), leaf, path: prefix.clone(), values: leaf, labels, vi: Some(vi) });
179            if !leaf {
180                self.flatten(nodes, depth, Some(vi), prefix, out);
181            }
182        }
183    }
184
185    pub(super) fn flatten(&self, nodes: &[Node], depth: usize, vi: Option<usize>, prefix: &mut Vec<u32>, out: &mut Vec<Line>) {
186        if self.values_here(depth, vi) {
187            self.flatten_values(nodes, depth, prefix, out);
188            return;
189        }
190        let n = self.rows.len();
191        // Σ Values still to come below this level: totals need one line per value field.
192        let values_below = vi.is_none() && self.vpos.is_some();
193        let (col, indent) = self.level_cell(depth + usize::from(vi.is_some() && self.vpos.is_some()));
194        for node in nodes {
195            prefix.push(node.item);
196            let (v, fmt, label) = self.label_cell(depth, node.item);
197            let collapsed = depth + 1 < n && self.rows.get(depth).is_some_and(|f| is_collapsed(f, &label));
198            let leaf = depth + 1 >= n || collapsed || node.children.is_empty();
199            if leaf && !values_below {
200                out.push(Line {
201                    kind: LineKind::Item(depth + 1 < n),
202                    leaf: true,
203                    path: prefix.clone(),
204                    values: true,
205                    labels: vec![(col, v, indent, fmt)],
206                    vi,
207                });
208            } else if leaf {
209                out.push(Line {
210                    kind: LineKind::Item(true),
211                    leaf: false,
212                    path: prefix.clone(),
213                    values: false,
214                    labels: vec![(col, v, indent, fmt)],
215                    vi,
216                });
217                // Outline gives each level its own column, so the captions go in the Values
218                // column even when the levels between are collapsed away.
219                let level = if self.pt.layout == PivotLayout::Outline { self.vpos.unwrap_or(depth + 1) } else { depth + 1 };
220                self.flatten_values(&[], level, prefix, out);
221            } else if values_below {
222                // Subtotals per value field, below the group (one line can't hold them all).
223                out.push(Line {
224                    kind: LineKind::Item(true),
225                    leaf: false,
226                    path: prefix.clone(),
227                    values: false,
228                    labels: vec![(col, v, indent, fmt)],
229                    vi,
230                });
231                self.flatten(&node.children, depth + 1, None, prefix, out);
232                if self.pt.subtotals {
233                    self.value_subtotals(col, indent, &label, prefix, out);
234                }
235            } else {
236                let top = self.pt.subtotals && self.pt.subtotals_top;
237                out.push(Line {
238                    kind: LineKind::Item(true),
239                    leaf: false,
240                    path: prefix.clone(),
241                    values: top,
242                    labels: vec![(col, v, indent, fmt)],
243                    vi,
244                });
245                self.flatten(&node.children, depth + 1, vi, prefix, out);
246                if self.pt.subtotals && !self.pt.subtotals_top {
247                    out.push(Line {
248                        kind: LineKind::Subtotal,
249                        leaf: false,
250                        path: prefix.clone(),
251                        values: true,
252                        labels: vec![(col, Value::text(format!("{label} Total")), indent, None)],
253                        vi,
254                    });
255                }
256            }
257            prefix.pop();
258        }
259    }
260
261    /// Tabular form of [`Self::flatten_values`]: the first line also carries the parents' labels.
262    fn flatten_tabular_values(
263        &self,
264        nodes: &[Node],
265        depth: usize,
266        prefix: &mut Vec<u32>,
267        pending: &[(usize, Value, u8, Option<String>)],
268        out: &mut Vec<Line>,
269    ) {
270        for (vi, name) in self.val_names.iter().enumerate() {
271            let mut labels: Vec<(usize, Value, u8, Option<String>)> = if vi == 0 { pending.to_vec() } else { vec![] };
272            labels.push((depth, Value::text(name.as_str()), 0, None));
273            if nodes.is_empty() {
274                out.push(Line { kind: LineKind::Item(false), leaf: true, path: prefix.clone(), values: true, labels, vi: Some(vi) });
275            } else {
276                self.flatten_tabular(nodes, depth, Some(vi), prefix, &labels, out);
277            }
278        }
279    }
280
281    pub(super) fn flatten_tabular(
282        &self,
283        nodes: &[Node],
284        depth: usize,
285        vi: Option<usize>,
286        prefix: &mut Vec<u32>,
287        pending: &[(usize, Value, u8, Option<String>)],
288        out: &mut Vec<Line>,
289    ) {
290        if self.values_here(depth, vi) {
291            self.flatten_tabular_values(nodes, depth, prefix, pending, out);
292            return;
293        }
294        let n = self.rows.len();
295        let values_below = vi.is_none() && self.vpos.is_some();
296        let col = depth + usize::from(vi.is_some() && self.vpos.is_some());
297        for (i, node) in nodes.iter().enumerate() {
298            prefix.push(node.item);
299            let (v, fmt, label) = self.label_cell(depth, node.item);
300            let mut labels: Vec<(usize, Value, u8, Option<String>)> = if i == 0 { pending.to_vec() } else { vec![] };
301            labels.push((col, v, 0, fmt));
302            let collapsed = depth + 1 < n && self.rows.get(depth).is_some_and(|f| is_collapsed(f, &label));
303            if depth + 1 >= n || collapsed || node.children.is_empty() {
304                if values_below {
305                    self.flatten_tabular_values(&[], self.vpos.unwrap_or(depth + 1), prefix, &labels, out);
306                } else {
307                    out.push(Line { kind: LineKind::Item(false), leaf: true, path: prefix.clone(), values: true, labels, vi });
308                }
309            } else {
310                self.flatten_tabular(&node.children, depth + 1, vi, prefix, &labels, out);
311                if self.pt.subtotals && values_below {
312                    self.value_subtotals(col, 0, &label, prefix, out);
313                } else if self.pt.subtotals {
314                    out.push(Line {
315                        kind: LineKind::Subtotal,
316                        leaf: false,
317                        path: prefix.clone(),
318                        values: true,
319                        labels: vec![(col, Value::text(format!("{label} Total")), 0, None)],
320                        vi,
321                    });
322                }
323            }
324            prefix.pop();
325        }
326    }
327}
328
329pub(super) fn flatten_cols(
330    nodes: &[Node],
331    depth: usize,
332    fields: &[&PivotField],
333    items: &[FieldItems],
334    subtotals: bool,
335    prefix: &mut Vec<u32>,
336    out: &mut Vec<ColSpec>,
337) {
338    let n = fields.len();
339    for node in nodes {
340        prefix.push(node.item);
341        let label = items.get(depth).map(|fi| fi.label(node.item)).unwrap_or_default();
342        let collapsed = depth + 1 < n && fields.get(depth).is_some_and(|f| is_collapsed(f, &label));
343        if depth + 1 >= n || collapsed || node.children.is_empty() {
344            out.push(ColSpec { kind: ColKind::Leaf, path: prefix.clone() });
345        } else {
346            flatten_cols(&node.children, depth + 1, fields, items, subtotals, prefix, out);
347            if subtotals {
348                out.push(ColSpec { kind: ColKind::Subtotal, path: prefix.clone() });
349            }
350        }
351        prefix.pop();
352    }
353}
354
355pub(super) type AggMap = HashMap<(Vec<u32>, Vec<u32>), Vec<Acc>>;
356
357/// Where a value field's numbers come from.
358#[derive(Clone, Copy)]
359pub(super) enum ValSrc {
360    /// A source column.
361    Col(usize),
362    /// A calculated field (index into the PivotTable's calculated fields).
363    Calc(usize),
364}
365
366/// True when `col_groups` distinct column leaves (plus the grand-total column) times `values` value fields
367/// (at least one data column per group) already exceed the sheet width. This is a lower bound on the final column count.
368pub(super) fn data_cols_exceed_sheet(col_groups: usize, grand_total: bool, values: usize) -> bool {
369    (col_groups + usize::from(grand_total)).saturating_mul(values.max(1)) > MAX_COLS as usize
370}
371
372// ---------------------------------------------------------------- styling and writing
373
374/// Colours for a PivotTable style name (our own palette, keyed by the familiar
375/// `PivotStyleLight1…28 / Medium1…28 / Dark1…28` names).
376pub(super) struct Look {
377    header_fill: Color,
378    header_font: Color,
379    line: Color,
380    filter_fill: Color,
381}
382
383pub(super) fn look(style: &str) -> Look {
384    let rest = style.strip_prefix("PivotStyle").unwrap_or(style);
385    let (fam, n) = ["Light", "Medium", "Dark"]
386        .iter()
387        .find_map(|f| rest.strip_prefix(f).map(|num| (*f, num.parse::<u32>().unwrap_or(16))))
388        .unwrap_or(("Light", 16));
389    let k = n.saturating_sub(1) % 7;
390    let acc = if k == 0 { None } else { Some(3 + k as u8) };
391    let tone = |tint: i16| match acc {
392        Some(a) => Color::Theme(a, tint),
393        None => Color::Theme(1, tint.clamp(-1000, 1000)),
394    };
395    match fam {
396        "Dark" => Look { header_fill: Color::Theme(1, 0), header_font: Color::Theme(0, 0), line: tone(0), filter_fill: tone(600) },
397        "Medium" => Look { header_fill: tone(0), header_font: Color::Theme(0, 0), line: tone(0), filter_fill: tone(800) },
398        _ => Look { header_fill: tone(800), header_font: Color::Auto, line: tone(400), filter_fill: tone(800) },
399    }
400}
401
402pub(super) fn cell_style(lk: &Look, rk: RowKind, cell: &OutCell, last_header: bool) -> Style {
403    let mut s = Style::default();
404    match rk {
405        RowKind::Header => {
406            s.font.bold = true;
407            s.fill = Fill::solid(lk.header_fill);
408            s.font.color = lk.header_font;
409            if last_header {
410                s.borders.bottom = BorderLine { style: BorderStyle::Thin, color: lk.line };
411            }
412        }
413        RowKind::Filter => {
414            s.fill = Fill::solid(lk.filter_fill);
415            s.font.bold = last_header;
416        }
417        RowKind::Item(parent) => s.font.bold = parent,
418        RowKind::Subtotal => s.font.bold = true,
419        RowKind::Grand => {
420            s.font.bold = true;
421            s.borders.top = BorderLine { style: BorderStyle::Thin, color: lk.line };
422        }
423        RowKind::Placeholder => {
424            s.font.italic = true;
425            s.font.color = Color::Theme(1, 500);
426        }
427        RowKind::Blank => {}
428    }
429    if matches!(cell.kind, CellKind::RowLabel | CellKind::ColLabel | CellKind::Subtotal | CellKind::GrandTotal) && !cell.value.is_number() {
430        s.align.h = HAlign::Left;
431    }
432    if matches!(cell.kind, CellKind::RowLabel | CellKind::ColLabel) && cell.value.is_number() {
433        s.align.h = HAlign::Left;
434    }
435    s.align.indent = cell.indent.min(15);
436    if let Some(f) = &cell.num_fmt {
437        s.num_fmt = NumFmt::new(f);
438    }
439    s
440}