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