Data: Analysis ToolPak with 13 tools and the Data Analysis dialog (#312)
data.analysisToolPak runs Descriptive Statistics, Histogram, Correlation, Covariance, Regression, Moving Average, Exponential Smoothing, Rank and Percentile, the three t-Tests, F-Test and Anova: Single Factor, writing a static report to an output range or a new sheet. Non-empty output cells are refused unless overwrite is set; the dialog asks instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
9f896c1249
commit
0dfd29efde
File diff suppressed because it is too large
Load Diff
|
|
@ -1,6 +1,7 @@
|
|||
//! The command registry. Ids follow Excel's ribbon (`home.bold`, `insert.chart`,
|
||||
//! `data.sortAscending`) plus primitive editing commands (`cell.set`, `selection.set`).
|
||||
|
||||
pub mod analysis;
|
||||
pub mod data;
|
||||
pub mod draw;
|
||||
pub mod edit;
|
||||
|
|
@ -120,6 +121,7 @@ pub fn command_specs() -> &'static [CommandSpec] {
|
|||
v.extend(spelling::specs());
|
||||
v.extend(pivot::specs());
|
||||
v.extend(whatif::specs());
|
||||
v.extend(analysis::specs());
|
||||
v.extend(print::specs());
|
||||
v
|
||||
})
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ fn cell_arg(s: &Session, p: &Json, key: &str, cmd: &str) -> Result<(usize, CellR
|
|||
|
||||
/// A range text (`"A1:C9"`, `"Sheet2!A:C"`) resolved against the workbook. Whole rows/columns
|
||||
/// are trimmed to the used range.
|
||||
fn parse_range(wb: &Workbook, t: &str, cmd: &str) -> Result<(usize, RangeRef)> {
|
||||
pub(crate) fn parse_range(wb: &Workbook, t: &str, cmd: &str) -> Result<(usize, RangeRef)> {
|
||||
let (sh, body) = split_sheet(t);
|
||||
let si = sheet_named(wb, sh, wb.active_sheet, cmd)?;
|
||||
let r = RangeRef::parse(body.trim()).ok_or_else(|| bad(cmd, format!("not a range: `{t}`")))?;
|
||||
|
|
|
|||
|
|
@ -317,6 +317,16 @@ impl Dialog {
|
|||
json!({"header": true}),
|
||||
),
|
||||
"goalSeek" => Dialog::custom("goalSeek", "Goal Seek", json!({"set": active, "to": "0", "changing": ""})),
|
||||
"analysisToolPak" => {
|
||||
// Opened with a known `tool`, it goes straight to that tool's form.
|
||||
let tool = params.get("tool").and_then(Json::as_str).unwrap_or("");
|
||||
let mut d = Dialog::custom("analysisToolPak", "Data Analysis", json!({"tool": "", "pick": tool}));
|
||||
if gridcraft_engine::cmd::analysis::TOOLS.iter().any(|(id, _)| *id == tool) {
|
||||
d.values = toolpak_defaults(&sel, tool);
|
||||
}
|
||||
d.command = Some("data.analysisToolPak");
|
||||
d
|
||||
}
|
||||
"subtotal" => Dialog::form(
|
||||
"subtotal",
|
||||
"Subtotal",
|
||||
|
|
@ -659,6 +669,7 @@ pub fn show(app: &mut SheetApp, ctx: &egui::Context) {
|
|||
"formatCells" => 560.0,
|
||||
"about" => 660.0,
|
||||
"insertFunction" | "commandSearch" | "nameManager" | "manageRules" | "journal" | "agents" => 520.0,
|
||||
"analysisToolPak" => 420.0,
|
||||
_ => 380.0,
|
||||
};
|
||||
let window = egui::Window::new(d.title.clone())
|
||||
|
|
@ -681,6 +692,7 @@ pub fn show(app: &mut SheetApp, ctx: &egui::Context) {
|
|||
"manageRules" => manage_rules(app, ui),
|
||||
"commandSearch" => command_search(app, ui, &mut d, &mut confirm),
|
||||
"goalSeek" => goal_seek(app, ui, &mut d, &mut confirm),
|
||||
"analysisToolPak" => analysis_toolpak(app, ui, &mut d, &mut confirm, &mut open),
|
||||
"spelling" => spelling(app, ui, &mut d, &mut open),
|
||||
"agents" => {
|
||||
ui.label("GridCraft is fully drivable by agents. Every menu item, button and gesture is a command:");
|
||||
|
|
@ -878,6 +890,10 @@ pub fn show(app: &mut SheetApp, ctx: &egui::Context) {
|
|||
let params = build_params(&d);
|
||||
match app.run(cmd, params) {
|
||||
Ok(_) => open = false,
|
||||
// Data Analysis asks before replacing data in its output range.
|
||||
Err(e) if d.name == "analysisToolPak" && e.contains(gridcraft_engine::cmd::analysis::OVERWRITE_ERROR) => {
|
||||
d.values.insert("askOverwrite".into(), json!(true));
|
||||
}
|
||||
Err(e) => app.message = Some((d.title.clone(), crate::clean_error(&e))),
|
||||
}
|
||||
} else if confirm {
|
||||
|
|
@ -967,6 +983,12 @@ fn ok_cancel(ui: &mut egui::Ui, confirm: &mut bool, open: &mut bool) {
|
|||
}
|
||||
|
||||
fn form(ui: &mut egui::Ui, d: &mut Dialog, confirm: &mut bool, open: &mut bool) {
|
||||
form_fields(ui, d);
|
||||
ok_cancel(ui, confirm, open);
|
||||
}
|
||||
|
||||
/// Draws a dialog's `fields`, editing its values.
|
||||
fn form_fields(ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
egui::Grid::new("form").num_columns(1).spacing(vec2(8.0, 6.0)).show(ui, |ui| {
|
||||
for f in d.fields.clone() {
|
||||
match f {
|
||||
|
|
@ -1022,7 +1044,6 @@ fn form(ui: &mut egui::Ui, d: &mut Dialog, confirm: &mut bool, open: &mut bool)
|
|||
ui.end_row();
|
||||
}
|
||||
});
|
||||
ok_cancel(ui, confirm, open);
|
||||
}
|
||||
|
||||
/// Help ▸ About GridCraft: the app, its contributors and the AI models that helped. The credits
|
||||
|
|
@ -1607,6 +1628,144 @@ fn goal_seek(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dialog, confirm: &mu
|
|||
}
|
||||
}
|
||||
|
||||
/// The fields of one Analysis ToolPak tool's form (input and options; output is drawn apart).
|
||||
fn toolpak_fields(tool: &str) -> Vec<Field> {
|
||||
use Field::*;
|
||||
let grouped = || Choice { key: "groupedBy", label: "Grouped By:", options: vec![("columns", "Columns"), ("rows", "Rows")] };
|
||||
let labels = || Check { key: "labels", label: "Labels in first row" };
|
||||
let range = || Text { key: "range", label: "Input Range:" };
|
||||
let two = || vec![Text { key: "range1", label: "Variable 1 Range:" }, Text { key: "range2", label: "Variable 2 Range:" }];
|
||||
match tool {
|
||||
"descriptive" => vec![
|
||||
range(),
|
||||
grouped(),
|
||||
labels(),
|
||||
Check { key: "summary", label: "Summary statistics" },
|
||||
Text { key: "confidence", label: "Confidence Level for Mean (%), blank for none:" },
|
||||
Text { key: "kthLargest", label: "Kth Largest (blank for none):" },
|
||||
Text { key: "kthSmallest", label: "Kth Smallest (blank for none):" },
|
||||
],
|
||||
"histogram" => vec![
|
||||
range(),
|
||||
Text { key: "bins", label: "Bin Range (blank for automatic bins):" },
|
||||
labels(),
|
||||
Check { key: "pareto", label: "Pareto (sorted histogram)" },
|
||||
Check { key: "cumulative", label: "Cumulative Percentage" },
|
||||
],
|
||||
"correlation" | "covariance" | "rankPercentile" => vec![range(), grouped(), labels()],
|
||||
"anovaSingle" => vec![range(), grouped(), labels(), Text { key: "alpha", label: "Alpha:" }],
|
||||
"regression" => vec![
|
||||
Text { key: "inputY", label: "Input Y Range:" },
|
||||
Text { key: "inputX", label: "Input X Range:" },
|
||||
labels(),
|
||||
Check { key: "constantZero", label: "Constant is Zero" },
|
||||
Text { key: "confidence", label: "Confidence Level (%):" },
|
||||
Check { key: "residuals", label: "Residuals" },
|
||||
Check { key: "standardizedResiduals", label: "Standardized Residuals" },
|
||||
Check { key: "normalProbability", label: "Normal Probability" },
|
||||
],
|
||||
"movingAverage" => {
|
||||
vec![range(), labels(), Text { key: "interval", label: "Interval:" }, Check { key: "standardErrors", label: "Standard Errors" }]
|
||||
}
|
||||
"exponentialSmoothing" => {
|
||||
vec![range(), labels(), Text { key: "damping", label: "Damping factor:" }, Check { key: "standardErrors", label: "Standard Errors" }]
|
||||
}
|
||||
"fTest" => {
|
||||
let mut v = two();
|
||||
v.extend([labels(), Text { key: "alpha", label: "Alpha:" }]);
|
||||
v
|
||||
}
|
||||
_ => {
|
||||
let mut v = two();
|
||||
v.extend([
|
||||
Text { key: "hypothesizedDifference", label: "Hypothesized Mean Difference:" },
|
||||
labels(),
|
||||
Text { key: "alpha", label: "Alpha:" },
|
||||
]);
|
||||
v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The starting values of a tool's form: the selection as its input, a new sheet as output.
|
||||
fn toolpak_defaults(sel: &str, tool: &str) -> Map<String, Json> {
|
||||
let mut v = json!({
|
||||
"tool": tool, "range": sel, "inputY": "", "inputX": "", "range1": sel, "range2": "", "bins": "",
|
||||
"groupedBy": "columns", "labels": false, "summary": true, "confidence": "", "kthLargest": "", "kthSmallest": "",
|
||||
"interval": "3", "damping": "0.3", "alpha": "0.05", "hypothesizedDifference": "0",
|
||||
"outputTo": "newSheet", "output": "", "newSheet": "",
|
||||
});
|
||||
if tool == "regression" {
|
||||
v["confidence"] = json!("95");
|
||||
v["inputY"] = json!(sel);
|
||||
}
|
||||
v.as_object().cloned().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Data › Data Analysis: pick a tool, then fill in its form. OK runs `data.analysisToolPak`
|
||||
/// (through the dialog's command); a report that would replace data asks first.
|
||||
fn analysis_toolpak(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dialog, confirm: &mut bool, open: &mut bool) {
|
||||
use gridcraft_engine::cmd::analysis::TOOLS;
|
||||
if d.values.get("askOverwrite").and_then(Json::as_bool) == Some(true) {
|
||||
ui.label("The output range already holds data. Replace it with the report?");
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Replace").clicked() {
|
||||
d.values.remove("askOverwrite");
|
||||
d.values.insert("overwrite".into(), json!(true));
|
||||
*confirm = true;
|
||||
}
|
||||
if ui.button("Cancel").clicked() {
|
||||
d.values.remove("askOverwrite");
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
let tool = d.values.get("tool").and_then(Json::as_str).unwrap_or("").to_string();
|
||||
let Some((_, label)) = TOOLS.iter().find(|(id, _)| *id == tool) else {
|
||||
ui.label(egui::RichText::new("Analysis Tools").strong());
|
||||
let pick = d.values.get("pick").and_then(Json::as_str).unwrap_or("").to_string();
|
||||
let mut chosen = false;
|
||||
egui::ScrollArea::vertical().max_height(260.0).show(ui, |ui| {
|
||||
for (id, label) in TOOLS {
|
||||
let r = ui.selectable_label(pick == *id, *label);
|
||||
if r.clicked() {
|
||||
d.values.insert("pick".into(), json!(id));
|
||||
}
|
||||
if r.double_clicked() {
|
||||
chosen = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
let mut ok = false;
|
||||
ok_cancel(ui, &mut ok, open);
|
||||
let pick = d.values.get("pick").and_then(Json::as_str).unwrap_or("").to_string();
|
||||
if (ok || chosen) && TOOLS.iter().any(|(id, _)| *id == pick) {
|
||||
let sel = app.session.active().map(|doc| doc.selection.current().a1()).unwrap_or_default();
|
||||
d.values = toolpak_defaults(&sel, &pick);
|
||||
}
|
||||
return;
|
||||
};
|
||||
d.title = label.to_string();
|
||||
d.fields = toolpak_fields(&tool);
|
||||
form_fields(ui, d);
|
||||
ui.separator();
|
||||
ui.label(egui::RichText::new("Output options").strong());
|
||||
let to = d.values.get("outputTo").and_then(Json::as_str).unwrap_or("newSheet").to_string();
|
||||
for (key, choice, text_label) in [("output", "range", "Output Range:"), ("newSheet", "newSheet", "New Worksheet Ply:")] {
|
||||
ui.horizontal(|ui| {
|
||||
if ui.radio(to == choice, text_label).clicked() {
|
||||
d.values.insert("outputTo".into(), json!(choice));
|
||||
}
|
||||
let mut t = d.values.get(key).and_then(Json::as_str).unwrap_or("").to_string();
|
||||
if ui.add(egui::TextEdit::singleline(&mut t).desired_width(160.0)).changed() {
|
||||
d.values.insert(key.into(), json!(t));
|
||||
d.values.insert("outputTo".into(), json!(choice));
|
||||
}
|
||||
});
|
||||
}
|
||||
ok_cancel(ui, confirm, open);
|
||||
}
|
||||
|
||||
fn spelling(app: &mut SheetApp, ui: &mut egui::Ui, d: &mut Dialog, open: &mut bool) {
|
||||
let r = d.result.clone().unwrap_or(Json::Null);
|
||||
if let Some(e) = r.get("error").and_then(Json::as_str) {
|
||||
|
|
|
|||
|
|
@ -313,6 +313,8 @@ const JAPANESE: &[(&str, &str)] = &[
|
|||
("Group", "グループ化"),
|
||||
("Ungroup", "グループ解除"),
|
||||
("Subtotal", "小計"),
|
||||
("Data\nAnalysis", "データ分析"),
|
||||
("Data Analysis", "データ分析"),
|
||||
("What-If\nAnalysis", "What-If 分析"),
|
||||
("Data Table…", "データ テーブル…"),
|
||||
("Scenario Manager…", "シナリオ マネージャー…"),
|
||||
|
|
|
|||
|
|
@ -164,6 +164,8 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
|||
"Group" => "그룹",
|
||||
"Ungroup" => "그룹 해제",
|
||||
"Subtotal" => "부분합",
|
||||
"Data\nAnalysis" => "데이터 분석",
|
||||
"Data Analysis" => "데이터 분석",
|
||||
"What-If\nAnalysis" => "가상 분석",
|
||||
"Data Table…" => "데이터 표…",
|
||||
"Scenario Manager…" => "시나리오 관리자…",
|
||||
|
|
|
|||
|
|
@ -165,6 +165,8 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
|||
"Group" => "Agrupar",
|
||||
"Ungroup" => "Desagrupar",
|
||||
"Subtotal" => "Subtotal",
|
||||
"Data\nAnalysis" => "Análise de Dados",
|
||||
"Data Analysis" => "Análise de Dados",
|
||||
"What-If\nAnalysis" => "Análise de\nHipóteses",
|
||||
"Data Table…" => "Tabela de Dados…",
|
||||
"Scenario Manager…" => "Gerenciador de Cenários…",
|
||||
|
|
|
|||
|
|
@ -164,6 +164,8 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
|||
"Group" => "Группировать",
|
||||
"Ungroup" => "Разгруппировать",
|
||||
"Subtotal" => "Промежуточный итог",
|
||||
"Data\nAnalysis" => "Анализ данных",
|
||||
"Data Analysis" => "Анализ данных",
|
||||
"What-If\nAnalysis" => "Анализ «что\nесли»",
|
||||
"Data Table…" => "Таблица данных…",
|
||||
"Scenario Manager…" => "Диспетчер сценариев…",
|
||||
|
|
|
|||
|
|
@ -164,6 +164,8 @@ pub(super) fn translate(text: &str) -> Option<&'static str> {
|
|||
"Group" => "组合",
|
||||
"Ungroup" => "取消组合",
|
||||
"Subtotal" => "分类汇总",
|
||||
"Data\nAnalysis" => "数据分析",
|
||||
"Data Analysis" => "数据分析",
|
||||
"What-If\nAnalysis" => "假设分析",
|
||||
"Data Table…" => "数据表…",
|
||||
"Scenario Manager…" => "方案管理器…",
|
||||
|
|
|
|||
|
|
@ -1580,6 +1580,10 @@ fn data(app: &mut SheetApp, ui: &mut Ui) {
|
|||
app.open_dialog("subtotal", json!({}));
|
||||
}
|
||||
});
|
||||
sep(ui);
|
||||
if big_button(ui, Icon::ChartBar, "Data\nAnalysis", "Data Analysis", false).clicked() {
|
||||
app.open_dialog("analysisToolPak", json!({}));
|
||||
}
|
||||
}
|
||||
|
||||
fn review(app: &mut SheetApp, ui: &mut Ui) {
|
||||
|
|
|
|||
|
|
@ -228,6 +228,7 @@ fn dialogs_open_and_close() {
|
|||
"spelling",
|
||||
"goTo",
|
||||
"about",
|
||||
"analysisToolPak",
|
||||
] {
|
||||
h.state_mut().open_dialog(name, json!({}));
|
||||
h.run_steps(2);
|
||||
|
|
@ -237,6 +238,39 @@ fn dialogs_open_and_close() {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_analysis_dialog_runs_a_tool_and_asks_before_overwriting() {
|
||||
let mut s = blank();
|
||||
s.execute("range.setValues", json!({"range": "A1", "values": [[1], [2], [3], [4], [5]]})).unwrap();
|
||||
let mut h = harness(s);
|
||||
h.state_mut().open_dialog("analysisToolPak", json!({}));
|
||||
h.state_mut().dialog.as_mut().unwrap().values.insert("pick".into(), json!("descriptive"));
|
||||
h.run_steps(2);
|
||||
key(&mut h, Key::Enter, Modifiers::NONE);
|
||||
assert_eq!(h.state().dialog.as_ref().map(|d| d.title.clone()).as_deref(), Some("Descriptive Statistics"));
|
||||
let fill = |h: &mut egui_kittest::Harness<'static, SheetApp>| {
|
||||
let d = h.state_mut().dialog.as_mut().unwrap();
|
||||
d.values.insert("range".into(), json!("A1:A5"));
|
||||
d.values.insert("outputTo".into(), json!("range"));
|
||||
d.values.insert("output".into(), json!("C1"));
|
||||
};
|
||||
fill(&mut h);
|
||||
h.run_steps(2);
|
||||
key(&mut h, Key::Enter, Modifiers::NONE);
|
||||
assert!(h.state().dialog.is_none(), "OK runs the tool and closes the dialog");
|
||||
assert_eq!(value(&h, "C3"), Value::from("Mean"));
|
||||
assert_eq!(value(&h, "D3"), Value::Number(3.0));
|
||||
// A second report into the same cells asks before replacing them (opened with a tool, the
|
||||
// dialog starts at that tool's form).
|
||||
h.state_mut().open_dialog("analysisToolPak", json!({"tool": "descriptive"}));
|
||||
fill(&mut h);
|
||||
h.run_steps(2);
|
||||
key(&mut h, Key::Enter, Modifiers::NONE);
|
||||
let d = h.state().dialog.as_ref().expect("the dialog stays open to ask");
|
||||
assert_eq!(d.values.get("askOverwrite"), Some(&json!(true)));
|
||||
assert!(h.state().message.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn about_renders_every_tab() {
|
||||
let mut h = harness(blank());
|
||||
|
|
|
|||
|
|
@ -98,8 +98,9 @@ No alpha blockers: every core workflow passes the [alpha gate](roadmap.md#alpha-
|
|||
data model (`data.fromWeb`, `data.relationships` missing). **Estimate:** 60–100 h.
|
||||
|
||||
### 13. Analysis tools: Solver, Analysis ToolPak, Forecast Sheet (F)
|
||||
- `data.solver`, `data.analysisToolPak`, `data.forecastSheet` missing; FORECAST.ETS family missing.
|
||||
**Estimate:** 22–35 h.
|
||||
- `data.solver`, `data.forecastSheet` missing; FORECAST.ETS family missing. Analysis ToolPak
|
||||
(`data.analysisToolPak`, #312) has 13 tools; Anova two-factor, z-Test, Sampling, Random Number
|
||||
Generation, Fourier Analysis and the tools' charts remain. **Estimate:** 18–30 h.
|
||||
|
||||
### 14. Page Layout view and print polish (U, F)
|
||||
- No paper view with rulers and in-place header/footer editing; web print exports only (#141, PR
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
Each entry is an Excel ribbon or menu command (feature names only). It counts as implemented when GridCraft registers a command with the same id. This measures breadth only; the overall assessment (depth, fidelity, effort) is in [target-app-parity.md](target-app-parity.md).
|
||||
|
||||
**Overall: 258 / 290 commands implemented (89.0%).**
|
||||
**Overall: 259 / 290 commands implemented (89.3%).**
|
||||
|
||||
| Tab | Implemented | Total | Parity |
|
||||
|---|---|---|---|
|
||||
|
|
@ -14,7 +14,7 @@ Each entry is an Excel ribbon or menu command (feature names only). It counts as
|
|||
| Draw | 4 | 4 | 100% |
|
||||
| Page Layout | 23 | 23 | 100% |
|
||||
| Formulas | 23 | 23 | 100% |
|
||||
| Data | 20 | 28 | 71% |
|
||||
| Data | 21 | 28 | 75% |
|
||||
| Review | 9 | 18 | 50% |
|
||||
| View | 18 | 20 | 90% |
|
||||
| Automate | 3 | 3 | 100% |
|
||||
|
|
@ -240,7 +240,7 @@ Implemented:
|
|||
- `formulas.calculateNow` Calculate Now (Calculation)
|
||||
- `formulas.calculateSheet` Calculate Sheet (Calculation)
|
||||
|
||||
## Data (20 / 28, 71%)
|
||||
## Data (21 / 28, 75%)
|
||||
|
||||
Implemented:
|
||||
|
||||
|
|
@ -264,6 +264,7 @@ Implemented:
|
|||
- `data.subtotal` Subtotal (Outline)
|
||||
- `data.showDetail` Show Detail (Outline)
|
||||
- `data.hideDetail` Hide Detail (Outline)
|
||||
- `data.analysisToolPak` Data Analysis (Analysis)
|
||||
|
||||
Missing:
|
||||
|
||||
|
|
@ -274,7 +275,6 @@ Missing:
|
|||
- `data.forecastSheet` Forecast Sheet (Forecast)
|
||||
- `data.analyzeData` Analyze Data (Analysis)
|
||||
- `data.solver` Solver (Analysis)
|
||||
- `data.analysisToolPak` Data Analysis (Analysis)
|
||||
|
||||
## Review (9 / 18, 50%)
|
||||
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ performance 8, stability 8, localization 4, hardware 2, platforms 2, ecosystem 2
|
|||
| PivotTables | 7 | ~65% | ~45% | 25–40 | Insert/Recommended, field list, three layouts, date grouping, show-values-as, refresh, XLSX round-trip. Missing: slicers, timelines, calculated fields/items, PivotCharts, value/label filters, number grouping, GETPIVOTDATA, data-model pivots. Round-trip never checked in Excel |
|
||||
| Charts and sparklines | 8 | ~50% | ~30% | 40–60 | 25 chart kinds, all 2-D. The chart model holds title, legend position, a data-labels flag, gridlines flag and axis titles; no axis scaling, trendlines, error bars, per-point formatting, chart sheets, 3-D, surface, map, Pareto. Several kinds don't survive XLSX round trip (open PRs #158, #161). [chart-parity.md](chart-parity.md) |
|
||||
| Page layout, print, PDF | 5 | ~80% | ~55% | 12–20 | Page setup, print titles, breaks, Page Break Preview, headers/footers, PDF export and printing. No true Page Layout view; the PDF writer uses only the 14 standard fonts without embedding (no CJK or other non-Latin text in PDF); web print only exports (#141, #143) |
|
||||
| What-if and analysis | 4 | ~50% | ~40% | 22–35 | Goal Seek, Scenario Manager, data tables. Missing: Solver, Analysis ToolPak, Forecast Sheet, Analyze Data |
|
||||
| What-if and analysis | 4 | ~50% | ~40% | 22–35 | Goal Seek, Scenario Manager, data tables, Analysis ToolPak (13 of 19 tools, no charts). Missing: Solver, Forecast Sheet, Analyze Data |
|
||||
| Get & Transform, external data, data model | 5 | ~15% | ~10% | 60–100 | Import of CSV/TSV, JSON, HTML. No Power Query editor, no From Web, database or folder sources, no connections or refresh, no relationships or data model, no external workbook links (dropped with a warning) |
|
||||
| Insert objects | 3 | ~55% | ~45% | 25–40 | Pictures, 8 shape kinds (Excel has ~170), text boxes, own icon set, ink with Ink to Shape, hyperlinks, checkboxes. Missing: SmartArt, WordArt, equations, signature line, objects, 3-D models, screenshot, pictures in cells (PR #105) |
|
||||
| Review, comments, protection, collaboration | 4 | ~55% | ~40% | 35–55 | Threaded comments, notes, spelling (system dictionary), thesaurus, sheet and workbook protection. Missing: comment navigation, Show Changes, Allow Edit Ranges, notes-to-comments conversion, accessibility checker, co-authoring, opening password-encrypted files (refused with a message since #17) |
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ shortcut lists for Windows and Mac, then fill the gaps: 6–10 h.
|
|||
|
||||
Format Cells, Insert Function/Formula Builder, Find and Replace, Go To and Go To Special, Sort,
|
||||
Name Manager, Data Validation, Paste Special, Conditional Formatting manager, Page Setup, Goal
|
||||
Seek, Scenario Manager and others exist. Missing or partial: Options, Solver, Data Analysis,
|
||||
Seek, Scenario Manager, Data Analysis and others exist. Missing or partial: Options, Solver,
|
||||
Forecast Sheet, Power Query editor, Insert Object, Equation editor, Protect/Allow Edit Ranges,
|
||||
Workbook Links, Accessibility checker.
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue