Skip to main content

ravenrs_sheet/engine/pivot/
charts.rs

1use super::output::Output;
2use super::{CellRef, Chart, Series, Workbook, quote_sheet};
3
4// ---------------------------------------------------------------- PivotCharts
5
6/// The link a PivotChart stores to its PivotTable: `Sheet2!PivotTable1`.
7pub fn chart_link(sheet: &str, pivot: &str) -> String {
8    format!("{}!{pivot}", quote_sheet(sheet))
9}
10
11/// (sheet, PivotTable name) of a PivotChart link; a leading `[Book.xlsx]` is ignored.
12pub fn parse_chart_link(link: &str) -> Option<(String, String)> {
13    let t = link.trim();
14    let t = match t.strip_prefix('[') {
15        Some(rest) => rest.split_once(']').map(|(_, r)| r).unwrap_or(rest),
16        None => t,
17    };
18    let (sheet, name) = t.rsplit_once('!')?;
19    let sheet = sheet.trim();
20    let sheet = crate::base::unquote_sheet(sheet);
21    let name = name.trim();
22    if sheet.is_empty() || name.is_empty() {
23        return None;
24    }
25    Some((sheet, name.to_string()))
26}
27
28/// Whether a PivotChart link points at `pivot` on `sheet`.
29pub fn links_to(link: &str, sheet: &str, pivot: &str) -> bool {
30    parse_chart_link(link).is_some_and(|(s, n)| s.eq_ignore_ascii_case(sheet) && n.eq_ignore_ascii_case(pivot))
31}
32
33/// An absolute reference to `col` over grid `rows` (contiguous runs joined into a union).
34fn rows_ref(sheet: &str, origin: CellRef, col: u32, rows: &[u32]) -> Option<String> {
35    let q = quote_sheet(sheet);
36    let c = origin.col.checked_add(col)?;
37    let letters = crate::base::col_to_letters(c);
38    let mut parts: Vec<String> = Vec::new();
39    let mut i = 0;
40    while let Some(&start) = rows.get(i) {
41        let mut end = start;
42        while rows.get(i + 1).is_some_and(|n| Some(*n) == end.checked_add(1)) {
43            end += 1;
44            i += 1;
45        }
46        i += 1;
47        let (a, b) = (origin.row.checked_add(start)? + 1, origin.row.checked_add(end)? + 1);
48        parts.push(if a == b { format!("{q}!${letters}${a}") } else { format!("{q}!${letters}${a}:${letters}${b}") });
49    }
50    match parts.len() {
51        0 => None,
52        1 => parts.pop(),
53        _ => Some(format!("({})", parts.join(","))),
54    }
55}
56
57/// Most category rows a PivotChart plots.
58const MAX_CHART_ROWS: usize = 10_000;
59
60/// A PivotChart's series for a report written at `origin`: categories are the innermost row
61/// items, one series per item column (column items × value fields); totals are left out.
62pub fn chart_series(sheet: &str, origin: CellRef, out: &Output) -> Vec<Series> {
63    let rows: Vec<u32> = out.chart_rows.iter().copied().take(MAX_CHART_ROWS).collect();
64    let categories = out.label_col.and_then(|c| rows_ref(sheet, origin, c, &rows));
65    out.chart_cols
66        .iter()
67        .filter_map(|(c, name)| {
68            Some(Series {
69                name: Some(format!("\"{}\"", name.replace('"', "\"\""))),
70                categories: categories.clone(),
71                values: rows_ref(sheet, origin, *c, &rows)?,
72                bubble_sizes: None,
73                color: None,
74                secondary: false,
75                kind: None,
76                quartiles: Default::default(),
77                smooth: false,
78                trendlines: Vec::new(),
79            })
80        })
81        .collect()
82}
83
84/// Rebuilds the series of every PivotChart of a PivotTable after its report was written.
85pub(super) fn rebind_charts(wb: &mut Workbook, sheet: &str, pivot: &str, origin: CellRef, out: &Output) {
86    let linked = |ch: &Chart| ch.pivot.as_deref().is_some_and(|l| links_to(l, sheet, pivot));
87    let hits: Vec<usize> = (0..wb.sheets.len()).filter(|i| wb.sheet(*i).is_some_and(|sh| sh.charts.iter().any(linked))).collect();
88    if hits.is_empty() {
89        return;
90    }
91    let series = chart_series(sheet, origin, out);
92    for i in hits {
93        let Some(sh) = wb.sheet_mut(i) else { continue };
94        for ch in sh.charts.iter_mut().filter(|c| linked(c)) {
95            ch.series = series.clone();
96        }
97    }
98}
99
100/// Clears a pivot's last output area.
101pub fn clear_output(wb: &mut Workbook, sheet: usize, idx: usize) {
102    let Some(shm) = wb.sheet_mut(sheet) else { return };
103    let Some(r) = shm.pivots.get(idx).and_then(|p| p.last_range) else { return };
104    shm.take_cells(r);
105    if let Some(p) = shm.pivots.get_mut(idx) {
106        p.last_range = None;
107    }
108}