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