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