Commit Graph
334 Commits
Author SHA1 Message Date
prxnvt b3a3590db6 Merge main into Python cells as data 2026-11-01 14:03:09 -05:00
prxnvt 84d4f6aa84 XLSX: a Python script's length is measured after its escapes are decoded
A script's code was cut at the character limit before its ST_Xstring escapes
were decoded, so a script whose escapes (`_x0009_`, `_x000D_`) made the file's
text longer than the limit lost its end without a warning, although its code
fits. The code is now decoded first and then cut one character past the limit,
so a script that is too long once decoded still fails the check and its cell
keeps only its value, as before. Decoding is linear and reads at most a bounded
number of encoded characters, enough for any code that fits.
2026-11-01 12:58:13 -05:00
prxnvt e82e91b451 XLSX: a Python cell without its code keeps its whole cached result as values
A PY formula whose script the file doesn't hold (the Python part missing or
damaged, or the script over the code budget) keeps only its cached value, but
its array range stayed recorded, so the cached values over the rest of its
spill were dropped as a formula's spill would be, and nothing filled them
again. The range of such a cell is now forgotten when it is read: the anchor
and the cells of its spill keep their cached values as plain values, as the
file shows them, with no live spill. An ordinary formula's dynamic-array spill
is handled as before.
2026-11-01 12:56:55 -05:00
prxnvt 113aeff07c XLSX: a damaged Python part is a warning, and the workbook opens with its values
A Python part whose zip entry failed to read (a bad checksum or compressed
stream) made the whole workbook fail to open: the error was passed up, and the
failed read had also ended the package's read budget, so even a warning would
have left the sheets unread. The part is now read with
Package::read_optional_limited, which charges a failed read only that part's
allowance, and every error other than TooLarge is a warning. The Python cells
then keep only their cached values, as when the part is missing. Malformed XML
in the part was already a warning; the test covers it too.
2026-11-01 12:36:04 -05:00
Brandon Thomas e129c072a1 Merge PR #3075
# Conflicts:
#	crates/xlsx/src/read.rs
#	docs/file-format-parity.md
2026-11-01 12:20:06 -05:00
Brandon Thomas bc173ead8f Merge PR #3062
# Conflicts:
#	ROADMAP.md
#	docs/file-format-parity.md
2026-11-01 12:19:55 -05:00
Brandon Thomas 7f1574b573 Merge PR #3052
# Conflicts:
#	docs/file-format-parity.md
2026-11-01 12:19:55 -05:00
prxnvt c03c61fd05 Merge main into Python cells as data 2026-11-01 12:00:09 -05:00
scks1 60a267b4ef #3057: select visible workbook views and normalize tab ratios 2026-11-01 08:06:44 -05:00
Harrison SmithandClaude Opus 5.5 a1e0383056 #2871: unwrap Google Sheets' wrapper only when GridCraft calculates the formula
A formula calling a Google-only function (GOOGLETRANSLATE, QUERY,
ARRAYFORMULA...) opened as #NAME? where the wrapper had shown the cached
value, as Excel does. The XLSX reader now unwraps a cell formula only when
every function inside is known: read_xlsx_with takes the predicate (the
xlsx crate can't depend on the function library) and the engine passes
gridcraft_calc::is_known_function. Unwrapping stays in the reader so shared
formulas are shifted from the unwrapped master.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 00:08:40 -06:00
Harrison SmithandClaude Opus 5.5 2c0a00fbae #2871: open Google Sheets' DUMMYFUNCTION export wrapper as the original formula
Google Sheets' XLSX export writes a formula that uses a function it thinks
Excel lacks as IFERROR(__xludf.DUMMYFUNCTION("<formula, quotes doubled>"),
<cached value>). GridCraft showed the wrapper as the formula.

fmla::from_file, which every cell formula read goes through (plain, shared
master and t="array"), now recognises exactly that shape: IFERROR (with or
without _xlfn.) of a single-string __xludf.DUMMYFUNCTION call, any case.
If the string parses, the formula inside replaces the wrapper, as the whole
formula or nested in a larger one (parenthesised unless it is a single
operand). The cell keeps the file's cached value.

Bounded on hostile input: inner text over MAX_FORMULA_LEN is not parsed,
only one level is unwrapped, and a result that no longer parses (nesting
past the parser's limit) keeps the file's formula. Text that doesn't parse
keeps the wrapper.

Saving writes the real formula with Excel's _xlfn. prefixes, as Excel does
with the _xlfn functions it knows. Keeping cached results for functions
GridCraft can't calculate is #2770 and not part of this change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 19:19:20 -06:00
Simon Koch-Sultan 9276827b1a xlsx: keep the workbook view settings across an open + save (#3057)
The reader took only activeTab from <workbookView> and the writer wrote
that alone back, so a workbook Excel saved with the sheet tabs and both
scroll bars hidden (and a non-default tab split) opened with all three
shown and lost the settings on save.

Workbook gains a view field (WorkbookView: show_sheet_tabs,
show_horizontal_scroll, show_vertical_scroll, tab_ratio; the format
defaults are shown/shown/shown/600). The reader fills it from the
<workbookView> attributes; the writer emits each attribute only when it
differs from the default, so an ordinary workbook file is unchanged.

Regression test: workbook_view_settings_survive_a_round_trip reads a
package carrying the hidden settings, checks the model, and checks the
written workbook.xml keeps all four attributes (and that a default
workbook still writes the bare element).

Scope: this is the data-loss half of #3057. The app does not act on the
settings yet and File > Options has no controls for them; that is the
other half the ticket was split into.
2026-10-31 15:31:07 -04:00
Daniel LubrykaandClaude Sonnet 5.5 d25445f4e3 M2.1: document the .xls style helpers
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-31 19:14:33 +01:00
Daniel LubrykaandClaude Sonnet 5.5 879ea7385f M2.1: test that .xls fonts keep their family and charset
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-31 19:01:26 +01:00
Daniel LubrykaandClaude Sonnet 5.5 f2a75eca1b M2.1: keep BIFF8 font family and charset when importing .xls fonts
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-31 18:47:15 +01:00
Max Murphy 72b1310a5a #2765: CSV and text open detect each column's date order 2026-10-31 16:57:58 +01:00
Brandon Thomas ac27e3da14 Merge PR #2793 2026-10-31 10:17:09 -04:00
Harrison Booth b612141726 xlsx: keep intl date functions in xlfn list 2026-10-30 15:56:52 -05:00
Harrison Booth c82a138462 M2.1: fix XLS style test clippy lints 2026-10-30 15:47:18 -05:00
Harrison Booth 591c6ed6f9 Fix XLS style test Clippy complexity 2026-10-30 15:40:56 -05:00
Harrison Booth 9587f08253 Merge upstream main into gc-xls-cell-styles 2026-10-30 15:31:49 -05:00
truongitandClaude Opus 5.5 94433a3c4f Document the Agile decryption steps
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 16:51:55 +07:00
truongitandClaude Opus 5.5 9e0f14cab9 Open Excel's password-protected workbooks again: accept a hash-size integrity key (#2712)
Since #2425 checks Agile encryption's dataIntegrity, every workbook Excel
encrypts fails with "the decrypted data failed its integrity check". The
reader keyed the HMAC with the decrypted encryptedHmacKey cut to the salt
size (16 bytes), as [MS-OFFCRYPTO] 2.3.4.14 describes it, but Excel's key
is as long as the hash: 64 bytes for SHA-512. The check now accepts a key
of either length, so spec-literal writers still open and a modified
package still fails.

The test encryptor writes the integrity key as Excel does, so every
encrypted fixture now has Excel's layout; a new test covers both key
lengths, a key of neither length, and tampering with each.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 14:42:37 +07:00
Dylan M. Taylor 84e684e586 Refactor xlsx for strict Clippy checks 2026-10-28 10:52:53 +00:00
Harrison Booth f6015a7e6e Pin COPILOT as written without an _xlfn. prefix 2026-10-27 21:21:04 -05:00
Harrison Booth 25816f0009 xlsx: write _xlfn. for STOCKHISTORY, TRANSLATE, DETECTLANGUAGE; none for NETWORKDAYS.INTL, WORKDAY.INTL 2026-10-27 20:57:17 -05:00
Harrison Booth 0e643e9026 Merge remote-tracking branch 'upstream/main' into gc-xls-cell-styles 2026-10-27 15:59:33 -05:00
NetterSilverandClaude Opus 5.5 393a524aa3 Speed up XLSX worksheet writing
Saving a large workbook spent about half its time building worksheet XML,
mostly in small per-cell allocations. The writer now appends to the part
directly:

- Cell references, row numbers, style ids, shared string indexes and
  numbers are appended without temporary strings (push_a1, push_u64,
  push_num), and text is escaped in place (push_esc, with a fast path for
  plain ASCII).
- sheetData walks the cell store in its own row-major order instead of
  copying every address into a BTreeSet and a per-row BTreeMap and looking
  each cell up again. Cells outside the store (pictures, spill values,
  table headers) still go through the sorted set.
- Formulas with nothing the file spells differently (no LET/LAMBDA, no
  _xlfn function, no spill, @, trim operator or structured reference) are
  printed from the shared expression instead of being cloned and rebuilt
  twice by the file rewrite.
- The A1 printer in gridcraft-formula appends references to the output
  instead of formatting each anchor into its own string.

The output is byte-for-byte unchanged.

write_perf, 200,000 rows (2.0M cells, 14.2 MB package), release, Ryzen
16 threads / Linux, 36 interleaved runs each:
  before: median 2083.9 ms, min 1615.7 ms
  after:  median 1501.3 ms, min 1192.3 ms  (-28.0 % median, -26.2 % min)

What remains is mostly deflate (zlib-rs, level 6), about 70 % of the save.

Tests pin the exact sheetData of a workbook covering row settings between
cell rows, spill values, every value kind, number spellings, escaping and
formulas with and without a file spelling (generated with the previous
writer), and check each appending helper and the formula fast path against
the code they replace.

The formula fast path is guarded by a test that runs both paths over every
function in the gridcraft-functions registry (a new dev-dependency, allowed
by the layer rules), in 15 surrounding shapes covering every operator and
rewrite, so a function or rewrite added later without updating the trigger
list fails it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 16:46:25 +01:00
aiongg 7f1ccf2e81 refactor(xlsx): move sheet_read.rs items to sheet_read/ 2026-10-26 20:43:39 +00:00
aiongg af54396e64 refactor(xlsx): move sheet_write.rs items to sheet_write/ 2026-10-26 20:43:39 +00:00
aiongg d1c8de48e7 refactor(xlsx): move passthrough.rs items to passthrough/ 2026-10-26 20:43:39 +00:00
aiongg e8f3b6f0cf refactor(xlsx): move pivot.rs items to pivot/ 2026-10-26 20:43:39 +00:00
aiongg b3e2a6a2be refactor(xlsx): move crates/xlsx/src/tests/mod.rs to crates/xlsx/src/tests.rs 2026-10-26 18:15:26 +00:00
aiongg 24a18620d2 refactor(xlsx): move crates/xlsx/src/xls/mod.rs to crates/xlsx/src/xls.rs 2026-10-26 18:15:26 +00:00
prxnvt fcbbfa5142 XLSX: Python cells as _xlfn._xlws.PY() formulas, code in the Python part, cached values; opening limits
A Python cell is written to xlsx as [MS-XLSX] 2.3.10 specifies: its
formula is _xlfn._xlws.PY(i,0|1), where i is the zero-based index of the
cell's script in the workbook's Python part and 0|1 the return type, 0
for an Excel value and 1 for a Python object. The part (2.1.23) is
xl/python.xml, content type application/vnd.ms-excel.python+xml,
related from the workbook part with
http://schemas.microsoft.com/office/2023/09/relationships/Python; its
root python holds pythonScripts, one pythonScript per Python cell in the
order the cells are written, with the code in code. The code keeps its
xl() references as text, so the formula has no further arguments. The
cell keeps its cached value. A spilling result is written like a dynamic
array (cm="1", t="array" ref=...) with the spilled values cached, so
other applications show the values (and #NAME? if they recalculate).

The reader turns a PY( formula with or without the _xlfn. and _xlws.
prefixes into a Python cell. An index names a script of the Python part,
and each %Px% in the script becomes the text of the formula's argument x
(2.3.10.4). The string-literal form, PY("chunk"&"chunk",0|1), still
reads, from the formula's own text so line breaks in the code survive. A
formula naming a script the part doesn't hold keeps only its cached
value, with a warning: no calculation can evaluate it. The reader keeps
the cached spilled values as the cell's result, so a workbook from Excel
shows its arrays without running anything. Python errors are cached as
#VALUE!, as #SPILL! and #CALC! already are. Native JSON keeps Cell.py
whole: code, return type, the last result and the exact error.

Calc lays out a Python cell's kept result: Python cells are nodes of the
dependency graph that read nothing, and a pass takes a dirty Python
cell's result instead of evaluating it, so its array spills, is blocked
(#SPILL!) and unblocked, and feeds its dependents exactly like a
formula's. An array that is malformed or has more than 1,000,000 cells
(only a crafted file holds one) shows #SPILL!. A dirty Python cell is
pending in the pass, as a dirty formula is, so a formula, a range read
or a cell-by-cell range walk that reaches it before its turn lays it out
first rather than reading its old value.

Opening a file can't make the app loop over a claimed size: the reader
keeps a Python cell's spill range only when it has at most 1,000,000
cells (a range claiming the whole sheet is dropped with its values, as a
formula's spilled values are), and io::adopt_python rebuilds results for
at most 4,000,000 cells over the whole workbook, with a warning for the
cells it skips. The Python part is read up to 64 MB and 1,000,000
scripts, a script is kept up to one character past what a cell may
hold, and the cells take at most 64 MB of code from the part in all,
since several formulas can name one script. io::enforce_python_limits
drops the code of a cell whose code doesn't fit in 8,192 characters as
PY(...) (the cell keeps its cached value, with a warning) and a kept
result that isn't a well-formed array within the cap, each with its
spill range and spilled values, so the sheet and a save no longer show
the old spill. Formats that keep PY() as an ordinary formula (xls, xlsb,
ods) are adopted the same way. Opening a file runs nothing.

The command-sequence fuzzer sets Python cells (code with xl()
references, quotes, line breaks, and code at and over the size limit),
and its xlsx round-trip check allows the Python errors becoming
#VALUE!, which is what the file format stores.

Tests: the written formula naming its script, the Python part with its
content type and relationship, return types, a spilling table saved as a
dynamic array, Python errors as #VALUE!, the round trip of values, code,
long code with quotes and line breaks and the error, and no Python part
without Python cells; a workbook laid out as the format specifies (an
index, a parameter named as %P2%) read as Python cells with their
spilled values and saved back with its code in a Python part; the
string-literal form; a formula naming a missing script, with and without
a Python part; 20,000 cells naming one long script against the code
budget; the part's reader and writer (escaping, odd %P tokens, a long
script, a repeated parameter); a blocker in the spill area giving #SPILL!
and clearing it spilling again; the native JSON round trip; a spill
range claiming the whole sheet; the workbook-wide budget; hostile JSON
(code over the limit, a result claiming far more cells than it holds); a
malformed result that never spills; PY() formulas at 8,079 and 8,080
characters of code. Calc: formulas, MAX, SUM, COUNTA and SUMPRODUCT read
a dirty Python cell whose array is blocked before its turn, in a full
and an incremental recalculation, and see the array once it is
unblocked; enforce_python_limits removes the spill of dropped code and
of a dropped result, and keeps a sound one.
2026-10-26 10:18:38 -04:00
prxnvt 44c663aea2 Model: Python cells, and the #PYTHON!, #BUSY!, #TIMEOUT! and #BLOCKED! errors
Cell gains `py: Option<Arc<PyCode>>`, read with a serde default and not
written for ordinary cells, so older native files load and ordinary
cells serialize as before. PyCode holds the code, the return type (an
Excel value or a Python object), the last result (value, type name,
preview, whether it converted to cells) and a stale flag for manual
calculation. The cell's `value` stays the cached result, so formulas,
display and saving read a Python cell like any other. is_blank,
CellStore::has and input_text (which returns the code) take the code
into account; Cell::has_content and clear_content cover value, formula
and code together.

The model reads and writes the PY function's syntax as Microsoft
documents it, PY("chunk"&"chunk",0|1), with string literals of at most
255 characters and the return type 0 for an Excel value, 1 for a Python
object. The reader takes the `_xlfn._xlws.` prefix (and `_xlfn.` or
`_xlws.` alone, in any letter case); to_file_formula writes it. (An xlsx
file keeps the code in its Python part instead; the xlsx commit adds
it.) The size limit is Excel's 8,192 characters for that formula, not a
count of code characters: doubled quotes and the "&" between literals
count, so code with many quotes fits in fewer characters (8,079
characters of plain code fit exactly). The model also finds the
xl("...") literals in code, skipping comments and other strings; a plain
literal may escape a quote or a backslash. A rewritten reference that
holds the literal's quote (a sheet name that needs quotes, 'Q4
2026'!B2) takes the other quote as the literal's delimiter, or an
escaped quote when it holds both, so it never ends the string early.

Edits: cell.set over a Python cell, Clear Contents, Clear All and the
cells a merge empties remove the code; Clear Formats keeps it. Copy and
paste (All, Formulas), Fill, sort, cut-move, sheet copy and undo carry
it; a cut to another sheet first qualifies its xl() references with the
source sheet, as a moved formula's references are, so they keep
reading the same cells. Paste Values and Paste Link do not carry it,
and paste operations do not combine numbers with a Python cell. Fill
copies Python cells instead of extending them as a series. cell.set
with =PY(...) is refused with a message that points to python.set, and
Text to Columns keeps a piece that reads as =PY(...) as text instead of
clearing it. Goal Seek refuses a Python cell as its changing cell, and
Solver as a variable cell, as both refuse formulas: the number written
there would replace the code. The sheet-protection check compares the
code, so a locked Python cell cannot be changed. cell.get reports
{python: code, output, typeName, preview, stale}.

The xl() literals follow reference-moving edits: rewrite_all_formulas
(row and column insert and delete, cell insert and delete, cut-move),
sheet rename and sheet delete adjust each literal like a formula
reference, and replace it only when it changed.

Errors: CellError gains Python, Busy, Timeout and Blocked, and ALL has
14 entries, so the lexer reads them as literals. ERROR.TYPE gives 11 for
#BLOCKED!, the number Excel returns. Microsoft documents no ERROR.TYPE
number for #PYTHON!, #BUSY! or #TIMEOUT!; they take 19, 20 and 21, after
the numbers Excel is known to use. The xlsx writer caches the four as
#VALUE!, as it already does #SPILL! and #CALC!, because they are not
error codes of the file format.

Tests cover the =PY refusal, removal by entry and clearing, undo, copy,
paste values, fill and sort, xl() rewrites through row insert, sheet
rename (also to names that need quotes, with one quote and with both),
cut-move (also to another sheet) and column delete, Goal Seek and
Solver refusing Python cells, Text to Columns keeping a =PY piece,
native JSON round trips, the new errors, the prefixes, escaped quotes in
literals, and the encoded size limit at 8,192 characters exactly and
with doubled quotes.
2026-10-26 10:18:38 -04:00
aiongg f102fe29a1 Merge g3 into g4
# Conflicts:
#	ROADMAP.md
#	crates/engine/src/cmd/edit.rs
#	crates/engine/src/cmd/formulas.rs
#	crates/engine/src/cmd/inspect.rs
#	crates/engine/src/cmd/pivot.rs
#	crates/ui-egui/src/lib.rs
#	crates/ui-egui/src/tabs.rs
#	docs/gaps.md
#	docs/ui-parity.md
2026-10-26 02:45:41 +00:00
Dylan M. Taylor 52cf825395 refactor: split complex functions for Sonar S3776 (#2609)
Extract helpers from auto_sum (selection and single-cell guess), update_cursor
(drag, hover, freeze, and header-edge cursors) and read_styles (dxf reader).
No behaviour change.
2026-10-25 21:37:09 -04:00
3b65b9af58 Sonar maintainability: explicit imports, method paths, smaller functions (#2412)
* Sonar: explicit imports, fn paths for closures, extract helpers for cognitive complexity

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

* Fix imports

* Fix serde imports

* Fix engine imports

* Fix clippy gate and a type error from the explicit-import change

Remove the unused imports the review flagged (cf.rs, pivot.rs, tables.rs, cmd/sheet.rs) so clippy -D warnings passes, and point the backstage parent-folder lookup at Path::to_str, which the fn-path rewrite had broken.

Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>

* Apply rustfmt to the explicit-import cleanup so the fmt gate passes

* Share the per-directory font path expansion in theme.rs

The candidate font lists repeated the same loop over the Linux font
directories in every role. A single helper expands them in the same order,
so the lookup order is unchanged and the duplicated blocks go away.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>

* Read the Windows fonts folder in one place in theme.rs

Every script-font list repeated the same WINDIR lookup. A single helper
returns the same path, so the duplicated lines go away without changing
which font files are tried.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>

* Build the light and dark UI palettes from one table in theme.rs

Tokens::light() and Tokens::dark() listed the same 40 colour fields in the
same order, so every change had to be made twice. A single palette
constructor takes the mode and picks each colour's value, so each field is
written once and both modes keep their exact colours.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>

* Share the Noto CJK font list between the CJK script lists in theme.rs

The Japanese, Simplified and Korean lists each repeated the same loop over
the Linux CJK directories with a different locale file name. One helper
builds that loop, and each list keeps its own face index, so the lookup
order is unchanged.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>

* Share Linux Noto directory list across Thai and Hebrew font lookups

Removes the duplicated directory arrays flagged by SonarCloud new-code duplication.

* Reuse in_dirs for Thai and Hebrew Noto font candidates

Removes the remaining duplicated per-directory loops flagged by SonarCloud.

* Share one font-list builder for the Thai and Hebrew faces

The two per-script functions repeated the same Windows, macOS, Linux and
variable-font setup. A single script_fonts helper now builds both lists,
keeping the candidate order unchanged, which clears the Sonar duplication gate.

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
2026-10-25 12:32:02 -04:00
Brandon Thomas 0f5227ddde Merge pull request #2513 from Fubeo:fub/keep-sheet-id 2026-10-25 09:36:35 -04:00
Brandon Thomas 2c81adf94f Merge pull request #2557 from Silvertree2010:fix/2517-a-new-font-derived-from-the-them 2026-10-25 09:36:33 -04:00
NetterSilverandClaude Opus 5.5 85b6f564f9 Keep <scheme val="minor"/> on every body-font font on save
Fixes #2517

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 13:20:17 +01:00
NetterSilverandClaude Haiku 5.5 d07639c7c4 Warn for each skipped drawing object and chart on XLSX open
Fixes #2407

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-25 12:25:18 +01:00
aiongg 6194f8241d Merge storytold/gridcraft#2522: Charts: 100% Stacked Area draws each value as a share of its category's total
# Conflicts:
#	ROADMAP.md
2026-10-25 02:54:23 +00:00
aiongg b58846a549 Merge storytold/gridcraft#2482: Outline: Settings puts summary rows above and summary columns left of their detail
# Conflicts:
#	docs/ui-parity.md
2026-10-25 02:54:23 +00:00
dotCooCoo 0eb2f1f6f0 Charts: 100% Stacked Area draws each value as a share of its category's total
Insert > Line or Area and the All Charts list offer 100% Stacked Area,
and insert.chart and chart.set take it as type area with subtype
stacked100. Each series' area stacks on the ones before it as its
share of the category's total. When every series on the value axis is
100% stacked and no value is negative, the axis runs from 0% to 100%.
A blank cell counts as zero, and a category whose total is zero stays
on the baseline.

An XLSX area chart with percentStacked grouping now opens as 100%
stacked and is saved back as percentStacked. It used to open as a
stacked area and was saved as stacked.
2026-10-24 16:42:59 -07:00
Fubeo 85e8f7909e fix(xlsx): keep each sheet's sheetId across a rewrite
The writer numbers sheetIds from 1 in sheet order, so a rewritten workbook
changes the id of any sheet that is not part of a gapless series from 1.
Anything that recognises a sheet by its id (parts carried through as they
are, external tools) loses it. The model keeps the sheetId it read
(Sheet::file_id, PreservedSheet::sheet_id, not serialised to JSON) and the
writer writes it back; a new sheet, or a copy with a taken id, gets the first
free one.
2026-10-25 01:31:19 +02:00
Harrison Booth 9ed1001035 Merge remote-tracking branch 'origin/main' into gc-xls-cell-styles
# Conflicts:
#	ROADMAP.md
2026-10-24 15:32:17 -05:00
dotCooCoo 118cd15d93 Outline: Settings puts summary rows above and summary columns left of their detail
Data > Outline > Settings now has Summary rows below detail and Summary
columns to right of detail. With one cleared, each group's - / + button
sits on the line before the group, its bracket runs down to the last
detail line, and Hide Detail, Show Detail, the level buttons and
Ungroup mark or clear the collapsed flag on that line. Changing a
direction moves the flags of collapsed groups to their new summary
lines.

- model: Sheet::summary_below and summary_right (on by default, also
  for older JSON).
- xlsx: read and written as sheetPr/outlinePr summaryBelow and
  summaryRight; files that set them no longer lose them on save.
- engine: data.outlineSettings {sheet?, summaryBelow?, summaryRight?},
  undoable; outline::groups_with places the summary line.
- ui: a Settings button in the Data tab's Outline group opens the
  dialog; the outline band follows the setting.
2026-10-24 11:34:35 -07:00
dotCooCoo 30126a709a PivotTables: Enable show details turns Show Details off for a PivotTable
A PivotTable has an Enable show details option, set with the new
pivot.options command ({pivot?, enableShowDetails?: bool}). While it is
off, pivot.showDetails and double-clicking a value cell list no records:
the double-click shows a message and does not open the cell for editing.
Labels and headers still edit. Changing the option is one undo step and
does not refresh the report. pivot.fields reports the setting.

XLSX files keep it: an off option is written as enableDrill="0" on the
pivotTableDefinition, and enableDrill is read on open (on when absent).
2026-10-24 07:46:29 -07:00