1use std::sync::Arc;
8
9use crate::base::{CellRef, RangeRef};
10use crate::model::{
11 Anchor, Cell, Chart, ChartKind, LegendPos, NumFmt, PivotCalcField, PivotDateGroup, PivotField, PivotFilter, PivotFunc, PivotLayout, PivotShowAs,
12 PivotSort, PivotTable, PivotValue, Sheet, StyleId, Workbook,
13};
14use serde_json::{Value as Json, json};
15
16use super::{
17 CommandSpec, Ctx, EngineError, Result, Session, anchor_mode_param, bad, bool_param, cell_param, cmd, edit, f64_param, has_doc, ok, str_param,
18 u32_param,
19};
20use crate::engine::pivot as pv;
21use crate::engine::selection::current_region;
22
23mod charts;
24mod create;
25mod fields;
26mod layout;
27mod params;
28
29use charts::{insert_pivot_chart, recommended};
30use create::{insert_pivot, options, show_details, summarize_table};
31use fields::{add_field, calculated_field, fields, list, move_field, remove_field, value_settings};
32use layout::{change_source, collapse, delete, filter, grand_totals, group, layout, refresh, refresh_all, sort};
33
34pub fn specs() -> Vec<CommandSpec> {
35 vec![
36 cmd!(
37 "insert.pivotTable",
38 "PivotTable",
39 ["Insert", "Tables"],
40 None,
41 "{source?: \"Sheet1!$A$1:$E$200\" | table name (default: the table or current region of the selection), destination?: \"new\" (default: new sheet before the current one, report at A3) | \"Sheet1!H3\", name?, rows?: [field | {field, group?: years|quarters|months|days}], columns?: [..], values?: [field | {field, func?, name?}], filters?: [field], replace?: bool} → {id, name, sheet, anchor, fields}",
42 has_doc,
43 insert_pivot
44 ),
45 cmd!(
46 "insert.recommendedPivotTables",
47 "Recommended PivotTables",
48 ["Insert", "Tables"],
49 None,
50 "{source?, apply?: index, destination?} → suggestions [{index, title, rows, columns, values}]; with apply, creates that one",
51 has_doc,
52 recommended
53 ),
54 cmd!(
55 "table.summarizePivot",
56 "Summarize with PivotTable",
57 ["Table Design", "Tools"],
58 None,
59 "{table?, destination?} creates a PivotTable from the table",
60 has_doc,
61 summarize_table
62 ),
63 cmd!(
64 "pivot.calculatedField",
65 "Calculated Field",
66 ["PivotTable Analyze", "Calculations"],
67 None,
68 "{pivot?, name, formula?: \"=Sales*0.1\" (fields bare or 'quoted'; evaluated on each cell's sums), delete?: bool, values?: bool (add a new field to Values, default true)} adds, changes or deletes a calculated field; without `name` opens the dialog → {name, formula, calculatedFields}",
69 has_doc,
70 calculated_field
71 ),
72 cmd!(
73 "insert.pivotChart",
74 "PivotChart",
75 ["Insert", "Charts"],
76 None,
77 "{pivot?: name | id | a cell of the report (\"Sheet2!B5\"; default: the PivotTable at the active cell), type?: column|bar|line|pie|area|… (default column), subtype?, title?, at?: \"H2\", width?, height?} adds a chart bound to the PivotTable's report: categories are the innermost row items, series the column items × value fields; it follows refreshes and layout changes → {chart, pivot, series}",
78 has_doc,
79 insert_pivot_chart
80 ),
81 cmd!(query "pivot.fields", "PivotTable Fields", [], None, "{pivot?} → source fields (name, numeric, date, samples, areas) and the current layout", has_doc, fields),
82 cmd!(query "pivot.fieldList", "Field List", ["PivotTable Analyze", "Show"], None, "{pivot?} (same as pivot.fields)", has_doc, fields),
83 cmd!(query "pivot.list", "PivotTables", [], None, "{} → every PivotTable in the workbook", has_doc, list),
84 cmd!(
85 "pivot.addField",
86 "Add Field",
87 [],
88 None,
89 "{pivot?, field, area: rows|columns|values|filters, func?: sum|count|average|max|min|product|countNumbers|stdDev|stdDevP|var|varP, name?, position?: index, replace?}",
90 has_doc,
91 add_field
92 ),
93 cmd!(
94 "pivot.removeField",
95 "Remove Field",
96 [],
97 None,
98 "{pivot?, field (or value caption), area?: rows|columns|values|filters, replace?}",
99 has_doc,
100 remove_field
101 ),
102 cmd!(
103 "pivot.moveField",
104 "Move Field",
105 [],
106 None,
107 "{pivot?, field (\"Σ Values\" moves the value fields between rows and columns; in columns it stays after the last field), from: rows|columns|values|filters, to: rows|columns|values|filters, position?: index, replace?}",
108 has_doc,
109 move_field
110 ),
111 cmd!(
112 "pivot.valueSettings",
113 "Value Field Settings",
114 ["PivotTable Analyze", "Active Field"],
115 None,
116 "{pivot?, field: caption or source field | index: n, func?, name?, showAs?: normal|percentOfGrandTotal|percentOfColumnTotal|percentOfRowTotal|runningTotal|rank, numberFormat?: \"#,##0\" | null, replace?}",
117 has_doc,
118 value_settings
119 ),
120 cmd!("pivot.filter", "Filter Items", [], None, "{pivot?, field, selected: [\"East\", ..] | null (all), replace?}", has_doc, filter),
121 cmd!("pivot.sort", "Sort Field", [], None, "{pivot?, field, order: asc|desc|none, replace?}", has_doc, sort),
122 cmd!(
123 "pivot.group",
124 "Group Field",
125 ["PivotTable Analyze", "Group"],
126 None,
127 "{pivot?, field, by: years|quarters|months|days|none, replace?}",
128 has_doc,
129 group
130 ),
131 cmd!(
132 "pivot.collapse",
133 "Expand/Collapse Field",
134 ["PivotTable Analyze", "Active Field"],
135 None,
136 "{pivot?, field, items?: [labels] (default: all), collapse?: true, replace?}",
137 has_doc,
138 collapse
139 ),
140 cmd!(
141 "pivot.layout",
142 "PivotTable Layout",
143 ["PivotTable Design", "Layout"],
144 None,
145 "{pivot?, layout?: compact|outline|tabular, grandTotalsRows?: bool, grandTotalsCols?: bool, subtotals?: bool|top|bottom|none, style?: \"PivotStyleLight16\", showHeaders?: bool, replace?}",
146 has_doc,
147 layout
148 ),
149 cmd!(
150 "pivot.grandTotals",
151 "Grand Totals",
152 ["PivotTable Design", "Layout"],
153 None,
154 "{pivot?, mode?: off|on|rows|columns, rows?: bool, cols?: bool}",
155 has_doc,
156 grand_totals
157 ),
158 cmd!("pivot.subtotals", "Subtotals", ["PivotTable Design", "Layout"], None, "{pivot?, mode: none|top|bottom}", has_doc, |s, p| {
159 let mode = str_param(p, "mode").unwrap_or("top");
160 layout(s, &with(p, "subtotals", json!(mode)))
161 }),
162 cmd!(
163 "pivot.reportLayout",
164 "Report Layout",
165 ["PivotTable Design", "Layout"],
166 None,
167 "{pivot?, layout: compact|outline|tabular}",
168 has_doc,
169 layout
170 ),
171 cmd!(
172 "pivot.style",
173 "PivotTable Styles",
174 ["PivotTable Design", "PivotTable Styles"],
175 None,
176 "{pivot?, style: \"PivotStyleMedium9\"}",
177 has_doc,
178 layout
179 ),
180 cmd!("pivot.refresh", "Refresh", ["PivotTable Analyze", "Data"], Some("Alt+F5"), "{pivot?, replace?}", has_doc, refresh),
181 cmd!(
182 "data.refreshAll",
183 "Refresh All",
184 ["Data", "Queries & Connections"],
185 Some("Cmd+Alt+F5"),
186 "{replace?} refreshes every PivotTable → {refreshed, errors}",
187 has_doc,
188 refresh_all
189 ),
190 cmd!(
191 "pivot.changeSource",
192 "Change Data Source",
193 ["PivotTable Analyze", "Data"],
194 None,
195 "{pivot?, source: \"Sheet1!$A$1:$E$300\" | table name, replace?} (fields missing from the new source are removed)",
196 has_doc,
197 change_source
198 ),
199 cmd!(
200 "pivot.showDetails",
201 "Show Details",
202 [],
203 None,
204 "{cell?: \"B5\" (a value, subtotal or grand total cell of a PivotTable on the active sheet; default: the active cell)} puts the source records behind that value on a new sheet before the current one → {sheet, records}. An error when the PivotTable's Enable show details option is off (`pivot.options` `{enableShowDetails: true}` turns it on).",
205 has_doc,
206 show_details
207 ),
208 cmd!(
209 "pivot.options",
210 "PivotTable Options",
211 ["PivotTable Analyze", "PivotTable"],
212 None,
213 "{pivot?, enableShowDetails?: bool (Data › Enable show details: off, Show Details and double-clicking a value cell list no records)} → {enableShowDetails}",
214 has_doc,
215 options
216 ),
217 cmd!(
218 "pivot.delete",
219 "Delete PivotTable",
220 ["PivotTable Analyze", "Actions"],
221 None,
222 "{pivot?} clears the report and removes the PivotTable",
223 has_doc,
224 delete
225 ),
226 ]
227}
228
229fn with(p: &Json, k: &str, v: Json) -> Json {
230 let mut m = p.as_object().cloned().unwrap_or_default();
231 m.insert(k.into(), v);
232 Json::Object(m)
233}
234
235fn other(msg: impl Into<String>) -> EngineError {
236 EngineError::Other(msg.into())
237}
238
239fn find_pivot(s: &Session, p: &Json) -> Result<(usize, usize)> {
243 let d = s.doc()?;
244 let active = d.wb.active_sheet();
245 if let Some(v) = p.get("pivot").filter(|v| !v.is_null()) {
246 let mut order: Vec<usize> = vec![active];
247 order.extend((0..d.wb.sheets.len()).filter(|i| *i != active));
248 for si in order {
249 let Some(sh) = d.wb.sheet(si) else { continue };
250 let hit = sh.pivots.iter().position(|pt| match v {
251 Json::String(n) => pt.name.eq_ignore_ascii_case(n),
252 Json::Number(n) => n.as_u64() == Some(pt.id as u64),
253 _ => false,
254 });
255 if let Some(i) = hit {
256 return Ok((si, i));
257 }
258 }
259 return Err(bad("pivot", format!("no PivotTable `{}`", v.as_str().map(str::to_string).unwrap_or_else(|| v.to_string()))));
260 }
261 let sh = d.wb.active().ok_or(EngineError::NoDocument)?;
262 let at = d.selection.active;
263 if let Some(i) = sh.pivots.iter().position(|pt| pt.last_range.is_some_and(|r| r.contains(at))) {
264 return Ok((active, i));
265 }
266 if !sh.pivots.is_empty() {
267 return Ok((active, sh.pivots.len() - 1));
268 }
269 Err(EngineError::Disabled("pivot".into(), "select a cell in a PivotTable (or pass `pivot`)".into()))
270}
271
272#[cfg(test)]
273mod tests;