Skip to content

Data: live What-If data tables with the Data Table dialog (completes #548) - #1195

Open
Herr-Enklo wants to merge 11 commits into
storytold:mainfrom
Herr-Enklo:feat/live-data-tables
Open

Herr-Enklo wants to merge 11 commits into
storytold:mainfrom
Herr-Enklo:feat/live-data-tables

Conversation

@Herr-Enklo

Copy link
Copy Markdown

This completes #548 by @prxnvt: live What-If data tables, {=TABLE()} that recalculate, with XLSX round-trip.

#548's three commits are kept unchanged. The branch merges current main and adds the following on top. The merge needed two adaptations to main: Solver's new trial callers, and per-area checks for multi-area row/column delete.

Data Table dialog and menu

  • Data › What-If Analysis ▾ lists Scenario Manager…, Goal Seek… and Data Table…, in Excel's order.
  • Data Table… (key tip Alt, A, W, T) opens Excel's dialog over the selection, with Row input cell and Column input cell.
  • An invalid input cell shows Excel's "Input cell reference is not valid." and the dialog stays open.
  • A programmatic data.dataTable {} still returns an error and never opens a dialog.
  • German labels follow German Excel: Datentabelle, Zeileneingabewert:, Spalteneingabewert:, Automatisch außer bei Datentabellen.

Formula text, editing, calculation modes

  • The formula bar shows {=TABLE(,B2)} with the formula language's separators. The cell editor and Show Formulas show =TABLE(B1,B2), as Excel does. cell.get names the table a result belongs to.
  • Typing over one result shows "Cannot change part of a data table.", and clearing part of the results is refused. Clearing the whole table removes it; undo brings it back live.
  • Automatic Except for Data Tables and Manual wait for F9. Switching from Manual to Automatic Except for Data Tables now recalculates formulas edited in the meantime (before, they stayed stale).

Recalculation speed

  • Each table runs its substitutions on one working copy instead of one copy per substitution. It uses the dependency graph when the input cells hold constants.
  • An edited input value recalculates only its own row or column. A changed formula or precedent recalculates the whole table.
  • Each refresh has a budget of 25M formula evaluations, so a hostile file can't hang the app. A table past the budget keeps its values until the next refresh.

Measured in release builds, against #548:

Case #548 This PR
300 × 300 two-variable table, edit a precedent 53 s 3.8 s
100,000-row one-variable table, edit a precedent 7.9 s 1.5 s
Small table in a big workbook, edit 82 ms 8 ms
Edit one input value on the edge of the 100,000-row table whole table 27 ms

Spills

The command-sequence fuzz now builds data tables, and it found one bug:

  • Dynamic arrays could spill over a table's blank results. Results now block spills, as in Excel.
  • Tables now follow input values that arrive by spill.

XLSX

  • Two-variable tables are written with dtr="1", as Excel writes them.
  • Excel's own form (ca, shared-string results) is read.
  • Checked black-box against Excel 365: a CLI-written workbook with column, row, two-variable and text-result tables opens without repair, and its tables stay live and recalculate.
  • static: true keeps the old snapshot behaviour.

Notes

Tests

  • calc: partial-vs-full recalculation agreement over 80 random edits, the work budget, edge values recalculating their own results, dynamic arrays with tables.
  • engine: calculation modes and F9, editing rules, formula text.
  • xlsx: Excel's form, plus Data: live What-If data tables ({=TABLE()}) that recalculate, with XLSX round-trip #548's round-trip tests.
  • UI: the dialog and its German labels.
  • The command-sequence fuzz now makes live and static tables.
  • cargo xtask ci: all 8 steps pass (1625 tests).

🤖 Generated with Claude Code

prxnvt and others added 11 commits October 12, 2026 01:18
…rmulas

A What-If data table is now part of the model (Sheet::data_tables: its
range and its row and column input cells) and of recalculation. After the
formulas, every table with an edge cell (an input value or a formula) in
the changed or dirty set is calculated again: each result substitutes the
edge value into the input cell on a copy of the workbook and reads the
formula (Trial, moved from the engine to calc). Results are written as
values and their dependents recalculated; a table whose formulas read
another table's results is calculated after it.

F9 and opening a workbook calculate every table. Automatic Except for Data
Tables and Manual calculation leave tables to F9. A table whose edge cells
depend on its own results, directly or through other tables, is a circular
reference: its results are #CIRC!. Calculation is bounded: at most 100,000
result cells per table and 1,000,000 per refresh, no table calculation
inside a trial, and at most 64 passes.
data.dataTable now adds a live data table over the results instead of
writing a snapshot: the results recalculate when an input value, a formula
or a precedent of the formulas changes (on F9 in Automatic Except for Data
Tables mode), and undo removes the table with its results. The result
cells show {=TABLE(row_input,column_input)} in the formula bar, in Show
Formulas and in cell.get. Input cells must be single cells on the table's
sheet, outside it; a table may not overlap another one, and running Data
Table again over a table's range replaces that table.

`static: true` keeps the previous behaviour: every result is computed once
and written as a value, with input cells on any sheet. The response also
gains `formula`, and `static` is now false for a live table.

Like Excel, an edit of some of a table's results (typing, pasting,
clearing part of them, sorting) is refused with "Cannot change part of a
data table."; clearing all of them removes the table. Inserting or
deleting rows, columns or cells through a table, and pasting one table
onto another, are refused the same way; a table above or beside the edit,
or cut and pasted whole, moves with its input cells, and a table whose
input cell is deleted or ends up inside it keeps its results as values.
TABLE() typed into a cell is refused ("That function isn't valid.").
A data table is written as Excel stores it: the first result cell carries
<f t="dataTable" ref="..." dt2D=".." dtr=".." r1=".." r2=".."/> for the
whole results area, and every result cell its cached value. Reading
rebuilds the table from that formula (previously it was dropped with a
warning and only the values kept), so the table is live again after
opening. Tables without a results area, with a deleted or misplaced input
cell, with more than 100,000 result cells, overlapping an earlier table,
or beyond 1,000 per sheet keep their values with a warning.

Docs: target-app and file-format parity, ROADMAP progress log.
Brings storytold#548's live data tables up to date with main (413 commits). Conflicts were only in
the docs and the calc tests (both sides' rows and tests kept). Main's Solver now has more
trial users and Delete Rows/Columns takes several areas: Solver builds its trials through
whatif::trial, and every deleted area is checked against the sheet's data tables.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… changed

A data table runs one substitution per result row, column or cell. Each one cloned the
workbook and wrote into the clone, which copied the 64-row cell bands around the input
cells and formulas every time: with the results filled in, a 300 × 300 table over a
50-step model took 53 s to recalculate after an edit. Now the substitutions of a table run
one after another on a single working copy (they all set the same input cells, so each one
recalculates everything that depends on them and reads what a fresh copy would), and with
input cells that hold constants they use the calculation's own dependency graph instead of
a copy of it. The same edit takes 3.4 s, a 100,000-row one-variable table 2 s instead of
8 s, a small table in a large workbook 9 ms instead of 82 ms (release build).

A refresh also recalculates less: a changed formula, or a changed precedent of one,
recalculates the whole table, but an edited input value along an edge only its own row,
column or cells (typing a value into a 100,000-row table: 29 ms). A random-edit test checks
that this always agrees with a full recalculation.

A work budget joins the cell budget: a refresh spends at most 25 million formula
evaluations on tables (substitutions times the formulas that depend on the input cells),
so a hostile file can't hang the application; a table past it keeps its values until the
next refresh. GRIDCRAFT_PROFILE is read once instead of several times per pass, the
substitutions don't print profile lines, and they leave the calculation's circular
reference list, random state and cell count as they found them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eaving Manual recalculates

Observed in Excel 365: with Show Formulas on, a data table's results show `=TABLE(,B2)`
without the braces the formula bar adds, so `display::cell_text` drops them. `cell.get`
says which table a result cell belongs to (`dataTable: {range, rowInput, columnInput}`).

Switching from Manual to Automatic Except for Data Tables recalculates the formulas edited
in the meantime, as switching to Automatic already did (they stayed stale until F9).

Tests: an edited input value recalculates its own result and agrees with F9; Manual and
Automatic Except for Data Tables modes with F9 and the switch back to Automatic; clearing
the whole table removes it, with undo making it live again; clearing part is refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Observed black-box in Excel 365: it writes `dtr="1"` on two-variable tables as well (with
`r1` the row input and `r2` the column input) and `ca="1"` on some tables, and it reads a
two-variable table the same whatever `dtr` says. GridCraft now writes `dtr="1"` there too;
the reader already ignored `dtr` and `ca` for two-variable tables, which a new test reading
a sheet in Excel's form (row, column and two-variable tables, a shared-string result) pins.

A workbook GridCraft wrote with all three kinds of table and text results opened in Excel
365 without repair, with live tables over the same ranges that recalculated when their
inputs changed. The Excel-side files stay out of the repository.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Data › Forecast › What-If Analysis ▾ › Data Table… opened nothing: it ran `data.dataTable`
without parameters, which only reported the missing input cells. It now opens Excel's Data
Table dialog (Row input cell, Column input cell) over the selection; OK runs
`data.dataTable`, and an input cell Excel refuses ("Input cell reference is not valid.")
keeps the dialog open behind the message. The key tip Alt, A, W, T opens it too, and the
What-If Analysis menu has Excel's order: Scenario Manager…, Goal Seek…, Data Table….
Programmatic calls of `data.dataTable` still never open a dialog.

A data table's result shows its formula in the formula language: `{=TABLE(,B2)}` in the
formula bar (with `;` where the formula language separates arguments with it), `=TABLE(,B2)`
in the cell editor and, with Show Formulas on, in the grid. Enter on a result with the
formula unchanged changes nothing; anything else typed there gets Excel's "Cannot change
part of a data table."

German labels as German Excel has them: Datentabelle, Zeileneingabewert:,
Spalteneingabewert:, the Was-wäre-wenn-Analyse tooltip, "Automatisch außer bei
Datentabellen" in Calculation Options, and Abbrechen on dialog buttons.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found by the command-sequence fuzz once it made data tables. A data table's results are
formula cells in Excel, so a dynamic array can't spill over them, even while they're blank;
GridCraft let `=SEQUENCE(5,2)` spill into a table's empty results, and a full recalculation
then disagreed with the incremental one. Spills are now blocked by a table's results (in
the pass and when the results are written back).

A dynamic array may spill along a table's edges, and the spilled values are then the
table's input values or formulas. They aren't formula cells, so a refresh missed them: the
table is now triggered by the areas the recalculated formulas spill over before and after
the pass, as well as by the cells that changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`data.dataTable` joins the curated commands of the command-sequence fuzz: live (mostly) and
static tables near the data, one- and two-variable, with input cells near the data too
(sometimes inside the table, which is refused). Every step then checks that undo/redo, an
XLSX save and reload and a full recalculation agree with the live tables.

A soak of 150 seeds × 120 steps found the spill bug fixed in the previous commit. The
other failing seeds shrink to known problems outside data tables: dynamic-array spills (28,
29, 133), `SUBTOTAL` structured references to deleted columns (75, 132), Page Break Preview
and undo (138), and a `#SPILL!` result that a static table writes as a constant, which XLSX
stores as `#VALUE!` like any `#SPILL!` constant (78). Seeds 15, 79 and 110 are slow in fill
down and whole-column merges, with no data table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… trip

ROADMAP progress log, target-app parity (What-if row), file-format parity (XLSX data tables
checked against Excel 365 both ways) and UI parity (Data Table dialog), with their
Last updated lines and revision history. No score changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants