@odoo/o-spreadsheet 19.1.0-alpha.7 → 19.1.0-alpha.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,9 +2,9 @@
2
2
  /**
3
3
  * This file is generated by o-spreadsheet build tools. Do not edit it.
4
4
  * @see https://github.com/odoo/o-spreadsheet
5
- * @version 19.1.0-alpha.7
6
- * @date 2025-10-17T11:09:35.690Z
7
- * @hash a11279d
5
+ * @version 19.1.0-alpha.9
6
+ * @date 2025-10-23T11:12:55.400Z
7
+ * @hash bd756dd
8
8
  */
9
9
 
10
10
  import { useEnv, useSubEnv, onWillUnmount, useComponent, status, Component, useRef, onMounted, useEffect, App, blockDom, useState, onPatched, useExternalListener, onWillUpdateProps, onWillStart, onWillPatch, xml, useChildSubEnv, markRaw, toRaw } from '@odoo/owl';
@@ -487,8 +487,17 @@ function isObjectEmptyRecursive(argument) {
487
487
  /**
488
488
  * Returns a function, that, as long as it continues to be invoked, will not
489
489
  * be triggered. The function will be called after it stops being called for
490
- * N milliseconds. If `immediate` is passed, trigger the function on the
491
- * leading edge, instead of the trailing.
490
+ * N milliseconds. If `immediate` is passed, the function is called is called
491
+ * immediately on the first call and the debouncing is triggered starting the second
492
+ * call in the defined time window.
493
+ *
494
+ * Example:
495
+ * debouncedFunction = debounce(() => console.log('Hello!'), 250);
496
+ * debouncedFunction(); debouncedFunction(); // Will log 'Hello!' after 250ms
497
+ *
498
+ * debouncedFunction = debounce(() => console.log('Hello!'), 250, true);
499
+ * debouncedFunction(); debouncedFunction(); // Will log 'Hello!' and relog it after 250ms
500
+ *
492
501
  *
493
502
  * Also decorate the argument function with two methods: stopDebounce and isDebouncePending.
494
503
  *
@@ -496,21 +505,21 @@ function isObjectEmptyRecursive(argument) {
496
505
  */
497
506
  function debounce(func, wait, immediate) {
498
507
  let timeout = undefined;
508
+ let firstCalled = false;
499
509
  const debounced = function () {
500
510
  const context = this;
501
511
  const args = Array.from(arguments);
512
+ if (!firstCalled && immediate) {
513
+ firstCalled = true;
514
+ return func.apply(context, args);
515
+ }
502
516
  function later() {
503
517
  timeout = undefined;
504
- if (!immediate) {
505
- func.apply(context, args);
506
- }
518
+ firstCalled = false;
519
+ func.apply(context, args);
507
520
  }
508
- const callNow = immediate && !timeout;
509
521
  clearTimeout(timeout);
510
522
  timeout = setTimeout(later, wait);
511
- if (callNow) {
512
- func.apply(context, args);
513
- }
514
523
  };
515
524
  debounced.isDebouncePending = () => timeout !== undefined;
516
525
  debounced.stopDebounce = () => {
@@ -1153,6 +1162,29 @@ profilesStartingPosition, profiles, zones, toRemove = false) {
1153
1162
  removeContiguousProfiles(profilesStartingPosition, profiles, leftIndex, rightIndex);
1154
1163
  }
1155
1164
  }
1165
+ function profilesContainsZone(profilesStartingPosition, profiles, zone) {
1166
+ const leftValue = zone.left;
1167
+ const rightValue = zone.right;
1168
+ const topValue = zone.top;
1169
+ const bottomValue = zone.bottom + 1;
1170
+ const leftIndex = binaryPredecessorSearch(profilesStartingPosition, leftValue, 0);
1171
+ const rightIndex = binaryPredecessorSearch(profilesStartingPosition, rightValue, leftIndex);
1172
+ if (leftIndex === -1 || rightIndex === -1) {
1173
+ return false;
1174
+ }
1175
+ for (let i = leftIndex; i <= rightIndex; i++) {
1176
+ const profile = profiles.get(profilesStartingPosition[i]);
1177
+ const topPredIndex = binaryPredecessorSearch(profile, topValue, 0, true);
1178
+ const bottomSuccIndex = binarySuccessorSearch(profile, bottomValue, 0, true);
1179
+ if (topPredIndex === -1 || topPredIndex % 2 !== 0) {
1180
+ return false;
1181
+ }
1182
+ if (topValue < profile[topPredIndex] || bottomValue > profile[bottomSuccIndex]) {
1183
+ return false;
1184
+ }
1185
+ }
1186
+ return true;
1187
+ }
1156
1188
  function findIndexAndCreateProfile(profilesStartingPosition, profiles, value, searchLeft, startIndex) {
1157
1189
  if (value === undefined) {
1158
1190
  // this is only the case when the value correspond to a bottom value that could be undefined
@@ -1237,7 +1269,18 @@ function modifyProfile(profile, zone, toRemove = false) {
1237
1269
  }
1238
1270
  // add the top and bottom value to the profile and
1239
1271
  // remove all information between the top and bottom index
1240
- profile.splice(topPredIndex + 1, bottomSuccIndex - topPredIndex - 1, ...newPoints);
1272
+ const toDelete = bottomSuccIndex - topPredIndex - 1;
1273
+ const toInsert = newPoints.length;
1274
+ const start = topPredIndex + 1;
1275
+ // fast path and slow path
1276
+ if (start === profile.length - 1 && toDelete === 1 && toInsert === 1) {
1277
+ // fast path: we just need to replace the last element
1278
+ profile[start] = newPoints[0] ?? newPoints[1];
1279
+ }
1280
+ else {
1281
+ // equivalent but slower and with memory allocation
1282
+ profile.splice(start, toDelete, ...newPoints);
1283
+ }
1241
1284
  }
1242
1285
  function removeContiguousProfiles(profilesStartingPosition, profiles, leftIndex, rightIndex) {
1243
1286
  const start = leftIndex - 1 === -1 ? 0 : leftIndex - 1;
@@ -1276,8 +1319,10 @@ function constructZonesFromProfiles(profilesStartingPosition, profiles) {
1276
1319
  left,
1277
1320
  bottom,
1278
1321
  right,
1279
- hasHeader: (bottom === undefined && top !== 0) || (right === undefined && left !== 0),
1280
1322
  };
1323
+ if ((bottom === undefined && top !== 0) || (right === undefined && left !== 0)) {
1324
+ profileZone.hasHeader = true;
1325
+ }
1281
1326
  let findCorrespondingZone = false;
1282
1327
  for (let j = pendingZones.length - 1; j >= 0; j--) {
1283
1328
  const pendingZone = pendingZones[j];
@@ -1762,17 +1807,6 @@ function excludeTopLeft(zone) {
1762
1807
  }
1763
1808
  return [leftColumnZone, rightPartZone];
1764
1809
  }
1765
- function aggregatePositionsToZones(positions) {
1766
- const result = {};
1767
- for (const position of positions) {
1768
- result[position.sheetId] ??= [];
1769
- result[position.sheetId].push(positionToZone(position));
1770
- }
1771
- for (const sheetId in result) {
1772
- result[sheetId] = recomputeZones(result[sheetId]);
1773
- }
1774
- return result;
1775
- }
1776
1810
  /**
1777
1811
  * Array of all positions in the zone.
1778
1812
  */
@@ -14881,6 +14915,13 @@ pivotTimeAdapterRegistry
14881
14915
  .add("minute_number", nullHandlerDecorator(minuteNumberAdapter))
14882
14916
  .add("second_number", nullHandlerDecorator(secondNumberAdapter));
14883
14917
 
14918
+ const DEFAULT_PIVOT_STYLE = {
14919
+ displayTotals: true,
14920
+ displayColumnHeaders: true,
14921
+ displayMeasuresRow: true,
14922
+ numberOfRows: Number.MAX_VALUE,
14923
+ numberOfColumns: Number.MAX_VALUE,
14924
+ };
14884
14925
  const AGGREGATOR_NAMES = {
14885
14926
  count: _t$1("Count"),
14886
14927
  count_distinct: _t$1("Count Distinct"),
@@ -15214,6 +15255,25 @@ function togglePivotCollapse(position, env) {
15214
15255
  pivot: { ...definition, collapsedDomains: newDomains },
15215
15256
  });
15216
15257
  }
15258
+ function getPivotStyleFromFnArgs(definition, rowCountArg, includeTotalArg, includeColumnHeadersArg, columnCountArg, includeMeasuresRowArg, locale) {
15259
+ const style = definition.style;
15260
+ const numberOfRows = rowCountArg !== undefined
15261
+ ? toNumber(rowCountArg, locale)
15262
+ : style?.numberOfRows ?? DEFAULT_PIVOT_STYLE.numberOfRows;
15263
+ const numberOfColumns = columnCountArg !== undefined
15264
+ ? toNumber(columnCountArg, locale)
15265
+ : style?.numberOfColumns ?? DEFAULT_PIVOT_STYLE.numberOfColumns;
15266
+ const displayTotals = includeTotalArg !== undefined
15267
+ ? toBoolean(includeTotalArg)
15268
+ : style?.displayTotals ?? DEFAULT_PIVOT_STYLE.displayTotals;
15269
+ const displayColumnHeaders = includeColumnHeadersArg !== undefined
15270
+ ? toBoolean(includeColumnHeadersArg)
15271
+ : style?.displayColumnHeaders ?? DEFAULT_PIVOT_STYLE.displayColumnHeaders;
15272
+ const displayMeasuresRow = includeMeasuresRowArg !== undefined
15273
+ ? toBoolean(includeMeasuresRowArg)
15274
+ : style?.displayMeasuresRow ?? DEFAULT_PIVOT_STYLE.displayMeasuresRow;
15275
+ return { numberOfRows, numberOfColumns, displayTotals, displayColumnHeaders, displayMeasuresRow };
15276
+ }
15217
15277
 
15218
15278
  /**
15219
15279
  * Get the pivot ID from the formula pivot ID.
@@ -15828,24 +15888,18 @@ const PIVOT = {
15828
15888
  arg("column_count (number, optional)", _t$1("number of columns")),
15829
15889
  arg("include_measure_titles (boolean, default=TRUE)", _t$1("Whether to include the measure titles row or not.")),
15830
15890
  ],
15831
- compute: function (pivotFormulaId, rowCount = { value: 10000 }, includeTotal = { value: true }, includeColumnHeaders = { value: true }, columnCount = { value: Number.MAX_VALUE }, includeMeasureTitles = { value: true }) {
15891
+ compute: function (pivotFormulaId, rowCount, includeTotal, includeColumnHeaders, columnCount, includeMeasureTitles) {
15832
15892
  const _pivotFormulaId = toString(pivotFormulaId);
15833
- const _rowCount = toNumber(rowCount, this.locale);
15834
- if (_rowCount < 0) {
15893
+ const pivotId = getPivotId(_pivotFormulaId, this.getters);
15894
+ const pivot = this.getters.getPivot(pivotId);
15895
+ const coreDefinition = this.getters.getPivotCoreDefinition(pivotId);
15896
+ const pivotStyle = getPivotStyleFromFnArgs(coreDefinition, rowCount, includeTotal, includeColumnHeaders, columnCount, includeMeasureTitles, this.locale);
15897
+ if (pivotStyle.numberOfRows < 0) {
15835
15898
  return new EvaluationError(_t$1("The number of rows must be positive."));
15836
15899
  }
15837
- const _columnCount = toNumber(columnCount, this.locale);
15838
- if (_columnCount < 0) {
15900
+ if (pivotStyle.numberOfColumns < 0) {
15839
15901
  return new EvaluationError(_t$1("The number of columns must be positive."));
15840
15902
  }
15841
- const visibilityOptions = {
15842
- displayColumnHeaders: toBoolean(includeColumnHeaders),
15843
- displayTotals: toBoolean(includeTotal),
15844
- displayMeasuresRow: toBoolean(includeMeasureTitles),
15845
- };
15846
- const pivotId = getPivotId(_pivotFormulaId, this.getters);
15847
- const pivot = this.getters.getPivot(pivotId);
15848
- const coreDefinition = this.getters.getPivotCoreDefinition(pivotId);
15849
15903
  addPivotDependencies(this, coreDefinition, coreDefinition.measures);
15850
15904
  pivot.init({ reload: pivot.needsReevaluation });
15851
15905
  const error = pivot.assertIsValid({ throwOnError: false });
@@ -15856,20 +15910,20 @@ const PIVOT = {
15856
15910
  if (table.numberOfCells > PIVOT_MAX_NUMBER_OF_CELLS) {
15857
15911
  return new EvaluationError(getPivotTooBigErrorMessage$1(table.numberOfCells, this.locale));
15858
15912
  }
15859
- const cells = table.getPivotCells(visibilityOptions);
15913
+ const cells = table.getPivotCells(pivotStyle);
15860
15914
  let headerRows = 0;
15861
- if (visibilityOptions.displayColumnHeaders) {
15915
+ if (pivotStyle.displayColumnHeaders) {
15862
15916
  headerRows = table.columns.length - 1;
15863
15917
  }
15864
- if (visibilityOptions.displayMeasuresRow) {
15918
+ if (pivotStyle.displayMeasuresRow) {
15865
15919
  headerRows++;
15866
15920
  }
15867
15921
  const pivotTitle = this.getters.getPivotName(pivotId);
15868
- const tableHeight = Math.min(headerRows + _rowCount, cells[0].length);
15922
+ const tableHeight = Math.min(headerRows + pivotStyle.numberOfRows, cells[0].length);
15869
15923
  if (tableHeight === 0) {
15870
15924
  return [[{ value: pivotTitle }]];
15871
15925
  }
15872
- const tableWidth = Math.min(1 + _columnCount, cells.length);
15926
+ const tableWidth = Math.min(1 + pivotStyle.numberOfColumns, cells.length);
15873
15927
  const result = [];
15874
15928
  for (const col of range$1(0, tableWidth)) {
15875
15929
  result[col] = [];
@@ -15892,7 +15946,7 @@ const PIVOT = {
15892
15946
  }
15893
15947
  }
15894
15948
  }
15895
- if (visibilityOptions.displayColumnHeaders || visibilityOptions.displayMeasuresRow) {
15949
+ if (pivotStyle.displayColumnHeaders || pivotStyle.displayMeasuresRow) {
15896
15950
  result[0][0] = { value: pivotTitle };
15897
15951
  }
15898
15952
  return result;
@@ -19494,6 +19548,10 @@ function getRangeParts(xc, zone) {
19494
19548
  }
19495
19549
  return parts;
19496
19550
  }
19551
+ function positionToBoundedRange(position) {
19552
+ const zone = { left: position.col, top: position.row, right: position.col, bottom: position.row };
19553
+ return { sheetId: position.sheetId, zone };
19554
+ }
19497
19555
  /**
19498
19556
  * Check that a zone is valid regarding the order of top-bottom and left-right.
19499
19557
  * Left should be smaller than right, top should be smaller than bottom.
@@ -21360,6 +21418,9 @@ function getBarChartDatasets(definition, args) {
21360
21418
  backgroundColor,
21361
21419
  yAxisID: definition.horizontal ? "y" : definition.dataSets?.[index].yAxisId || "y",
21362
21420
  xAxisID: "x",
21421
+ barPercentage: 0.9,
21422
+ categoryPercentage: dataSetsValues.length > 1 ? 0.8 : 1,
21423
+ borderRadius: 2,
21363
21424
  };
21364
21425
  dataSets.push(dataset);
21365
21426
  const trendConfig = definition.dataSets?.[index].trend;
@@ -21488,6 +21549,7 @@ function getComboChartDatasets(definition, args) {
21488
21549
  const dataSets = [];
21489
21550
  const colors = getChartColorsGenerator(definition, dataSetsValues.length);
21490
21551
  const trendDatasets = [];
21552
+ const barDatasets = dataSetsValues.filter((_, i) => (definition.dataSets?.[i].type ?? "line") === "bar");
21491
21553
  for (let index = 0; index < dataSetsValues.length; index++) {
21492
21554
  let { label, data, hidden } = dataSetsValues[index];
21493
21555
  label = definition.dataSets?.[index].label || label;
@@ -21506,6 +21568,11 @@ function getComboChartDatasets(definition, args) {
21506
21568
  order: type === "bar" ? dataSetsValues.length + index : index,
21507
21569
  pointRadius: definition.hideDataMarkers ? 0 : LINE_DATA_POINT_RADIUS,
21508
21570
  };
21571
+ if (dataset.type === "bar") {
21572
+ dataset.barPercentage = 0.9;
21573
+ dataset.categoryPercentage = barDatasets.length > 1 ? 0.8 : 1;
21574
+ dataset.borderRadius = 2;
21575
+ }
21509
21576
  dataSets.push(dataset);
21510
21577
  const trendConfig = definition.dataSets?.[index].trend;
21511
21578
  const trendData = args.trendDataSetsValues?.[index];
@@ -33488,6 +33555,7 @@ function forceUnicityOfFigure(data) {
33488
33555
  return data;
33489
33556
  }
33490
33557
  const figureIds = new Set();
33558
+ const chartIds = new Set();
33491
33559
  const uuidGenerator = new UuidGenerator();
33492
33560
  for (const sheet of data.sheets || []) {
33493
33561
  for (const figure of sheet.figures || []) {
@@ -33495,6 +33563,12 @@ function forceUnicityOfFigure(data) {
33495
33563
  figure.id += uuidGenerator.smallUuid();
33496
33564
  }
33497
33565
  figureIds.add(figure.id);
33566
+ if (figure.tag === "chart") {
33567
+ if (chartIds.has(figure.data?.chartId)) {
33568
+ figure.data.chartId += uuidGenerator.smallUuid();
33569
+ }
33570
+ chartIds.add(figure.data?.chartId);
33571
+ }
33498
33572
  }
33499
33573
  }
33500
33574
  data.uniqueFigureIds = true;
@@ -33862,11 +33936,6 @@ class CorePlugin extends BasePlugin {
33862
33936
  * @param sheetName couple of old and new sheet names to adapt ranges pointing to that sheet
33863
33937
  */
33864
33938
  adaptRanges(applyChange, sheetId, sheetName) { }
33865
- /**
33866
- * Implement this method to clean unused external resources, such as images
33867
- * stored on a server which have been deleted.
33868
- */
33869
- garbageCollectExternalResources() { }
33870
33939
  }
33871
33940
 
33872
33941
  class BordersPlugin extends CorePlugin {
@@ -37710,17 +37779,6 @@ class ImagePlugin extends CorePlugin {
37710
37779
  break;
37711
37780
  }
37712
37781
  }
37713
- /**
37714
- * Delete unused images from the file store
37715
- */
37716
- garbageCollectExternalResources() {
37717
- const images = new Set(this.getAllImages().map((image) => image.path));
37718
- for (const path of this.syncedImages) {
37719
- if (!images.has(path)) {
37720
- this.fileStore?.delete(path);
37721
- }
37722
- }
37723
- }
37724
37782
  // ---------------------------------------------------------------------------
37725
37783
  // Getters
37726
37784
  // ---------------------------------------------------------------------------
@@ -37782,13 +37840,6 @@ class ImagePlugin extends CorePlugin {
37782
37840
  sheet.images = [...sheet.images, ...images];
37783
37841
  }
37784
37842
  }
37785
- getAllImages() {
37786
- const images = [];
37787
- for (const sheetId in this.images) {
37788
- images.push(...Object.values(this.images[sheetId] || {}).filter(isDefined$1));
37789
- }
37790
- return images;
37791
- }
37792
37843
  }
37793
37844
 
37794
37845
  class MergePlugin extends CorePlugin {
@@ -38561,26 +38612,22 @@ class SpreadsheetPivotTable {
38561
38612
  getNumberOfDataColumns() {
38562
38613
  return this.columns.at(-1)?.length || 0;
38563
38614
  }
38564
- getSkippedRows(visibilityOptions) {
38615
+ getSkippedRows(pivotStyle) {
38565
38616
  const skippedRows = new Set();
38566
- if (!visibilityOptions.displayColumnHeaders) {
38617
+ if (!pivotStyle.displayColumnHeaders) {
38567
38618
  for (let i = 0; i < this.columns.length - 1; i++) {
38568
38619
  skippedRows.add(i);
38569
38620
  }
38570
38621
  }
38571
- if (!visibilityOptions.displayMeasuresRow) {
38622
+ if (!pivotStyle.displayMeasuresRow) {
38572
38623
  skippedRows.add(this.columns.length - 1);
38573
38624
  }
38574
38625
  return skippedRows;
38575
38626
  }
38576
- getPivotCells(visibilityOptions = {
38577
- displayColumnHeaders: true,
38578
- displayTotals: true,
38579
- displayMeasuresRow: true,
38580
- }) {
38581
- const key = JSON.stringify(visibilityOptions);
38627
+ getPivotCells(pivotStyle = DEFAULT_PIVOT_STYLE) {
38628
+ const key = JSON.stringify(pivotStyle);
38582
38629
  if (!this.pivotCells[key]) {
38583
- const { displayTotals } = visibilityOptions;
38630
+ const { displayTotals } = pivotStyle;
38584
38631
  const numberOfDataRows = this.rows.length;
38585
38632
  const numberOfDataColumns = this.getNumberOfDataColumns();
38586
38633
  let pivotHeight = this.columns.length + numberOfDataRows;
@@ -38592,7 +38639,7 @@ class SpreadsheetPivotTable {
38592
38639
  pivotWidth -= this.measures.length;
38593
38640
  }
38594
38641
  const domainArray = [];
38595
- const skippedRows = this.getSkippedRows(visibilityOptions);
38642
+ const skippedRows = this.getSkippedRows(pivotStyle);
38596
38643
  for (let col = 0; col < pivotWidth; col++) {
38597
38644
  domainArray.push([]);
38598
38645
  for (let row = 0; row < pivotHeight; row++) {
@@ -41684,6 +41731,281 @@ class ZoneRBush extends RBush {
41684
41731
  }
41685
41732
  }
41686
41733
 
41734
+ class ZoneSet {
41735
+ profilesStartingPosition = [0];
41736
+ profiles = new Map([[0, []]]);
41737
+ constructor(zones = []) {
41738
+ for (const zone of zones) {
41739
+ this.add(zone);
41740
+ }
41741
+ }
41742
+ isEmpty() {
41743
+ return this.profiles.size === 1 && this.profiles.get(0)?.length === 0;
41744
+ }
41745
+ add(zone) {
41746
+ modifyProfiles(this.profilesStartingPosition, this.profiles, [zone]);
41747
+ }
41748
+ delete(zone) {
41749
+ modifyProfiles(this.profilesStartingPosition, this.profiles, [zone], true);
41750
+ }
41751
+ has(zone) {
41752
+ return profilesContainsZone(this.profilesStartingPosition, this.profiles, zone);
41753
+ }
41754
+ difference(other) {
41755
+ const result = this.copy();
41756
+ for (const zone of other) {
41757
+ result.delete(zone);
41758
+ }
41759
+ return result;
41760
+ }
41761
+ copy() {
41762
+ const result = new ZoneSet();
41763
+ result.profilesStartingPosition = [...this.profilesStartingPosition];
41764
+ result.profiles = new Map();
41765
+ for (const [key, value] of this.profiles) {
41766
+ result.profiles.set(key, [...value]);
41767
+ }
41768
+ return result;
41769
+ }
41770
+ size() {
41771
+ let size = 0;
41772
+ for (const profile of this.profiles.values()) {
41773
+ size += profile.length;
41774
+ }
41775
+ return size / 2;
41776
+ }
41777
+ /**
41778
+ * iterator of all the zones in the ZoneSet
41779
+ */
41780
+ [Symbol.iterator]() {
41781
+ return constructZonesFromProfiles(this.profilesStartingPosition, this.profiles)[Symbol.iterator]();
41782
+ }
41783
+ }
41784
+
41785
+ class RangeSet {
41786
+ setsBySheetId = {};
41787
+ constructor(ranges = []) {
41788
+ for (const range of ranges) {
41789
+ this.add(range);
41790
+ }
41791
+ }
41792
+ add(range) {
41793
+ if (!this.setsBySheetId[range.sheetId]) {
41794
+ this.setsBySheetId[range.sheetId] = new ZoneSet();
41795
+ }
41796
+ this.setsBySheetId[range.sheetId].add(range.zone);
41797
+ }
41798
+ addMany(ranges) {
41799
+ for (const range of ranges) {
41800
+ this.add(range);
41801
+ }
41802
+ }
41803
+ addPosition(position) {
41804
+ this.add(positionToBoundedRange(position));
41805
+ }
41806
+ addManyPositions(positions) {
41807
+ for (const position of positions) {
41808
+ this.addPosition(position);
41809
+ }
41810
+ }
41811
+ has(range) {
41812
+ if (!this.setsBySheetId[range.sheetId]) {
41813
+ return false;
41814
+ }
41815
+ return this.setsBySheetId[range.sheetId].has(range.zone);
41816
+ }
41817
+ hasPosition(position) {
41818
+ return this.has(positionToBoundedRange(position));
41819
+ }
41820
+ delete(range) {
41821
+ if (!this.setsBySheetId[range.sheetId]) {
41822
+ return;
41823
+ }
41824
+ this.setsBySheetId[range.sheetId].delete(range.zone);
41825
+ }
41826
+ deleteMany(ranges) {
41827
+ for (const range of ranges) {
41828
+ this.delete(range);
41829
+ }
41830
+ }
41831
+ deleteManyPositions(positions) {
41832
+ for (const position of positions) {
41833
+ this.delete(positionToBoundedRange(position));
41834
+ }
41835
+ }
41836
+ difference(other) {
41837
+ const result = new RangeSet();
41838
+ for (const sheetId in this.setsBySheetId) {
41839
+ result.setsBySheetId[sheetId] = this.setsBySheetId[sheetId];
41840
+ }
41841
+ for (const sheetId in other.setsBySheetId) {
41842
+ if (result.setsBySheetId[sheetId]) {
41843
+ result.setsBySheetId[sheetId] = result.setsBySheetId[sheetId].difference(other.setsBySheetId[sheetId]);
41844
+ }
41845
+ }
41846
+ return result;
41847
+ }
41848
+ copy() {
41849
+ const result = new RangeSet();
41850
+ for (const sheetId in this.setsBySheetId) {
41851
+ result.setsBySheetId[sheetId] = this.setsBySheetId[sheetId].copy();
41852
+ }
41853
+ return result;
41854
+ }
41855
+ clear() {
41856
+ this.setsBySheetId = {};
41857
+ }
41858
+ size() {
41859
+ let size = 0;
41860
+ for (const sheetId in this.setsBySheetId) {
41861
+ size += this.setsBySheetId[sheetId].size();
41862
+ }
41863
+ return size;
41864
+ }
41865
+ isEmpty() {
41866
+ for (const sheetId in this.setsBySheetId) {
41867
+ if (!this.setsBySheetId[sheetId].isEmpty()) {
41868
+ return false;
41869
+ }
41870
+ }
41871
+ return true;
41872
+ }
41873
+ /**
41874
+ * iterator of all the ranges in the RangeSet
41875
+ */
41876
+ [Symbol.iterator]() {
41877
+ const result = [];
41878
+ for (const sheetId in this.setsBySheetId) {
41879
+ for (const zone of this.setsBySheetId[sheetId]) {
41880
+ result.push({ sheetId: sheetId, zone });
41881
+ }
41882
+ }
41883
+ return result[Symbol.iterator]();
41884
+ }
41885
+ }
41886
+
41887
+ /**
41888
+ * R-Tree of ranges, mapping zones (r-tree bounding boxes) to ranges (data of the r-tree item).
41889
+ * Ranges associated to the exact same bounding box are grouped together
41890
+ * to reduce the number of nodes in the R-tree.
41891
+ */
41892
+ class DependenciesRTree {
41893
+ rTree;
41894
+ constructor(items = []) {
41895
+ const compactedBoxes = groupSameBoundingBoxes(items);
41896
+ this.rTree = new SpreadsheetRTree(compactedBoxes);
41897
+ }
41898
+ insert(item) {
41899
+ const data = this.rTree.search(item.boundingBox);
41900
+ const itemBoundingBox = item.boundingBox;
41901
+ const exactBoundingBox = data.find(({ boundingBox }) => boundingBox.sheetId === itemBoundingBox.sheetId &&
41902
+ boundingBox.zone.left === itemBoundingBox.zone.left &&
41903
+ boundingBox.zone.top === itemBoundingBox.zone.top &&
41904
+ boundingBox.zone.right === itemBoundingBox.zone.right &&
41905
+ boundingBox.zone.bottom === itemBoundingBox.zone.bottom);
41906
+ if (exactBoundingBox) {
41907
+ exactBoundingBox.data.add(item.data);
41908
+ }
41909
+ else {
41910
+ this.rTree.insert({ ...item, data: new RangeSet([item.data]) });
41911
+ }
41912
+ }
41913
+ search({ zone, sheetId }) {
41914
+ const results = new RangeSet();
41915
+ for (const { data } of this.rTree.search({ zone, sheetId })) {
41916
+ results.addMany(data);
41917
+ }
41918
+ return results;
41919
+ }
41920
+ remove(item) {
41921
+ const data = this.rTree.search(item.boundingBox);
41922
+ const itemBoundingBox = item.boundingBox;
41923
+ const exactBoundingBox = data.find(({ boundingBox }) => boundingBox.sheetId === itemBoundingBox.sheetId &&
41924
+ boundingBox.zone.left === itemBoundingBox.zone.left &&
41925
+ boundingBox.zone.top === itemBoundingBox.zone.top &&
41926
+ boundingBox.zone.right === itemBoundingBox.zone.right &&
41927
+ boundingBox.zone.bottom === itemBoundingBox.zone.bottom);
41928
+ if (exactBoundingBox) {
41929
+ exactBoundingBox.data.delete(item.data);
41930
+ }
41931
+ else {
41932
+ this.rTree.remove({ ...item, data: new RangeSet([item.data]) });
41933
+ }
41934
+ }
41935
+ }
41936
+ /**
41937
+ * Group together all formulas pointing to the exact same dependency (bounding box).
41938
+ * The goal is to optimize the following case:
41939
+ * - if any cell in B1:B1000 changes, C1 must be recomputed
41940
+ * - if any cell in B1:B1000 changes, C2 must be recomputed
41941
+ * - if any cell in B1:B1000 changes, C3 must be recomputed
41942
+ * ...
41943
+ * - if any cell in B1:B1000 changes, C1000 must be recomputed
41944
+ *
41945
+ * Instead of having 1000 entries in the R-tree, we want to have a single entry
41946
+ * with B1:B1000 (bounding box) pointing to C1:C1000 (formulas).
41947
+ */
41948
+ function groupSameBoundingBoxes(items) {
41949
+ // Important: this function must be as fast as possible. It is on the evaluation hot path.
41950
+ let maxCol = 0;
41951
+ let maxRow = 0;
41952
+ for (let i = 0; i < items.length; i++) {
41953
+ const zone = items[i].boundingBox.zone;
41954
+ if (zone.right > maxCol) {
41955
+ maxCol = zone.right;
41956
+ }
41957
+ if (zone.bottom > maxRow) {
41958
+ maxRow = zone.bottom;
41959
+ }
41960
+ }
41961
+ maxCol += 1;
41962
+ maxRow += 1;
41963
+ // in most real-world cases, we can use a fast numeric key
41964
+ // but if the zones are too far right or bottom, we fallback to a slower string key
41965
+ const maxPossibleKey = (((maxRow + 1) * maxCol + 1) * maxRow + 1) * maxCol;
41966
+ const useFastKey = maxPossibleKey <= Number.MAX_SAFE_INTEGER;
41967
+ if (!useFastKey) {
41968
+ console.warn("Max col/row size exceeded, using slow zone key");
41969
+ }
41970
+ const groupedByBBox = {};
41971
+ for (const item of items) {
41972
+ const sheetId = item.boundingBox.sheetId;
41973
+ if (!groupedByBBox[sheetId]) {
41974
+ groupedByBBox[sheetId] = {};
41975
+ }
41976
+ const bBox = item.boundingBox.zone;
41977
+ let bBoxKey = 0;
41978
+ if (useFastKey) {
41979
+ bBoxKey =
41980
+ bBox.left +
41981
+ bBox.top * maxCol +
41982
+ bBox.right * maxCol * maxRow +
41983
+ bBox.bottom * maxCol * maxRow * maxCol;
41984
+ }
41985
+ else {
41986
+ bBoxKey = `${bBox.left},${bBox.top},${bBox.right},${bBox.bottom}`;
41987
+ }
41988
+ if (groupedByBBox[sheetId][bBoxKey]) {
41989
+ const ranges = groupedByBBox[sheetId][bBoxKey].data;
41990
+ ranges.add(item.data);
41991
+ }
41992
+ else {
41993
+ groupedByBBox[sheetId][bBoxKey] = {
41994
+ boundingBox: item.boundingBox,
41995
+ data: new RangeSet([item.data]),
41996
+ };
41997
+ }
41998
+ }
41999
+ const result = [];
42000
+ for (const sheetId in groupedByBBox) {
42001
+ const map = groupedByBBox[sheetId];
42002
+ for (const key in map) {
42003
+ result.push(map[key]);
42004
+ }
42005
+ }
42006
+ return result;
42007
+ }
42008
+
41687
42009
  /**
41688
42010
  * Implementation of a dependency Graph.
41689
42011
  * The graph is used to evaluate the cells in the correct
@@ -41692,12 +42014,10 @@ class ZoneRBush extends RBush {
41692
42014
  * It uses an R-Tree data structure to efficiently find dependent cells.
41693
42015
  */
41694
42016
  class FormulaDependencyGraph {
41695
- createEmptyPositionSet;
41696
42017
  dependencies = new PositionMap();
41697
42018
  rTree;
41698
- constructor(createEmptyPositionSet, data = []) {
41699
- this.createEmptyPositionSet = createEmptyPositionSet;
41700
- this.rTree = new SpreadsheetRTree(data);
42019
+ constructor(data = []) {
42020
+ this.rTree = new DependenciesRTree(data);
41701
42021
  }
41702
42022
  removeAllDependencies(formulaPosition) {
41703
42023
  const ranges = this.dependencies.get(formulaPosition);
@@ -41711,7 +42031,10 @@ class FormulaDependencyGraph {
41711
42031
  }
41712
42032
  addDependencies(formulaPosition, dependencies) {
41713
42033
  const rTreeItems = dependencies.map(({ sheetId, zone }) => ({
41714
- data: formulaPosition,
42034
+ data: {
42035
+ sheetId: formulaPosition.sheetId,
42036
+ zone: positionToZone(formulaPosition),
42037
+ },
41715
42038
  boundingBox: {
41716
42039
  zone,
41717
42040
  sheetId,
@@ -41729,46 +42052,20 @@ class FormulaDependencyGraph {
41729
42052
  }
41730
42053
  }
41731
42054
  /**
41732
- * Return all the cells that depend on the provided ranges,
41733
- * in the correct order they should be evaluated.
41734
- * This is called a topological ordering (excluding cycles)
42055
+ * Return all the cells that depend on the provided ranges.
41735
42056
  */
41736
- getCellsDependingOn(ranges, ignore) {
41737
- const visited = this.createEmptyPositionSet();
42057
+ getCellsDependingOn(ranges, visited = new RangeSet()) {
42058
+ visited = visited.copy();
41738
42059
  const queue = Array.from(ranges).reverse();
41739
42060
  while (queue.length > 0) {
41740
42061
  const range = queue.pop();
41741
- const zone = range.zone;
41742
- const sheetId = range.sheetId;
41743
- for (let col = zone.left; col <= zone.right; col++) {
41744
- for (let row = zone.top; row <= zone.bottom; row++) {
41745
- visited.add({ sheetId, col, row });
41746
- }
41747
- }
41748
- const impactedPositions = this.rTree.search(range).map((dep) => dep.data);
41749
- const nextInQueue = {};
41750
- for (const position of impactedPositions) {
41751
- if (!visited.has(position) && !ignore.has(position)) {
41752
- if (!nextInQueue[position.sheetId]) {
41753
- nextInQueue[position.sheetId] = [];
41754
- }
41755
- nextInQueue[position.sheetId].push(positionToZone(position));
41756
- }
41757
- }
41758
- for (const sheetId in nextInQueue) {
41759
- const zones = recomputeZones(nextInQueue[sheetId], []);
41760
- queue.push(...zones.map((zone) => ({ sheetId, zone })));
41761
- }
42062
+ visited.add(range);
42063
+ const impactedRanges = this.rTree.search(range);
42064
+ queue.push(...impactedRanges.difference(visited));
41762
42065
  }
41763
42066
  // remove initial ranges
41764
42067
  for (const range of ranges) {
41765
- const zone = range.zone;
41766
- const sheetId = range.sheetId;
41767
- for (let col = zone.left; col <= zone.right; col++) {
41768
- for (let row = zone.top; row <= zone.bottom; row++) {
41769
- visited.delete({ sheetId, col, row });
41770
- }
41771
- }
42068
+ visited.delete(range);
41772
42069
  }
41773
42070
  return visited;
41774
42071
  }
@@ -42045,7 +42342,7 @@ class Evaluator {
42045
42342
  getters;
42046
42343
  compilationParams;
42047
42344
  evaluatedCells = new PositionMap();
42048
- formulaDependencies = lazy(new FormulaDependencyGraph(this.createEmptyPositionSet.bind(this)));
42345
+ formulaDependencies = lazy(new FormulaDependencyGraph());
42049
42346
  blockedArrayFormulas = new PositionSet({});
42050
42347
  spreadingRelations = new SpreadingRelation();
42051
42348
  constructor(context, getters) {
@@ -42080,7 +42377,7 @@ class Evaluator {
42080
42377
  return undefined;
42081
42378
  }
42082
42379
  const arrayFormulas = this.spreadingRelations.searchFormulaPositionsSpreadingOn(position.sheetId, positionToZone(position));
42083
- return Array.from(arrayFormulas).find((position) => !this.blockedArrayFormulas.has(position));
42380
+ return arrayFormulas.find((position) => !this.blockedArrayFormulas.has(position));
42084
42381
  }
42085
42382
  updateDependencies(position) {
42086
42383
  // removing dependencies is slow because it requires
@@ -42124,57 +42421,72 @@ class Evaluator {
42124
42421
  }
42125
42422
  evaluateCells(positions) {
42126
42423
  const start = performance.now();
42127
- const cellsToCompute = this.createEmptyPositionSet();
42128
- cellsToCompute.addMany(positions);
42424
+ const rangesToCompute = new RangeSet();
42425
+ rangesToCompute.addManyPositions(positions);
42129
42426
  const arrayFormulasPositions = this.getArrayFormulasImpactedByChangesOf(positions);
42130
- cellsToCompute.addMany(this.getCellsDependingOn(positions));
42131
- cellsToCompute.addMany(arrayFormulasPositions);
42132
- cellsToCompute.addMany(this.getCellsDependingOn(arrayFormulasPositions));
42133
- this.evaluate(cellsToCompute);
42427
+ rangesToCompute.addMany(this.getCellsDependingOn(rangesToCompute));
42428
+ rangesToCompute.addMany(arrayFormulasPositions);
42429
+ rangesToCompute.addMany(this.getCellsDependingOn(arrayFormulasPositions));
42430
+ this.evaluate(rangesToCompute);
42134
42431
  console.debug("evaluate Cells", performance.now() - start, "ms");
42135
42432
  }
42136
42433
  getArrayFormulasImpactedByChangesOf(positions) {
42137
- const impactedPositions = this.createEmptyPositionSet();
42434
+ const impactedRanges = new RangeSet();
42138
42435
  for (const position of positions) {
42139
42436
  const content = this.getters.getCell(position)?.content;
42140
42437
  const arrayFormulaPosition = this.getArrayFormulaSpreadingOn(position);
42141
42438
  if (arrayFormulaPosition !== undefined) {
42142
42439
  // take into account new collisions.
42143
- impactedPositions.add(arrayFormulaPosition);
42440
+ impactedRanges.addPosition(arrayFormulaPosition);
42144
42441
  }
42145
42442
  if (!content) {
42146
42443
  // The previous content could have blocked some array formulas
42147
- impactedPositions.add(position);
42444
+ impactedRanges.addPosition(position);
42148
42445
  }
42149
42446
  }
42150
- const zonesBySheetIds = aggregatePositionsToZones(impactedPositions);
42151
- for (const sheetId in zonesBySheetIds) {
42152
- for (const zone of zonesBySheetIds[sheetId]) {
42153
- impactedPositions.addMany(this.getArrayFormulasBlockedBy(sheetId, zone));
42154
- }
42447
+ for (const range of [...impactedRanges]) {
42448
+ impactedRanges.addMany(this.getArrayFormulasBlockedBy(range.sheetId, range.zone));
42155
42449
  }
42156
- return impactedPositions;
42450
+ return impactedRanges;
42157
42451
  }
42158
42452
  buildDependencyGraph() {
42159
42453
  this.blockedArrayFormulas = this.createEmptyPositionSet();
42160
42454
  this.spreadingRelations = new SpreadingRelation();
42161
42455
  this.formulaDependencies = lazy(() => {
42162
- const dependencies = [...this.getAllCells()].flatMap((position) => this.getDirectDependencies(position)
42163
- .filter((range) => !range.invalidSheetName && !range.invalidXc)
42164
- .map((range) => ({
42165
- data: position,
42166
- boundingBox: {
42167
- zone: range.zone,
42168
- sheetId: range.sheetId,
42169
- },
42170
- })));
42171
- return new FormulaDependencyGraph(this.createEmptyPositionSet.bind(this), dependencies);
42456
+ const rTreeItems = [];
42457
+ for (const sheetId of this.getters.getSheetIds()) {
42458
+ const cells = this.getters.getCells(sheetId);
42459
+ for (const cellId in cells) {
42460
+ const cell = cells[cellId];
42461
+ if (cell.isFormula) {
42462
+ const directDependencies = cell.compiledFormula.dependencies;
42463
+ for (const range of directDependencies) {
42464
+ if (range.invalidSheetName || range.invalidXc) {
42465
+ continue;
42466
+ }
42467
+ rTreeItems.push({
42468
+ data: {
42469
+ sheetId,
42470
+ zone: positionToZone(this.getters.getCellPosition(cellId)),
42471
+ },
42472
+ boundingBox: { sheetId: range.sheetId, zone: range.zone },
42473
+ });
42474
+ }
42475
+ }
42476
+ }
42477
+ }
42478
+ return new FormulaDependencyGraph(rTreeItems);
42172
42479
  });
42173
42480
  }
42174
42481
  evaluateAllCells() {
42175
42482
  const start = performance.now();
42176
42483
  this.evaluatedCells = new PositionMap();
42177
- this.evaluate(this.getAllCells());
42484
+ const ranges = [];
42485
+ for (const sheetId of this.getters.getSheetIds()) {
42486
+ const zone = this.getters.getSheetZone(sheetId);
42487
+ ranges.push({ sheetId, zone });
42488
+ }
42489
+ this.evaluate(ranges);
42178
42490
  console.debug("evaluate all cells", performance.now() - start, "ms");
42179
42491
  }
42180
42492
  evaluateFormulaResult(sheetId, formulaString) {
@@ -42198,48 +42510,47 @@ class Evaluator {
42198
42510
  return handleError(error, "");
42199
42511
  }
42200
42512
  }
42201
- getAllCells() {
42202
- const positions = this.createEmptyPositionSet();
42203
- positions.fillAllPositions();
42204
- return positions;
42205
- }
42206
42513
  /**
42207
42514
  * Return the position of formulas blocked by the given positions
42208
42515
  * as well as all their dependencies.
42209
42516
  */
42210
42517
  getArrayFormulasBlockedBy(sheetId, zone) {
42211
- const arrayFormulaPositions = this.createEmptyPositionSet();
42518
+ const arrayFormulaPositions = new RangeSet();
42212
42519
  const arrayFormulas = this.spreadingRelations.searchFormulaPositionsSpreadingOn(sheetId, zone);
42213
- arrayFormulaPositions.addMany(arrayFormulas);
42520
+ arrayFormulaPositions.addManyPositions(arrayFormulas);
42214
42521
  const spilledPositions = [...arrayFormulas].filter((position) => !this.blockedArrayFormulas.has(position));
42215
42522
  if (spilledPositions.length) {
42216
42523
  // ignore the formula spreading on the position. Keep only the blocked ones
42217
- arrayFormulaPositions.deleteMany(spilledPositions);
42524
+ arrayFormulaPositions.deleteManyPositions(spilledPositions);
42218
42525
  }
42219
42526
  arrayFormulaPositions.addMany(this.getCellsDependingOn(arrayFormulaPositions));
42220
42527
  return arrayFormulaPositions;
42221
42528
  }
42222
- nextPositionsToUpdate = new PositionSet({});
42529
+ nextRangesToUpdate = new RangeSet();
42223
42530
  cellsBeingComputed = new Set();
42224
42531
  symbolsBeingComputed = new Set();
42225
- evaluate(positions) {
42532
+ evaluate(ranges) {
42226
42533
  this.cellsBeingComputed = new Set();
42227
- this.nextPositionsToUpdate = positions;
42534
+ this.nextRangesToUpdate = new RangeSet(ranges);
42228
42535
  let currentIteration = 0;
42229
- while (!this.nextPositionsToUpdate.isEmpty() && currentIteration++ < MAX_ITERATION) {
42536
+ while (!this.nextRangesToUpdate.isEmpty() && currentIteration++ < MAX_ITERATION) {
42230
42537
  this.updateCompilationParameters();
42231
- const positions = this.nextPositionsToUpdate.clear();
42232
- for (let i = 0; i < positions.length; ++i) {
42233
- this.evaluatedCells.delete(positions[i]);
42234
- }
42235
- for (let i = 0; i < positions.length; ++i) {
42236
- const position = positions[i];
42237
- if (this.nextPositionsToUpdate.has(position)) {
42238
- continue;
42239
- }
42240
- const evaluatedCell = this.computeCell(position);
42241
- if (evaluatedCell !== EMPTY_CELL) {
42242
- this.evaluatedCells.set(position, evaluatedCell);
42538
+ const ranges = [...this.nextRangesToUpdate];
42539
+ this.nextRangesToUpdate.clear();
42540
+ this.clearEvaluatedRanges(ranges);
42541
+ for (const range of ranges) {
42542
+ const { left, bottom, right, top } = range.zone;
42543
+ for (let col = left; col <= right; col++) {
42544
+ for (let row = top; row <= bottom; row++) {
42545
+ const position = { sheetId: range.sheetId, col, row };
42546
+ if (this.nextRangesToUpdate.hasPosition(position)) {
42547
+ continue;
42548
+ }
42549
+ const evaluatedCell = this.computeCell(position);
42550
+ if (evaluatedCell !== EMPTY_CELL) {
42551
+ this.evaluatedCells.set(position, evaluatedCell);
42552
+ }
42553
+ }
42243
42554
  }
42244
42555
  }
42245
42556
  onIterationEndEvaluationRegistry.getAll().forEach((callback) => callback(this.getters));
@@ -42248,6 +42559,16 @@ class Evaluator {
42248
42559
  console.warn("Maximum iteration reached while evaluating cells");
42249
42560
  }
42250
42561
  }
42562
+ clearEvaluatedRanges(ranges) {
42563
+ for (const range of ranges) {
42564
+ const { left, bottom, right, top } = range.zone;
42565
+ for (let col = left; col <= right; col++) {
42566
+ for (let row = top; row <= bottom; row++) {
42567
+ this.evaluatedCells.delete({ sheetId: range.sheetId, col, row });
42568
+ }
42569
+ }
42570
+ }
42571
+ }
42251
42572
  computeCell(position) {
42252
42573
  const evaluation = this.evaluatedCells.get(position);
42253
42574
  if (evaluation) {
@@ -42320,9 +42641,9 @@ class Evaluator {
42320
42641
  }
42321
42642
  invalidatePositionsDependingOnSpread(sheetId, resultZone) {
42322
42643
  // the result matrix is split in 2 zones to exclude the array formula position
42323
- const invalidatedPositions = this.formulaDependencies().getCellsDependingOn(excludeTopLeft(resultZone).map((zone) => ({ sheetId, zone })), this.nextPositionsToUpdate);
42324
- invalidatedPositions.delete({ sheetId, col: resultZone.left, row: resultZone.top });
42325
- this.nextPositionsToUpdate.addMany(invalidatedPositions);
42644
+ const invalidatedPositions = this.getCellsDependingOn(excludeTopLeft(resultZone).map((zone) => ({ sheetId, zone })));
42645
+ invalidatedPositions.delete({ sheetId, zone: resultZone });
42646
+ this.nextRangesToUpdate.addMany(invalidatedPositions);
42326
42647
  }
42327
42648
  assertSheetHasEnoughSpaceToSpreadFormulaResult({ sheetId, col, row }, matrixResult) {
42328
42649
  const numberOfCols = this.getters.getNumberCols(sheetId);
@@ -42397,7 +42718,7 @@ class Evaluator {
42397
42718
  }
42398
42719
  const sheetId = position.sheetId;
42399
42720
  this.invalidatePositionsDependingOnSpread(sheetId, zone);
42400
- this.nextPositionsToUpdate.addMany(this.getArrayFormulasBlockedBy(sheetId, zone));
42721
+ this.nextRangesToUpdate.addMany(this.getArrayFormulasBlockedBy(sheetId, zone));
42401
42722
  }
42402
42723
  /**
42403
42724
  * Wraps a GetSymbolValue function to add cycle detection
@@ -42432,13 +42753,8 @@ class Evaluator {
42432
42753
  }
42433
42754
  return cell.compiledFormula.dependencies;
42434
42755
  }
42435
- getCellsDependingOn(positions) {
42436
- const ranges = [];
42437
- const zonesBySheetIds = aggregatePositionsToZones(positions);
42438
- for (const sheetId in zonesBySheetIds) {
42439
- ranges.push(...zonesBySheetIds[sheetId].map((zone) => ({ sheetId, zone })));
42440
- }
42441
- return this.formulaDependencies().getCellsDependingOn(ranges, this.nextPositionsToUpdate);
42756
+ getCellsDependingOn(ranges) {
42757
+ return this.formulaDependencies().getCellsDependingOn(ranges, this.nextRangesToUpdate);
42442
42758
  }
42443
42759
  }
42444
42760
  function forEachSpreadPositionInMatrix(nbColumns, nbRows, callback) {
@@ -46105,18 +46421,8 @@ class PivotUIPlugin extends CoreViewPlugin {
46105
46421
  return EMPTY_PIVOT_CELL;
46106
46422
  }
46107
46423
  if (functionName === "PIVOT") {
46108
- const includeTotal = toScalar(args[2]);
46109
- const shouldIncludeTotal = includeTotal === undefined ? true : toBoolean(includeTotal);
46110
- const includeColumnHeaders = toScalar(args[3]);
46111
- const includeMeasures = toScalar(args[5]);
46112
- const shouldIncludeMeasures = includeMeasures === undefined ? true : toBoolean(includeMeasures);
46113
- const shouldIncludeColumnHeaders = includeColumnHeaders === undefined ? true : toBoolean(includeColumnHeaders);
46114
- const visibilityOptions = {
46115
- displayColumnHeaders: shouldIncludeColumnHeaders,
46116
- displayTotals: shouldIncludeTotal,
46117
- displayMeasuresRow: shouldIncludeMeasures,
46118
- };
46119
- const pivotCells = pivot.getCollapsedTableStructure().getPivotCells(visibilityOptions);
46424
+ const pivotStyle = getPivotStyleFromFnArgs(this.getters.getPivotCoreDefinition(pivotId), toScalar(args[1]), toScalar(args[2]), toScalar(args[3]), toScalar(args[4]), toScalar(args[5]), this.getters.getLocale());
46425
+ const pivotCells = pivot.getCollapsedTableStructure().getPivotCells(pivotStyle);
46120
46426
  const pivotCol = position.col - mainPosition.col;
46121
46427
  const pivotRow = position.row - mainPosition.row;
46122
46428
  return pivotCells[pivotCol][pivotRow];
@@ -55831,7 +56137,6 @@ class Model extends EventBus {
55831
56137
  const startSnapshot = performance.now();
55832
56138
  console.debug("Snapshot requested");
55833
56139
  this.session.snapshot(this.exportData());
55834
- this.garbageCollectExternalResources();
55835
56140
  console.debug("Snapshot taken in", performance.now() - startSnapshot, "ms");
55836
56141
  }
55837
56142
  console.debug("Model created in", performance.now() - start, "ms");
@@ -56217,11 +56522,6 @@ class Model extends EventBus {
56217
56522
  data = deepCopy$1(data);
56218
56523
  return getXLSX(data);
56219
56524
  }
56220
- garbageCollectExternalResources() {
56221
- for (const plugin of this.corePlugins) {
56222
- plugin.garbageCollectExternalResources();
56223
- }
56224
- }
56225
56525
  }
56226
56526
  function createCommand(type, payload = {}) {
56227
56527
  const command = deepCopy$1(payload);
@@ -65612,12 +65912,23 @@ cellPopoverRegistry
65612
65912
  .add("LinkEditor", LinkEditorPopoverBuilder)
65613
65913
  .add("FilterMenu", FilterMenuPopoverBuilder);
65614
65914
 
65615
- const CHART_LIMITS = {
65616
- MAX_PIE_CATEGORIES: 7,
65617
- MAX_PIE_CATEGORIES_NO_TITLE: 6,
65618
- MIN_RADAR_CATEGORIES: 3,
65619
- MAX_RADAR_CATEGORIES: 12,
65620
- PERCENTAGE_THRESHOLD: 100,
65915
+ const DEFAULT_BAR_CHART_CONFIG = {
65916
+ type: "bar",
65917
+ title: {},
65918
+ dataSets: [],
65919
+ legendPosition: "none",
65920
+ dataSetsHaveTitle: false,
65921
+ stacked: false,
65922
+ };
65923
+ const DEFAULT_LINE_CHART_CONFIG = {
65924
+ type: "line",
65925
+ title: {},
65926
+ dataSets: [],
65927
+ legendPosition: "none",
65928
+ dataSetsHaveTitle: false,
65929
+ stacked: false,
65930
+ cumulative: false,
65931
+ labelsAsText: false,
65621
65932
  };
65622
65933
  function getUnboundRange(getters, zone) {
65623
65934
  return zoneToXc(getters.getUnboundedZone(getters.getActiveSheetId(), zone));
@@ -65656,43 +65967,19 @@ function detectColumnType(cells) {
65656
65967
  return detectedType;
65657
65968
  }
65658
65969
  function categorizeColumns(zones, getters) {
65659
- const columns = {
65660
- number: [],
65661
- text: [],
65662
- date: [],
65663
- };
65970
+ const columns = [];
65664
65971
  for (const zone of getZonesByColumns(zones)) {
65665
65972
  const cells = getters.getEvaluatedCellsInZone(getters.getActiveSheetId(), zone);
65666
- const type = detectColumnType(cells);
65667
- if (type !== "empty") {
65668
- const targetType = type === "percentage" ? "number" : type;
65669
- columns[targetType].push({ zone, type });
65670
- }
65973
+ columns.push({ zone, type: detectColumnType(cells) });
65671
65974
  }
65672
65975
  return columns;
65673
65976
  }
65674
65977
  function getCellStats(getters, zone) {
65675
65978
  const cells = getters.getEvaluatedCellsInZone(getters.getActiveSheetId(), zone);
65676
- const uniqueValues = new Set();
65677
- let totalCount = 0;
65678
- let percentageSum = 0;
65679
- for (let i = 0; i < cells.length; i++) {
65680
- const { value } = cells[i];
65681
- const str = value?.toString().trim();
65682
- if (!str) {
65683
- continue;
65684
- }
65685
- uniqueValues.add(str);
65686
- totalCount++;
65687
- const num = Number(value);
65688
- if (!isNaN(num)) {
65689
- percentageSum += Math.abs(num) * 100;
65690
- }
65691
- }
65979
+ const values = cells.map((c) => c.value?.toString().trim() || "").filter((s) => s);
65692
65980
  return {
65693
- uniqueCount: uniqueValues.size,
65694
- totalCount,
65695
- percentageSum,
65981
+ uniqueCount: new Set(values).size,
65982
+ totalCount: values.length,
65696
65983
  };
65697
65984
  }
65698
65985
  function isDatasetTitled(getters, column) {
@@ -65703,167 +65990,191 @@ function isDatasetTitled(getters, column) {
65703
65990
  });
65704
65991
  return ![CellValueType.number, CellValueType.empty].includes(titleCell.type);
65705
65992
  }
65706
- function createBaseChart(type, dataSets, options = {}) {
65707
- return {
65708
- type,
65709
- title: {},
65710
- dataSets,
65711
- legendPosition: "none",
65712
- ...options,
65713
- };
65714
- }
65993
+ /**
65994
+ * Builds a chart definition for a single column selection. The logic to detect the chart type is as follows:
65995
+ * - If the column contains a single cell, create a scorecard.
65996
+ * - If the column type is "percentage", create a pie chart.
65997
+ * - If the column type is "text", create a pie chart
65998
+ * - If the column type is "date", create a line chart.
65999
+ * - Otherwise, create a bar chart.
66000
+ */
65715
66001
  function buildSingleColumnChart(column, getters) {
65716
66002
  const { type, zone } = column;
65717
66003
  const sheetId = getters.getActiveSheetId();
65718
66004
  const dataSetsHaveTitle = isDatasetTitled(getters, column);
65719
66005
  const dataRange = getUnboundRange(getters, zone);
65720
66006
  const titleCell = getters.getEvaluatedCell({ sheetId, col: zone.left, row: zone.top });
66007
+ if (getZoneArea(zone) === 1) {
66008
+ return buildScorecard(zone, getters);
66009
+ }
65721
66010
  switch (type) {
65722
66011
  case "percentage":
65723
- const { percentageSum } = getCellStats(getters, zone);
65724
- return createBaseChart("pie", [{ dataRange }], {
66012
+ return {
66013
+ type: "pie",
65725
66014
  title: dataSetsHaveTitle ? { text: String(titleCell.value) } : {},
66015
+ dataSets: [{ dataRange }],
66016
+ legendPosition: "none",
65726
66017
  dataSetsHaveTitle,
65727
- isDoughnut: percentageSum < CHART_LIMITS.PERCENTAGE_THRESHOLD,
65728
- });
66018
+ };
65729
66019
  case "text":
65730
66020
  const cells = getters.getEvaluatedCellsInZone(sheetId, zone);
65731
66021
  const titleCount = cells.reduce((count, cell) => (cell.value === titleCell.value ? count + 1 : count), 0);
65732
66022
  const hasUniqueTitle = titleCell.value !== null && titleCount === 1;
65733
- return createBaseChart("pie", [{ dataRange }], {
66023
+ return {
66024
+ type: "pie",
65734
66025
  title: hasUniqueTitle ? { text: String(titleCell.value) } : {},
66026
+ dataSets: [{ dataRange }],
65735
66027
  labelRange: dataRange,
65736
66028
  dataSetsHaveTitle: hasUniqueTitle,
65737
- isDoughnut: false,
65738
66029
  aggregated: true,
65739
66030
  legendPosition: "top",
65740
- });
65741
- // TODO: Handle date column with matrix chart when matrix chart is supported
66031
+ };
65742
66032
  case "date":
65743
- return createBaseChart("line", [{ dataRange }], {
65744
- labelRange: dataRange,
66033
+ return {
66034
+ ...DEFAULT_LINE_CHART_CONFIG,
66035
+ type: "line",
66036
+ title: dataSetsHaveTitle ? { text: String(titleCell.value) } : {},
66037
+ dataSets: [{ dataRange }],
65745
66038
  dataSetsHaveTitle,
65746
- cumulative: false,
65747
- labelsAsText: false,
65748
- });
66039
+ };
65749
66040
  }
65750
- return createBaseChart("bar", [{ dataRange }], { dataSetsHaveTitle });
66041
+ return {
66042
+ ...DEFAULT_BAR_CHART_CONFIG,
66043
+ title: dataSetsHaveTitle ? { text: String(titleCell.value) } : {},
66044
+ dataSets: [{ dataRange }],
66045
+ dataSetsHaveTitle,
66046
+ };
65751
66047
  }
66048
+ /**
66049
+ * Builds a chart definition for a selection of two columns. The logic to detect the chart type always consider the
66050
+ * columns left to right, and is as follows:
66051
+ * - any type + percentage columns: pie chart
66052
+ * - number + number columns: scatter chart
66053
+ * - date + number columns: line chart
66054
+ * - text + number columns: treemap if repetition in labels
66055
+ * - any other combination: bar chart
66056
+ */
65752
66057
  function buildTwoColumnChart(columns, getters) {
65753
- const { number: numberColumns, text: textColumns, date: dateColumns } = columns;
65754
- if (numberColumns.length === 2) {
65755
- return createBaseChart("scatter", [{ dataRange: getUnboundRange(getters, numberColumns[1].zone) }], {
65756
- labelRange: getUnboundRange(getters, numberColumns[0].zone),
65757
- dataSetsHaveTitle: isDatasetTitled(getters, numberColumns[1]),
65758
- labelsAsText: false,
65759
- });
66058
+ if (columns.length !== 2) {
66059
+ throw new Error("buildTwoColumnChart expects exactly two columns");
65760
66060
  }
65761
- // TODO: Handle date + number with matrix chart when matrix chart is supported
65762
- if (dateColumns.length === 1 && numberColumns.length === 1) {
65763
- return createBaseChart("line", [{ dataRange: getUnboundRange(getters, numberColumns[0].zone) }], {
65764
- labelRange: getUnboundRange(getters, dateColumns[0].zone),
65765
- dataSetsHaveTitle: isDatasetTitled(getters, numberColumns[0]),
65766
- aggregated: false,
65767
- cumulative: false,
66061
+ if (columns[1].type === "percentage") {
66062
+ return {
66063
+ type: "pie",
66064
+ title: {},
66065
+ dataSets: [{ dataRange: getUnboundRange(getters, columns[1].zone) }],
66066
+ labelRange: getUnboundRange(getters, columns[0].zone),
66067
+ dataSetsHaveTitle: isDatasetTitled(getters, columns[1]),
66068
+ aggregated: true,
66069
+ legendPosition: "none",
66070
+ };
66071
+ }
66072
+ if (columns[0].type === "number" && columns[1].type === "number") {
66073
+ return {
66074
+ type: "scatter",
66075
+ title: {},
66076
+ dataSets: [{ dataRange: getUnboundRange(getters, columns[1].zone) }],
66077
+ labelRange: getUnboundRange(getters, columns[0].zone),
66078
+ dataSetsHaveTitle: isDatasetTitled(getters, columns[1]),
65768
66079
  labelsAsText: false,
65769
- });
66080
+ legendPosition: "none",
66081
+ };
65770
66082
  }
65771
- if (textColumns.length === 1 && numberColumns.length === 1) {
65772
- const [textColumn] = textColumns;
65773
- const [numberColumn] = numberColumns;
66083
+ // TODO: Handle date + number with calendar chart when implemented (and change the docstring)
66084
+ if (columns[0].type === "date" && columns[1].type === "number") {
66085
+ return {
66086
+ ...DEFAULT_LINE_CHART_CONFIG,
66087
+ type: "line",
66088
+ dataSets: [{ dataRange: getUnboundRange(getters, columns[1].zone) }],
66089
+ labelRange: getUnboundRange(getters, columns[0].zone),
66090
+ dataSetsHaveTitle: isDatasetTitled(getters, columns[0]),
66091
+ };
66092
+ }
66093
+ if (columns[0].type === "text" && columns[1].type === "number") {
66094
+ const textColumn = columns[0];
66095
+ const numberColumn = columns[1];
65774
66096
  const { uniqueCount, totalCount } = getCellStats(getters, textColumn.zone);
65775
66097
  const dataSetsHaveTitle = isDatasetTitled(getters, numberColumn);
65776
- const maxCategories = dataSetsHaveTitle
65777
- ? CHART_LIMITS.MAX_PIE_CATEGORIES
65778
- : CHART_LIMITS.MAX_PIE_CATEGORIES_NO_TITLE;
65779
- const labelRange = getUnboundRange(getters, textColumn.zone);
65780
- const dataRange = getUnboundRange(getters, numberColumn.zone);
65781
- if (uniqueCount <= maxCategories) {
65782
- const { percentageSum } = getCellStats(getters, numberColumn.zone);
65783
- return createBaseChart("pie", [{ dataRange }], {
65784
- labelRange,
65785
- dataSetsHaveTitle,
65786
- isDoughnut: numberColumn.type === "percentage" && percentageSum < CHART_LIMITS.PERCENTAGE_THRESHOLD,
65787
- aggregated: true,
65788
- legendPosition: "top",
65789
- });
65790
- }
65791
- // Use treemap when categories repeat, as pie chart would be cluttered
65792
66098
  if (uniqueCount !== totalCount) {
65793
- return createBaseChart("treemap", [{ dataRange: labelRange }], {
65794
- labelRange: dataRange,
66099
+ return {
66100
+ type: "treemap",
66101
+ title: {},
66102
+ dataSets: [{ dataRange: getUnboundRange(getters, textColumn.zone) }],
66103
+ labelRange: getUnboundRange(getters, numberColumn.zone),
65795
66104
  dataSetsHaveTitle,
65796
- });
66105
+ legendPosition: "none",
66106
+ };
65797
66107
  }
65798
- return createBaseChart("bar", [{ dataRange }], {
65799
- labelRange,
65800
- dataSetsHaveTitle,
65801
- });
65802
66108
  }
65803
- const labelColumn = textColumns[0] || dateColumns[0] || numberColumns[0];
65804
- const dataColumn = numberColumns[0] || textColumns[0] || dateColumns[0];
65805
- return createBaseChart("line", [{ dataRange: getUnboundRange(getters, dataColumn.zone) }], {
65806
- labelRange: getUnboundRange(getters, labelColumn.zone),
65807
- dataSetsHaveTitle: isDatasetTitled(getters, dataColumn),
65808
- cumulative: false,
65809
- labelsAsText: true,
65810
- });
66109
+ return {
66110
+ ...DEFAULT_BAR_CHART_CONFIG,
66111
+ dataSets: [{ dataRange: getUnboundRange(getters, columns[1].zone) }],
66112
+ labelRange: getUnboundRange(getters, columns[0].zone),
66113
+ dataSetsHaveTitle: isDatasetTitled(getters, columns[1]),
66114
+ };
65811
66115
  }
66116
+ /**
66117
+ * Builds a chart definition for a selection more than two columns. The logic to detect the chart type always consider
66118
+ * the columns left to right, and is as follows:
66119
+ * - multiple text + single number/percentage columns: sunburst if 3+ text columns, treemap otherwise
66120
+ * - any type + multiple percentage columns: pie chart
66121
+ * - date + multiple number columns: line chart
66122
+ * - any other combination: bar chart
66123
+ */
65812
66124
  function buildMultiColumnChart(columns, getters) {
65813
- const { number: numberColumns, text: textColumns, date: dateColumns } = columns;
65814
- const dataSetsHaveTitle = numberColumns.some((col) => isDatasetTitled(getters, col));
65815
- if (textColumns.length >= 2 && numberColumns.length === 1) {
65816
- const sortedTextColumns = textColumns.sort((colA, colB) => getCellStats(getters, colA.zone).uniqueCount - getCellStats(getters, colB.zone).uniqueCount);
65817
- const dataSets = sortedTextColumns.map(({ zone }) => ({
66125
+ if (columns.length < 3) {
66126
+ throw new Error("buildMultiColumnChart expects at least three columns");
66127
+ }
66128
+ const dataSetsHaveTitle = columns.some((col) => col.type !== "text" && isDatasetTitled(getters, col));
66129
+ const lastColumn = columns[columns.length - 1];
66130
+ const columnsExceptLast = columns.slice(0, columns.length - 1);
66131
+ if ((lastColumn.type === "percentage" || lastColumn.type === "number") &&
66132
+ columnsExceptLast.every((col) => col.type === "text")) {
66133
+ const dataSets = columnsExceptLast.map(({ zone }) => ({
65818
66134
  dataRange: getUnboundRange(getters, zone),
65819
66135
  }));
65820
- return createBaseChart(textColumns.length >= 3 ? "sunburst" : "treemap", dataSets, {
65821
- labelRange: getUnboundRange(getters, numberColumns[0].zone),
66136
+ return {
66137
+ type: columnsExceptLast.length >= 3 ? "sunburst" : "treemap",
66138
+ title: {},
66139
+ dataSets,
66140
+ labelRange: getUnboundRange(getters, lastColumn.zone),
65822
66141
  dataSetsHaveTitle,
65823
- });
66142
+ legendPosition: "none",
66143
+ };
65824
66144
  }
65825
- const dataSets = recomputeZones(numberColumns.map((col) => col.zone)).map((zone) => ({
66145
+ const firstColumn = columns[0];
66146
+ const columnsExceptFirst = columns.slice(1);
66147
+ const rangesOfColumnsExceptFirst = columnsExceptFirst.map(({ zone }) => ({
65826
66148
  dataRange: getUnboundRange(getters, zone),
65827
66149
  }));
65828
- if (dateColumns.length === 1 && numberColumns.length > 1) {
65829
- return createBaseChart("line", dataSets, {
65830
- labelRange: getUnboundRange(getters, dateColumns[0].zone),
66150
+ if (columnsExceptFirst.every((col) => col.type === "percentage")) {
66151
+ return {
66152
+ type: "pie",
66153
+ title: {},
66154
+ dataSets: rangesOfColumnsExceptFirst,
66155
+ labelRange: getUnboundRange(getters, firstColumn.zone),
65831
66156
  dataSetsHaveTitle,
65832
- cumulative: false,
65833
- labelsAsText: false,
66157
+ aggregated: false,
65834
66158
  legendPosition: "top",
65835
- });
66159
+ };
65836
66160
  }
65837
- if (textColumns.length === 1 && numberColumns.length >= 2) {
65838
- const [textColumn] = textColumns;
65839
- const firstCell = getters.getEvaluatedCell({
65840
- sheetId: getters.getActiveSheetId(),
65841
- row: textColumn.zone.top,
65842
- col: textColumn.zone.left,
65843
- });
65844
- const { uniqueCount, totalCount } = getCellStats(getters, textColumn.zone);
65845
- const categoryCount = dataSetsHaveTitle && firstCell.value ? uniqueCount - 1 : uniqueCount;
65846
- const expectedDataCount = categoryCount * numberColumns.length + (dataSetsHaveTitle ? numberColumns.length : 0);
65847
- const actualDataCount = numberColumns.reduce((sum, dataCol) => sum + getCellStats(getters, dataCol.zone).totalCount, 0);
65848
- if (uniqueCount === totalCount &&
65849
- uniqueCount >= CHART_LIMITS.MIN_RADAR_CATEGORIES &&
65850
- uniqueCount <= CHART_LIMITS.MAX_RADAR_CATEGORIES &&
65851
- expectedDataCount === actualDataCount) {
65852
- return createBaseChart("radar", dataSets, {
65853
- title: dataSetsHaveTitle && firstCell.value ? { text: String(firstCell.value) } : {},
65854
- labelRange: getUnboundRange(getters, textColumn.zone),
65855
- dataSetsHaveTitle,
65856
- legendPosition: "top",
65857
- });
65858
- }
66161
+ if (firstColumn.type === "date" && columnsExceptFirst.every((col) => col.type === "number")) {
66162
+ return {
66163
+ ...DEFAULT_LINE_CHART_CONFIG,
66164
+ type: "line",
66165
+ dataSets: rangesOfColumnsExceptFirst,
66166
+ labelRange: getUnboundRange(getters, firstColumn.zone),
66167
+ dataSetsHaveTitle,
66168
+ legendPosition: "top",
66169
+ };
65859
66170
  }
65860
- const labelColumn = textColumns[0] || dateColumns[0] || numberColumns[0];
65861
- return createBaseChart("bar", dataSets, {
65862
- labelRange: dataSets.length ? getUnboundRange(getters, labelColumn.zone) : "",
66171
+ return {
66172
+ ...DEFAULT_BAR_CHART_CONFIG,
66173
+ dataSets: rangesOfColumnsExceptFirst,
66174
+ labelRange: getUnboundRange(getters, firstColumn.zone),
65863
66175
  dataSetsHaveTitle,
65864
- aggregated: true,
65865
66176
  legendPosition: "top",
65866
- });
66177
+ };
65867
66178
  }
65868
66179
  function buildScorecard(zone, getters) {
65869
66180
  const cell = getters.getCell({
@@ -65886,22 +66197,18 @@ function buildScorecard(zone, getters) {
65886
66197
  */
65887
66198
  function getSmartChartDefinition(zones, getters) {
65888
66199
  const columns = categorizeColumns(zones, getters);
65889
- const { number: numberColumns, text: textColumns, date: dateColumns } = columns;
65890
- const columnCount = numberColumns.length + textColumns.length + dateColumns.length;
65891
- switch (columnCount) {
65892
- case 0:
65893
- return createBaseChart("bar", [{ dataRange: getUnboundRange(getters, zones[0]) }], {
65894
- dataSetsHaveTitle: false,
65895
- });
66200
+ if (columns.length === 0 || columns.every((col) => col.type === "empty")) {
66201
+ const dataSets = columns.map(({ zone }) => ({ dataRange: getUnboundRange(getters, zone) }));
66202
+ return { ...DEFAULT_BAR_CHART_CONFIG, dataSets };
66203
+ }
66204
+ const nonEmptyColumns = columns.filter((col) => col.type !== "empty");
66205
+ switch (nonEmptyColumns.length) {
65896
66206
  case 1:
65897
- const singleColumn = numberColumns[0] || textColumns[0] || dateColumns[0];
65898
- return getZoneArea(singleColumn.zone) === 1
65899
- ? buildScorecard(singleColumn.zone, getters)
65900
- : buildSingleColumnChart(singleColumn, getters);
66207
+ return buildSingleColumnChart(nonEmptyColumns[0], getters);
65901
66208
  case 2:
65902
- return buildTwoColumnChart(columns, getters);
66209
+ return buildTwoColumnChart(nonEmptyColumns, getters);
65903
66210
  default:
65904
- return buildMultiColumnChart(columns, getters);
66211
+ return buildMultiColumnChart(nonEmptyColumns, getters);
65905
66212
  }
65906
66213
  }
65907
66214
 
@@ -66434,23 +66741,11 @@ const REINSERT_STATIC_PIVOT_CHILDREN = (env) => env.model.getters.getPivotIds().
66434
66741
  //------------------------------------------------------------------------------
66435
66742
  // Image
66436
66743
  //------------------------------------------------------------------------------
66437
- async function requestImage(env) {
66438
- try {
66439
- return await env.imageProvider.requestImage();
66440
- }
66441
- catch {
66442
- env.raiseError(_t$1("An unexpected error occurred during the image transfer"));
66443
- return;
66444
- }
66445
- }
66446
66744
  const CREATE_IMAGE = async (env) => {
66447
66745
  if (env.imageProvider) {
66448
66746
  const sheetId = env.model.getters.getActiveSheetId();
66449
66747
  const figureId = env.model.uuidGenerator.smallUuid();
66450
- const image = await requestImage(env);
66451
- if (!image) {
66452
- return;
66453
- }
66748
+ const image = await env.imageProvider.requestImage();
66454
66749
  const size = getMaxFigureSize(env.model.getters, image.size);
66455
66750
  const { col, row, offset } = centerFigurePosition(env.model.getters, size);
66456
66751
  env.model.dispatch("CREATE_IMAGE", {
@@ -72838,39 +73133,38 @@ function useAutofocus({ refName }) {
72838
73133
  }, () => [ref.el]);
72839
73134
  }
72840
73135
 
72841
- class TextInput extends Component {
72842
- static template = "o-spreadsheet-TextInput";
73136
+ class GenericInput extends Component {
72843
73137
  static props = {
72844
- value: String,
73138
+ value: [Number, String],
72845
73139
  onChange: Function,
72846
- class: {
72847
- type: String,
72848
- optional: true,
72849
- },
72850
- id: {
72851
- type: String,
72852
- optional: true,
72853
- },
72854
- placeholder: {
72855
- type: String,
72856
- optional: true,
72857
- },
72858
- autofocus: {
72859
- type: Boolean,
72860
- optional: true,
72861
- },
73140
+ class: { type: String, optional: true },
73141
+ id: { type: String, optional: true },
73142
+ placeholder: { type: String, optional: true },
73143
+ autofocus: { type: Boolean, optional: true },
72862
73144
  alwaysShowBorder: { type: Boolean, optional: true },
73145
+ selectContentOnFocus: { type: Boolean, optional: true },
72863
73146
  };
72864
- inputRef = useRef("input");
73147
+ refName = "input";
73148
+ inputRef;
72865
73149
  setup() {
73150
+ this.inputRef = useRef(this.refName);
72866
73151
  useExternalListener(window, "click", (ev) => {
72867
73152
  if (ev.target !== this.inputRef.el && this.inputRef.el?.value !== this.props.value) {
72868
73153
  this.save();
72869
73154
  }
72870
73155
  }, { capture: true });
72871
73156
  if (this.props.autofocus) {
72872
- useAutofocus({ refName: "input" });
73157
+ useAutofocus({ refName: this.refName });
72873
73158
  }
73159
+ onWillUpdateProps((nextProps) => {
73160
+ if (document.activeElement !== this.inputRef.el && this.inputRef.el) {
73161
+ this.inputRef.el.value = nextProps.value;
73162
+ }
73163
+ });
73164
+ onMounted(() => {
73165
+ if (this.inputRef.el)
73166
+ this.inputRef.el.value = this.props.value.toString();
73167
+ });
72874
73168
  }
72875
73169
  onKeyDown(ev) {
72876
73170
  switch (ev.key) {
@@ -72881,7 +73175,7 @@ class TextInput extends Component {
72881
73175
  break;
72882
73176
  case "Escape":
72883
73177
  if (this.inputRef.el) {
72884
- this.inputRef.el.value = this.props.value;
73178
+ this.inputRef.el.value = this.props.value.toString();
72885
73179
  this.inputRef.el.blur();
72886
73180
  }
72887
73181
  ev.preventDefault();
@@ -72889,12 +73183,14 @@ class TextInput extends Component {
72889
73183
  break;
72890
73184
  }
72891
73185
  }
72892
- save() {
73186
+ save(keepFocus = false) {
72893
73187
  const currentValue = (this.inputRef.el?.value || "").trim();
72894
- if (currentValue !== this.props.value) {
73188
+ if (currentValue !== this.props.value.toString()) {
72895
73189
  this.props.onChange(currentValue);
72896
73190
  }
72897
- this.inputRef.el?.blur();
73191
+ if (!keepFocus) {
73192
+ this.inputRef.el?.blur();
73193
+ }
72898
73194
  }
72899
73195
  onMouseDown(ev) {
72900
73196
  // Stop the event if the input is not focused, we handle everything in onMouseUp
@@ -72907,13 +73203,25 @@ class TextInput extends Component {
72907
73203
  const target = ev.target;
72908
73204
  if (target !== document.activeElement) {
72909
73205
  target.focus();
72910
- target.select();
73206
+ if (this.props.selectContentOnFocus) {
73207
+ target.select();
73208
+ }
72911
73209
  ev.preventDefault();
72912
73210
  ev.stopPropagation();
72913
73211
  }
72914
73212
  }
73213
+ }
73214
+
73215
+ class TextInput extends GenericInput {
73216
+ static template = "o-spreadsheet-TextInput";
73217
+ static components = {};
73218
+ static props = GenericInput.props;
72915
73219
  get inputClass() {
72916
- return [this.props.class, this.props.alwaysShowBorder ? "o-input-border" : undefined]
73220
+ return [
73221
+ this.props.class,
73222
+ "w-100 os-input",
73223
+ this.props.alwaysShowBorder ? "o-input-border" : undefined,
73224
+ ]
72917
73225
  .filter(isDefined$1)
72918
73226
  .join(" ");
72919
73227
  }
@@ -73023,6 +73331,16 @@ class FontSizeEditor extends Component {
73023
73331
  fontSizeListRef = useRef("fontSizeList");
73024
73332
  setup() {
73025
73333
  useExternalListener(window, "click", this.onExternalClick, { capture: true });
73334
+ onWillUpdateProps((nextProps) => {
73335
+ if (this.inputRef.el && document.activeElement !== this.inputRef.el) {
73336
+ this.inputRef.el.value = nextProps.currentFontSize;
73337
+ }
73338
+ });
73339
+ onMounted(() => {
73340
+ if (this.inputRef.el) {
73341
+ this.inputRef.el.value = this.props.currentFontSize.toString();
73342
+ }
73343
+ });
73026
73344
  }
73027
73345
  get popoverProps() {
73028
73346
  const { x, y, width, height } = this.rootEditorRef.el.getBoundingClientRect();
@@ -73916,7 +74234,7 @@ class BadgeSelection extends Component {
73916
74234
 
73917
74235
  class ChartTitle extends Component {
73918
74236
  static template = "o-spreadsheet.ChartTitle";
73919
- static components = { Section, TextStyler };
74237
+ static components = { Section, TextStyler, TextInput };
73920
74238
  static props = {
73921
74239
  title: { type: String, optional: true },
73922
74240
  placeholder: { type: String, optional: true },
@@ -73930,8 +74248,8 @@ class ChartTitle extends Component {
73930
74248
  title: "",
73931
74249
  placeholder: "",
73932
74250
  };
73933
- updateTitle(ev) {
73934
- this.props.updateTitle(ev.target.value);
74251
+ updateTitle(value) {
74252
+ this.props.updateTitle(value);
73935
74253
  }
73936
74254
  }
73937
74255
 
@@ -74075,6 +74393,27 @@ class ChartLegend extends Component {
74075
74393
  }
74076
74394
  }
74077
74395
 
74396
+ class NumberInput extends GenericInput {
74397
+ static template = "o-spreadsheet-NumberInput";
74398
+ static components = {};
74399
+ static props = {
74400
+ ...GenericInput.props,
74401
+ min: { type: Number, optional: true },
74402
+ max: { type: Number, optional: true },
74403
+ };
74404
+ // Very short debounce to prevent up/down arrow on number input to spam the onChange
74405
+ debouncedOnChange = debounce(this.props.onChange.bind(this), 100, true);
74406
+ save() {
74407
+ const currentValue = (this.inputRef.el?.value || "").trim();
74408
+ if (currentValue !== this.props.value.toString()) {
74409
+ this.debouncedOnChange(currentValue);
74410
+ }
74411
+ }
74412
+ get inputClass() {
74413
+ return [this.props.class, "o-input"].join(" ");
74414
+ }
74415
+ }
74416
+
74078
74417
  class SeriesDesignEditor extends Component {
74079
74418
  static template = "o-spreadsheet-SeriesDesignEditor";
74080
74419
  static components = {
@@ -74146,6 +74485,7 @@ class SeriesWithAxisDesignEditor extends Component {
74146
74485
  RadioSelection,
74147
74486
  Section,
74148
74487
  RoundColorPicker,
74488
+ NumberInput,
74149
74489
  };
74150
74490
  static props = {
74151
74491
  chartId: String,
@@ -74237,9 +74577,8 @@ class SeriesWithAxisDesignEditor extends Component {
74237
74577
  get defaultWindowSize() {
74238
74578
  return DEFAULT_WINDOW_SIZE;
74239
74579
  }
74240
- onChangeMovingAverageWindow(index, ev) {
74241
- const element = ev.target;
74242
- let window = parseInt(element.value) || DEFAULT_WINDOW_SIZE;
74580
+ onChangeMovingAverageWindow(index, value) {
74581
+ let window = parseInt(value) || DEFAULT_WINDOW_SIZE;
74243
74582
  if (window <= 1) {
74244
74583
  window = DEFAULT_WINDOW_SIZE;
74245
74584
  }
@@ -74768,10 +75107,8 @@ class LineChartDesignPanel extends GenericZoomableChartDesignPanel {
74768
75107
 
74769
75108
  class PieHoleSize extends Component {
74770
75109
  static template = "o-spreadsheet.PieHoleSize";
74771
- static components = { Section };
75110
+ static components = { Section, NumberInput };
74772
75111
  static props = { onValueChange: Function, value: Number };
74773
- // Very short debounce to prevent up/down arrow on number input to spam the onChange
74774
- debouncedOnChange = debounce(this.onChange.bind(this), 100);
74775
75112
  onChange(value) {
74776
75113
  if (!isNaN(Number(value))) {
74777
75114
  this.props.onValueChange(clip(Number(value), 0, 95));
@@ -78049,16 +78386,18 @@ class PivotTitleSection extends Component {
78049
78386
 
78050
78387
  class PivotSidePanelStore extends SpreadsheetStore {
78051
78388
  pivotId;
78389
+ updateMode;
78052
78390
  mutators = ["reset", "deferUpdates", "applyUpdate", "discardPendingUpdate", "update"];
78053
- updatesAreDeferred;
78391
+ _updatesAreDeferred;
78054
78392
  draft = null;
78055
78393
  notification = this.get(NotificationStore);
78056
78394
  alreadyNotified = false;
78057
78395
  alreadyNotifiedForPivotSize = false;
78058
- constructor(get, pivotId) {
78396
+ constructor(get, pivotId, updateMode = "canDefer") {
78059
78397
  super(get);
78060
78398
  this.pivotId = pivotId;
78061
- this.updatesAreDeferred =
78399
+ this.updateMode = updateMode;
78400
+ this._updatesAreDeferred =
78062
78401
  this.getters.getPivotCoreDefinition(this.pivotId).deferUpdates ?? false;
78063
78402
  }
78064
78403
  handle(cmd) {
@@ -78069,6 +78408,9 @@ class PivotSidePanelStore extends SpreadsheetStore {
78069
78408
  }
78070
78409
  }
78071
78410
  }
78411
+ get updatesAreDeferred() {
78412
+ return this.updateMode === "neverDefer" ? false : this._updatesAreDeferred;
78413
+ }
78072
78414
  get fields() {
78073
78415
  return this.pivot.getFields();
78074
78416
  }
@@ -78143,7 +78485,7 @@ class PivotSidePanelStore extends SpreadsheetStore {
78143
78485
  }
78144
78486
  reset(pivotId) {
78145
78487
  this.pivotId = pivotId;
78146
- this.updatesAreDeferred = true;
78488
+ this._updatesAreDeferred = true;
78147
78489
  this.draft = null;
78148
78490
  }
78149
78491
  deferUpdates(shouldDefer) {
@@ -78154,7 +78496,7 @@ class PivotSidePanelStore extends SpreadsheetStore {
78154
78496
  else {
78155
78497
  this.update({ deferUpdates: shouldDefer });
78156
78498
  }
78157
- this.updatesAreDeferred = shouldDefer;
78499
+ this._updatesAreDeferred = shouldDefer;
78158
78500
  }
78159
78501
  applyUpdate() {
78160
78502
  if (this.draft) {
@@ -78408,6 +78750,26 @@ pivotSidePanelRegistry.add("SPREADSHEET", {
78408
78750
  editor: PivotSpreadsheetSidePanel,
78409
78751
  });
78410
78752
 
78753
+ class PivotDesignPanel extends Component {
78754
+ static template = "o-spreadsheet-PivotDesignPanel";
78755
+ static props = { pivotId: String };
78756
+ static components = { Section, Checkbox };
78757
+ store;
78758
+ setup() {
78759
+ this.store = useLocalStore(PivotSidePanelStore, this.props.pivotId, "neverDefer");
78760
+ }
78761
+ updatePivotStyleProperty(key, value) {
78762
+ this.store.update({ style: { ...this.pivotStyle, [key]: value } });
78763
+ }
78764
+ get pivotStyle() {
78765
+ const pivot = this.env.model.getters.getPivotCoreDefinition(this.props.pivotId);
78766
+ return pivot.style || {};
78767
+ }
78768
+ get defaultStyle() {
78769
+ return DEFAULT_PIVOT_STYLE;
78770
+ }
78771
+ }
78772
+
78411
78773
  class PivotSidePanel extends Component {
78412
78774
  static template = "o-spreadsheet-PivotSidePanel";
78413
78775
  static props = {
@@ -78417,6 +78779,13 @@ class PivotSidePanel extends Component {
78417
78779
  static components = {
78418
78780
  PivotLayoutConfigurator,
78419
78781
  Section,
78782
+ PivotDesignPanel,
78783
+ };
78784
+ state = useState({ panel: "configuration" });
78785
+ panelContentRef = useRef("panelContent");
78786
+ scrollPositions = {
78787
+ configuration: 0,
78788
+ design: 0,
78420
78789
  };
78421
78790
  setup() {
78422
78791
  useHighlights(this);
@@ -78431,6 +78800,13 @@ class PivotSidePanel extends Component {
78431
78800
  get highlights() {
78432
78801
  return getPivotHighlights(this.env.model.getters, this.props.pivotId);
78433
78802
  }
78803
+ switchPanel(panel) {
78804
+ const el = this.panelContentRef.el;
78805
+ if (el) {
78806
+ this.scrollPositions[this.state.panel] = el.scrollTop;
78807
+ }
78808
+ this.state.panel = panel;
78809
+ }
78434
78810
  }
78435
78811
 
78436
78812
  class RemoveDuplicatesPanel extends Component {
@@ -78936,7 +79312,14 @@ class TableStylePicker extends Component {
78936
79312
 
78937
79313
  class TablePanel extends Component {
78938
79314
  static template = "o-spreadsheet-TablePanel";
78939
- static components = { TableStylePicker, SelectionInput, ValidationMessages, Checkbox, Section };
79315
+ static components = {
79316
+ TableStylePicker,
79317
+ SelectionInput,
79318
+ ValidationMessages,
79319
+ Checkbox,
79320
+ Section,
79321
+ NumberInput,
79322
+ };
78940
79323
  static props = { onCloseSidePanel: Function, table: Object };
78941
79324
  state;
78942
79325
  setup() {
@@ -78986,13 +79369,9 @@ class TablePanel extends Component {
78986
79369
  this.state.tableZoneErrors = [];
78987
79370
  }
78988
79371
  }
78989
- onChangeNumberOfHeaders(ev) {
78990
- const input = ev.target;
78991
- const numberOfHeaders = parseInt(input.value);
78992
- const result = this.updateNumberOfHeaders(numberOfHeaders);
78993
- if (!result.isSuccessful) {
78994
- input.value = this.props.table.config.numberOfHeaders.toString();
78995
- }
79372
+ onChangeNumberOfHeaders(value) {
79373
+ const numberOfHeaders = parseInt(value);
79374
+ this.updateNumberOfHeaders(numberOfHeaders);
78996
79375
  }
78997
79376
  updateNumberOfHeaders(numberOfHeaders) {
78998
79377
  const hasFilters = numberOfHeaders > 0 && (this.tableConfig.hasFilters || this.state.filtersEnabledIfPossible);
@@ -87477,6 +87856,7 @@ const components = {
87477
87856
  GeoChartRegionSelectSection,
87478
87857
  ChartDashboardMenu,
87479
87858
  FullScreenFigure,
87859
+ NumberInput,
87480
87860
  };
87481
87861
  const hooks = {
87482
87862
  useDragAndDropListItems,
@@ -87526,6 +87906,6 @@ const chartHelpers = { ...CHART_HELPERS, ...CHART_RUNTIME_HELPERS };
87526
87906
  export { AbstractCellClipboardHandler, AbstractChart, AbstractFigureClipboardHandler, CellErrorType$1 as CellErrorType, ClientDisconnectedError, CommandResult, CorePlugin, CoreViewPlugin, DEFAULT_LOCALE, DEFAULT_LOCALES, DispatchResult, EvaluationError, LocalTransportService, Model, PivotRuntimeDefinition, Registry, Revision, SPREADSHEET_DIMENSIONS, Spreadsheet, SpreadsheetPivotTable, UIPlugin, __info__, addFunction, addRenderingLayer, astToFormula, categories, chartHelpers, compile, compileTokens, components, constants, convertAstNodes, coreTypes, createAutocompleteArgumentsProvider, findCellInNewZone, functionCache, getCaretDownSvg, getCaretUpSvg, helpers, hooks, invalidateCFEvaluationCommands, invalidateChartEvaluationCommands, invalidateDependenciesCommands, invalidateEvaluationCommands, iterateAstNodes, links, load, parse, parseTokens, readonlyAllowedCommands, registries, setDefaultSheetViewSize, setTranslationMethod, stores, tokenColors, tokenize };
87527
87907
 
87528
87908
 
87529
- __info__.version = "19.1.0-alpha.7";
87530
- __info__.date = "2025-10-17T11:09:35.690Z";
87531
- __info__.hash = "a11279d";
87909
+ __info__.version = "19.1.0-alpha.9";
87910
+ __info__.date = "2025-10-23T11:12:55.400Z";
87911
+ __info__.hash = "bd756dd";