Changelog
Unreleased
Section titled “Unreleased”API changes
Section titled “API changes”- New public getter
Table.styleexposes the table’s visual style. Use it instead ofTable.jsf.style; thejsfis internal and absent from the published type declarations.
Bug fixes
Section titled “Bug fixes”- Defined names and sheet names that collide with JavaScript object property names (
__proto__,constructor, …) now behave like any others, instead of silently vanishing on creation or misreporting as existing. Model serialization is now version 9; earlier payloads still load. YEARFRACwith basis 0 (US 30/360) now returns a whole number of years when the start and end dates are both the last day of February, matching Excel:YEARFRAC("2022-02-28", "2023-02-28", 0)is 1, not 0.994444…- A formula cell that calls a function the engine does not implement, or names a defined name the file does not define, now keeps its saved value, matching Excel; previously such a cell could be recalculated to
#NAME?when its workbook was added, and again on every lateraddWorkbookinto the same model. INDEX(range, ...)no longer reports a false circular dependency through cells ofrangethe formula never uses; previously a workbook with such formulas computed cleanly in Excel while Apiary evaluated part of the false cycle as0.- Formula cells that depend on a pivot table, through
GETPIVOTDATAor a plain reference to an output cell, now update after a cache refresh whose only change is to the fields (a source-column rename, reorder, drop or add); previously they kept results computed from the old fields until something else forced a recalculation. - A cell whose text is the name of a JavaScript object property (
constructor,toString,valueOf,hasOwnProperty) now survives CSF import as that text; previously it was mistaken for an error name and replaced with the inherited function object, after whichModel.readValuethrew. - A formula cell that the file cached no value for now reads
#NAME?or#REF!when nothing its formula names resolves, instead of possibly reading blank; it still recovers to a real value if a lateraddWorkbooksupplies what was missing. - A workbook that references a defined name in another (typically external) workbook no longer fails to load with an internal assertion error when the referencing sheet defines a sheet-scoped name by that name.
- In a model with several workbooks, a formula reading a defined name now depends on the name it actually evaluates to, so editing that name recalculates the formula, and editing a like-named one in another workbook no longer does.
- A workbook-prefixed defined name, such as
[other.xlsx]total, is taken from that workbook’s own scope, never from the sheet the referencing formula sits on. - A reference naming a workbook, such as
=[other.xlsx]!total, now yields#NAME?when that workbook has no such name, instead of silently reading a like-named one from the referencing workbook. - An unprefixed defined name is no longer claimed by a like-named sheet in a different workbook. In an Excel-mode workbook, a name defined only in another workbook now stays unresolved, matching the
#NAME?it evaluates to. - A sheet-qualified reference into another workbook, such as
[other.xlsx]Data!A1, now looks forDatain that workbook only, instead of possibly landing on a like-named sheet of some other workbook. - Moving cells no longer fails with an internal assertion error when a formula in another workbook of the model references the range being moved.
- A defined name in an external workbook that reads a sheet, name, or table the external workbook does not have no longer silently resolves to one belonging to the workbook that referenced it, and reports
#REF!or#NAME?instead. - A defined name in an external workbook is now evaluated in the referencing workbook’s mode, so Excel-only functions in such a name no longer report
#NAME?. - A reference to a sheet whose name reads as something else (a cell address such as
A1, an R1C1-style name such asRorR1C1, a name leading with a digit such as2020, orTRUE/FALSE) is now written with quotes, as Excel writes it. Workbook.fromXLSXandWorkbook.fromXLSXFilenow include Excel-only functions such asANCHORARRAYin workbooks. This matches the existing behaviour ofModel.fromXLSXandModel.addWorkbookFromXLSX.- An XLSX file exported from Google Sheets now loads as a Google Sheets workbook and evaluates under Google Sheets rules, instead of Excel’s. A file with external workbook references is still loaded as an Excel workbook.
- A range whose two endpoints name different sheets, such as
=SUM(A1:Sheet2!B2)written anywhere but onSheet2, now returns#VALUE!as Excel does; previously the right endpoint’s sheet prefix was ignored. - A string literal longer than 255 characters now evaluates to that string instead of
#NAME?. Excel stores such a literal as a call to its internal_LONGTEXTfunction over shorter chunks. - A formula that mixes an unresolvable reference with an
OFFSETcall now keeps its saved value when another workbook is added to the model, instead of being recomputed even though the added workbook resolves none of its references. - A formula that was edited away from a reference it could not resolve now keeps its value when a workbook is added to the model; previously the addition recomputed the formula and overwrote whatever the cell held.
- Adding, removing, reordering or copying a sheet in one workbook no longer disturbs a pending recalculation in another workbook of the same model, which could leave a formula in that workbook holding a stale value.
Performance
Section titled “Performance”- Editing a formula no longer leaks memory, and removing a workbook now releases nearly all of the memory it occupied. The dependency graph kept a record of every edge it ever removed, and each record pinned the cells at both ends along with the parsed formula it came from.
v17.0.0 (2026-07-10)
Section titled “v17.0.0 (2026-07-10)”The stable 17.0 release. Its headline feature, pivot-table support, landed in the beta series and is stabilized here.
Upgrading from 16.x
Section titled “Upgrading from 16.x”The significant breaking changes across the whole 17.0 line are:
- Edit guards. Edits that would corrupt a what-if data table, a pivot table’s output, a worksheet table, or a merged cell now throw instead of silently damaging the model. Every refusal is an exported subclass of a common
EditBlockedError, so oneinstanceofcatches them all:DataTableEditBlockedError,PivotEditBlockedError,PivotCellWriteBlockedError,PivotOutputOverlapsDataError,PivotDuplicateSourceHeadersError,TableEditBlockedError,SortCellsBlockedError, andReorderMergeBlockedError. - Writes are persistent. Transient writes and
resetare gone: write throughWorkbook.editCelland capture/revert withValueSnapshot. Removed with them areModel.reset/writes/writeState, the named-values feature, range writes, writes to defined names, and theGRID.SCENARIO()function (now#NAME?). - Default style slot.
StyleManagerreserves style index 0 as the workbook default, sostyles.lengthstarts at 1. - Renames.
moveRows/moveColumns→reorderRows/reorderColumns(the old names are gone);RecalcUreachedEvent→RecalcUnreachedEvent; theRewriteFormulacallback now takes sheet/workbook context. - Externals round-trip in full.
Workbook.toJSF()now preserves all input-provided external cells, not only the referenced ones, so round-tripped JSF contains strictly more external cells than before. - The
Cellvalue setter no longer accepts aLambda. Callers must coerce a possible lambda to an error value themselves. - Internals are no longer typed. Members documented
@internal— among themCell.userZ— are stripped from the published TypeScript declarations, so code reaching into engine plumbing will now see a type error.
Full detail is in the v17.0.0-alpha.0, -beta.0, and -beta.1 notes below and at https://docs.grid.is/apiary/.
Breaking changes since v17.0.0-beta.1
Section titled “Breaking changes since v17.0.0-beta.1”Workbook.toJSF()now preserves all input-provided external cells (andrefreshError) underexternals[].sheets[].cells, not only those the host’s dependency graph referenced, so round-tripped JSF contains strictly more external cells than before.- The
Cellvalue setter (cell.v = …) now takesMaybeBoxed<CellValue>and no longer accepts aLambda; callers must coerce a possible lambda to an error value themselves (previously the setter coerced it to#CALC!). - The pivot layout cluster is now internal:
PivotLayout(and itsPivotCellWrite,ComputeOptions, andColumnSubtotaltypes) is no longer exported. It was materialize/repaint plumbing; the renderer-facing surface remainsPivotTable.rowRoles/colRoles/pageAreaand the already-materialized cells. - The
SubtotalRow/AggregateSubtotalRow/SeparatorSubtotalRow/BlankSubtotalRowtypes are no longer exported. They were reachable only through the now-internalPivotLayout; renderers should readPivotTable.rowRoles/colRolesinstead. PivotCacheis now a type-only export: no public API returns or accepts a constructed instance, so the value export offered only a constructor and lifecycle methods that bypass the pivot manager.- The
moveRows/moveColumnsnames, deprecated as aliases whenreorderRows/reorderColumnswere introduced earlier in the 17.0 line, are removed; themove*names stay reserved for a possible future overwrite-move variant (the row/column counterpart ofmoveCells, which overwrites the destination instead of reflowing). - The edit-guard error classes are renamed to the package’s
*Errorconvention (PivotEditBlockedError,PivotCellWriteBlockedError,PivotOutputOverlapsDataError,DataTableEditBlockedError) and now extend a common exported base classEditBlockedError, so a host can catch every guard refusal with oneinstanceof. Payloads are made consistent and richer: each carriessheetName,PivotEditBlockedErrorgains anoperationdiscriminator, andPivotCellWriteBlockedError.cellRefis now a plain A1 string. Refusals that previously threw bareErrornow throw typed subclasses:PivotDuplicateSourceHeadersError(duplicate pivot-source headers),TableEditBlockedError(the worksheet-table-protectinginsertCells/deleteCellsrefusals),MergedCellWriteBlockedError(a write througheditCellto a merged non-anchor cell), andSheetEdgeBlockedError(an insert that would push non-empty cells or a drawing off the sheet edge).Workbook.editCellandWorkbook.sortCellsthrow the now-exportedSheetNotFoundErrorfor a missing sheet, and the newWorkbook.fillthrowsPivotEditBlockedError(operation'fill') over a pivot’s output. - Public getters that returned live internal objects now return copies and/or
readonly-typed values, so mutating a returned value can no longer corrupt engine state:PivotTable.getOutputBounds()/getProtectedBounds()(copies typedReadonly<OutputBounds>),PivotTable.dataFields(areadonlycopy), andStyleManager.defaultStyle(a copy;setDefaultStylestores one). - Members documented
@internalno longer appear in the published TypeScript declarations (dist/index.d.ts/index.d.cts); code referencing engine plumbing such asPivotCache._permuteRecordsorPivotManager._cacheswill now see a type error. PivotColRole.kindis typed'subtotal'(the only value the engine produces); drop any comparison to'subheading'.
Features since v17.0.0-beta.1
Section titled “Features since v17.0.0-beta.1”- Formula parsing now handles Excel’s structured-reference
@this-row forms (Tbl[@col],Tbl[@[col]],Tbl[@x:y]). A bareTbl[@](this row, all columns) evaluates to#VALUE!, matching Excel. - New
AGGREGATEfunction: all 19 aggregations and options 0–7, in both the reference form (function_num1–13) and the array form (14–19). Matching Excel, only the array form accepts array expressions: the reference form returns#VALUE!for an argument such asA1:A3*2or2*A1, and for an extra argument that is not a further range (a scalar such as=AGGREGATE(9,4,A1:A3,2)returns#VALUE!; an array such as{2,3}spills a per-element#VALUE!). LikeSUBTOTAL, it skips range cells whose own formula nests aSUBTOTAL/AGGREGATEcall, so subtotals-of-subtotals are not double-counted. - New
MODE,MODE.SNGL, andMODE.MULTfunctions (previously#NAME?);MODE.MULTspills a vertical array of all values tied at the highest count. - New
Workbook.fill(source, destination, type?)editing operation models Excel’s fill-handle drag — values, formulas (relative references shifted), and formatting projected across the destination, with Excel’s series/date/text-number semantics andFillTypemodes. Text series follow Excel’s rules: quarter labels wrap modulo 4 (Q3→Q4, Q1, Q2, notQ5, Q6), an ordinal like1st Periodrecomputes its suffix per value (2nd, 3rd, 4th Period), text with more than one number steps only its last run (A1B2→A1B3, A1B4), and zero-padded seeds keep their padding. See the docs for the full rules. - New
Workbook.sortCells(sheetName, range, sortKeys, options?)editing operation sorts rows within a range by one or more key columns, matching Excel’s Data > Sort: cell contents, formulas, and styles move with their rows (relative references adjust for the row change; absolute and sheet-prefixed references stay put), row heights stay at their positions, hidden rows are left in place, and blanks sort to the bottom. Merged cells and multi-row spill ranges are refused, as in Excel; so is a spill reaching into the range from outside. So is a sort that would disturb a worksheet table — touching its header or totals rows, crossing its data-body boundary, or leaving some of its columns unsorted — and one overlapping a pivot table’s output or a what-if data table’s result cells. Every refusal throws a subclass ofEditBlockedError, so a host can catch them all with oneinstanceof:SortCellsBlockedError(carrying areason) for the merge and spill cases, andTableEditBlockedError/PivotEditBlockedError/DataTableEditBlockedError(each carrying the structure’s name and anoperation) for the three structures. A whole-column or whole-row selection is clamped to the contiguous data region rather than scanning the sheet’s full height.
API changes since v17.0.0-beta.1
Section titled “API changes since v17.0.0-beta.1”PivotManager(the type ofworkbook.pivots) is now exported from the package entry, the last pivot-cluster manager type that was not already named there.- The
Dimensiontype ('row' | 'col', the axis parameter of pivot range-adjustment methods) and theFILL_TYPESlist (the validWorkbook.fill()modes) are now exported, andfunctions.CHOOSE/functions.SWITCHare restored to the publicfunctionsmap. - The error classes
SheetNotFoundError,NameNotFoundError, andWorkbookNotFoundError— long documented in@throwsannotations — are now exported, so hosts caninstanceofthem instead of string-matching;NameNotFoundErrorgains adefinedNamefield. isPivotLookupErrornow narrows correctly as a type guard on a value typedunknown(its true branch narrows toPivotLookupErrorinstead ofnever).
Bug fixes since v17.0.0-beta.1
Section titled “Bug fixes since v17.0.0-beta.1”- Excel’s
@implicit-intersection operator no longer evaluates to#NAME?:=@A1:A3,=B1*@A1:A3, and the like now collapse the range to the value in the intersecting row or column (or#VALUE!when there is none), matching Excel. CHOOSEandSWITCHnow short-circuit likeIF, matching Excel: a reference in an unchosen branch is not evaluated (no false circular-dependency error), and an array selector spills and broadcasts element-wise.CHOOSEnow truncates a fractionalindex_numtoward zero before selecting, so e.g.CHOOSE(3.5, …)returns the third value instead of#VALUE!, matching Excel.TEXTBEFORE/TEXTAFTERnow accept an array or rangedelimiter(matching any of them) instead of returning#VALUE!, counting occurrences non-overlapping exactly as the same scalar delimiter would; and an out-of-rangeinstance_numnow yields#VALUE!rather than#N/A.- A multi-cell range fed to an array-typed (but not range-typed) parameter in a non-array formula now dereferences to a matrix instead of being intersected to one cell, so
=SUM(MMULT(A1:B2,A1:B2))works, matching Excel. Matrix.ofTransposedof an empty matrix now yields an empty matrix instead of a corrupted one (width 1, height 0).- A formula entered by editing that needs array-context evaluation — a bare range in a scalar-wanting argument (e.g.
=TYPE(A1:A3)), or one that evaluates over an array without itself returning one (e.g.INDEX(range, SMALL(IF(…), k))) — is now classified as an array formula (t="array") and evaluated over the whole array, matching Excel, instead of being implicitly intersected. SUMPRODUCTnow returns#VALUE!for mismatched argument dimensions even when the cell counts match (e.g. a 1×2 row against a 2×1 column).SUBTOTALnow skips range cells whose own formula nests aSUBTOTAL/AGGREGATEcall, matching Excel, so subtotals-of-subtotals are not double-counted.FORMULATEXTno longer returns a stale formula after a value write removes the cell’s formula.- A broken cross-sheet reference now keeps its sheet qualifier —
=Sheet1!#REF!rather than=#REF!— when the referenced cell is deleted or pushed off-grid, matching Excel. - Structural edits (insert/delete/move/reorder of rows, columns, and cell blocks) now update references in charts, drawings, model errors, and view selections, which were previously left pointing at stale coordinates.
- Renaming or deleting a sheet now rewrites chart and
ModelErrorreferences (and drops a chart hosted on a deleted sheet), matching Excel. - A table referenced by its implicit defined name (e.g.
=ROWS(People)) no longer stays stale after a structural edit resizes or moves the table. - A table is now kept in sync when its sheet is renamed or removed (previously only pivot tables were), so
Table1[Col]no longer breaks to#REF!and the table’s[[#Data]]name does not linger on a nonexistent sheet. - A sheet-scoped defined name referencing a like-named name on another sheet no longer raises a false
#NAME?circular-dependency error. - Resizing one column or row no longer collapses the following columns or rows that shared its size (and drops their style).
- Resizing a column or row no longer silently changes its own formatting: size runs are merged only when both size and style match, so setting a column’s width or a row’s height to a neighbour’s size keeps its own style instead of adopting the neighbour’s.
Workbook.reorderRows/reorderColumnsno longer silently dissolve a merged range the move would cut through (one whose rows or columns would not all travel together); the reorder is refused before anything moves — throwing the new exportedReorderMergeBlockedError(anEditBlockedError, carrying theaxisand the merge’s A1 range) — matching Excel, which never cuts through part of a merged cell. Merges that travel whole with their rows/columns still do.- Copying a sheet now duplicates its tables and pivot tables (pivots sharing the source cache) and rewrites the copy’s own structured references, matching Excel; previously copied structured references were unresolvable and copied pivot output had no backing
PivotTable. - Copying a sheet then deleting the original no longer drops the copy’s chart.
- Copying a sheet no longer leaves phantom spilled cells that keep a stale value when the spill later shrinks on the copy.
- Cells holding an empty string or
#N/Aare no longer treated as blank when resolving an area, correctingcropTo: 'cells-with-non-blank-values'cropping and spilled-range reads over such cells. Model.metano longer inflates graph and volatile counts (manifested only with more than one workbook).Model.writenow honors theforceRecalcoption on a same-value write, asModel.writeMultiplealready did.- Chained what-if data tables now recompute in dependency order, so a data table that reads another’s output no longer locks in a stale result.
- Typing a value over the single result cell of a one-cell what-if data table now removes the table and keeps the value, matching Excel; previously the table survived, so the next recalculation overwrote the typed value. Clearing the cell already removed the table, and an edit covering only part of a larger result range is still refused.
- A what-if data table that cannot be recomputed (a broken input-cell reference, or an input cell spilled into by an array formula) now shows the error in every result cell instead of only the anchor with the rest blank, matching Excel.
ValueSnapshotcapture and apply no longer leave the reverted model diverging in several cases — dependents of mid-spill cells, an anchor that was scalar at capture but now spills, a defined name deleted between capture and apply, and a global-vs-sheet-scoped name collision.- Reverting a captured
ValueSnapshotno longer fails partway and leaves the model half-restored: when a cell captured as a plain value had since become part of a data table’s result range (or another guarded region),applyTocould throw the write guard mid-restore. Restores now bypass those guards, soapplyTocompletes atomically. Model.off()with no arguments, and removing one of severalmetrics/addsheetlisteners, no longer tear down the workbook-level event forwarders that other listeners still need.- A pivot table’s merged ranges are now recomputed on refresh instead of leaving the file’s saved merges in place, which garbled the refreshed output.
- On refresh, a pivot’s label cells are repainted the way Excel does — centered under
mergeItem, with stale system-white borders dropped from merge anchors — fixing misaligned subtotal and total labels. - Auto-materialized pivot caption cells no longer carry a baked-in font colour from third-party producers, so the header band is no longer broken up under a dark table style.
- A pivot’s calculated-field values are now recomputed from the current source on every refresh, so a data-only refresh no longer leaves an appended record’s value empty or an edited record’s value stale.
- A pivot value or top-N filter measured on a calculated field no longer empties the pivot table.
- A pivot calculated field that references another calculated field now composes the referenced field’s formula over the bucket’s summed source values, matching Excel, instead of summing its per-record values — so
B = A(withA = Qty*Price) reproducesA’s own cell (SUM(Qty)*SUM(Price)) rather thanSUM(Qty*Price). A reference cycle between calculated fields yields#NAME?. - Pivot value cells for degenerate groups now match Excel: an all-blank group renders blank under every aggregation (Average and Product previously
#DIV/0!and0), and a text-only group renders0for Count Numbers/Min/Max and#DIV/0!for StdDev/Var (all previously blank). - Deserializing a model no longer re-runs a pivot’s
refreshOnLoad, keeping stored pivot values through a JSF round-trip. - A native workbook serialized before 17.0 no longer loads with its first user-defined cell style promoted to the workbook default. Older payloads reserved no default slot, so the 17.0 read path — which treats style index 0 as the default — repainted every unstyled cell with that first style;
deserializeModelnow recognises these payloads by their serialization version and reinstates the reserved slot. Workbooks loaded from xlsx or Google Sheets were unaffected. - On a Values-on-column-axis pivot refresh, the first row-label header keeps the field name instead of being overwritten with the literal
Row Labels. - Refreshing a multi-value pivot whose row axis holds only the Values grouping (data-field names down the rows, no other row field) no longer overwrites the value-name labels with numbers; the
Sum of X/Sum of Ylabels stay in their own first column and the data begins one column to the right, matching Excel. - Refreshing a pivot table no longer turns a date-formatted source value outside Excel’s representable date range (a serial before 1899-12-30, or in year 10000 and later) into text; such values stay numeric in the cache, matching Excel, so sorting, grouping, aggregation, and export see a number rather than a string.
- A pivot table’s default row/column item order now sorts error items (such as
#DIV/0!) before blank items instead of interleaving them, matching Excel: ascending is numbers, then text, booleans, errors, and blanks last (descending is the exact reverse). - On refresh, a pivot’s stored
location/@refis rewritten to the materialized output range, matching Excel, so a stale ref no longer understates the pivot. PivotTable.getOutputBounds()now returns the correct rectangle for a row-axis Values layout, which previously mis-sized the exported<location ref>and made the pivot render#SPILL!on refresh.- A multi-value pivot with Values on the row axis and no column field now exports a definition matching Excel (value names nested as row blocks, no reserved value-caption row).
- A tabular outline-above pivot with data fields on the row axis now lays out the way Excel does — a separator row opening each value, data fields stacked beneath, each outer field’s subtotal split below its group.
- An exported pivot with data fields on the row axis and no other row fields no longer produces a file Excel repairs on open.
- An exported pivot with data fields on the column axis and non-compact outline row fields no longer serializes blank-separator and opener rows as
<i t="default">, which Excel repairs on open. - A pivot whose source loses a column referenced by a calculated field no longer exports a file Excel repairs; the dangling reference is rewritten to the
#NAME?literal, as Excel does. - A pivot calculated-field formula combining a double-quoted string literal (containing an apostrophe) with a single-quoted field name is no longer mis-tokenized.
- A pivot’s page-field (report-filter) caption cells are now protected from range and structural edits, consistently with single-cell writes.
- An exported pivot’s styles table now matches Excel’s two-
cellXfform, so unstyled cells no longer inherit the data cells’ explicit-General number format. - Exporting a pivot that re-materializes to a smaller footprint no longer leaves stray style-only cells behind in the vacated region.
- A pivot shrinking its footprint no longer leaves a stale number format on the freed cells.
- Show Values As subtotals now partition by outer group for an inner base field, matching Excel.
- A pivot’s axis-relative Show Values As modes (Difference From, % Of, % Difference From, Running Total, % Running Total, Rank) now transform the subtotals on the axis across from the base field instead of leaving them raw — e.g. with Region on rows, a Category-with-subtotals › Product column axis, and Difference From East, the Category Total column shows
30 − 4 = 26rather than the raw West sum, matching Excel. - The axis-relative Show Values As modes based on an outer row or column field now transform the subtotals nested inside that base field instead of leaving them raw, matching Excel.
- The axis-relative Show Values As modes now also transform the intersection cells where a row subtotal meets a column subtotal (both axes subtotaled), each like an ordinary cell of its block, instead of leaving them raw, matching Excel.
- A Show Values As Difference From / % Of / % Difference From whose fixed base item is then hidden on the base field now renders
#N/Aacross that whole field, matching Excel, instead of comparing against the wrong visible item or falling back to raw aggregates. - A pivot value shown as % Of a
(previous)/(next)base item on an outer axis now renders the no-neighbour boundary item’s subtotal cell as 100% (matching its detail cells and Excel) instead of blank; Difference From / % Difference From stay blank there, as before. - % of Parent Row Total / % of Parent Column Total on a single-level row (resp. column) axis now divide each cell by its column (resp. row) grand total, so the columns (resp. rows) sum to 100%, matching Excel; previously the two axes were swapped, so % of Parent Row Total behaved like % of Row Total.
- % of Parent Row Total / % of Parent Column Total on a multi-level axis now show 100% in every column (resp. row) of the grand-total line instead of a wrong fraction, matching Excel — that line is its own parent, so each cell is its own value over itself.
- % of Parent Total based on a field whose own subtotals are turned off now divides each inner subtotal and leaf by the base group’s true total, matching Excel: a non-SUM measure such as Average or Max divides by the real group aggregate rather than a sum of the inner values, and a base group containing an error is left blank (Excel propagates the error through the group total).
- A pivot’s parent-relative Show Values As modes (% of Parent Row Total, % of Parent Column Total, % of Parent Total) now transform the subtotals on the axis across from the base field, and the row-by-column subtotal intersections, instead of leaving them raw, matching Excel.
- The x14
hideValuesRowpivot extension now round-trips through load and export. - A declared pivot style’s subtotal shading and bold now land on the actual subtotal rows in data-on-rows and mid-position-Values layouts instead of on the data rows (
PivotTable.rowRoles/colRolespreviously reported the wrong rows). - Two pivot tables sharing one cache keep sharing it through a JSON round-trip of the exported JSF; previously the shared cache split into independent copies, so refreshing one no longer refreshed its siblings.
- A pivot refresh now detects overlap with another pivot table’s cell-less regions (report-filter and empty rows), and when it cannot complete it refuses atomically instead of leaving the model half-updated.
Workbook.moveCellsnow relocates a whole pivot table when the source range fully encompasses it, matching Excel’s cut/paste of an entire pivot table: the output moves to the destination (same sheet or another), stays live and refreshable, and the cache source is left untouched. Previously any move overlapping a pivot’s output was refused; a move covering only part of a pivot table, or landing on another, still is.- A pivot caption/label filter now matches the item’s number-formatted label instead of the raw value, and is a no-op on date fields (which Excel filters by date, not by label).
GETPIVOTDATAnow accepts a criterion naming a report-filter (page) field’s currently selected item, treating it as a no-op qualifier and returning the value instead of#REF!. Naming a non-selected item, or a page field showing(All), still returns#REF!— matching Excel, which never reaches through the page filter to a slice the pivot is not displaying.- A pivot table loaded with duplicate field-item references (e.g. from LibreOffice) no longer re-exports a file Excel repairs on open; the duplicate references are dropped on load, as Excel’s own repair does.
- An external sheet that is listed but never cached now keeps its
noSheetDatamarker through the JSF round-trip, matching Excel’s shape. alternateUrlsandpathMissingon external references are now preserved throughWorkbook.toJSF(), so a round-tripped file stays re-resolvable by Excel.Workbook.insertCells(range, shift)no longer breaks a referenced range to#REF!when the insert pushes it against the last row or column; the range’s leading edge advances and its trailing edge clips at the sheet edge, matching Excel.Workbook.insertRows/insertColumnsno longer silently destroy non-empty cells pushed past the bottom or right edge of the sheet; they refuse the edit (as Excel does) and, for an edge-touching reference, clip it instead of breaking it to#REF!— matching the boundedinsertCellspath, including whole-row/whole-column references.- Full-axis
Workbook.deleteRows/deleteColumnsno longer corrupt whole-row/whole-column references past the deleted band:=SUM(E:E)after deleting columns B:C now becomes=SUM(C:C)instead of=SUM(B:C). - A bounded
insertCells/deleteCellsoverlapping a what-if data table is now refused (like the full-row/column guards) instead of silently corrupting the table, and its references are adjusted when the whole block shifts. - A what-if data table whose input cell lives on another sheet no longer computes wrong results after rows are inserted on that sheet or the sheet is renamed.
- A what-if data table that reads another table’s output through an intermediate helper cell no longer shows stale results that never self-correct.
- Moving the rows or columns that hold a pivot table’s whole source range no longer permanently breaks the pivot table’s refresh; the cache’s source reference follows the moved block instead of being silently cleared.
- The
@implicit-intersection operator now works in native GRID-mode workbooks instead of evaluating to#NAME?(it already worked in Excel- and Google-mode workbooks). - Entering a formula that references circular defined names (e.g.
foo==bar,bar==foo) no longer crashes with a stack overflow; the cell evaluates to the usual circular-reference#NAME?error.
Dependency upgrades since v17.0.0-beta.1
Section titled “Dependency upgrades since v17.0.0-beta.1”- Upgrade
@borgar/xlsx-convertto^5.6.1: cells carrying only formatting (a style but no value) are now preserved on load, so they survive a load/save round-trip. - Upgrade
@grid-is/waspiaryto^1.17.0, bringing reference-rewrite fixes (the cross-sheet#REF!qualifier, and whole-row/whole-column reference handling on insert and full-axis delete) and parser support for the structured-reference@this-row forms. - Upgrade
@grid-is/jsf2xlsxto^2.3.3, correcting the exported pivot styles table to Excel’s two-cellXfform, and fixing the round-trip of single-argumentIFNAandIFERROR. - Upgrade
@jsfkit/typesto^2.5.0for the external-reference field declarations the externals round-trip fixes depend on.
v17.0.0-beta.1 (2026-06-29)
Section titled “v17.0.0-beta.1 (2026-06-29)”The second 17.0 beta, with further pivot-table fixes following beta.0. Still subject to further breaking changes before the stable release.
Features
Section titled “Features”Workbook.deleteCells(range, shift)andWorkbook.insertCells(range, shift)implement Excel’s “Delete cells → shift up/left” and “Insert cells → shift down/right” for an arbitrary bounded rectangle (e.g.B3:C5), the bounded-rectangle counterparts to the existing full-row/columndeleteRows/deleteColumns/insertRows/insertColumns. Surviving cells slide in (delete) or displaced cells push out (insert) to fill or open the gap; this workbook’s own formulas are rewritten in place, with references to removed or pushed-off cells becoming#REF!, and aRewriteFormulacallback is returned to adjust other workbooks’ references. Overlapping tables, pivot tables, and merges move whole, grow, or dissolve per Excel’s rules; an edit that would tear a table or pivot table, or push non-empty content off the grid, is refused.- New
PivotTable.pageAreareports the geometry of the page-field (report filter) area that sits above a pivot’s output range, so a renderer can locate and style those cells. - New
PivotTable.rowRoles/PivotTable.colRoleslist which output rows and columns are subheadings or subtotals (as 0-based offsets from the output’s top-left, each with its kind and 1-based nesting level), so a renderer can apply a declared pivot style’s subheading and subtotal styling.
Bug fixes
Section titled “Bug fixes”- What-if data tables now evaluate each scenario with a full model recalculation (substitute the inputs, recalculate, read the source cell), matching Excel. Previously only the source formula’s own AST was re-evaluated, giving wrong results whenever it reached the input indirectly — through other formula cells,
INDIRECT/OFFSET, defined names, cross-sheet references, volatiles, spills, or iterative cycles. - A custom workbook theme now survives model serialization;
deserializeModelpreviously dropped it and reverted every workbook to the default palette. Already-serialized models recover their theme on load, since the data was present all along and only the read path was at fault. - A pivot table’s saved output cells are now kept as-is on load instead of being recomputed, matching Excel — computed output fills only the cells the file left empty. A cache flagged
refreshOnLoad="1"is instead rebuilt from its worksheet source and re-rendered once at load time. - Refreshing a pivot cache that holds a derived field (a calculated field or a derived grouped field) no longer throws when the source columns are reordered, renamed, added, or deleted. The derived fields are kept and reconciled against the new column set, matching Excel; previously the presence of any derived field, even one unrelated to the change, aborted the refresh.
- The single-data-field pivot header (“corner”) cell now matches modern Excel: the data field’s display name (e.g.
Sum of Sales) in every layout, the bare field name only on pivots carrying a legacy AutoFormat, and an empty cell when the data field has no name. Previously it wrote the bare cache-field name unconditionally. - “Show items with no data” (
showAll) densification on load now matches Excel: dense by default for ordinary discrete fields, listing only items that survive the pivot’s active filters (a caption filter is evaluated against the field’s full item set), and including orphan cache items as empty “no data” rows. - Pivots mixing compact and tabular row-field layouts now match Excel: an outer-compact mix collapses to one compact “Row Labels” column, an outer-tabular mix captions its first column “Row Labels”, and a tabular outer field writes its group label on the group’s first output row. Field-name row captions appear only on row-only pivots whose row fields are all tabular with
compactDataoff. - A pivot that places the Values pseudo-field at a mid position among its column fields now lays the data fields out as interleaved sub-blocks at that depth, with the field-button row in raw field order, the way Excel does. Previously any non-first Values position was treated as innermost.
- A Values-last multi-data pivot now serializes its
colItemswith the data field varying fastest (df-innermost interleaving), matching Excel. Previously even a dormant load→save rewrote them into a df-major order Excel does not use. - A Values-outer pivot’s column axis-label row no longer drops the
Valuesbutton: it now readsValuesahead of the real field buttons, matching Excel. - A multi-data pivot whose Values pseudo-field sits on the row axis now lays the data fields out as row blocks — a synthetic “Values” row-label column, with per-data-field subtotal and grand-total captions — matching Excel. Previously the row-axis Values position was ignored and the data fields were always emitted on the column axis.
- A multi-data pivot with no column field now omits the synthetic “Values” caption row — the data-field names form the header row — matching Excel, instead of emitting an extra caption row that pushed the body down.
- On refresh, a pivot’s header-row offsets (
firstHeaderRow/firstDataRow) are re-derived from its definition so the output lands where Excel’s refresh would, regardless of the stored offsets: an over-reserved blank header row is removed, and a multi-data pivot’s missing reserved Values header row is rebuilt (instead of collapsing to a single data field and inventinghideValuesRow="1"on export). - Pivot column widths are sized to the formatted value text (currency, percent, and date number formats) instead of the raw number’s digits, so formatted value columns are no longer too narrow.
- Date fields on a pivot’s row or column axis now keep their number format on refresh, so header cells show formatted dates instead of raw serials. Previously only data, subtotal, and grand-total cells kept the format through re-materialization.
- Pivots with report filters now emit their page-field cells — a caption/selected-item cell pair per page field, in Excel’s single-column stack or wrapped multi-column grid — in the rows above the output range. Previously the report-filter area was omitted entirely.
- A pivot page-field (report filter) selection cell now carries the field’s own number format, so a date filter shows a formatted date instead of a raw serial.
- Pivot structural cell styles now match Excel: the column-caption dropdown-button flag is stamped on the axis-label row and only when the pivot has a real column field, and row-label left alignment is written only when the row axis has at least one compact field (not on fully-tabular layouts).
- Classic (
gridDropZones) pivots now show field-name captions on their row and column axes instead of the modern literalRow Labels/Column Labels, matching Excel. - Pivot calculated-field formulas now evaluate through the engine’s normal formula path, removing divergences from Excel’s coercion semantics that the old per-record closure tree had drifted into (e.g. unary-plus coercion on text, and empty-string arithmetic).
- Exported pivots now stamp
showAll="0"on axis fields that were materialized sparse (“show items with no data” off). Excel reads an absentshowAllas dense, so refreshing a previously-exported pivot re-densified the layout to the full item set and overflowed the saved footprint. - A fully non-compact (tabular/outline) pivot now writes the table-level
compact="0" compactData="0"attributes on export. Excel derives the row-label caption from these, so without them an Apiary-saved tabular pivot came back retitled (field names →Row Labels) on its first refresh. - A grouped (date/range/discrete) pivot field now exports its full item universe in
<items>(backfilled from the field group’s items), not just the materialized subset. Excel rejects a grouped field whose items list omits any bucket. - A pivot field carrying an explicit
defaultSubtotal: falsewithoutsubtotalFunctionsno longer gets a contradictory trailing default-subtotal item on export — a combination Excel offers to “repair” once the field also carriesshowAll="0". - Exporting a page-fields-only single-data pivot no longer produces a file Excel offers to repair: its
<location>geometry (anchor, header/data offsets, and the blank separator below the page area) now matches what Excel writes. - Exported pivots now compress members repeated from the previous item of
<rowItems>/<colItems>into ther(“repeated item count”) attribute, the way Excel writes them. Without it, Excel drew phantom expand/collapse buttons on the blank continuation label cells of saved multi-row-field pivots until the table was refreshed. Workbook.deleteRows/deleteColumnsnow match Excel in guarding a pivot table’s output range. A deletion that partially overlaps a pivot table is refused (throwsPivotEditBlocked, like the analogousinsertRows/insertColumnsguard); a deletion that fully encompasses a pivot table removes it (and its cache if not also referenced by another pivot table).
Performance
Section titled “Performance”- Data tables now recompute only when something they depend on changed (source formula, parameters, input cells, or any cell those transitively read), and at most once per recalculation, instead of on every recalculation. This holds even for a dynamic source reference (
INDIRECT/OFFSET), whose per-scenario resolved targets are recorded as the table’s dependencies — except inside an iterative-calculation cycle, where those targets cannot be tracked and the table still recomputes every time, conservatively. - Per-call function and operator timing now reads
performance.now()only when ametricsCallbackis set, removing a small per-call cost from the common no-metrics path (including hot inner loops). Behaviour is unchanged when metrics are enabled.
dist/now ships code-split output alongside theindex.{js,cjs,d.ts,d.cts}entry bundles. The public entry points (exports,main,module,types) and what you get fromimport '@grid-is/apiary'are unchanged; only consumers reaching pastexportsintodist/are affected, since chunk filenames are now content-hashed.
v17.0.0-beta.0 (2026-06-11)
Section titled “v17.0.0-beta.0 (2026-06-11)”First beta of the 17.0 major line. Pivot tables — the headline 17.0 feature — land here, in beta: some known kinks are still being ironed out. Subject to further breaking changes before the stable release.
Breaking changes
Section titled “Breaking changes”(Note, these are breaking change since v17.0.0-alpha.0; note also the breaking changes reported for that version.)
Operations on Workbook may now throw new errors when they conflict with pivot-table output:
PivotEditBlocked— thrown byinsertRows/insertColumnswhen the insert lands strictly inside a pivot’s output area, bymoveRows/moveColumnswhen the move’s source or destination crosses a pivot’s boundary, and bymoveCells/clearCellswhen the affected range overlaps pivot output. Excel’s GUI refuses the same operations (usually silently); Apiary throws so the host can surface the refusal.PivotCellWriteBlocked— thrown byWorkbook.editCell(and thewritewrappers around it) when the target cell falls inside a pivot’s output region.PivotOutputOverlapsData— thrown by a pivot cache refresh when the refreshed pivot’s output would grow onto populated cells previously outside it.
StyleManager now reserves styles index 0 as the workbook default style (XLSX cellXf 0). A workbook built from scratch seeds slot 0 with the empty style {}, so workbook.styles.length starts at 1 and the first style created via findOrCreate gets index 1 (previously 0). Exported JSF for styled built workbooks carries the {} default at styles[0]; unstyled built workbooks still export with no styles array. Consumers should resolve styles through cell s indices rather than assuming the first created style sits at index 0.
Features
Section titled “Features”-
Pivot table support (beta — some known kinks are still being ironed out):
- Evaluation matches Excel: all OOXML aggregations and
showDataAsmodes, calculated fields, grouped fields, label/value/top-N filters, custom-list andautoSortScopesorting. - The
GETPIVOTDATAspreadsheet function is now supported. PivotManager.refreshCacherefreshes a pivot cache from its source; structural edits adjust pivot output and cache source ranges; pivots round-trip through JSF and XLSX.- Look up pivots via
workbook.pivots.get(name, sheetName?)andworkbook.pivots.findAt(sheetName, row, col). - Visual pivot styling is not baked into cells: as in Excel, only the declared pivot style name is stored, and resolving it to an appearance is the renderer’s job.
- Evaluation matches Excel: all OOXML aggregations and
-
Formula parsing accepts more of Excel’s grammar (via
@grid-is/waspiary1.13.0): the@implicit-intersection prefix operator and the structured-reference@this-row shorthand, reference operators and calls after parenthesised references ((A1):B2,(A1)#,(A1)(B2), …), and workbook/sheet prefixes on structured references ([0]Sheet1!Table1[Col]). Calling a parenthesised non-lambda reference, e.g.(A1)(B2), evaluates to#REF!as in Excel.
API changes
Section titled “API changes”- New
StyleManager.defaultStylegetter andsetDefaultStyle(style)method read and replace the workbook default style (slot 0). The default cannot carryextendsStyle. - New
recalcAll?: booleanonAddWorkbookOptionsforces (or, whenfalse, suppresses) a full recalculation when the workbook is attached. The default honours the file’sfullCalcOnLoadsignal (see Bug fixes).
Bug fixes
Section titled “Bug fixes”<calcPr fullCalcOnLoad="1"/>(ECMA-376 §18.2.2) is now honoured: attaching a workbook that carries the flag (and is not in manual calculation mode) performs a full recalculation instead of trusting cached values as-is, matching Excel. Previously the flag was ignored, which could leave formula cells without a usable value when the writer emitted empty cached values. The flag is cleared after a successful full recalculation, as the spec requires.ValueSnapshot.applyTono longer leaves a stale cell node in the R-tree when it rebuilds a spill anchor that had been collapsed to a value cell beforehand; previously a later value write over the restored anchor tripped an internal invariant.- A formula written via
editCellto a blank-but-formatted cell that a pre-existing formula depends on now evaluates. Previously its dependency-graph vertex kept pointing at a throwaway cell object created at load, so reads returnednulland recalculation recorded an “Invariant violation” model error, with no recovery even via a full recalc.
v17.0.0-alpha.0 (2026-05-30)
Section titled “v17.0.0-alpha.0 (2026-05-30)”First alpha of the 17.0 major line. Subject to further breaking changes before the stable release; notably, pivot-table support is still to come.
Breaking changes
Section titled “Breaking changes”Structural edits and writes that would change part of a what-if data table now throw DataTableEditBlocked (exported from the package index) instead of silently corrupting the table. Excel refuses each of these edits in its UI (via a modal alert or a silent no-op); Apiary surfaces the refusal so the host application can decide how to react. The refused edits, matching Excel’s empirically verified rules, are:
- row/column inserts strictly inside a data table’s block (the range spanning its source cell, parameter cells, and result range);
- row/column deletes overlapping the block without covering the whole result range (covering it removes the table, like Excel);
moveCells/moveRows/moveColumnstaking part of the block (a move must contain all of it --- the whole table then moves) or landing on part of the result range (landing on all of it removes the table);- writes (
editCelland thewritewrappers) targeting a result cell, andclearCellscovering part but not all of the result range.
The error carries dataTableRef (the A1 range of the table’s result cells) and operation (a stable DataTableBlockedOperation identifier of the refused edit kind, e.g. 'insert-rows' or 'move-cells-onto') for programmatic handling, e.g. a localized host-application message.
The source and parameter cells remain freely editable, and the table recomputes from the new values. Allowed structural edits now also maintain the table’s metadata --- previously any insert/delete/move silently desynchronized it.
Transient writes and “reset” are gone, replaced by value snapshots (see below). Model.write, Model.writeMultiple, and Workbook.write still exist but are now thin wrappers around Workbook.editCell. They are persistent edits, not transient overlays. Notable differences in behaviour:
-
One state, no
reset. Each write mutates the workbook in place; there is no separate transient state to enumerate or roll back. To undo a value edit later, capture aValueSnapshotbeforehand and apply it to revert. -
No formula neutralization option. Writing a value to a formula cell now unconditionally replaces the formula with that value. The
neutralizeFormulaOnSingleCellWriteflag is removed. -
No range writes.
Model.write/Workbook.writeno longer accept a multi-cell range reference. To assign values to many cells, useModel.writeMultiple([[refA, val], [refB, val], …]); for a real spill, useWorkbook.editCellwith an array formula. -
No writes inside merges.
Model.writeetc. now wrapWorkbook.editCell, which throws when the target is a non-anchor cell of a merge range. Write to the merge anchor instead, or unmerge first. -
No writes to defined names.
Model.write/Model.writeMultiple/Workbook.writeacceptstring | A1Referenceand throw on a defined-name reference (including a string that parses to one):- The “named values” feature (writing a literal value as a defined name’s formula) is gone.
Workbook.editCellon a name reference now requires an explicitfformula - To write through a chained defined-name reference, resolve first:
const ref = Reference.from(name, { ctx: model }).resolveToNonName()and thenmodel.write(ref, v) - Existing workbooks with literal-formula defined names still load and evaluate read-only
- The “named values” feature (writing a literal value as a defined name’s formula) is gone.
The following public APIs are removed:
Model.reset,Workbook.reset,WorkSheet.reset.- The
'reset'model event. Model.writes,Workbook.writes,Model.writeState,Workbook.writeState, theModelStateTreetype, and theGRID.SCENARIO()spreadsheet function. No replacement: these all reported transient write state, which no longer exists.=GRID.SCENARIO()now resolves to#NAME?.- The
neutralizeFormulaOnSingleCellWriteandresetproperties ofWriteOptions. The surviving properties areforceRecalc,skipRecalc, andskipVolatiles. Cell._vandCell.resetValueBoxed(the per-cell reset value).
Serialization: the _rangeWrites and _spillResetState fields and the per-cell reset value are no longer written. v17.x still loads models serialized by older Apiary versions; the dropped fields ride along in the payload and are silently ignored. Older Apiary versions cannot load v17.x serializations.
The RecalcUreachedEvent type is renamed to RecalcUnreachedEvent (the old name was a typo). The event payload is unchanged.
Features
Section titled “Features”- Value snapshots:
ValueSnapshot.capture(model)returns a reusable snapshot of cell and defined-name values (boxed, so number formats are preserved) and spill matrices across every workbook; formulas are captured as fingerprints only.snapshot.applyTo(model, { skipRecalc?, includeDeferred? })restores values and spills, then triggers a recalc. Any cell or name whose formula changed (added, edited, or removed) since capture is skipped, and one WARNING-levelModelErrorlisting every affected cell/name is recorded per workbook. The check is per-position and best-effort: a structural edit that rearranges cells may produce a partially-warned, partially-corrupted revert.- Styles and structural edits (row/column inserts/deletes, sheet adds/removes/renames,
moveCells) are out of scope and invalidate the snapshot; applying after such edits may leave the model in an incorrect state with no per-cell WARNING. For guaranteed-correct reverts across formula or structural edits, serialise the model (binary, JSF, or XLSX) and restore from that.
API changes
Section titled “API changes”Workbook.editCellis the canonical cell-mutation entry point: it keeps recalc bookkeeping in sync automatically.Model.write,Model.writeMultiple, andWorkbook.writeare now thin wrappers around it.Cell.userZis now marked@internaland excluded from the public.d.tstype. To read the effective number format useCell.z, orCell.stylefor the fullStyleRelaxed(which includes the user-assigned format undernumberFormat). To set a number format useWorkbook.editCell({ z })orcell.style = { ... }. No runtime change.- The
RewriteFormulacallback returned by structural-edit methods (deleteRows/deleteColumns/insertRows/insertColumns/moveRows/moveColumns/moveCells) gains two context parameters:(formula, formulaSheet, formulaWorkbook) => string. Callers that apply the returned rewriter to formulas in another sheet or workbook must now pass that context. Previously the rewriter assumed every unprefixed reference lived in the mutation’s source sheet/workbook, so it corrupted formulas applied elsewhere;moveCellsin particular returned a rewriter that was a silent no-op across workbooks.
Bug fixes
Section titled “Bug fixes”- XLSX export font fix (via
@grid-is/jsf2xlsx2.2.0): the cell style cascade did not resolve fonts correctly, so cells could export with the wrong typeface.
Packaging
Section titled “Packaging”- Bumped Excel import/export dependencies:
@grid-is/jsf2xlsx^2.1.0→^2.2.0,@borgar/xlsx-convert^5.2.0→^5.3.0, and@jsfkit/types^2.2.0→^2.4.0.
v16.1.0
Section titled “v16.1.0”Features
Section titled “Features”Workbook.calcModeis now a public getter/setter. Reading it returns the workbook’s current calculation mode ('auto','autoNoTable', or'manual'). Setting it to'autoNoTable'also enables data-table deferral (equivalent toModel.deferDataTables = true).
Bug fixes
Section titled “Bug fixes”deleteRowsanddeleteColumnsno longer rewrite formula references on sheets other than the one being modified. Previously, unqualified references on every sheet were treated as if they lived on the target sheet.- Loading JSF containing external workbooks whose defined names carry no value no longer crashes with “Cannot set defined name with invalid name or empty formula”.
Model.writeMultiplenow correctly honours theskipVolatilesoption. Previously the option was silently ignored, causing volatile functions such asRAND()andNOW()to be recalculated even when the caller asked to skip them.
v16.0.2
Section titled “v16.0.2”API changes
Section titled “API changes”Cell.editandDefinedName.editare now@deprecatedaliases that forward to internal_editmethods. Consumers should useWorkbook.editCellinstead —editCellkeeps the dependency graph and recalc bookkeeping in sync, which the raweditpaths do not. No runtime behaviour change.
Bug fixes
Section titled “Bug fixes”Workbook.editCell({ v: '#N/A' })(andCell.edit) no longer coerces error-looking strings to error values —vis preserved as a string, matching theCellconstructor. Coercion still happens when the input includest: 'e'.Workbook.editCellno longer clears existingModelErrors on a cell before its per-branch validation guards run, so a formula edit rejected by the merge-follower guard now leaves prior errors intact.toJSFtyping fix:External.namesentries are typed asExternalDefinedName(wasJSFDefinedName, structurally compatible but wrong). Type-only fix, no behaviour change.
Performance
Section titled “Performance”Model.fromJSFexternal-load loop is no longer O(externals × main-cells)._addExternalsdefers per-externalrecalculate()andoptimize()to a single trailing pass; on a real 125-external, 18k-cell workbook,Model.fromJSFdrops from ~16s to ~2s. Callers passingrecalcVolatiles: false, recalcErrors: false(the round-trip harnesses) still see no recalc.
Packaging
Section titled “Packaging”numfmtmoved frompeerDependenciestodependencies(range unchanged,~3.2.2). Apiary usesnumfmtinternally and consumers never pass instances in or out of the API; the peer-dependency placement was incidental, and demoting it meansnpm install @grid-is/apiaryworks without consumers having to add a matchingnumfmtentry.@types/nodemoved fromdependenciesto optionalpeerDependencies(range:>=20.19.23 <21 || >=22.19.0 <23 || >=24.10.0). The publisheddist/index.d.tsstill references Node’sBuffer, so a TypeScript Node consumer needs@types/noderesolvable — but the consumer’s own pin now wins. Pure-JS consumers see no missing-peer warning thanks tooptional: true. The per-major minimum reflects the late-2025 DefinitelyTyped round that narrowedBuffer.bufferfromArrayBufferLiketoArrayBuffer; below that, our publicWorkbook.fromXLSX/Model.fromXlsxtypes are unsound.@borgar/xlsx-convertbumped to 5.2.0, picking up an XLSX color/theme parsing fix that surfaced throughModel.fromXlsxin an add-ins consumer (undefined is not an object (evaluating 'readColor(elm.children[0], …)')).
v16.0.1
Section titled “v16.0.1”API changes
Section titled “API changes”- Removed the
NamedStyleandStylere-exports from the package entry point. These were unintentionally exposed during the 16.0 cycle and were never meant to be part of Apiary’s public API; import them from@jsfkit/typesdirectly. Type-only change with no runtime impact.
Bug fixes
Section titled “Bug fixes”Modelnow emitsaddsheetwhen a sheet is added (previously dropped silently), and no longer emits a spuriousrecalcfor that operation.- Three independent deserialization-fidelity bugs in the binary cache, jointly affecting
autoNoTable-mode roundtrips, are fixed:Workbook._calcModeis restored from the payload (was silently reverting to'auto').Cells._dataTableCells(the data-table-anchor index) is repopulated, soiterDataTableCells()works on deserialized workbooks — unblocking theModel.deferDataTables = truesetter andModel.attachWorkbook’sautoNoTablehandling.Model._deferDataTablesand the deferral cell Sets (cellsToDefer,staleDeferredCells) are now serialized and restored, so a model in deferral mode no longer loses it through a roundtrip.
toJSFcompacts full-row/full-column spill ranges incell.F(A3:XFD3→3:3,B1:B1048576→B:B), eliminating spurious roundtrip diffs when a code path holds an expandedF.- Error subclasses now set their own
namefield, so stack traces,console.log,inspect(), and Sentry display the actual class name (e.g.FormulaSyntaxError) instead of the inheritedError. @jsfkit/typesmoved fromdevDependenciestodependenciesso it is installed transitively when consumersnpm install @grid-is/apiary. The publisheddist/index.d.{ts,cts}references@jsfkit/typesdirectly because those types appear in Apiary’s public API; consumers’ type-checking previously failed unless they happened to depend on@jsfkit/typesfor other reasons.typescriptmoved fromdependenciestodevDependencies. It is only used here as a CLI build tool (tsc --noEmit,tsup); nothing inlib/imports it and the published types do not reference it. There is no public-API reason for consumers to receive Apiary’s pinnedtypescriptversion transitively. Originally added todependenciesduring the apiary monorepo extraction without a deliberate placement choice.
Serialization
Section titled “Serialization”CURRENT_SERIALIZATION_VERSIONbumped 5 → 6 to carry the previously-omittedModel._deferDataTablesboolean and the two deferral cell Sets.MINIMUM_ACCEPTABLE_SERIALIZATION_VERSIONis unchanged — older payloads still load, with the new fields defaulted to off / empty.
Documentation
Section titled “Documentation”- JSF types referenced from the generated API reference (
NamedStyle,Note,Style,Theme,ThemeColorScheme,ThemeCustomColor,ThemeFontCollection,ThreadedComment) now link tojsfkit.github.ioinstead of being mirrored as local Apiary type-alias pages. EvaluateExpressionOptions.cropTois now documented.- Cleaned up a confusing JSF-normalizer docstring.
v16.0.0 (2026-04-30)
Section titled “v16.0.0 (2026-04-30)”Breaking changes
Section titled “Breaking changes”Workbook.viewsis now aViewManager(wasWorkbookView[] | undefined).Workbook.getViews()andWorkSheet.getViews()are removed;WorkSheet.viewsis now@internal. Per-sheet view state is now read/written viaworkbook.views.get(index).getSheetView(sheetName).Workbook.themeis now aThemeManager(always present), notTheme | undefined.WorkSheet.showGridLinesproperty removed; gridline visibility now lives onWorkSheet.views[].showGridLines(per-view).@jsfkit/types1.x → 2.x: colours are now structuredColorobjects instead of hex strings, themes are restructured, and several types are reshaped.Workbook.rowHeight(rowIndex, sheetName)now throwsSheetNotFoundErrorwhen the sheet does not exist (previously returned the default16silently), and consultssheet.defaults.rowHeightbefore falling back to the Excel default. The newWorkbook.columnWidth()follows the same precedence on its axis.WorkSheet.rowHeightsandWorkSheet.colWidthsfields removed. Per-row height and per-column width are now read fromWorkSheet.rowsandWorkSheet.columns(theGridSize[]span arrays).
Removed @deprecated symbols (each deprecation note already pointed at the replacement shown here):
Cell.hasValue()/DefinedName.hasValue()andCell.isBlank()/DefinedName.isBlank()removed. UsehasValueOrFormula().Workbook.getTables()andWorkbook.getTable(name)removed. Useworkbook.tables.getAll()andworkbook.tables.get(name).WorkSheet.getComments()andWorkSheet.getNotes()removed. Useworkbook.comments.get(sheetName)andworkbook.notes.get(sheetName).Model.fromDataremoved. UseModel.fromCsf.Workbook.lazyImportPromise,Model.lazyImportPromise,loadLazy(), and theloadLazyre-export removed. All four were already no-ops; just delete the calls.- A batch of
@deprecated @internalre-exports removed from the package entry point: formula-evaluation helpers (run,errorForCode,errorTable, …) and dependency graph helpers (cellToVertexId,wbNameToKey, …). Usecell.vertexId/definedName.vertexIdinstead ofcellToVertexId(cell).
(Several dependency-graph items previously marked @deprecated @internal (or not exported at all) are now part of the public API: VertexId, CellVertexId, NameVertexId, RangeVertexId, KnownVertexId, VertexIdSet. The helpers referenceToVertexId, vertexIdToCell, and vertexIdToReference are re-exported with @internal markers — available, but excluded from documentation.)
Features
Section titled “Features”- Views CRUD via
Workbook.views, with two new public typesViewManagerandWorkbookView. - Theme CRUD via
Workbook.theme(newThemeManager): read/write the theme as a whole, the 12-colour scheme, major/minor fonts, and custom colours. - Named-style CRUD via
styleManager.named(newNamedStylescollection), with case-insensitive lookup and original casing preserved onNamedStyle.name. Workbook.reorderSheet(name, newIndex)andWorkbook.copySheet(name, newName?).Tableis now a public type export with new methods (appendRow,insertRow,rename,rowRef) and a newdataRowCountgetter.TableManageris also exported as a type. (The runtime classes are not re-exported: instances come fromworkbook.tables.get(name)/workbook.tables.add().)- Public-API types now exported from the package entry point (previously reachable only via
Parameters<typeof model.write>[2]-style tricks):WriteOptions,AddWorkbookOptions,WorkbookOptions,ReplaceTableReferencesFn,ModelEventArgs,ModelEventType,ModelEventListener,EvaluateExpressionOptions,ModelMeta. NotesManagerandCommentsManagerare now exported so they show up in the generated public docs.Workbook.toXLSX(outputType?, options?)gains both arguments.outputTypeis'nodebuffer'(default) or'arraybuffer'for browser-compatible XLSX export.options(typed asToXLSXOptions) carries acompressionLevelsetting (DEFLATE 0–9), now defaulting to6, down from9— measurably faster export with negligible size impact on typical XLSX payloads.Workbook.columnWidth(columnIndex, sheetName)— analogous toWorkbook.rowHeight(). Returns the column width in pixels.Workbook.moveCellsandWorkbook.rewriteFormulasaccept an optionalrecalcNow(defaulttrue); passfalseto defer the trailingrecalculate().
API changes
Section titled “API changes”Workbook.editCell()added as the preferred name forwriteCellData()(which stays as a@deprecatedalias). Its parameter type simplifies to plainCellData, which now includes the optionalftfield.- Identifier casing standardized on uppercase
JSF/XLSX:Model.fromJSF,toJSF,csfToJSF,normalizeJSFForComparison,toXLSX,toXLSXFile,validateXLSXFilePath, plus typeXLSXOutputType. The old title-case spellings remain as@deprecatedaliases with identical signatures — no breaking change.
Bug fixes
Section titled “Bug fixes”moveCellsnow handles merged cells. Destination merges are dissolved silently (matching Excel Scripts and Office.js behavior), and cells moved out of a source merge no longer carry stale.Mmerge pointers.- Loading a workbook with a table whose name is not valid as a defined name (e.g.
T1,A1,R1C1) now sanitizes and uniquifies the name instead of dropping the table with aModelError. - Iterative calculation now converges correctly when the iterative set contains a spill; previously the calc ran to
maxIterationsevery time and dependents of unchanged spills could be flagged as false-positive circular references. getEntitiesno longer crashes on workbooks containing sheet-scoped defined names.SORTNsilently ignores out-of-bounds sort-column indices instead of returning#VALUE!, matching verified Google Sheets behavior.toJSFpreserves values in merged-cell “ghosts” (non-anchor cells);mergeCellskeeps styles on follower cells and only clears values, matching Excel.- Deserializing models serialized by older Apiary versions no longer crashes on missing
views/comments/notes/people/externals/rowsproperties or on thestable.csf->stable.jsfTable key rename. replaceTableReferencesnow correctly rewrites bare-name references to tables (not just bracketed structured references).- Dynamic dependencies of LAMBDA closures passed to
BYROW/BYCOL/MAP/REDUCE/SCANwithINDIRECT/OFFSET/ANCHORARRAYin the body are now tracked, so edits to referenced cells correctly trigger recalc. - Lambda parameter resolution during reference analysis no longer crashes on
INDIRECT/OFFSETinside directly-called lambdas or nested lambda scopes. toJSFno longer crashes on full-row or full-column spill ranges.rewriteFormulasskips unparseable formulas instead of throwing.- Loading CSF workbooks: the literal string
"#SPILL!"in a cell is no longer coerced to an error value, fixing an R-tree invariant violation that crasheddescribeWorkbookin some cases. - Cells in JSF with error-looking string values (
"#VALUE!","#N/A", …) but no explicitt: 'e'marker are preserved as strings, not coerced to errors.
Performance
Section titled “Performance”- Faster label detection in
describeWorkbookvia cheaper subordinate-style checks (weighted geomean ~23.5% faster, worst-case workbooks up to 43% faster).
Serialization
Section titled “Serialization”CURRENT_SERIALIZATION_VERSIONbumped 3 → 5 over the v16 cycle. The 3 → 4 step was a marker for shape changes that had already landed without a version bump (Tablecsf→jsfkey,cellsWithDynamicDependencies, notes/comments/views/tables), giving consumers that cached v3 payloads a proper signal to invalidate. The 4 → 5 step adds thenamedStylesfield and folds the legacy parallelrowHeights/colWidthsmaps into theWorkSheet.rows/WorkSheet.columnsspan arrays.MINIMUM_ACCEPTABLE_SERIALIZATION_VERSIONstays at 1 — older cached payloads still load.
Documentation
Section titled “Documentation”ModelError.toJSONAPI documentation cleaned up (the raw inferred-return-type property list is gone); the MDN link gets a readable link text.
Dependencies
Section titled “Dependencies”@jsfkit/types^1.4.1→^2.2.0(see Breaking changes for the structural shifts).@borgar/xlsx-convertand@grid-is/jsf2xlsxupgraded for XLSX import/export of named styles and the CSE array-formula flag, plus round-trip and repair-dialog fixes.@grid-is/waspiary→ 1.11.0 for structured-reference bare-name rewrite.
v16.0.0-rc.0 (2026-04-16)
Section titled “v16.0.0-rc.0 (2026-04-16)”First release candidate for the 16.0 major line. Covers the deprecation cleanup, API additions, and fixes landed since v16.0.0-alpha.0.
Breaking changes
Section titled “Breaking changes”All of the following remove symbols that were already @deprecated, with their deprecation notes pointing at the replacement shown here.
Cell.hasValue()/DefinedName.hasValue()andCell.isBlank()/DefinedName.isBlank()removed. UsehasValueOrFormula().Workbook.getTables()andWorkbook.getTable(name)removed. Useworkbook.tables.getAll()andworkbook.tables.get(name).WorkSheet.getComments()andWorkSheet.getNotes()removed. Useworkbook.comments.get(sheetName)andworkbook.notes.get(sheetName).Model.fromDataremoved. UseModel.fromCsf.Workbook.lazyImportPromise,Model.lazyImportPromise,loadLazy(), and theloadLazyre-export removed. All four were already no-ops; just delete the calls.- A batch of
@deprecated @internalre-exports removed from the package entry point: formula-evaluation helpers (run,errorForCode,errorTable, …) and dependency graph helpers (cellToVertexId,wbNameToKey, …). Usecell.vertexId/definedName.vertexIdinstead ofcellToVertexId(cell).
(Some dependency graph classes and utilities were previously marked @deprecated @internal but are now instead made public: VertexId, CellVertexId, NameVertexId, RangeVertexId, KnownVertexId, VertexIdSet, referenceToVertexId, vertexIdToCell, vertexIdToReference.)
Features
Section titled “Features”- Public-API types now exported from the package entry point (previously reachable only via
Parameters<typeof model.write>[2]-style tricks):WriteOptions,AddWorkbookOptions,WorkbookOptions,ReplaceTableReferencesFn,ModelEventArgs,ModelEventType,ModelEventListener,EvaluateExpressionOptions,ModelMeta. - Named-style CRUD via
styleManager.named(newNamedStylescollection), with case-insensitive lookup and original casing preserved onNamedStyle.name.
API changes
Section titled “API changes”Workbook.editCell()added as the preferred name forwriteCellData()(which stays as a@deprecatedalias). Its parameter type simplifies to plainCellData, which now includes the optionalftfield.- Identifier casing standardized on uppercase
JSF/XLSX:Model.fromJSF,toJSF,csfToJSF,normalizeJSFForComparison,toXLSX,toXLSXFile,validateXLSXFilePath, plus typesXLSXOutputMapandXLSXOutputType. The old title-case spellings remain as@deprecatedaliases with identical signatures — no breaking change.
Bug fixes
Section titled “Bug fixes”moveCellsnow handles merged cells. Destination merges are dissolved silently (matching Excel Scripts and Office.js behavior), and cells moved out of a source merge no longer carry stale.Mmerge pointers.- Loading a workbook with a table whose name is not valid as a defined name (e.g.
T1,A1,R1C1) now sanitizes and uniquifies the name instead of dropping the table with aModelError.
Serialization
Section titled “Serialization”CURRENT_SERIALIZATION_VERSIONbumped 3 → 4 as a marker for shape changes that had landed without a version bump (Tablecsf→jsfkey,cellsWithDynamicDependencies, notes/comments/views/tables), giving consumers that cached v3 payloads a proper signal to invalidate.CURRENT_SERIALIZATION_VERSIONbumped 4 → 5 for the named-styles addition.MINIMUM_ACCEPTABLE_SERIALIZATION_VERSIONstays at 1 — older cached payloads still load.
Documentation
Section titled “Documentation”ModelError.toJSONAPI documentation cleaned up (the raw inferred-return-type property list is gone); the MDN link gets a readable link text.
v16.0.0-alpha.0 (2026-04-08)
Section titled “v16.0.0-alpha.0 (2026-04-08)”First alpha of the 16.0 major line. Subject to further breaking changes before the stable release.
Breaking changes
Section titled “Breaking changes”Workbook.viewsis now aViewManager(wasWorkbookView[] | undefined).Workbook.getViews()andWorkSheet.getViews()are removed;WorkSheet.viewsis now@internal. Per-sheet view state is now read/written viaworkbook.views.get(index).getSheetView(sheetName).Workbook.themeis now aThemeManager(always present), notTheme | undefined.WorkSheet.showGridLinesproperty removed; gridline visibility now lives onWorkSheet.views[].showGridLines(per-view).@jsfkit/types1.x → 2.0.0: colours are now structuredColorobjects instead of hex strings, themes are restructured, andGridSizegains an optionalsize.
Features
Section titled “Features”- Views CRUD via
Workbook.views, with two new public typesViewManagerandWorkbookView. - Theme CRUD via
Workbook.theme(newThemeManager): read/write the theme as a whole, the 12-colour scheme, major/minor fonts, and custom colours. Workbook.reorderSheet(name, newIndex)andWorkbook.copySheet(name, newName?).Tableclass is now a public export with new methods (appendRow,insertRow,rename,rowRef) and a newdataRowCountgetter.TableManageris also exported.toXlsxaccepts an optionaloutputTypeargument ('nodebuffer'(default) or'arraybuffer') for browser-compatible XLSX export. New exportsXlsxOutputTypeandXlsxOutputMap.NotesManagerandCommentsManagerare now exported so they show up in the generated public docs.Workbook.moveCellsandWorkbook.rewriteFormulasaccept an optionalrecalcNow(defaulttrue); passfalseto defer the trailingrecalculate().
Bug fixes
Section titled “Bug fixes”getEntitiesno longer crashes on workbooks containing sheet-scoped defined names.SORTNsilently ignores out-of-bounds sort-column indices instead of returning#VALUE!, matching verified Google Sheets behavior.toJsfpreserves values in merged-cell “ghosts” (non-anchor cells);mergeCellskeeps styles on follower cells and only clears values, matching Excel.- Deserializing models serialized by older Apiary versions no longer crashes on missing
views/comments/notes/people/externals/rowsproperties or on thestable.csf->stable.jsfTable key rename. replaceTableReferencesnow correctly rewrites bare-name references to tables (not just bracketed structured references).- Dynamic dependencies of LAMBDA closures passed to
BYROW/BYCOL/MAP/REDUCE/SCANwithINDIRECT/OFFSET/ANCHORARRAYin the body are now tracked, so edits to referenced cells correctly trigger recalc. - Lambda parameter resolution during reference analysis no longer crashes on
INDIRECT/OFFSETinside directly-called lambdas or nested lambda scopes. toJsfno longer crashes on full-row or full-column spill ranges.rewriteFormulasskips unparseable formulas instead of throwing.- Loading CSF workbooks: the literal string
"#SPILL!"in a cell is no longer coerced to an error value, fixing an R-tree invariant violation that crasheddescribeWorkbookin some cases. - Cells in JSF with error-looking string values (
"#VALUE!","#N/A", …) but no explicitt: 'e'marker are preserved as strings, not coerced to errors.
Performance
Section titled “Performance”- Faster label detection in
describeWorkbookvia cheaper subordinate-style checks (weighted geomean ~23.5% faster, worst-case workbooks up to 43% faster).
Dependencies
Section titled “Dependencies”@jsfkit/types^1.4.1->^2.0.0(see Breaking changes).@grid-is/waspiary->1.11.0for structured-reference bare-name rewrite.
v15.5.0 (2026-03-05)
Section titled “v15.5.0 (2026-03-05)”Features
Section titled “Features”- Cell notes CRUD via
NotesManager(workbook.notes):add,get,getByCell,update,delete
Bug fixes
Section titled “Bug fixes”AND/OR/XOR/NOTevaluation with strings and other edge-case arguments, matching Excel and Google Sheets behaviorTEXTJOINandJOINnow apply cell display formats (e.g. date formats) in Google mode
Interface changes
Section titled “Interface changes”These are technically breaking, but very minor and in very recent features, so did not demand a major-version bump.
CommentsManagernow throwsSheetNotFoundError(instead of plainError) for missing sheets, with a differently worded message.WorkSheet.notestype changed toNote[](no longer possibly undefined)
Dependencies
Section titled “Dependencies”- Update
@grid-is/jsf2xlsxto ^1.3.2, updating default column width and fixing extraneousactiveTab="0"attributes inworkbookViewelements.
v15.4.0 (2026-03-02)
Section titled “v15.4.0 (2026-03-02)”Features
Section titled “Features”- Respect ‘autoNoTables’ from XLSX file, plus
deferDataTablesoption for manual control- And more generally, mark any formula cells to be deferred in recalculation
- Threaded comments CRUD via
CommentsManager(workbook.comments)
Bug fixes
Section titled “Bug fixes”MATCHreturned#N/Ainstead of#REF!in Excel missing-sheet edge casetoJsfexport expanded external workbooks to nonexistent cells if referenced in ranges- Error-like strings (e.g.
#REF!) not treated as error values when loading styled cells from CSF - Default (named) styles were not preserved in
toJsfexport
Performance
Section titled “Performance”- Much faster worst-case
describeWorkbook(label-detection) performance
Documentation
Section titled “Documentation”- Update quickstart to use
Model.fromXlsxFileinstead of manualxlsx-convert
v15.3.1 (2026-02-23)
Section titled “v15.3.1 (2026-02-23)”Bug fixes
Section titled “Bug fixes”- Preserve formula type (array or not) when result is 1x1, and classify correctly on edit
- Serialization did not preserve views, annotations, rows, and externals
- Remove Node.js util imports so consumers need not polyfill for browser compatibility
Documentation
Section titled “Documentation”- Add changelog to public docs
v15.3.0 (2026-02-19)
Section titled “v15.3.0 (2026-02-19)”Features
Section titled “Features”- Excel data tables
- Function
SORTN getViewsmethod inWorkSheetandWorkbook- Google Sheets
FLATTENfunction - Merge/unmerge cells
Bug fixes
Section titled “Bug fixes”- Make
HYPERLINK()-derived hyperlinks ephemeral - Preserve external workbooks in JSF roundtrip
- Update comment and note cell refs on move, insert, and delete
- Wrap
fs/promisesimports in try-catch to avoid bundler errors
Performance
Section titled “Performance”- Speed up
describeWorkbookby pre-filteringparseDatecalls
Documentation
Section titled “Documentation”- Pare down public API, marking stuff
@deprecatedand/or@internal - Use code blocks in Typedoc-generated docs
v15.2.0 (2026-02-10)
Section titled “v15.2.0 (2026-02-10)”Features
Section titled “Features”- Support JSF cell notes and threaded comments
Bug fixes
Section titled “Bug fixes”describeWorkbookcrash on cross-workbook ref to formula cell- Support
QUERYwithCOUNT(DISTINCT value)
v15.1.1 (2026-02-10)
Section titled “v15.1.1 (2026-02-10)”Bug fixes
Section titled “Bug fixes”- Missed dynamic dependencies in lambda-calling functions at init time
- Preserve cells with explicit empty styles through round-trip
- Strip
numberFormatFromFormulafrom exported styles intoJSF
v15.1.0 (2026-02-04)
Section titled “v15.1.0 (2026-02-04)”Features
Section titled “Features”toJsftoXlsx- Add
fromXlsxmethods for direct XLSX loading - Native
QUERYwithoutpg-mem(calling it “feat” because it supports a lot that the old one did not) - Output style indexes on rows and columns
Bug fixes
Section titled “Bug fixes”- Real workbooks override external workbooks with same name
- Fix invariant error on insert after row is deleted
- Export
ftproperty intoCSFfor array formulas - Init recalc was skipped if
!recalcVolatiles && !recalcErrors - Load XLSX files in Excel mode by default
- Respect
recalcVolatilesoption during initial recalculation - Preserve text strings that look like errors in JSF loading
ERROR.TYPEdid not support modern error codesdescribeWorkbookthrows on JSF workbook with externals- Upgrade
xlsx-convertto 3.6.4 for bugfixes - Fix
handlerRelsto work with absolute paths - Fix external refs order
- The
rproperty on cells is optional
Breaking changes
Section titled “Breaking changes”- Remove
Flagsmodule andflagsexport
v15.0.0 (2026-01-14)
Section titled “v15.0.0 (2026-01-14)”Features
Section titled “Features”- Public
Model.attachWorkbook, and documentaddWorkbookas just a wrapper - Export
StyleManager - Export
CellDatatype - Add support for JSF workbook/worksheet view props
- Specific
Errorsubclass for invalid reference strings - Add missing detail messages on
#CALC!errors
Bug fixes
Section titled “Bug fixes”- Google-mode early-date date function discrepancies
Matrix.expandmakes fully-populated matrices, causing performance disasters- Nonresolving name in
LOOKUPvector arg borks reference analysis - Propagate
markDirtyto all dependents of spill ranges - Blank spill anchor missing in
Cells.getCells - Don’t propagate recalc from unchanged spill matrix
numberFormatFromFormulapropagationSUMIFSetc. wrong forMatrixwith defaulted regions- Incorrect swap in Brent root-finding setup
QUERYwithnow()not treated as volatile if no dependencies- GSDV blocker in extended range should not block other columns
- Evaluate array expressions with mismatched row widths as
#VALUE! - Fix default column-width units discrepancy
- Fix
Tableserialization property name mismatch - Fix serialization invariant violation
- Fix errors in
toCSFexport structures - Fix GSDV spill range inference: use offset not absolute row
- Fix regression in
evaluateExpression - Preserve CSF style array when loading workbooks
Performance
Section titled “Performance”- Add
vertexIdtoCellandDefinedName - Non-volatile
OFFSETandINDIRECTandANCHORARRAYwith dynamic dependencies
Breaking changes
Section titled “Breaking changes”- CSF type should have
filename, notname - Change
NameReference.widthand.heightto beundefined, not throw
Dependencies
Section titled “Dependencies”- Update
fxto 5.0 - Upgrade Waspiary to 1.10.0 to permit
RandCnames
v15.0.0-rc3 (2026-01-08)
Section titled “v15.0.0-rc3 (2026-01-08)”Features
Section titled “Features”- Add missing detail messages on
#CALC!errors - Export
CellDatatype - Specific
Errorsubclass for invalid reference strings - Add support for JSF workbook/worksheet view props
- Preserve CSF style array when loading workbooks
Bug fixes
Section titled “Bug fixes”- GSDV blocker in extended range should not block other columns
numberFormatFromFormulapropagationSUMIFSetc. wrong forMatrixwith defaulted regions- Incorrect swap in Brent root-finding setup
QUERYwithnow()not treated as volatile if no dependencies- Evaluate array expressions with mismatched row widths as
#VALUE! - Fix GSDV spill range inference: use offset not absolute row
- Fix
Tableserialization property name mismatch - Fix serialization invariant violation
- Fix errors in
toCSFexport structures
v15.0.0-rc2 (2025-12-10)
Section titled “v15.0.0-rc2 (2025-12-10)”Bug fixes
Section titled “Bug fixes”- Blank spill anchor missing in
Cells.getCells - Don’t propagate recalc from unchanged spill matrix
- Fix regression in
evaluateExpression - CSF type should have
filename, notname
Performance
Section titled “Performance”- Non-volatile
OFFSETandINDIRECTandANCHORARRAYwith dynamic dependencies
v15.0.0-rc1 (2025-12-08)
Section titled “v15.0.0-rc1 (2025-12-08)”Features
Section titled “Features”- Public
Model.attachWorkbook, and documentaddWorkbookas just a wrapper - Export
StyleManager
Bug fixes
Section titled “Bug fixes”- Google-mode early-date date function discrepancies
Matrix.expandmakes fully-populated matrices, causing performance disasters- Non-resolving name in
LOOKUPvector arg borks reference analysis - Propagate
markDirtyto all dependents of spill ranges - Fix default column-width units discrepancy
Performance
Section titled “Performance”- Add
vertexIdtoCellandDefinedName
Breaking changes
Section titled “Breaking changes”- Change
NameReference.widthand.heightto beundefined, not throw
Dependencies
Section titled “Dependencies”- Update
fxto 5.0
v14.13.0 (2025-11-19)
Section titled “v14.13.0 (2025-11-19)”Features
Section titled “Features”- Read JSF directly
Bug fixes
Section titled “Bug fixes”- Bugs in row and column deletion and moves
- Support R1C1 syntax in
INDIRECT - Handle changed serialization
- Replace
ERROR_SPILL'foo'placeholder with proper detail message - Serialize model with lambdas in arrays
VLOOKUP/HLOOKUPcan return non-up-to-date value- Direct call to
UPLUSfails with internal error - Recalculate affected formulas if
clearGsdvclears cells
Breaking changes
Section titled “Breaking changes”- Remove
resetvalue fromDefinedNameandCellInterface
Dependencies
Section titled “Dependencies”- Switch to
@jsfkit/typesfor JSF type definitions
- Install from, and publish to, npmjs.com registry
v14.12.0 (2025-10-17)
Section titled “v14.12.0 (2025-10-17)”Breaking changes
Section titled “Breaking changes”- Make
FormulaErrorandReferencefully immutable
Features
Section titled “Features”- Store origin cell in
FormulaError - Add and improve error-value detail messages
- Add detail messages to
#SPILL!errors
Bug fixes
Section titled “Bug fixes”- Make
Cell.isBlank()closer to what its name implies - Minor bugs in reference intersection and add a test
- Rename and deprecate
Cellmethods
Documentation
Section titled “Documentation”- Deploy public API docs to GitHub Pages site
- Remove internal stuff from docs
- Mention case-insensitivity in name parameters
v14.11.0 (2025-10-15)
Section titled “v14.11.0 (2025-10-15)”Features
Section titled “Features”- Recalculate error-valued cells at init time
- Remove defined name
Bug fixes
Section titled “Bug fixes”- Wrong spill-operator result for blocked spill
QUERYcoerces dates to string by local timezone- Support structured references in
INDIRECT IFresult should not be marked dynamic- Make apiary bin work again
v14.10.1 (2025-10-01)
Section titled “v14.10.1 (2025-10-01)”Features
Section titled “Features”- Allow disabling common-subexpression extraction
Bug fixes
Section titled “Bug fixes”- Fix 1904 epoch bug when converting JSF to CSF
v14.10.0 (2025-09-25)
Section titled “v14.10.0 (2025-09-25)”Features
Section titled “Features”- Export
FormulaSyntaxErrorandEvaluationError - Add
Model.empty()convenience method - Export recalculation option symbols
Bug fixes
Section titled “Bug fixes”ADDRESSwith invalid coordinates yields wrong results or throws exception- Use source number format in
TO_TEXT - Set number format in
TO_*functions XLOOKUPthird argument wrongly forced up-to-dateROWetc. fail to update when name ref updates- False-pos circdep in
SUMIF/AVERAGEIFon target range extension Workbook.iterFormulaCellshad a bug and a redundant sibling- Update JSF support
Documentation
Section titled “Documentation”- Add a quick start guide
- Add Starlight-based documentation system
- Add tutorial for creating a spreadsheet from scratch
- Document some methods on the
Modelclass
v14.9.5 (2025-09-01)
Section titled “v14.9.5 (2025-09-01)”Features
Section titled “Features”- Add support for loading and converting JSF format
v14.9.3 (2025-08-12)
Section titled “v14.9.3 (2025-08-12)”Bug fixes
Section titled “Bug fixes”CONVERTfailed to handle reference value argument
Dependencies
Section titled “Dependencies”- Bump
fxfrom 4.11.2 to 4.12.0 [bot] - Bump
msgpackrfrom 1.11.4 to 1.11.5 [bot]
v14.9.2 (2025-07-23)
Section titled “v14.9.2 (2025-07-23)”Bug fixes
Section titled “Bug fixes”- Fix defined names causing slow iterative calculation
v14.9.1 (2025-07-22)
Section titled “v14.9.1 (2025-07-22)”Version revoked due to incorrect fix.
v14.9.0 (2025-06-27)
Section titled “v14.9.0 (2025-06-27)”- Detect parent labels
v14.8.7 (2025-06-26)
Section titled “v14.8.7 (2025-06-26)”Bug fixes
Section titled “Bug fixes”- Label sequence confusion from sparse values at island top/left
Dependencies
Section titled “Dependencies”- Bump
component-emitterfrom 1.3.0 to 2.0.0 - Upgrade
pg-memto 2.6.15
v14.8.6 (2025-06-12)
Section titled “v14.8.6 (2025-06-12)”Bug fixes
Section titled “Bug fixes”- Leftmost-column data mistaken for a label sequence
v14.8.5 (2025-06-08)
Section titled “v14.8.5 (2025-06-08)”Bug fixes
Section titled “Bug fixes”- Skip over gaps in mixed-type label subsequences
v14.8.4 (2025-06-08)
Section titled “v14.8.4 (2025-06-08)”- Improve island-based label detection