@odoo/o-spreadsheet 19.0.6 → 19.0.7

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.0.6
6
- * @date 2025-10-16T06:39:36.282Z
7
- * @hash 0d4315a
5
+ * @version 19.0.7
6
+ * @date 2025-10-23T08:19:01.764Z
7
+ * @hash 1c1d1ec
8
8
  */
9
9
 
10
10
  (function (exports, owl) {
@@ -1892,6 +1892,29 @@
1892
1892
  removeContiguousProfiles(profilesStartingPosition, profiles, leftIndex, rightIndex);
1893
1893
  }
1894
1894
  }
1895
+ function profilesContainsZone(profilesStartingPosition, profiles, zone) {
1896
+ const leftValue = zone.left;
1897
+ const rightValue = zone.right;
1898
+ const topValue = zone.top;
1899
+ const bottomValue = zone.bottom + 1;
1900
+ const leftIndex = binaryPredecessorSearch(profilesStartingPosition, leftValue, 0);
1901
+ const rightIndex = binaryPredecessorSearch(profilesStartingPosition, rightValue, leftIndex);
1902
+ if (leftIndex === -1 || rightIndex === -1) {
1903
+ return false;
1904
+ }
1905
+ for (let i = leftIndex; i <= rightIndex; i++) {
1906
+ const profile = profiles.get(profilesStartingPosition[i]);
1907
+ const topPredIndex = binaryPredecessorSearch(profile, topValue, 0, true);
1908
+ const bottomSuccIndex = binarySuccessorSearch(profile, bottomValue, 0, true);
1909
+ if (topPredIndex === -1 || topPredIndex % 2 !== 0) {
1910
+ return false;
1911
+ }
1912
+ if (topValue < profile[topPredIndex] || bottomValue > profile[bottomSuccIndex]) {
1913
+ return false;
1914
+ }
1915
+ }
1916
+ return true;
1917
+ }
1895
1918
  function findIndexAndCreateProfile(profilesStartingPosition, profiles, value, searchLeft, startIndex) {
1896
1919
  if (value === undefined) {
1897
1920
  // this is only the case when the value correspond to a bottom value that could be undefined
@@ -1976,7 +1999,18 @@
1976
1999
  }
1977
2000
  // add the top and bottom value to the profile and
1978
2001
  // remove all information between the top and bottom index
1979
- profile.splice(topPredIndex + 1, bottomSuccIndex - topPredIndex - 1, ...newPoints);
2002
+ const toDelete = bottomSuccIndex - topPredIndex - 1;
2003
+ const toInsert = newPoints.length;
2004
+ const start = topPredIndex + 1;
2005
+ // fast path and slow path
2006
+ if (start === profile.length - 1 && toDelete === 1 && toInsert === 1) {
2007
+ // fast path: we just need to replace the last element
2008
+ profile[start] = newPoints[0] ?? newPoints[1];
2009
+ }
2010
+ else {
2011
+ // equivalent but slower and with memory allocation
2012
+ profile.splice(start, toDelete, ...newPoints);
2013
+ }
1980
2014
  }
1981
2015
  function removeContiguousProfiles(profilesStartingPosition, profiles, leftIndex, rightIndex) {
1982
2016
  const start = leftIndex - 1 === -1 ? 0 : leftIndex - 1;
@@ -2015,8 +2049,10 @@
2015
2049
  left,
2016
2050
  bottom,
2017
2051
  right,
2018
- hasHeader: (bottom === undefined && top !== 0) || (right === undefined && left !== 0),
2019
2052
  };
2053
+ if ((bottom === undefined && top !== 0) || (right === undefined && left !== 0)) {
2054
+ profileZone.hasHeader = true;
2055
+ }
2020
2056
  let findCorrespondingZone = false;
2021
2057
  for (let j = pendingZones.length - 1; j >= 0; j--) {
2022
2058
  const pendingZone = pendingZones[j];
@@ -2501,17 +2537,6 @@
2501
2537
  }
2502
2538
  return [leftColumnZone, rightPartZone];
2503
2539
  }
2504
- function aggregatePositionsToZones(positions) {
2505
- const result = {};
2506
- for (const position of positions) {
2507
- result[position.sheetId] ??= [];
2508
- result[position.sheetId].push(positionToZone(position));
2509
- }
2510
- for (const sheetId in result) {
2511
- result[sheetId] = recomputeZones(result[sheetId]);
2512
- }
2513
- return result;
2514
- }
2515
2540
  /**
2516
2541
  * Array of all positions in the zone.
2517
2542
  */
@@ -6943,6 +6968,10 @@
6943
6968
  }
6944
6969
  return parts;
6945
6970
  }
6971
+ function positionToBoundedRange(position) {
6972
+ const zone = { left: position.col, top: position.row, right: position.col, bottom: position.row };
6973
+ return { sheetId: position.sheetId, zone };
6974
+ }
6946
6975
  /**
6947
6976
  * Check that a zone is valid regarding the order of top-bottom and left-right.
6948
6977
  * Left should be smaller than right, top should be smaller than bottom.
@@ -38696,12 +38725,23 @@ stores.inject(MyMetaStore, storeInstance);
38696
38725
  .add("LinkEditor", LinkEditorPopoverBuilder)
38697
38726
  .add("FilterMenu", FilterMenuPopoverBuilder);
38698
38727
 
38699
- const CHART_LIMITS = {
38700
- MAX_PIE_CATEGORIES: 7,
38701
- MAX_PIE_CATEGORIES_NO_TITLE: 6,
38702
- MIN_RADAR_CATEGORIES: 3,
38703
- MAX_RADAR_CATEGORIES: 12,
38704
- PERCENTAGE_THRESHOLD: 100,
38728
+ const DEFAULT_BAR_CHART_CONFIG = {
38729
+ type: "bar",
38730
+ title: {},
38731
+ dataSets: [],
38732
+ legendPosition: "none",
38733
+ dataSetsHaveTitle: false,
38734
+ stacked: false,
38735
+ };
38736
+ const DEFAULT_LINE_CHART_CONFIG = {
38737
+ type: "line",
38738
+ title: {},
38739
+ dataSets: [],
38740
+ legendPosition: "none",
38741
+ dataSetsHaveTitle: false,
38742
+ stacked: false,
38743
+ cumulative: false,
38744
+ labelsAsText: false,
38705
38745
  };
38706
38746
  function getUnboundRange(getters, zone) {
38707
38747
  return zoneToXc(getters.getUnboundedZone(getters.getActiveSheetId(), zone));
@@ -38740,43 +38780,19 @@ stores.inject(MyMetaStore, storeInstance);
38740
38780
  return detectedType;
38741
38781
  }
38742
38782
  function categorizeColumns(zones, getters) {
38743
- const columns = {
38744
- number: [],
38745
- text: [],
38746
- date: [],
38747
- };
38783
+ const columns = [];
38748
38784
  for (const zone of getZonesByColumns(zones)) {
38749
38785
  const cells = getters.getEvaluatedCellsInZone(getters.getActiveSheetId(), zone);
38750
- const type = detectColumnType(cells);
38751
- if (type !== "empty") {
38752
- const targetType = type === "percentage" ? "number" : type;
38753
- columns[targetType].push({ zone, type });
38754
- }
38786
+ columns.push({ zone, type: detectColumnType(cells) });
38755
38787
  }
38756
38788
  return columns;
38757
38789
  }
38758
38790
  function getCellStats(getters, zone) {
38759
38791
  const cells = getters.getEvaluatedCellsInZone(getters.getActiveSheetId(), zone);
38760
- const uniqueValues = new Set();
38761
- let totalCount = 0;
38762
- let percentageSum = 0;
38763
- for (let i = 0; i < cells.length; i++) {
38764
- const { value } = cells[i];
38765
- const str = value?.toString().trim();
38766
- if (!str) {
38767
- continue;
38768
- }
38769
- uniqueValues.add(str);
38770
- totalCount++;
38771
- const num = Number(value);
38772
- if (!isNaN(num)) {
38773
- percentageSum += Math.abs(num) * 100;
38774
- }
38775
- }
38792
+ const values = cells.map((c) => c.value?.toString().trim() || "").filter((s) => s);
38776
38793
  return {
38777
- uniqueCount: uniqueValues.size,
38778
- totalCount,
38779
- percentageSum,
38794
+ uniqueCount: new Set(values).size,
38795
+ totalCount: values.length,
38780
38796
  };
38781
38797
  }
38782
38798
  function isDatasetTitled(getters, column) {
@@ -38787,167 +38803,191 @@ stores.inject(MyMetaStore, storeInstance);
38787
38803
  });
38788
38804
  return ![CellValueType.number, CellValueType.empty].includes(titleCell.type);
38789
38805
  }
38790
- function createBaseChart(type, dataSets, options = {}) {
38791
- return {
38792
- type,
38793
- title: {},
38794
- dataSets,
38795
- legendPosition: "none",
38796
- ...options,
38797
- };
38798
- }
38806
+ /**
38807
+ * Builds a chart definition for a single column selection. The logic to detect the chart type is as follows:
38808
+ * - If the column contains a single cell, create a scorecard.
38809
+ * - If the column type is "percentage", create a pie chart.
38810
+ * - If the column type is "text", create a pie chart
38811
+ * - If the column type is "date", create a line chart.
38812
+ * - Otherwise, create a bar chart.
38813
+ */
38799
38814
  function buildSingleColumnChart(column, getters) {
38800
38815
  const { type, zone } = column;
38801
38816
  const sheetId = getters.getActiveSheetId();
38802
38817
  const dataSetsHaveTitle = isDatasetTitled(getters, column);
38803
38818
  const dataRange = getUnboundRange(getters, zone);
38804
38819
  const titleCell = getters.getEvaluatedCell({ sheetId, col: zone.left, row: zone.top });
38820
+ if (getZoneArea(zone) === 1) {
38821
+ return buildScorecard(zone, getters);
38822
+ }
38805
38823
  switch (type) {
38806
38824
  case "percentage":
38807
- const { percentageSum } = getCellStats(getters, zone);
38808
- return createBaseChart("pie", [{ dataRange }], {
38825
+ return {
38826
+ type: "pie",
38809
38827
  title: dataSetsHaveTitle ? { text: String(titleCell.value) } : {},
38828
+ dataSets: [{ dataRange }],
38829
+ legendPosition: "none",
38810
38830
  dataSetsHaveTitle,
38811
- isDoughnut: percentageSum < CHART_LIMITS.PERCENTAGE_THRESHOLD,
38812
- });
38831
+ };
38813
38832
  case "text":
38814
38833
  const cells = getters.getEvaluatedCellsInZone(sheetId, zone);
38815
38834
  const titleCount = cells.reduce((count, cell) => (cell.value === titleCell.value ? count + 1 : count), 0);
38816
38835
  const hasUniqueTitle = titleCell.value !== null && titleCount === 1;
38817
- return createBaseChart("pie", [{ dataRange }], {
38836
+ return {
38837
+ type: "pie",
38818
38838
  title: hasUniqueTitle ? { text: String(titleCell.value) } : {},
38839
+ dataSets: [{ dataRange }],
38819
38840
  labelRange: dataRange,
38820
38841
  dataSetsHaveTitle: hasUniqueTitle,
38821
- isDoughnut: false,
38822
38842
  aggregated: true,
38823
38843
  legendPosition: "top",
38824
- });
38825
- // TODO: Handle date column with matrix chart when matrix chart is supported
38844
+ };
38826
38845
  case "date":
38827
- return createBaseChart("line", [{ dataRange }], {
38828
- labelRange: dataRange,
38846
+ return {
38847
+ ...DEFAULT_LINE_CHART_CONFIG,
38848
+ type: "line",
38849
+ title: dataSetsHaveTitle ? { text: String(titleCell.value) } : {},
38850
+ dataSets: [{ dataRange }],
38829
38851
  dataSetsHaveTitle,
38830
- cumulative: false,
38831
- labelsAsText: false,
38832
- });
38852
+ };
38833
38853
  }
38834
- return createBaseChart("bar", [{ dataRange }], { dataSetsHaveTitle });
38854
+ return {
38855
+ ...DEFAULT_BAR_CHART_CONFIG,
38856
+ title: dataSetsHaveTitle ? { text: String(titleCell.value) } : {},
38857
+ dataSets: [{ dataRange }],
38858
+ dataSetsHaveTitle,
38859
+ };
38835
38860
  }
38861
+ /**
38862
+ * Builds a chart definition for a selection of two columns. The logic to detect the chart type always consider the
38863
+ * columns left to right, and is as follows:
38864
+ * - any type + percentage columns: pie chart
38865
+ * - number + number columns: scatter chart
38866
+ * - date + number columns: line chart
38867
+ * - text + number columns: treemap if repetition in labels
38868
+ * - any other combination: bar chart
38869
+ */
38836
38870
  function buildTwoColumnChart(columns, getters) {
38837
- const { number: numberColumns, text: textColumns, date: dateColumns } = columns;
38838
- if (numberColumns.length === 2) {
38839
- return createBaseChart("scatter", [{ dataRange: getUnboundRange(getters, numberColumns[1].zone) }], {
38840
- labelRange: getUnboundRange(getters, numberColumns[0].zone),
38841
- dataSetsHaveTitle: isDatasetTitled(getters, numberColumns[1]),
38842
- labelsAsText: false,
38843
- });
38871
+ if (columns.length !== 2) {
38872
+ throw new Error("buildTwoColumnChart expects exactly two columns");
38844
38873
  }
38845
- // TODO: Handle date + number with matrix chart when matrix chart is supported
38846
- if (dateColumns.length === 1 && numberColumns.length === 1) {
38847
- return createBaseChart("line", [{ dataRange: getUnboundRange(getters, numberColumns[0].zone) }], {
38848
- labelRange: getUnboundRange(getters, dateColumns[0].zone),
38849
- dataSetsHaveTitle: isDatasetTitled(getters, numberColumns[0]),
38850
- aggregated: false,
38851
- cumulative: false,
38874
+ if (columns[1].type === "percentage") {
38875
+ return {
38876
+ type: "pie",
38877
+ title: {},
38878
+ dataSets: [{ dataRange: getUnboundRange(getters, columns[1].zone) }],
38879
+ labelRange: getUnboundRange(getters, columns[0].zone),
38880
+ dataSetsHaveTitle: isDatasetTitled(getters, columns[1]),
38881
+ aggregated: true,
38882
+ legendPosition: "none",
38883
+ };
38884
+ }
38885
+ if (columns[0].type === "number" && columns[1].type === "number") {
38886
+ return {
38887
+ type: "scatter",
38888
+ title: {},
38889
+ dataSets: [{ dataRange: getUnboundRange(getters, columns[1].zone) }],
38890
+ labelRange: getUnboundRange(getters, columns[0].zone),
38891
+ dataSetsHaveTitle: isDatasetTitled(getters, columns[1]),
38852
38892
  labelsAsText: false,
38853
- });
38893
+ legendPosition: "none",
38894
+ };
38854
38895
  }
38855
- if (textColumns.length === 1 && numberColumns.length === 1) {
38856
- const [textColumn] = textColumns;
38857
- const [numberColumn] = numberColumns;
38896
+ // TODO: Handle date + number with calendar chart when implemented (and change the docstring)
38897
+ if (columns[0].type === "date" && columns[1].type === "number") {
38898
+ return {
38899
+ ...DEFAULT_LINE_CHART_CONFIG,
38900
+ type: "line",
38901
+ dataSets: [{ dataRange: getUnboundRange(getters, columns[1].zone) }],
38902
+ labelRange: getUnboundRange(getters, columns[0].zone),
38903
+ dataSetsHaveTitle: isDatasetTitled(getters, columns[0]),
38904
+ };
38905
+ }
38906
+ if (columns[0].type === "text" && columns[1].type === "number") {
38907
+ const textColumn = columns[0];
38908
+ const numberColumn = columns[1];
38858
38909
  const { uniqueCount, totalCount } = getCellStats(getters, textColumn.zone);
38859
38910
  const dataSetsHaveTitle = isDatasetTitled(getters, numberColumn);
38860
- const maxCategories = dataSetsHaveTitle
38861
- ? CHART_LIMITS.MAX_PIE_CATEGORIES
38862
- : CHART_LIMITS.MAX_PIE_CATEGORIES_NO_TITLE;
38863
- const labelRange = getUnboundRange(getters, textColumn.zone);
38864
- const dataRange = getUnboundRange(getters, numberColumn.zone);
38865
- if (uniqueCount <= maxCategories) {
38866
- const { percentageSum } = getCellStats(getters, numberColumn.zone);
38867
- return createBaseChart("pie", [{ dataRange }], {
38868
- labelRange,
38869
- dataSetsHaveTitle,
38870
- isDoughnut: numberColumn.type === "percentage" && percentageSum < CHART_LIMITS.PERCENTAGE_THRESHOLD,
38871
- aggregated: true,
38872
- legendPosition: "top",
38873
- });
38874
- }
38875
- // Use treemap when categories repeat, as pie chart would be cluttered
38876
38911
  if (uniqueCount !== totalCount) {
38877
- return createBaseChart("treemap", [{ dataRange: labelRange }], {
38878
- labelRange: dataRange,
38912
+ return {
38913
+ type: "treemap",
38914
+ title: {},
38915
+ dataSets: [{ dataRange: getUnboundRange(getters, textColumn.zone) }],
38916
+ labelRange: getUnboundRange(getters, numberColumn.zone),
38879
38917
  dataSetsHaveTitle,
38880
- });
38918
+ legendPosition: "none",
38919
+ };
38881
38920
  }
38882
- return createBaseChart("bar", [{ dataRange }], {
38883
- labelRange,
38884
- dataSetsHaveTitle,
38885
- });
38886
38921
  }
38887
- const labelColumn = textColumns[0] || dateColumns[0] || numberColumns[0];
38888
- const dataColumn = numberColumns[0] || textColumns[0] || dateColumns[0];
38889
- return createBaseChart("line", [{ dataRange: getUnboundRange(getters, dataColumn.zone) }], {
38890
- labelRange: getUnboundRange(getters, labelColumn.zone),
38891
- dataSetsHaveTitle: isDatasetTitled(getters, dataColumn),
38892
- cumulative: false,
38893
- labelsAsText: true,
38894
- });
38922
+ return {
38923
+ ...DEFAULT_BAR_CHART_CONFIG,
38924
+ dataSets: [{ dataRange: getUnboundRange(getters, columns[1].zone) }],
38925
+ labelRange: getUnboundRange(getters, columns[0].zone),
38926
+ dataSetsHaveTitle: isDatasetTitled(getters, columns[1]),
38927
+ };
38895
38928
  }
38929
+ /**
38930
+ * Builds a chart definition for a selection more than two columns. The logic to detect the chart type always consider
38931
+ * the columns left to right, and is as follows:
38932
+ * - multiple text + single number/percentage columns: sunburst if 3+ text columns, treemap otherwise
38933
+ * - any type + multiple percentage columns: pie chart
38934
+ * - date + multiple number columns: line chart
38935
+ * - any other combination: bar chart
38936
+ */
38896
38937
  function buildMultiColumnChart(columns, getters) {
38897
- const { number: numberColumns, text: textColumns, date: dateColumns } = columns;
38898
- const dataSetsHaveTitle = numberColumns.some((col) => isDatasetTitled(getters, col));
38899
- if (textColumns.length >= 2 && numberColumns.length === 1) {
38900
- const sortedTextColumns = textColumns.sort((colA, colB) => getCellStats(getters, colA.zone).uniqueCount - getCellStats(getters, colB.zone).uniqueCount);
38901
- const dataSets = sortedTextColumns.map(({ zone }) => ({
38938
+ if (columns.length < 3) {
38939
+ throw new Error("buildMultiColumnChart expects at least three columns");
38940
+ }
38941
+ const dataSetsHaveTitle = columns.some((col) => col.type !== "text" && isDatasetTitled(getters, col));
38942
+ const lastColumn = columns[columns.length - 1];
38943
+ const columnsExceptLast = columns.slice(0, columns.length - 1);
38944
+ if ((lastColumn.type === "percentage" || lastColumn.type === "number") &&
38945
+ columnsExceptLast.every((col) => col.type === "text")) {
38946
+ const dataSets = columnsExceptLast.map(({ zone }) => ({
38902
38947
  dataRange: getUnboundRange(getters, zone),
38903
38948
  }));
38904
- return createBaseChart(textColumns.length >= 3 ? "sunburst" : "treemap", dataSets, {
38905
- labelRange: getUnboundRange(getters, numberColumns[0].zone),
38949
+ return {
38950
+ type: columnsExceptLast.length >= 3 ? "sunburst" : "treemap",
38951
+ title: {},
38952
+ dataSets,
38953
+ labelRange: getUnboundRange(getters, lastColumn.zone),
38906
38954
  dataSetsHaveTitle,
38907
- });
38955
+ legendPosition: "none",
38956
+ };
38908
38957
  }
38909
- const dataSets = recomputeZones(numberColumns.map((col) => col.zone)).map((zone) => ({
38958
+ const firstColumn = columns[0];
38959
+ const columnsExceptFirst = columns.slice(1);
38960
+ const rangesOfColumnsExceptFirst = columnsExceptFirst.map(({ zone }) => ({
38910
38961
  dataRange: getUnboundRange(getters, zone),
38911
38962
  }));
38912
- if (dateColumns.length === 1 && numberColumns.length > 1) {
38913
- return createBaseChart("line", dataSets, {
38914
- labelRange: getUnboundRange(getters, dateColumns[0].zone),
38963
+ if (columnsExceptFirst.every((col) => col.type === "percentage")) {
38964
+ return {
38965
+ type: "pie",
38966
+ title: {},
38967
+ dataSets: rangesOfColumnsExceptFirst,
38968
+ labelRange: getUnboundRange(getters, firstColumn.zone),
38915
38969
  dataSetsHaveTitle,
38916
- cumulative: false,
38917
- labelsAsText: false,
38970
+ aggregated: false,
38918
38971
  legendPosition: "top",
38919
- });
38972
+ };
38920
38973
  }
38921
- if (textColumns.length === 1 && numberColumns.length >= 2) {
38922
- const [textColumn] = textColumns;
38923
- const firstCell = getters.getEvaluatedCell({
38924
- sheetId: getters.getActiveSheetId(),
38925
- row: textColumn.zone.top,
38926
- col: textColumn.zone.left,
38927
- });
38928
- const { uniqueCount, totalCount } = getCellStats(getters, textColumn.zone);
38929
- const categoryCount = dataSetsHaveTitle && firstCell.value ? uniqueCount - 1 : uniqueCount;
38930
- const expectedDataCount = categoryCount * numberColumns.length + (dataSetsHaveTitle ? numberColumns.length : 0);
38931
- const actualDataCount = numberColumns.reduce((sum, dataCol) => sum + getCellStats(getters, dataCol.zone).totalCount, 0);
38932
- if (uniqueCount === totalCount &&
38933
- uniqueCount >= CHART_LIMITS.MIN_RADAR_CATEGORIES &&
38934
- uniqueCount <= CHART_LIMITS.MAX_RADAR_CATEGORIES &&
38935
- expectedDataCount === actualDataCount) {
38936
- return createBaseChart("radar", dataSets, {
38937
- title: dataSetsHaveTitle && firstCell.value ? { text: String(firstCell.value) } : {},
38938
- labelRange: getUnboundRange(getters, textColumn.zone),
38939
- dataSetsHaveTitle,
38940
- legendPosition: "top",
38941
- });
38942
- }
38974
+ if (firstColumn.type === "date" && columnsExceptFirst.every((col) => col.type === "number")) {
38975
+ return {
38976
+ ...DEFAULT_LINE_CHART_CONFIG,
38977
+ type: "line",
38978
+ dataSets: rangesOfColumnsExceptFirst,
38979
+ labelRange: getUnboundRange(getters, firstColumn.zone),
38980
+ dataSetsHaveTitle,
38981
+ legendPosition: "top",
38982
+ };
38943
38983
  }
38944
- const labelColumn = textColumns[0] || dateColumns[0] || numberColumns[0];
38945
- return createBaseChart("bar", dataSets, {
38946
- labelRange: dataSets.length ? getUnboundRange(getters, labelColumn.zone) : "",
38984
+ return {
38985
+ ...DEFAULT_BAR_CHART_CONFIG,
38986
+ dataSets: rangesOfColumnsExceptFirst,
38987
+ labelRange: getUnboundRange(getters, firstColumn.zone),
38947
38988
  dataSetsHaveTitle,
38948
- aggregated: true,
38949
38989
  legendPosition: "top",
38950
- });
38990
+ };
38951
38991
  }
38952
38992
  function buildScorecard(zone, getters) {
38953
38993
  const cell = getters.getCell({
@@ -38970,22 +39010,18 @@ stores.inject(MyMetaStore, storeInstance);
38970
39010
  */
38971
39011
  function getSmartChartDefinition(zones, getters) {
38972
39012
  const columns = categorizeColumns(zones, getters);
38973
- const { number: numberColumns, text: textColumns, date: dateColumns } = columns;
38974
- const columnCount = numberColumns.length + textColumns.length + dateColumns.length;
38975
- switch (columnCount) {
38976
- case 0:
38977
- return createBaseChart("bar", [{ dataRange: getUnboundRange(getters, zones[0]) }], {
38978
- dataSetsHaveTitle: false,
38979
- });
39013
+ if (columns.length === 0 || columns.every((col) => col.type === "empty")) {
39014
+ const dataSets = columns.map(({ zone }) => ({ dataRange: getUnboundRange(getters, zone) }));
39015
+ return { ...DEFAULT_BAR_CHART_CONFIG, dataSets };
39016
+ }
39017
+ const nonEmptyColumns = columns.filter((col) => col.type !== "empty");
39018
+ switch (nonEmptyColumns.length) {
38980
39019
  case 1:
38981
- const singleColumn = numberColumns[0] || textColumns[0] || dateColumns[0];
38982
- return getZoneArea(singleColumn.zone) === 1
38983
- ? buildScorecard(singleColumn.zone, getters)
38984
- : buildSingleColumnChart(singleColumn, getters);
39020
+ return buildSingleColumnChart(nonEmptyColumns[0], getters);
38985
39021
  case 2:
38986
- return buildTwoColumnChart(columns, getters);
39022
+ return buildTwoColumnChart(nonEmptyColumns, getters);
38987
39023
  default:
38988
- return buildMultiColumnChart(columns, getters);
39024
+ return buildMultiColumnChart(nonEmptyColumns, getters);
38989
39025
  }
38990
39026
  }
38991
39027
 
@@ -44026,6 +44062,7 @@ stores.inject(MyMetaStore, storeInstance);
44026
44062
  return data;
44027
44063
  }
44028
44064
  const figureIds = new Set();
44065
+ const chartIds = new Set();
44029
44066
  const uuidGenerator = new UuidGenerator();
44030
44067
  for (const sheet of data.sheets || []) {
44031
44068
  for (const figure of sheet.figures || []) {
@@ -44033,6 +44070,12 @@ stores.inject(MyMetaStore, storeInstance);
44033
44070
  figure.id += uuidGenerator.smallUuid();
44034
44071
  }
44035
44072
  figureIds.add(figure.id);
44073
+ if (figure.tag === "chart") {
44074
+ if (chartIds.has(figure.data?.chartId)) {
44075
+ figure.data.chartId += uuidGenerator.smallUuid();
44076
+ }
44077
+ chartIds.add(figure.data?.chartId);
44078
+ }
44036
44079
  }
44037
44080
  }
44038
44081
  data.uniqueFigureIds = true;
@@ -68768,6 +68811,281 @@ stores.inject(MyMetaStore, storeInstance);
68768
68811
  }
68769
68812
  }
68770
68813
 
68814
+ class ZoneSet {
68815
+ profilesStartingPosition = [0];
68816
+ profiles = new Map([[0, []]]);
68817
+ constructor(zones = []) {
68818
+ for (const zone of zones) {
68819
+ this.add(zone);
68820
+ }
68821
+ }
68822
+ isEmpty() {
68823
+ return this.profiles.size === 1 && this.profiles.get(0)?.length === 0;
68824
+ }
68825
+ add(zone) {
68826
+ modifyProfiles(this.profilesStartingPosition, this.profiles, [zone]);
68827
+ }
68828
+ delete(zone) {
68829
+ modifyProfiles(this.profilesStartingPosition, this.profiles, [zone], true);
68830
+ }
68831
+ has(zone) {
68832
+ return profilesContainsZone(this.profilesStartingPosition, this.profiles, zone);
68833
+ }
68834
+ difference(other) {
68835
+ const result = this.copy();
68836
+ for (const zone of other) {
68837
+ result.delete(zone);
68838
+ }
68839
+ return result;
68840
+ }
68841
+ copy() {
68842
+ const result = new ZoneSet();
68843
+ result.profilesStartingPosition = [...this.profilesStartingPosition];
68844
+ result.profiles = new Map();
68845
+ for (const [key, value] of this.profiles) {
68846
+ result.profiles.set(key, [...value]);
68847
+ }
68848
+ return result;
68849
+ }
68850
+ size() {
68851
+ let size = 0;
68852
+ for (const profile of this.profiles.values()) {
68853
+ size += profile.length;
68854
+ }
68855
+ return size / 2;
68856
+ }
68857
+ /**
68858
+ * iterator of all the zones in the ZoneSet
68859
+ */
68860
+ [Symbol.iterator]() {
68861
+ return constructZonesFromProfiles(this.profilesStartingPosition, this.profiles)[Symbol.iterator]();
68862
+ }
68863
+ }
68864
+
68865
+ class RangeSet {
68866
+ setsBySheetId = {};
68867
+ constructor(ranges = []) {
68868
+ for (const range of ranges) {
68869
+ this.add(range);
68870
+ }
68871
+ }
68872
+ add(range) {
68873
+ if (!this.setsBySheetId[range.sheetId]) {
68874
+ this.setsBySheetId[range.sheetId] = new ZoneSet();
68875
+ }
68876
+ this.setsBySheetId[range.sheetId].add(range.zone);
68877
+ }
68878
+ addMany(ranges) {
68879
+ for (const range of ranges) {
68880
+ this.add(range);
68881
+ }
68882
+ }
68883
+ addPosition(position) {
68884
+ this.add(positionToBoundedRange(position));
68885
+ }
68886
+ addManyPositions(positions) {
68887
+ for (const position of positions) {
68888
+ this.addPosition(position);
68889
+ }
68890
+ }
68891
+ has(range) {
68892
+ if (!this.setsBySheetId[range.sheetId]) {
68893
+ return false;
68894
+ }
68895
+ return this.setsBySheetId[range.sheetId].has(range.zone);
68896
+ }
68897
+ hasPosition(position) {
68898
+ return this.has(positionToBoundedRange(position));
68899
+ }
68900
+ delete(range) {
68901
+ if (!this.setsBySheetId[range.sheetId]) {
68902
+ return;
68903
+ }
68904
+ this.setsBySheetId[range.sheetId].delete(range.zone);
68905
+ }
68906
+ deleteMany(ranges) {
68907
+ for (const range of ranges) {
68908
+ this.delete(range);
68909
+ }
68910
+ }
68911
+ deleteManyPositions(positions) {
68912
+ for (const position of positions) {
68913
+ this.delete(positionToBoundedRange(position));
68914
+ }
68915
+ }
68916
+ difference(other) {
68917
+ const result = new RangeSet();
68918
+ for (const sheetId in this.setsBySheetId) {
68919
+ result.setsBySheetId[sheetId] = this.setsBySheetId[sheetId];
68920
+ }
68921
+ for (const sheetId in other.setsBySheetId) {
68922
+ if (result.setsBySheetId[sheetId]) {
68923
+ result.setsBySheetId[sheetId] = result.setsBySheetId[sheetId].difference(other.setsBySheetId[sheetId]);
68924
+ }
68925
+ }
68926
+ return result;
68927
+ }
68928
+ copy() {
68929
+ const result = new RangeSet();
68930
+ for (const sheetId in this.setsBySheetId) {
68931
+ result.setsBySheetId[sheetId] = this.setsBySheetId[sheetId].copy();
68932
+ }
68933
+ return result;
68934
+ }
68935
+ clear() {
68936
+ this.setsBySheetId = {};
68937
+ }
68938
+ size() {
68939
+ let size = 0;
68940
+ for (const sheetId in this.setsBySheetId) {
68941
+ size += this.setsBySheetId[sheetId].size();
68942
+ }
68943
+ return size;
68944
+ }
68945
+ isEmpty() {
68946
+ for (const sheetId in this.setsBySheetId) {
68947
+ if (!this.setsBySheetId[sheetId].isEmpty()) {
68948
+ return false;
68949
+ }
68950
+ }
68951
+ return true;
68952
+ }
68953
+ /**
68954
+ * iterator of all the ranges in the RangeSet
68955
+ */
68956
+ [Symbol.iterator]() {
68957
+ const result = [];
68958
+ for (const sheetId in this.setsBySheetId) {
68959
+ for (const zone of this.setsBySheetId[sheetId]) {
68960
+ result.push({ sheetId: sheetId, zone });
68961
+ }
68962
+ }
68963
+ return result[Symbol.iterator]();
68964
+ }
68965
+ }
68966
+
68967
+ /**
68968
+ * R-Tree of ranges, mapping zones (r-tree bounding boxes) to ranges (data of the r-tree item).
68969
+ * Ranges associated to the exact same bounding box are grouped together
68970
+ * to reduce the number of nodes in the R-tree.
68971
+ */
68972
+ class DependenciesRTree {
68973
+ rTree;
68974
+ constructor(items = []) {
68975
+ const compactedBoxes = groupSameBoundingBoxes(items);
68976
+ this.rTree = new SpreadsheetRTree(compactedBoxes);
68977
+ }
68978
+ insert(item) {
68979
+ const data = this.rTree.search(item.boundingBox);
68980
+ const itemBoundingBox = item.boundingBox;
68981
+ const exactBoundingBox = data.find(({ boundingBox }) => boundingBox.sheetId === itemBoundingBox.sheetId &&
68982
+ boundingBox.zone.left === itemBoundingBox.zone.left &&
68983
+ boundingBox.zone.top === itemBoundingBox.zone.top &&
68984
+ boundingBox.zone.right === itemBoundingBox.zone.right &&
68985
+ boundingBox.zone.bottom === itemBoundingBox.zone.bottom);
68986
+ if (exactBoundingBox) {
68987
+ exactBoundingBox.data.add(item.data);
68988
+ }
68989
+ else {
68990
+ this.rTree.insert({ ...item, data: new RangeSet([item.data]) });
68991
+ }
68992
+ }
68993
+ search({ zone, sheetId }) {
68994
+ const results = new RangeSet();
68995
+ for (const { data } of this.rTree.search({ zone, sheetId })) {
68996
+ results.addMany(data);
68997
+ }
68998
+ return results;
68999
+ }
69000
+ remove(item) {
69001
+ const data = this.rTree.search(item.boundingBox);
69002
+ const itemBoundingBox = item.boundingBox;
69003
+ const exactBoundingBox = data.find(({ boundingBox }) => boundingBox.sheetId === itemBoundingBox.sheetId &&
69004
+ boundingBox.zone.left === itemBoundingBox.zone.left &&
69005
+ boundingBox.zone.top === itemBoundingBox.zone.top &&
69006
+ boundingBox.zone.right === itemBoundingBox.zone.right &&
69007
+ boundingBox.zone.bottom === itemBoundingBox.zone.bottom);
69008
+ if (exactBoundingBox) {
69009
+ exactBoundingBox.data.delete(item.data);
69010
+ }
69011
+ else {
69012
+ this.rTree.remove({ ...item, data: new RangeSet([item.data]) });
69013
+ }
69014
+ }
69015
+ }
69016
+ /**
69017
+ * Group together all formulas pointing to the exact same dependency (bounding box).
69018
+ * The goal is to optimize the following case:
69019
+ * - if any cell in B1:B1000 changes, C1 must be recomputed
69020
+ * - if any cell in B1:B1000 changes, C2 must be recomputed
69021
+ * - if any cell in B1:B1000 changes, C3 must be recomputed
69022
+ * ...
69023
+ * - if any cell in B1:B1000 changes, C1000 must be recomputed
69024
+ *
69025
+ * Instead of having 1000 entries in the R-tree, we want to have a single entry
69026
+ * with B1:B1000 (bounding box) pointing to C1:C1000 (formulas).
69027
+ */
69028
+ function groupSameBoundingBoxes(items) {
69029
+ // Important: this function must be as fast as possible. It is on the evaluation hot path.
69030
+ let maxCol = 0;
69031
+ let maxRow = 0;
69032
+ for (let i = 0; i < items.length; i++) {
69033
+ const zone = items[i].boundingBox.zone;
69034
+ if (zone.right > maxCol) {
69035
+ maxCol = zone.right;
69036
+ }
69037
+ if (zone.bottom > maxRow) {
69038
+ maxRow = zone.bottom;
69039
+ }
69040
+ }
69041
+ maxCol += 1;
69042
+ maxRow += 1;
69043
+ // in most real-world cases, we can use a fast numeric key
69044
+ // but if the zones are too far right or bottom, we fallback to a slower string key
69045
+ const maxPossibleKey = (((maxRow + 1) * maxCol + 1) * maxRow + 1) * maxCol;
69046
+ const useFastKey = maxPossibleKey <= Number.MAX_SAFE_INTEGER;
69047
+ if (!useFastKey) {
69048
+ console.warn("Max col/row size exceeded, using slow zone key");
69049
+ }
69050
+ const groupedByBBox = {};
69051
+ for (const item of items) {
69052
+ const sheetId = item.boundingBox.sheetId;
69053
+ if (!groupedByBBox[sheetId]) {
69054
+ groupedByBBox[sheetId] = {};
69055
+ }
69056
+ const bBox = item.boundingBox.zone;
69057
+ let bBoxKey = 0;
69058
+ if (useFastKey) {
69059
+ bBoxKey =
69060
+ bBox.left +
69061
+ bBox.top * maxCol +
69062
+ bBox.right * maxCol * maxRow +
69063
+ bBox.bottom * maxCol * maxRow * maxCol;
69064
+ }
69065
+ else {
69066
+ bBoxKey = `${bBox.left},${bBox.top},${bBox.right},${bBox.bottom}`;
69067
+ }
69068
+ if (groupedByBBox[sheetId][bBoxKey]) {
69069
+ const ranges = groupedByBBox[sheetId][bBoxKey].data;
69070
+ ranges.add(item.data);
69071
+ }
69072
+ else {
69073
+ groupedByBBox[sheetId][bBoxKey] = {
69074
+ boundingBox: item.boundingBox,
69075
+ data: new RangeSet([item.data]),
69076
+ };
69077
+ }
69078
+ }
69079
+ const result = [];
69080
+ for (const sheetId in groupedByBBox) {
69081
+ const map = groupedByBBox[sheetId];
69082
+ for (const key in map) {
69083
+ result.push(map[key]);
69084
+ }
69085
+ }
69086
+ return result;
69087
+ }
69088
+
68771
69089
  /**
68772
69090
  * Implementation of a dependency Graph.
68773
69091
  * The graph is used to evaluate the cells in the correct
@@ -68776,12 +69094,10 @@ stores.inject(MyMetaStore, storeInstance);
68776
69094
  * It uses an R-Tree data structure to efficiently find dependent cells.
68777
69095
  */
68778
69096
  class FormulaDependencyGraph {
68779
- createEmptyPositionSet;
68780
69097
  dependencies = new PositionMap();
68781
69098
  rTree;
68782
- constructor(createEmptyPositionSet, data = []) {
68783
- this.createEmptyPositionSet = createEmptyPositionSet;
68784
- this.rTree = new SpreadsheetRTree(data);
69099
+ constructor(data = []) {
69100
+ this.rTree = new DependenciesRTree(data);
68785
69101
  }
68786
69102
  removeAllDependencies(formulaPosition) {
68787
69103
  const ranges = this.dependencies.get(formulaPosition);
@@ -68795,7 +69111,10 @@ stores.inject(MyMetaStore, storeInstance);
68795
69111
  }
68796
69112
  addDependencies(formulaPosition, dependencies) {
68797
69113
  const rTreeItems = dependencies.map(({ sheetId, zone }) => ({
68798
- data: formulaPosition,
69114
+ data: {
69115
+ sheetId: formulaPosition.sheetId,
69116
+ zone: positionToZone(formulaPosition),
69117
+ },
68799
69118
  boundingBox: {
68800
69119
  zone,
68801
69120
  sheetId,
@@ -68813,46 +69132,20 @@ stores.inject(MyMetaStore, storeInstance);
68813
69132
  }
68814
69133
  }
68815
69134
  /**
68816
- * Return all the cells that depend on the provided ranges,
68817
- * in the correct order they should be evaluated.
68818
- * This is called a topological ordering (excluding cycles)
69135
+ * Return all the cells that depend on the provided ranges.
68819
69136
  */
68820
- getCellsDependingOn(ranges, ignore) {
68821
- const visited = this.createEmptyPositionSet();
69137
+ getCellsDependingOn(ranges, visited = new RangeSet()) {
69138
+ visited = visited.copy();
68822
69139
  const queue = Array.from(ranges).reverse();
68823
69140
  while (queue.length > 0) {
68824
69141
  const range = queue.pop();
68825
- const zone = range.zone;
68826
- const sheetId = range.sheetId;
68827
- for (let col = zone.left; col <= zone.right; col++) {
68828
- for (let row = zone.top; row <= zone.bottom; row++) {
68829
- visited.add({ sheetId, col, row });
68830
- }
68831
- }
68832
- const impactedPositions = this.rTree.search(range).map((dep) => dep.data);
68833
- const nextInQueue = {};
68834
- for (const position of impactedPositions) {
68835
- if (!visited.has(position) && !ignore.has(position)) {
68836
- if (!nextInQueue[position.sheetId]) {
68837
- nextInQueue[position.sheetId] = [];
68838
- }
68839
- nextInQueue[position.sheetId].push(positionToZone(position));
68840
- }
68841
- }
68842
- for (const sheetId in nextInQueue) {
68843
- const zones = recomputeZones(nextInQueue[sheetId], []);
68844
- queue.push(...zones.map((zone) => ({ sheetId, zone })));
68845
- }
69142
+ visited.add(range);
69143
+ const impactedRanges = this.rTree.search(range);
69144
+ queue.push(...impactedRanges.difference(visited));
68846
69145
  }
68847
69146
  // remove initial ranges
68848
69147
  for (const range of ranges) {
68849
- const zone = range.zone;
68850
- const sheetId = range.sheetId;
68851
- for (let col = zone.left; col <= zone.right; col++) {
68852
- for (let row = zone.top; row <= zone.bottom; row++) {
68853
- visited.delete({ sheetId, col, row });
68854
- }
68855
- }
69148
+ visited.delete(range);
68856
69149
  }
68857
69150
  return visited;
68858
69151
  }
@@ -69115,7 +69408,7 @@ stores.inject(MyMetaStore, storeInstance);
69115
69408
  getters;
69116
69409
  compilationParams;
69117
69410
  evaluatedCells = new PositionMap();
69118
- formulaDependencies = lazy(new FormulaDependencyGraph(this.createEmptyPositionSet.bind(this)));
69411
+ formulaDependencies = lazy(new FormulaDependencyGraph());
69119
69412
  blockedArrayFormulas = new PositionSet({});
69120
69413
  spreadingRelations = new SpreadingRelation();
69121
69414
  constructor(context, getters) {
@@ -69150,7 +69443,7 @@ stores.inject(MyMetaStore, storeInstance);
69150
69443
  return undefined;
69151
69444
  }
69152
69445
  const arrayFormulas = this.spreadingRelations.searchFormulaPositionsSpreadingOn(position.sheetId, positionToZone(position));
69153
- return Array.from(arrayFormulas).find((position) => !this.blockedArrayFormulas.has(position));
69446
+ return arrayFormulas.find((position) => !this.blockedArrayFormulas.has(position));
69154
69447
  }
69155
69448
  updateDependencies(position) {
69156
69449
  // removing dependencies is slow because it requires
@@ -69194,57 +69487,72 @@ stores.inject(MyMetaStore, storeInstance);
69194
69487
  }
69195
69488
  evaluateCells(positions) {
69196
69489
  const start = performance.now();
69197
- const cellsToCompute = this.createEmptyPositionSet();
69198
- cellsToCompute.addMany(positions);
69490
+ const rangesToCompute = new RangeSet();
69491
+ rangesToCompute.addManyPositions(positions);
69199
69492
  const arrayFormulasPositions = this.getArrayFormulasImpactedByChangesOf(positions);
69200
- cellsToCompute.addMany(this.getCellsDependingOn(positions));
69201
- cellsToCompute.addMany(arrayFormulasPositions);
69202
- cellsToCompute.addMany(this.getCellsDependingOn(arrayFormulasPositions));
69203
- this.evaluate(cellsToCompute);
69493
+ rangesToCompute.addMany(this.getCellsDependingOn(rangesToCompute));
69494
+ rangesToCompute.addMany(arrayFormulasPositions);
69495
+ rangesToCompute.addMany(this.getCellsDependingOn(arrayFormulasPositions));
69496
+ this.evaluate(rangesToCompute);
69204
69497
  console.debug("evaluate Cells", performance.now() - start, "ms");
69205
69498
  }
69206
69499
  getArrayFormulasImpactedByChangesOf(positions) {
69207
- const impactedPositions = this.createEmptyPositionSet();
69500
+ const impactedRanges = new RangeSet();
69208
69501
  for (const position of positions) {
69209
69502
  const content = this.getters.getCell(position)?.content;
69210
69503
  const arrayFormulaPosition = this.getArrayFormulaSpreadingOn(position);
69211
69504
  if (arrayFormulaPosition !== undefined) {
69212
69505
  // take into account new collisions.
69213
- impactedPositions.add(arrayFormulaPosition);
69506
+ impactedRanges.addPosition(arrayFormulaPosition);
69214
69507
  }
69215
69508
  if (!content) {
69216
69509
  // The previous content could have blocked some array formulas
69217
- impactedPositions.add(position);
69510
+ impactedRanges.addPosition(position);
69218
69511
  }
69219
69512
  }
69220
- const zonesBySheetIds = aggregatePositionsToZones(impactedPositions);
69221
- for (const sheetId in zonesBySheetIds) {
69222
- for (const zone of zonesBySheetIds[sheetId]) {
69223
- impactedPositions.addMany(this.getArrayFormulasBlockedBy(sheetId, zone));
69224
- }
69513
+ for (const range of [...impactedRanges]) {
69514
+ impactedRanges.addMany(this.getArrayFormulasBlockedBy(range.sheetId, range.zone));
69225
69515
  }
69226
- return impactedPositions;
69516
+ return impactedRanges;
69227
69517
  }
69228
69518
  buildDependencyGraph() {
69229
69519
  this.blockedArrayFormulas = this.createEmptyPositionSet();
69230
69520
  this.spreadingRelations = new SpreadingRelation();
69231
69521
  this.formulaDependencies = lazy(() => {
69232
- const dependencies = [...this.getAllCells()].flatMap((position) => this.getDirectDependencies(position)
69233
- .filter((range) => !range.invalidSheetName && !range.invalidXc)
69234
- .map((range) => ({
69235
- data: position,
69236
- boundingBox: {
69237
- zone: range.zone,
69238
- sheetId: range.sheetId,
69239
- },
69240
- })));
69241
- return new FormulaDependencyGraph(this.createEmptyPositionSet.bind(this), dependencies);
69522
+ const rTreeItems = [];
69523
+ for (const sheetId of this.getters.getSheetIds()) {
69524
+ const cells = this.getters.getCells(sheetId);
69525
+ for (const cellId in cells) {
69526
+ const cell = cells[cellId];
69527
+ if (cell.isFormula) {
69528
+ const directDependencies = cell.compiledFormula.dependencies;
69529
+ for (const range of directDependencies) {
69530
+ if (range.invalidSheetName || range.invalidXc) {
69531
+ continue;
69532
+ }
69533
+ rTreeItems.push({
69534
+ data: {
69535
+ sheetId,
69536
+ zone: positionToZone(this.getters.getCellPosition(cellId)),
69537
+ },
69538
+ boundingBox: { sheetId: range.sheetId, zone: range.zone },
69539
+ });
69540
+ }
69541
+ }
69542
+ }
69543
+ }
69544
+ return new FormulaDependencyGraph(rTreeItems);
69242
69545
  });
69243
69546
  }
69244
69547
  evaluateAllCells() {
69245
69548
  const start = performance.now();
69246
69549
  this.evaluatedCells = new PositionMap();
69247
- this.evaluate(this.getAllCells());
69550
+ const ranges = [];
69551
+ for (const sheetId of this.getters.getSheetIds()) {
69552
+ const zone = this.getters.getSheetZone(sheetId);
69553
+ ranges.push({ sheetId, zone });
69554
+ }
69555
+ this.evaluate(ranges);
69248
69556
  console.debug("evaluate all cells", performance.now() - start, "ms");
69249
69557
  }
69250
69558
  evaluateFormulaResult(sheetId, formulaString) {
@@ -69268,48 +69576,47 @@ stores.inject(MyMetaStore, storeInstance);
69268
69576
  return handleError(error, "");
69269
69577
  }
69270
69578
  }
69271
- getAllCells() {
69272
- const positions = this.createEmptyPositionSet();
69273
- positions.fillAllPositions();
69274
- return positions;
69275
- }
69276
69579
  /**
69277
69580
  * Return the position of formulas blocked by the given positions
69278
69581
  * as well as all their dependencies.
69279
69582
  */
69280
69583
  getArrayFormulasBlockedBy(sheetId, zone) {
69281
- const arrayFormulaPositions = this.createEmptyPositionSet();
69584
+ const arrayFormulaPositions = new RangeSet();
69282
69585
  const arrayFormulas = this.spreadingRelations.searchFormulaPositionsSpreadingOn(sheetId, zone);
69283
- arrayFormulaPositions.addMany(arrayFormulas);
69586
+ arrayFormulaPositions.addManyPositions(arrayFormulas);
69284
69587
  const spilledPositions = [...arrayFormulas].filter((position) => !this.blockedArrayFormulas.has(position));
69285
69588
  if (spilledPositions.length) {
69286
69589
  // ignore the formula spreading on the position. Keep only the blocked ones
69287
- arrayFormulaPositions.deleteMany(spilledPositions);
69590
+ arrayFormulaPositions.deleteManyPositions(spilledPositions);
69288
69591
  }
69289
69592
  arrayFormulaPositions.addMany(this.getCellsDependingOn(arrayFormulaPositions));
69290
69593
  return arrayFormulaPositions;
69291
69594
  }
69292
- nextPositionsToUpdate = new PositionSet({});
69595
+ nextRangesToUpdate = new RangeSet();
69293
69596
  cellsBeingComputed = new Set();
69294
69597
  symbolsBeingComputed = new Set();
69295
- evaluate(positions) {
69598
+ evaluate(ranges) {
69296
69599
  this.cellsBeingComputed = new Set();
69297
- this.nextPositionsToUpdate = positions;
69600
+ this.nextRangesToUpdate = new RangeSet(ranges);
69298
69601
  let currentIteration = 0;
69299
- while (!this.nextPositionsToUpdate.isEmpty() && currentIteration++ < MAX_ITERATION) {
69602
+ while (!this.nextRangesToUpdate.isEmpty() && currentIteration++ < MAX_ITERATION) {
69300
69603
  this.updateCompilationParameters();
69301
- const positions = this.nextPositionsToUpdate.clear();
69302
- for (let i = 0; i < positions.length; ++i) {
69303
- this.evaluatedCells.delete(positions[i]);
69304
- }
69305
- for (let i = 0; i < positions.length; ++i) {
69306
- const position = positions[i];
69307
- if (this.nextPositionsToUpdate.has(position)) {
69308
- continue;
69309
- }
69310
- const evaluatedCell = this.computeCell(position);
69311
- if (evaluatedCell !== EMPTY_CELL) {
69312
- this.evaluatedCells.set(position, evaluatedCell);
69604
+ const ranges = [...this.nextRangesToUpdate];
69605
+ this.nextRangesToUpdate.clear();
69606
+ this.clearEvaluatedRanges(ranges);
69607
+ for (const range of ranges) {
69608
+ const { left, bottom, right, top } = range.zone;
69609
+ for (let col = left; col <= right; col++) {
69610
+ for (let row = top; row <= bottom; row++) {
69611
+ const position = { sheetId: range.sheetId, col, row };
69612
+ if (this.nextRangesToUpdate.hasPosition(position)) {
69613
+ continue;
69614
+ }
69615
+ const evaluatedCell = this.computeCell(position);
69616
+ if (evaluatedCell !== EMPTY_CELL) {
69617
+ this.evaluatedCells.set(position, evaluatedCell);
69618
+ }
69619
+ }
69313
69620
  }
69314
69621
  }
69315
69622
  onIterationEndEvaluationRegistry.getAll().forEach((callback) => callback(this.getters));
@@ -69318,6 +69625,16 @@ stores.inject(MyMetaStore, storeInstance);
69318
69625
  console.warn("Maximum iteration reached while evaluating cells");
69319
69626
  }
69320
69627
  }
69628
+ clearEvaluatedRanges(ranges) {
69629
+ for (const range of ranges) {
69630
+ const { left, bottom, right, top } = range.zone;
69631
+ for (let col = left; col <= right; col++) {
69632
+ for (let row = top; row <= bottom; row++) {
69633
+ this.evaluatedCells.delete({ sheetId: range.sheetId, col, row });
69634
+ }
69635
+ }
69636
+ }
69637
+ }
69321
69638
  computeCell(position) {
69322
69639
  const evaluation = this.evaluatedCells.get(position);
69323
69640
  if (evaluation) {
@@ -69390,9 +69707,9 @@ stores.inject(MyMetaStore, storeInstance);
69390
69707
  }
69391
69708
  invalidatePositionsDependingOnSpread(sheetId, resultZone) {
69392
69709
  // the result matrix is split in 2 zones to exclude the array formula position
69393
- const invalidatedPositions = this.formulaDependencies().getCellsDependingOn(excludeTopLeft(resultZone).map((zone) => ({ sheetId, zone })), this.nextPositionsToUpdate);
69394
- invalidatedPositions.delete({ sheetId, col: resultZone.left, row: resultZone.top });
69395
- this.nextPositionsToUpdate.addMany(invalidatedPositions);
69710
+ const invalidatedPositions = this.getCellsDependingOn(excludeTopLeft(resultZone).map((zone) => ({ sheetId, zone })));
69711
+ invalidatedPositions.delete({ sheetId, zone: resultZone });
69712
+ this.nextRangesToUpdate.addMany(invalidatedPositions);
69396
69713
  }
69397
69714
  assertSheetHasEnoughSpaceToSpreadFormulaResult({ sheetId, col, row }, matrixResult) {
69398
69715
  const numberOfCols = this.getters.getNumberCols(sheetId);
@@ -69467,7 +69784,7 @@ stores.inject(MyMetaStore, storeInstance);
69467
69784
  }
69468
69785
  const sheetId = position.sheetId;
69469
69786
  this.invalidatePositionsDependingOnSpread(sheetId, zone);
69470
- this.nextPositionsToUpdate.addMany(this.getArrayFormulasBlockedBy(sheetId, zone));
69787
+ this.nextRangesToUpdate.addMany(this.getArrayFormulasBlockedBy(sheetId, zone));
69471
69788
  }
69472
69789
  /**
69473
69790
  * Wraps a GetSymbolValue function to add cycle detection
@@ -69502,13 +69819,8 @@ stores.inject(MyMetaStore, storeInstance);
69502
69819
  }
69503
69820
  return cell.compiledFormula.dependencies;
69504
69821
  }
69505
- getCellsDependingOn(positions) {
69506
- const ranges = [];
69507
- const zonesBySheetIds = aggregatePositionsToZones(positions);
69508
- for (const sheetId in zonesBySheetIds) {
69509
- ranges.push(...zonesBySheetIds[sheetId].map((zone) => ({ sheetId, zone })));
69510
- }
69511
- return this.formulaDependencies().getCellsDependingOn(ranges, this.nextPositionsToUpdate);
69822
+ getCellsDependingOn(ranges) {
69823
+ return this.formulaDependencies().getCellsDependingOn(ranges, this.nextRangesToUpdate);
69512
69824
  }
69513
69825
  }
69514
69826
  function forEachSpreadPositionInMatrix(nbColumns, nbRows, callback) {
@@ -88623,9 +88935,9 @@ stores.inject(MyMetaStore, storeInstance);
88623
88935
  exports.tokenize = tokenize;
88624
88936
 
88625
88937
 
88626
- __info__.version = "19.0.6";
88627
- __info__.date = "2025-10-16T06:39:36.282Z";
88628
- __info__.hash = "0d4315a";
88938
+ __info__.version = "19.0.7";
88939
+ __info__.date = "2025-10-23T08:19:01.764Z";
88940
+ __info__.hash = "1c1d1ec";
88629
88941
 
88630
88942
 
88631
88943
  })(this.o_spreadsheet = this.o_spreadsheet || {}, owl);