ravenrs_sheet/engine/pivot/
charts.rs1use super::output::Output;
2use super::{CellRef, Chart, Series, Workbook, quote_sheet};
3
4pub fn chart_link(sheet: &str, pivot: &str) -> String {
8 format!("{}!{pivot}", quote_sheet(sheet))
9}
10
11pub 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
28pub 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
33fn 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
57const MAX_CHART_ROWS: usize = 10_000;
59
60pub 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
84pub(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
100pub 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}