@odoo/o-spreadsheet 19.0.6 → 19.0.8
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.
- package/dist/o-spreadsheet.cjs.js +616 -286
- package/dist/o-spreadsheet.d.ts +8 -1
- package/dist/o-spreadsheet.esm.js +616 -286
- package/dist/o-spreadsheet.iife.js +616 -286
- package/dist/o-spreadsheet.iife.min.js +413 -413
- package/dist/o_spreadsheet.xml +23 -13
- package/package.json +1 -1
- package/readme.md +1 -0
|
@@ -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
|
-
* @date 2025-10-
|
|
7
|
-
* @hash
|
|
5
|
+
* @version 19.0.8
|
|
6
|
+
* @date 2025-10-30T12:25:04.355Z
|
|
7
|
+
* @hash 559e4e5
|
|
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
|
-
|
|
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
|
*/
|
|
@@ -6241,10 +6266,18 @@ function _roundFormat(internalFormat) {
|
|
|
6241
6266
|
};
|
|
6242
6267
|
}
|
|
6243
6268
|
function humanizeNumber({ value, format }, locale) {
|
|
6244
|
-
const
|
|
6245
|
-
|
|
6246
|
-
|
|
6247
|
-
}
|
|
6269
|
+
const numberValue = tryToNumber(value, locale);
|
|
6270
|
+
if (numberValue === undefined) {
|
|
6271
|
+
return "";
|
|
6272
|
+
}
|
|
6273
|
+
let numberFormat = format;
|
|
6274
|
+
if (Math.abs(numberValue) < 1000) {
|
|
6275
|
+
const hasDecimal = numberValue % 1 !== 0;
|
|
6276
|
+
numberFormat = !format && hasDecimal ? "0.####" : format;
|
|
6277
|
+
}
|
|
6278
|
+
else {
|
|
6279
|
+
numberFormat = formatLargeNumber({ value, format }, undefined, locale);
|
|
6280
|
+
}
|
|
6248
6281
|
return formatValue(value, { format: numberFormat, locale });
|
|
6249
6282
|
}
|
|
6250
6283
|
function formatLargeNumber(arg, unit, locale) {
|
|
@@ -6944,6 +6977,10 @@ function getRangeParts(xc, zone) {
|
|
|
6944
6977
|
}
|
|
6945
6978
|
return parts;
|
|
6946
6979
|
}
|
|
6980
|
+
function positionToBoundedRange(position) {
|
|
6981
|
+
const zone = { left: position.col, top: position.row, right: position.col, bottom: position.row };
|
|
6982
|
+
return { sheetId: position.sheetId, zone };
|
|
6983
|
+
}
|
|
6947
6984
|
/**
|
|
6948
6985
|
* Check that a zone is valid regarding the order of top-bottom and left-right.
|
|
6949
6986
|
* Left should be smaller than right, top should be smaller than bottom.
|
|
@@ -38697,12 +38734,23 @@ cellPopoverRegistry
|
|
|
38697
38734
|
.add("LinkEditor", LinkEditorPopoverBuilder)
|
|
38698
38735
|
.add("FilterMenu", FilterMenuPopoverBuilder);
|
|
38699
38736
|
|
|
38700
|
-
const
|
|
38701
|
-
|
|
38702
|
-
|
|
38703
|
-
|
|
38704
|
-
|
|
38705
|
-
|
|
38737
|
+
const DEFAULT_BAR_CHART_CONFIG = {
|
|
38738
|
+
type: "bar",
|
|
38739
|
+
title: {},
|
|
38740
|
+
dataSets: [],
|
|
38741
|
+
legendPosition: "none",
|
|
38742
|
+
dataSetsHaveTitle: false,
|
|
38743
|
+
stacked: false,
|
|
38744
|
+
};
|
|
38745
|
+
const DEFAULT_LINE_CHART_CONFIG = {
|
|
38746
|
+
type: "line",
|
|
38747
|
+
title: {},
|
|
38748
|
+
dataSets: [],
|
|
38749
|
+
legendPosition: "none",
|
|
38750
|
+
dataSetsHaveTitle: false,
|
|
38751
|
+
stacked: false,
|
|
38752
|
+
cumulative: false,
|
|
38753
|
+
labelsAsText: false,
|
|
38706
38754
|
};
|
|
38707
38755
|
function getUnboundRange(getters, zone) {
|
|
38708
38756
|
return zoneToXc(getters.getUnboundedZone(getters.getActiveSheetId(), zone));
|
|
@@ -38741,43 +38789,19 @@ function detectColumnType(cells) {
|
|
|
38741
38789
|
return detectedType;
|
|
38742
38790
|
}
|
|
38743
38791
|
function categorizeColumns(zones, getters) {
|
|
38744
|
-
const columns =
|
|
38745
|
-
number: [],
|
|
38746
|
-
text: [],
|
|
38747
|
-
date: [],
|
|
38748
|
-
};
|
|
38792
|
+
const columns = [];
|
|
38749
38793
|
for (const zone of getZonesByColumns(zones)) {
|
|
38750
38794
|
const cells = getters.getEvaluatedCellsInZone(getters.getActiveSheetId(), zone);
|
|
38751
|
-
|
|
38752
|
-
if (type !== "empty") {
|
|
38753
|
-
const targetType = type === "percentage" ? "number" : type;
|
|
38754
|
-
columns[targetType].push({ zone, type });
|
|
38755
|
-
}
|
|
38795
|
+
columns.push({ zone, type: detectColumnType(cells) });
|
|
38756
38796
|
}
|
|
38757
38797
|
return columns;
|
|
38758
38798
|
}
|
|
38759
38799
|
function getCellStats(getters, zone) {
|
|
38760
38800
|
const cells = getters.getEvaluatedCellsInZone(getters.getActiveSheetId(), zone);
|
|
38761
|
-
const
|
|
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
|
-
}
|
|
38801
|
+
const values = cells.map((c) => c.value?.toString().trim() || "").filter((s) => s);
|
|
38777
38802
|
return {
|
|
38778
|
-
uniqueCount:
|
|
38779
|
-
totalCount,
|
|
38780
|
-
percentageSum,
|
|
38803
|
+
uniqueCount: new Set(values).size,
|
|
38804
|
+
totalCount: values.length,
|
|
38781
38805
|
};
|
|
38782
38806
|
}
|
|
38783
38807
|
function isDatasetTitled(getters, column) {
|
|
@@ -38788,167 +38812,191 @@ function isDatasetTitled(getters, column) {
|
|
|
38788
38812
|
});
|
|
38789
38813
|
return ![CellValueType.number, CellValueType.empty].includes(titleCell.type);
|
|
38790
38814
|
}
|
|
38791
|
-
|
|
38792
|
-
|
|
38793
|
-
|
|
38794
|
-
|
|
38795
|
-
|
|
38796
|
-
|
|
38797
|
-
|
|
38798
|
-
|
|
38799
|
-
}
|
|
38815
|
+
/**
|
|
38816
|
+
* Builds a chart definition for a single column selection. The logic to detect the chart type is as follows:
|
|
38817
|
+
* - If the column contains a single cell, create a scorecard.
|
|
38818
|
+
* - If the column type is "percentage", create a pie chart.
|
|
38819
|
+
* - If the column type is "text", create a pie chart
|
|
38820
|
+
* - If the column type is "date", create a line chart.
|
|
38821
|
+
* - Otherwise, create a bar chart.
|
|
38822
|
+
*/
|
|
38800
38823
|
function buildSingleColumnChart(column, getters) {
|
|
38801
38824
|
const { type, zone } = column;
|
|
38802
38825
|
const sheetId = getters.getActiveSheetId();
|
|
38803
38826
|
const dataSetsHaveTitle = isDatasetTitled(getters, column);
|
|
38804
38827
|
const dataRange = getUnboundRange(getters, zone);
|
|
38805
38828
|
const titleCell = getters.getEvaluatedCell({ sheetId, col: zone.left, row: zone.top });
|
|
38829
|
+
if (getZoneArea(zone) === 1) {
|
|
38830
|
+
return buildScorecard(zone, getters);
|
|
38831
|
+
}
|
|
38806
38832
|
switch (type) {
|
|
38807
38833
|
case "percentage":
|
|
38808
|
-
|
|
38809
|
-
|
|
38834
|
+
return {
|
|
38835
|
+
type: "pie",
|
|
38810
38836
|
title: dataSetsHaveTitle ? { text: String(titleCell.value) } : {},
|
|
38837
|
+
dataSets: [{ dataRange }],
|
|
38838
|
+
legendPosition: "none",
|
|
38811
38839
|
dataSetsHaveTitle,
|
|
38812
|
-
|
|
38813
|
-
});
|
|
38840
|
+
};
|
|
38814
38841
|
case "text":
|
|
38815
38842
|
const cells = getters.getEvaluatedCellsInZone(sheetId, zone);
|
|
38816
38843
|
const titleCount = cells.reduce((count, cell) => (cell.value === titleCell.value ? count + 1 : count), 0);
|
|
38817
38844
|
const hasUniqueTitle = titleCell.value !== null && titleCount === 1;
|
|
38818
|
-
return
|
|
38845
|
+
return {
|
|
38846
|
+
type: "pie",
|
|
38819
38847
|
title: hasUniqueTitle ? { text: String(titleCell.value) } : {},
|
|
38848
|
+
dataSets: [{ dataRange }],
|
|
38820
38849
|
labelRange: dataRange,
|
|
38821
38850
|
dataSetsHaveTitle: hasUniqueTitle,
|
|
38822
|
-
isDoughnut: false,
|
|
38823
38851
|
aggregated: true,
|
|
38824
38852
|
legendPosition: "top",
|
|
38825
|
-
}
|
|
38826
|
-
// TODO: Handle date column with matrix chart when matrix chart is supported
|
|
38853
|
+
};
|
|
38827
38854
|
case "date":
|
|
38828
|
-
return
|
|
38829
|
-
|
|
38855
|
+
return {
|
|
38856
|
+
...DEFAULT_LINE_CHART_CONFIG,
|
|
38857
|
+
type: "line",
|
|
38858
|
+
title: dataSetsHaveTitle ? { text: String(titleCell.value) } : {},
|
|
38859
|
+
dataSets: [{ dataRange }],
|
|
38830
38860
|
dataSetsHaveTitle,
|
|
38831
|
-
|
|
38832
|
-
labelsAsText: false,
|
|
38833
|
-
});
|
|
38861
|
+
};
|
|
38834
38862
|
}
|
|
38835
|
-
return
|
|
38863
|
+
return {
|
|
38864
|
+
...DEFAULT_BAR_CHART_CONFIG,
|
|
38865
|
+
title: dataSetsHaveTitle ? { text: String(titleCell.value) } : {},
|
|
38866
|
+
dataSets: [{ dataRange }],
|
|
38867
|
+
dataSetsHaveTitle,
|
|
38868
|
+
};
|
|
38836
38869
|
}
|
|
38870
|
+
/**
|
|
38871
|
+
* Builds a chart definition for a selection of two columns. The logic to detect the chart type always consider the
|
|
38872
|
+
* columns left to right, and is as follows:
|
|
38873
|
+
* - any type + percentage columns: pie chart
|
|
38874
|
+
* - number + number columns: scatter chart
|
|
38875
|
+
* - date + number columns: line chart
|
|
38876
|
+
* - text + number columns: treemap if repetition in labels
|
|
38877
|
+
* - any other combination: bar chart
|
|
38878
|
+
*/
|
|
38837
38879
|
function buildTwoColumnChart(columns, getters) {
|
|
38838
|
-
|
|
38839
|
-
|
|
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
|
-
});
|
|
38880
|
+
if (columns.length !== 2) {
|
|
38881
|
+
throw new Error("buildTwoColumnChart expects exactly two columns");
|
|
38845
38882
|
}
|
|
38846
|
-
|
|
38847
|
-
|
|
38848
|
-
|
|
38849
|
-
|
|
38850
|
-
|
|
38851
|
-
|
|
38852
|
-
|
|
38883
|
+
if (columns[1].type === "percentage") {
|
|
38884
|
+
return {
|
|
38885
|
+
type: "pie",
|
|
38886
|
+
title: {},
|
|
38887
|
+
dataSets: [{ dataRange: getUnboundRange(getters, columns[1].zone) }],
|
|
38888
|
+
labelRange: getUnboundRange(getters, columns[0].zone),
|
|
38889
|
+
dataSetsHaveTitle: isDatasetTitled(getters, columns[1]),
|
|
38890
|
+
aggregated: true,
|
|
38891
|
+
legendPosition: "none",
|
|
38892
|
+
};
|
|
38893
|
+
}
|
|
38894
|
+
if (columns[0].type === "number" && columns[1].type === "number") {
|
|
38895
|
+
return {
|
|
38896
|
+
type: "scatter",
|
|
38897
|
+
title: {},
|
|
38898
|
+
dataSets: [{ dataRange: getUnboundRange(getters, columns[1].zone) }],
|
|
38899
|
+
labelRange: getUnboundRange(getters, columns[0].zone),
|
|
38900
|
+
dataSetsHaveTitle: isDatasetTitled(getters, columns[1]),
|
|
38853
38901
|
labelsAsText: false,
|
|
38854
|
-
|
|
38902
|
+
legendPosition: "none",
|
|
38903
|
+
};
|
|
38855
38904
|
}
|
|
38856
|
-
|
|
38857
|
-
|
|
38858
|
-
|
|
38905
|
+
// TODO: Handle date + number with calendar chart when implemented (and change the docstring)
|
|
38906
|
+
if (columns[0].type === "date" && columns[1].type === "number") {
|
|
38907
|
+
return {
|
|
38908
|
+
...DEFAULT_LINE_CHART_CONFIG,
|
|
38909
|
+
type: "line",
|
|
38910
|
+
dataSets: [{ dataRange: getUnboundRange(getters, columns[1].zone) }],
|
|
38911
|
+
labelRange: getUnboundRange(getters, columns[0].zone),
|
|
38912
|
+
dataSetsHaveTitle: isDatasetTitled(getters, columns[0]),
|
|
38913
|
+
};
|
|
38914
|
+
}
|
|
38915
|
+
if (columns[0].type === "text" && columns[1].type === "number") {
|
|
38916
|
+
const textColumn = columns[0];
|
|
38917
|
+
const numberColumn = columns[1];
|
|
38859
38918
|
const { uniqueCount, totalCount } = getCellStats(getters, textColumn.zone);
|
|
38860
38919
|
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
38920
|
if (uniqueCount !== totalCount) {
|
|
38878
|
-
return
|
|
38879
|
-
|
|
38921
|
+
return {
|
|
38922
|
+
type: "treemap",
|
|
38923
|
+
title: {},
|
|
38924
|
+
dataSets: [{ dataRange: getUnboundRange(getters, textColumn.zone) }],
|
|
38925
|
+
labelRange: getUnboundRange(getters, numberColumn.zone),
|
|
38880
38926
|
dataSetsHaveTitle,
|
|
38881
|
-
|
|
38927
|
+
legendPosition: "none",
|
|
38928
|
+
};
|
|
38882
38929
|
}
|
|
38883
|
-
return createBaseChart("bar", [{ dataRange }], {
|
|
38884
|
-
labelRange,
|
|
38885
|
-
dataSetsHaveTitle,
|
|
38886
|
-
});
|
|
38887
38930
|
}
|
|
38888
|
-
|
|
38889
|
-
|
|
38890
|
-
|
|
38891
|
-
labelRange: getUnboundRange(getters,
|
|
38892
|
-
dataSetsHaveTitle: isDatasetTitled(getters,
|
|
38893
|
-
|
|
38894
|
-
labelsAsText: true,
|
|
38895
|
-
});
|
|
38931
|
+
return {
|
|
38932
|
+
...DEFAULT_BAR_CHART_CONFIG,
|
|
38933
|
+
dataSets: [{ dataRange: getUnboundRange(getters, columns[1].zone) }],
|
|
38934
|
+
labelRange: getUnboundRange(getters, columns[0].zone),
|
|
38935
|
+
dataSetsHaveTitle: isDatasetTitled(getters, columns[1]),
|
|
38936
|
+
};
|
|
38896
38937
|
}
|
|
38938
|
+
/**
|
|
38939
|
+
* Builds a chart definition for a selection more than two columns. The logic to detect the chart type always consider
|
|
38940
|
+
* the columns left to right, and is as follows:
|
|
38941
|
+
* - multiple text + single number/percentage columns: sunburst if 3+ text columns, treemap otherwise
|
|
38942
|
+
* - any type + multiple percentage columns: pie chart
|
|
38943
|
+
* - date + multiple number columns: line chart
|
|
38944
|
+
* - any other combination: bar chart
|
|
38945
|
+
*/
|
|
38897
38946
|
function buildMultiColumnChart(columns, getters) {
|
|
38898
|
-
|
|
38899
|
-
|
|
38900
|
-
|
|
38901
|
-
|
|
38902
|
-
|
|
38947
|
+
if (columns.length < 3) {
|
|
38948
|
+
throw new Error("buildMultiColumnChart expects at least three columns");
|
|
38949
|
+
}
|
|
38950
|
+
const dataSetsHaveTitle = columns.some((col) => col.type !== "text" && isDatasetTitled(getters, col));
|
|
38951
|
+
const lastColumn = columns[columns.length - 1];
|
|
38952
|
+
const columnsExceptLast = columns.slice(0, columns.length - 1);
|
|
38953
|
+
if ((lastColumn.type === "percentage" || lastColumn.type === "number") &&
|
|
38954
|
+
columnsExceptLast.every((col) => col.type === "text")) {
|
|
38955
|
+
const dataSets = columnsExceptLast.map(({ zone }) => ({
|
|
38903
38956
|
dataRange: getUnboundRange(getters, zone),
|
|
38904
38957
|
}));
|
|
38905
|
-
return
|
|
38906
|
-
|
|
38958
|
+
return {
|
|
38959
|
+
type: columnsExceptLast.length >= 3 ? "sunburst" : "treemap",
|
|
38960
|
+
title: {},
|
|
38961
|
+
dataSets,
|
|
38962
|
+
labelRange: getUnboundRange(getters, lastColumn.zone),
|
|
38907
38963
|
dataSetsHaveTitle,
|
|
38908
|
-
|
|
38964
|
+
legendPosition: "none",
|
|
38965
|
+
};
|
|
38909
38966
|
}
|
|
38910
|
-
const
|
|
38967
|
+
const firstColumn = columns[0];
|
|
38968
|
+
const columnsExceptFirst = columns.slice(1);
|
|
38969
|
+
const rangesOfColumnsExceptFirst = columnsExceptFirst.map(({ zone }) => ({
|
|
38911
38970
|
dataRange: getUnboundRange(getters, zone),
|
|
38912
38971
|
}));
|
|
38913
|
-
if (
|
|
38914
|
-
return
|
|
38915
|
-
|
|
38972
|
+
if (columnsExceptFirst.every((col) => col.type === "percentage")) {
|
|
38973
|
+
return {
|
|
38974
|
+
type: "pie",
|
|
38975
|
+
title: {},
|
|
38976
|
+
dataSets: rangesOfColumnsExceptFirst,
|
|
38977
|
+
labelRange: getUnboundRange(getters, firstColumn.zone),
|
|
38916
38978
|
dataSetsHaveTitle,
|
|
38917
|
-
|
|
38918
|
-
labelsAsText: false,
|
|
38979
|
+
aggregated: false,
|
|
38919
38980
|
legendPosition: "top",
|
|
38920
|
-
}
|
|
38981
|
+
};
|
|
38921
38982
|
}
|
|
38922
|
-
if (
|
|
38923
|
-
|
|
38924
|
-
|
|
38925
|
-
|
|
38926
|
-
|
|
38927
|
-
|
|
38928
|
-
|
|
38929
|
-
|
|
38930
|
-
|
|
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
|
-
}
|
|
38983
|
+
if (firstColumn.type === "date" && columnsExceptFirst.every((col) => col.type === "number")) {
|
|
38984
|
+
return {
|
|
38985
|
+
...DEFAULT_LINE_CHART_CONFIG,
|
|
38986
|
+
type: "line",
|
|
38987
|
+
dataSets: rangesOfColumnsExceptFirst,
|
|
38988
|
+
labelRange: getUnboundRange(getters, firstColumn.zone),
|
|
38989
|
+
dataSetsHaveTitle,
|
|
38990
|
+
legendPosition: "top",
|
|
38991
|
+
};
|
|
38944
38992
|
}
|
|
38945
|
-
|
|
38946
|
-
|
|
38947
|
-
|
|
38993
|
+
return {
|
|
38994
|
+
...DEFAULT_BAR_CHART_CONFIG,
|
|
38995
|
+
dataSets: rangesOfColumnsExceptFirst,
|
|
38996
|
+
labelRange: getUnboundRange(getters, firstColumn.zone),
|
|
38948
38997
|
dataSetsHaveTitle,
|
|
38949
|
-
aggregated: true,
|
|
38950
38998
|
legendPosition: "top",
|
|
38951
|
-
}
|
|
38999
|
+
};
|
|
38952
39000
|
}
|
|
38953
39001
|
function buildScorecard(zone, getters) {
|
|
38954
39002
|
const cell = getters.getCell({
|
|
@@ -38971,22 +39019,18 @@ function buildScorecard(zone, getters) {
|
|
|
38971
39019
|
*/
|
|
38972
39020
|
function getSmartChartDefinition(zones, getters) {
|
|
38973
39021
|
const columns = categorizeColumns(zones, getters);
|
|
38974
|
-
|
|
38975
|
-
|
|
38976
|
-
|
|
38977
|
-
|
|
38978
|
-
|
|
38979
|
-
|
|
38980
|
-
});
|
|
39022
|
+
if (columns.length === 0 || columns.every((col) => col.type === "empty")) {
|
|
39023
|
+
const dataSets = columns.map(({ zone }) => ({ dataRange: getUnboundRange(getters, zone) }));
|
|
39024
|
+
return { ...DEFAULT_BAR_CHART_CONFIG, dataSets };
|
|
39025
|
+
}
|
|
39026
|
+
const nonEmptyColumns = columns.filter((col) => col.type !== "empty");
|
|
39027
|
+
switch (nonEmptyColumns.length) {
|
|
38981
39028
|
case 1:
|
|
38982
|
-
|
|
38983
|
-
return getZoneArea(singleColumn.zone) === 1
|
|
38984
|
-
? buildScorecard(singleColumn.zone, getters)
|
|
38985
|
-
: buildSingleColumnChart(singleColumn, getters);
|
|
39029
|
+
return buildSingleColumnChart(nonEmptyColumns[0], getters);
|
|
38986
39030
|
case 2:
|
|
38987
|
-
return buildTwoColumnChart(
|
|
39031
|
+
return buildTwoColumnChart(nonEmptyColumns, getters);
|
|
38988
39032
|
default:
|
|
38989
|
-
return buildMultiColumnChart(
|
|
39033
|
+
return buildMultiColumnChart(nonEmptyColumns, getters);
|
|
38990
39034
|
}
|
|
38991
39035
|
}
|
|
38992
39036
|
|
|
@@ -43959,8 +44003,10 @@ const LEGACY_VERSION_MAPPING = {
|
|
|
43959
44003
|
17: "17.4",
|
|
43960
44004
|
16: "17.3",
|
|
43961
44005
|
15: "17.2",
|
|
44006
|
+
"14.5": "16.4.1",
|
|
43962
44007
|
14: "16.4",
|
|
43963
44008
|
13: "16.3",
|
|
44009
|
+
"12.5": "15.4.1",
|
|
43964
44010
|
12: "15.4",
|
|
43965
44011
|
// not accurate starting at this point
|
|
43966
44012
|
11: "0.10",
|
|
@@ -44027,6 +44073,7 @@ function forceUnicityOfFigure(data) {
|
|
|
44027
44073
|
return data;
|
|
44028
44074
|
}
|
|
44029
44075
|
const figureIds = new Set();
|
|
44076
|
+
const chartIds = new Set();
|
|
44030
44077
|
const uuidGenerator = new UuidGenerator();
|
|
44031
44078
|
for (const sheet of data.sheets || []) {
|
|
44032
44079
|
for (const figure of sheet.figures || []) {
|
|
@@ -44034,6 +44081,12 @@ function forceUnicityOfFigure(data) {
|
|
|
44034
44081
|
figure.id += uuidGenerator.smallUuid();
|
|
44035
44082
|
}
|
|
44036
44083
|
figureIds.add(figure.id);
|
|
44084
|
+
if (figure.tag === "chart") {
|
|
44085
|
+
if (chartIds.has(figure.data?.chartId)) {
|
|
44086
|
+
figure.data.chartId += uuidGenerator.smallUuid();
|
|
44087
|
+
}
|
|
44088
|
+
chartIds.add(figure.data?.chartId);
|
|
44089
|
+
}
|
|
44037
44090
|
}
|
|
44038
44091
|
}
|
|
44039
44092
|
data.uniqueFigureIds = true;
|
|
@@ -54861,6 +54914,12 @@ class ChartHumanizeNumbers extends owl.Component {
|
|
|
54861
54914
|
updateChart: Function,
|
|
54862
54915
|
canUpdateChart: Function,
|
|
54863
54916
|
};
|
|
54917
|
+
get title() {
|
|
54918
|
+
const locale = this.env.model.getters.getLocale();
|
|
54919
|
+
const format = formatLargeNumber({ value: 1234567 }, undefined, locale);
|
|
54920
|
+
const value = formatValue(1234567, { format, locale });
|
|
54921
|
+
return _t("E.g. 1234567 -> %(value)s", { value });
|
|
54922
|
+
}
|
|
54864
54923
|
}
|
|
54865
54924
|
|
|
54866
54925
|
class ChartLegend extends owl.Component {
|
|
@@ -55848,6 +55907,7 @@ class SunburstChartDesignPanel extends owl.Component {
|
|
|
55848
55907
|
RoundColorPicker,
|
|
55849
55908
|
ChartLegend,
|
|
55850
55909
|
PieHoleSize,
|
|
55910
|
+
ChartHumanizeNumbers,
|
|
55851
55911
|
};
|
|
55852
55912
|
static props = {
|
|
55853
55913
|
chartId: String,
|
|
@@ -55961,6 +56021,7 @@ class TreeMapChartDesignPanel extends owl.Component {
|
|
|
55961
56021
|
BadgeSelection,
|
|
55962
56022
|
TreeMapCategoryColors,
|
|
55963
56023
|
TreeMapColorScale,
|
|
56024
|
+
ChartHumanizeNumbers,
|
|
55964
56025
|
};
|
|
55965
56026
|
static props = {
|
|
55966
56027
|
chartId: String,
|
|
@@ -68769,6 +68830,281 @@ class ZoneRBush extends RBush {
|
|
|
68769
68830
|
}
|
|
68770
68831
|
}
|
|
68771
68832
|
|
|
68833
|
+
class ZoneSet {
|
|
68834
|
+
profilesStartingPosition = [0];
|
|
68835
|
+
profiles = new Map([[0, []]]);
|
|
68836
|
+
constructor(zones = []) {
|
|
68837
|
+
for (const zone of zones) {
|
|
68838
|
+
this.add(zone);
|
|
68839
|
+
}
|
|
68840
|
+
}
|
|
68841
|
+
isEmpty() {
|
|
68842
|
+
return this.profiles.size === 1 && this.profiles.get(0)?.length === 0;
|
|
68843
|
+
}
|
|
68844
|
+
add(zone) {
|
|
68845
|
+
modifyProfiles(this.profilesStartingPosition, this.profiles, [zone]);
|
|
68846
|
+
}
|
|
68847
|
+
delete(zone) {
|
|
68848
|
+
modifyProfiles(this.profilesStartingPosition, this.profiles, [zone], true);
|
|
68849
|
+
}
|
|
68850
|
+
has(zone) {
|
|
68851
|
+
return profilesContainsZone(this.profilesStartingPosition, this.profiles, zone);
|
|
68852
|
+
}
|
|
68853
|
+
difference(other) {
|
|
68854
|
+
const result = this.copy();
|
|
68855
|
+
for (const zone of other) {
|
|
68856
|
+
result.delete(zone);
|
|
68857
|
+
}
|
|
68858
|
+
return result;
|
|
68859
|
+
}
|
|
68860
|
+
copy() {
|
|
68861
|
+
const result = new ZoneSet();
|
|
68862
|
+
result.profilesStartingPosition = [...this.profilesStartingPosition];
|
|
68863
|
+
result.profiles = new Map();
|
|
68864
|
+
for (const [key, value] of this.profiles) {
|
|
68865
|
+
result.profiles.set(key, [...value]);
|
|
68866
|
+
}
|
|
68867
|
+
return result;
|
|
68868
|
+
}
|
|
68869
|
+
size() {
|
|
68870
|
+
let size = 0;
|
|
68871
|
+
for (const profile of this.profiles.values()) {
|
|
68872
|
+
size += profile.length;
|
|
68873
|
+
}
|
|
68874
|
+
return size / 2;
|
|
68875
|
+
}
|
|
68876
|
+
/**
|
|
68877
|
+
* iterator of all the zones in the ZoneSet
|
|
68878
|
+
*/
|
|
68879
|
+
[Symbol.iterator]() {
|
|
68880
|
+
return constructZonesFromProfiles(this.profilesStartingPosition, this.profiles)[Symbol.iterator]();
|
|
68881
|
+
}
|
|
68882
|
+
}
|
|
68883
|
+
|
|
68884
|
+
class RangeSet {
|
|
68885
|
+
setsBySheetId = {};
|
|
68886
|
+
constructor(ranges = []) {
|
|
68887
|
+
for (const range of ranges) {
|
|
68888
|
+
this.add(range);
|
|
68889
|
+
}
|
|
68890
|
+
}
|
|
68891
|
+
add(range) {
|
|
68892
|
+
if (!this.setsBySheetId[range.sheetId]) {
|
|
68893
|
+
this.setsBySheetId[range.sheetId] = new ZoneSet();
|
|
68894
|
+
}
|
|
68895
|
+
this.setsBySheetId[range.sheetId].add(range.zone);
|
|
68896
|
+
}
|
|
68897
|
+
addMany(ranges) {
|
|
68898
|
+
for (const range of ranges) {
|
|
68899
|
+
this.add(range);
|
|
68900
|
+
}
|
|
68901
|
+
}
|
|
68902
|
+
addPosition(position) {
|
|
68903
|
+
this.add(positionToBoundedRange(position));
|
|
68904
|
+
}
|
|
68905
|
+
addManyPositions(positions) {
|
|
68906
|
+
for (const position of positions) {
|
|
68907
|
+
this.addPosition(position);
|
|
68908
|
+
}
|
|
68909
|
+
}
|
|
68910
|
+
has(range) {
|
|
68911
|
+
if (!this.setsBySheetId[range.sheetId]) {
|
|
68912
|
+
return false;
|
|
68913
|
+
}
|
|
68914
|
+
return this.setsBySheetId[range.sheetId].has(range.zone);
|
|
68915
|
+
}
|
|
68916
|
+
hasPosition(position) {
|
|
68917
|
+
return this.has(positionToBoundedRange(position));
|
|
68918
|
+
}
|
|
68919
|
+
delete(range) {
|
|
68920
|
+
if (!this.setsBySheetId[range.sheetId]) {
|
|
68921
|
+
return;
|
|
68922
|
+
}
|
|
68923
|
+
this.setsBySheetId[range.sheetId].delete(range.zone);
|
|
68924
|
+
}
|
|
68925
|
+
deleteMany(ranges) {
|
|
68926
|
+
for (const range of ranges) {
|
|
68927
|
+
this.delete(range);
|
|
68928
|
+
}
|
|
68929
|
+
}
|
|
68930
|
+
deleteManyPositions(positions) {
|
|
68931
|
+
for (const position of positions) {
|
|
68932
|
+
this.delete(positionToBoundedRange(position));
|
|
68933
|
+
}
|
|
68934
|
+
}
|
|
68935
|
+
difference(other) {
|
|
68936
|
+
const result = new RangeSet();
|
|
68937
|
+
for (const sheetId in this.setsBySheetId) {
|
|
68938
|
+
result.setsBySheetId[sheetId] = this.setsBySheetId[sheetId];
|
|
68939
|
+
}
|
|
68940
|
+
for (const sheetId in other.setsBySheetId) {
|
|
68941
|
+
if (result.setsBySheetId[sheetId]) {
|
|
68942
|
+
result.setsBySheetId[sheetId] = result.setsBySheetId[sheetId].difference(other.setsBySheetId[sheetId]);
|
|
68943
|
+
}
|
|
68944
|
+
}
|
|
68945
|
+
return result;
|
|
68946
|
+
}
|
|
68947
|
+
copy() {
|
|
68948
|
+
const result = new RangeSet();
|
|
68949
|
+
for (const sheetId in this.setsBySheetId) {
|
|
68950
|
+
result.setsBySheetId[sheetId] = this.setsBySheetId[sheetId].copy();
|
|
68951
|
+
}
|
|
68952
|
+
return result;
|
|
68953
|
+
}
|
|
68954
|
+
clear() {
|
|
68955
|
+
this.setsBySheetId = {};
|
|
68956
|
+
}
|
|
68957
|
+
size() {
|
|
68958
|
+
let size = 0;
|
|
68959
|
+
for (const sheetId in this.setsBySheetId) {
|
|
68960
|
+
size += this.setsBySheetId[sheetId].size();
|
|
68961
|
+
}
|
|
68962
|
+
return size;
|
|
68963
|
+
}
|
|
68964
|
+
isEmpty() {
|
|
68965
|
+
for (const sheetId in this.setsBySheetId) {
|
|
68966
|
+
if (!this.setsBySheetId[sheetId].isEmpty()) {
|
|
68967
|
+
return false;
|
|
68968
|
+
}
|
|
68969
|
+
}
|
|
68970
|
+
return true;
|
|
68971
|
+
}
|
|
68972
|
+
/**
|
|
68973
|
+
* iterator of all the ranges in the RangeSet
|
|
68974
|
+
*/
|
|
68975
|
+
[Symbol.iterator]() {
|
|
68976
|
+
const result = [];
|
|
68977
|
+
for (const sheetId in this.setsBySheetId) {
|
|
68978
|
+
for (const zone of this.setsBySheetId[sheetId]) {
|
|
68979
|
+
result.push({ sheetId: sheetId, zone });
|
|
68980
|
+
}
|
|
68981
|
+
}
|
|
68982
|
+
return result[Symbol.iterator]();
|
|
68983
|
+
}
|
|
68984
|
+
}
|
|
68985
|
+
|
|
68986
|
+
/**
|
|
68987
|
+
* R-Tree of ranges, mapping zones (r-tree bounding boxes) to ranges (data of the r-tree item).
|
|
68988
|
+
* Ranges associated to the exact same bounding box are grouped together
|
|
68989
|
+
* to reduce the number of nodes in the R-tree.
|
|
68990
|
+
*/
|
|
68991
|
+
class DependenciesRTree {
|
|
68992
|
+
rTree;
|
|
68993
|
+
constructor(items = []) {
|
|
68994
|
+
const compactedBoxes = groupSameBoundingBoxes(items);
|
|
68995
|
+
this.rTree = new SpreadsheetRTree(compactedBoxes);
|
|
68996
|
+
}
|
|
68997
|
+
insert(item) {
|
|
68998
|
+
const data = this.rTree.search(item.boundingBox);
|
|
68999
|
+
const itemBoundingBox = item.boundingBox;
|
|
69000
|
+
const exactBoundingBox = data.find(({ boundingBox }) => boundingBox.sheetId === itemBoundingBox.sheetId &&
|
|
69001
|
+
boundingBox.zone.left === itemBoundingBox.zone.left &&
|
|
69002
|
+
boundingBox.zone.top === itemBoundingBox.zone.top &&
|
|
69003
|
+
boundingBox.zone.right === itemBoundingBox.zone.right &&
|
|
69004
|
+
boundingBox.zone.bottom === itemBoundingBox.zone.bottom);
|
|
69005
|
+
if (exactBoundingBox) {
|
|
69006
|
+
exactBoundingBox.data.add(item.data);
|
|
69007
|
+
}
|
|
69008
|
+
else {
|
|
69009
|
+
this.rTree.insert({ ...item, data: new RangeSet([item.data]) });
|
|
69010
|
+
}
|
|
69011
|
+
}
|
|
69012
|
+
search({ zone, sheetId }) {
|
|
69013
|
+
const results = new RangeSet();
|
|
69014
|
+
for (const { data } of this.rTree.search({ zone, sheetId })) {
|
|
69015
|
+
results.addMany(data);
|
|
69016
|
+
}
|
|
69017
|
+
return results;
|
|
69018
|
+
}
|
|
69019
|
+
remove(item) {
|
|
69020
|
+
const data = this.rTree.search(item.boundingBox);
|
|
69021
|
+
const itemBoundingBox = item.boundingBox;
|
|
69022
|
+
const exactBoundingBox = data.find(({ boundingBox }) => boundingBox.sheetId === itemBoundingBox.sheetId &&
|
|
69023
|
+
boundingBox.zone.left === itemBoundingBox.zone.left &&
|
|
69024
|
+
boundingBox.zone.top === itemBoundingBox.zone.top &&
|
|
69025
|
+
boundingBox.zone.right === itemBoundingBox.zone.right &&
|
|
69026
|
+
boundingBox.zone.bottom === itemBoundingBox.zone.bottom);
|
|
69027
|
+
if (exactBoundingBox) {
|
|
69028
|
+
exactBoundingBox.data.delete(item.data);
|
|
69029
|
+
}
|
|
69030
|
+
else {
|
|
69031
|
+
this.rTree.remove({ ...item, data: new RangeSet([item.data]) });
|
|
69032
|
+
}
|
|
69033
|
+
}
|
|
69034
|
+
}
|
|
69035
|
+
/**
|
|
69036
|
+
* Group together all formulas pointing to the exact same dependency (bounding box).
|
|
69037
|
+
* The goal is to optimize the following case:
|
|
69038
|
+
* - if any cell in B1:B1000 changes, C1 must be recomputed
|
|
69039
|
+
* - if any cell in B1:B1000 changes, C2 must be recomputed
|
|
69040
|
+
* - if any cell in B1:B1000 changes, C3 must be recomputed
|
|
69041
|
+
* ...
|
|
69042
|
+
* - if any cell in B1:B1000 changes, C1000 must be recomputed
|
|
69043
|
+
*
|
|
69044
|
+
* Instead of having 1000 entries in the R-tree, we want to have a single entry
|
|
69045
|
+
* with B1:B1000 (bounding box) pointing to C1:C1000 (formulas).
|
|
69046
|
+
*/
|
|
69047
|
+
function groupSameBoundingBoxes(items) {
|
|
69048
|
+
// Important: this function must be as fast as possible. It is on the evaluation hot path.
|
|
69049
|
+
let maxCol = 0;
|
|
69050
|
+
let maxRow = 0;
|
|
69051
|
+
for (let i = 0; i < items.length; i++) {
|
|
69052
|
+
const zone = items[i].boundingBox.zone;
|
|
69053
|
+
if (zone.right > maxCol) {
|
|
69054
|
+
maxCol = zone.right;
|
|
69055
|
+
}
|
|
69056
|
+
if (zone.bottom > maxRow) {
|
|
69057
|
+
maxRow = zone.bottom;
|
|
69058
|
+
}
|
|
69059
|
+
}
|
|
69060
|
+
maxCol += 1;
|
|
69061
|
+
maxRow += 1;
|
|
69062
|
+
// in most real-world cases, we can use a fast numeric key
|
|
69063
|
+
// but if the zones are too far right or bottom, we fallback to a slower string key
|
|
69064
|
+
const maxPossibleKey = (((maxRow + 1) * maxCol + 1) * maxRow + 1) * maxCol;
|
|
69065
|
+
const useFastKey = maxPossibleKey <= Number.MAX_SAFE_INTEGER;
|
|
69066
|
+
if (!useFastKey) {
|
|
69067
|
+
console.warn("Max col/row size exceeded, using slow zone key");
|
|
69068
|
+
}
|
|
69069
|
+
const groupedByBBox = {};
|
|
69070
|
+
for (const item of items) {
|
|
69071
|
+
const sheetId = item.boundingBox.sheetId;
|
|
69072
|
+
if (!groupedByBBox[sheetId]) {
|
|
69073
|
+
groupedByBBox[sheetId] = {};
|
|
69074
|
+
}
|
|
69075
|
+
const bBox = item.boundingBox.zone;
|
|
69076
|
+
let bBoxKey = 0;
|
|
69077
|
+
if (useFastKey) {
|
|
69078
|
+
bBoxKey =
|
|
69079
|
+
bBox.left +
|
|
69080
|
+
bBox.top * maxCol +
|
|
69081
|
+
bBox.right * maxCol * maxRow +
|
|
69082
|
+
bBox.bottom * maxCol * maxRow * maxCol;
|
|
69083
|
+
}
|
|
69084
|
+
else {
|
|
69085
|
+
bBoxKey = `${bBox.left},${bBox.top},${bBox.right},${bBox.bottom}`;
|
|
69086
|
+
}
|
|
69087
|
+
if (groupedByBBox[sheetId][bBoxKey]) {
|
|
69088
|
+
const ranges = groupedByBBox[sheetId][bBoxKey].data;
|
|
69089
|
+
ranges.add(item.data);
|
|
69090
|
+
}
|
|
69091
|
+
else {
|
|
69092
|
+
groupedByBBox[sheetId][bBoxKey] = {
|
|
69093
|
+
boundingBox: item.boundingBox,
|
|
69094
|
+
data: new RangeSet([item.data]),
|
|
69095
|
+
};
|
|
69096
|
+
}
|
|
69097
|
+
}
|
|
69098
|
+
const result = [];
|
|
69099
|
+
for (const sheetId in groupedByBBox) {
|
|
69100
|
+
const map = groupedByBBox[sheetId];
|
|
69101
|
+
for (const key in map) {
|
|
69102
|
+
result.push(map[key]);
|
|
69103
|
+
}
|
|
69104
|
+
}
|
|
69105
|
+
return result;
|
|
69106
|
+
}
|
|
69107
|
+
|
|
68772
69108
|
/**
|
|
68773
69109
|
* Implementation of a dependency Graph.
|
|
68774
69110
|
* The graph is used to evaluate the cells in the correct
|
|
@@ -68777,12 +69113,10 @@ class ZoneRBush extends RBush {
|
|
|
68777
69113
|
* It uses an R-Tree data structure to efficiently find dependent cells.
|
|
68778
69114
|
*/
|
|
68779
69115
|
class FormulaDependencyGraph {
|
|
68780
|
-
createEmptyPositionSet;
|
|
68781
69116
|
dependencies = new PositionMap();
|
|
68782
69117
|
rTree;
|
|
68783
|
-
constructor(
|
|
68784
|
-
this.
|
|
68785
|
-
this.rTree = new SpreadsheetRTree(data);
|
|
69118
|
+
constructor(data = []) {
|
|
69119
|
+
this.rTree = new DependenciesRTree(data);
|
|
68786
69120
|
}
|
|
68787
69121
|
removeAllDependencies(formulaPosition) {
|
|
68788
69122
|
const ranges = this.dependencies.get(formulaPosition);
|
|
@@ -68796,7 +69130,10 @@ class FormulaDependencyGraph {
|
|
|
68796
69130
|
}
|
|
68797
69131
|
addDependencies(formulaPosition, dependencies) {
|
|
68798
69132
|
const rTreeItems = dependencies.map(({ sheetId, zone }) => ({
|
|
68799
|
-
data:
|
|
69133
|
+
data: {
|
|
69134
|
+
sheetId: formulaPosition.sheetId,
|
|
69135
|
+
zone: positionToZone(formulaPosition),
|
|
69136
|
+
},
|
|
68800
69137
|
boundingBox: {
|
|
68801
69138
|
zone,
|
|
68802
69139
|
sheetId,
|
|
@@ -68814,46 +69151,20 @@ class FormulaDependencyGraph {
|
|
|
68814
69151
|
}
|
|
68815
69152
|
}
|
|
68816
69153
|
/**
|
|
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)
|
|
69154
|
+
* Return all the cells that depend on the provided ranges.
|
|
68820
69155
|
*/
|
|
68821
|
-
getCellsDependingOn(ranges,
|
|
68822
|
-
|
|
69156
|
+
getCellsDependingOn(ranges, visited = new RangeSet()) {
|
|
69157
|
+
visited = visited.copy();
|
|
68823
69158
|
const queue = Array.from(ranges).reverse();
|
|
68824
69159
|
while (queue.length > 0) {
|
|
68825
69160
|
const range = queue.pop();
|
|
68826
|
-
|
|
68827
|
-
const
|
|
68828
|
-
|
|
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
|
-
}
|
|
69161
|
+
visited.add(range);
|
|
69162
|
+
const impactedRanges = this.rTree.search(range);
|
|
69163
|
+
queue.push(...impactedRanges.difference(visited));
|
|
68847
69164
|
}
|
|
68848
69165
|
// remove initial ranges
|
|
68849
69166
|
for (const range of ranges) {
|
|
68850
|
-
|
|
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
|
-
}
|
|
69167
|
+
visited.delete(range);
|
|
68857
69168
|
}
|
|
68858
69169
|
return visited;
|
|
68859
69170
|
}
|
|
@@ -69116,7 +69427,7 @@ class Evaluator {
|
|
|
69116
69427
|
getters;
|
|
69117
69428
|
compilationParams;
|
|
69118
69429
|
evaluatedCells = new PositionMap();
|
|
69119
|
-
formulaDependencies = lazy(new FormulaDependencyGraph(
|
|
69430
|
+
formulaDependencies = lazy(new FormulaDependencyGraph());
|
|
69120
69431
|
blockedArrayFormulas = new PositionSet({});
|
|
69121
69432
|
spreadingRelations = new SpreadingRelation();
|
|
69122
69433
|
constructor(context, getters) {
|
|
@@ -69151,7 +69462,7 @@ class Evaluator {
|
|
|
69151
69462
|
return undefined;
|
|
69152
69463
|
}
|
|
69153
69464
|
const arrayFormulas = this.spreadingRelations.searchFormulaPositionsSpreadingOn(position.sheetId, positionToZone(position));
|
|
69154
|
-
return
|
|
69465
|
+
return arrayFormulas.find((position) => !this.blockedArrayFormulas.has(position));
|
|
69155
69466
|
}
|
|
69156
69467
|
updateDependencies(position) {
|
|
69157
69468
|
// removing dependencies is slow because it requires
|
|
@@ -69195,57 +69506,72 @@ class Evaluator {
|
|
|
69195
69506
|
}
|
|
69196
69507
|
evaluateCells(positions) {
|
|
69197
69508
|
const start = performance.now();
|
|
69198
|
-
const
|
|
69199
|
-
|
|
69509
|
+
const rangesToCompute = new RangeSet();
|
|
69510
|
+
rangesToCompute.addManyPositions(positions);
|
|
69200
69511
|
const arrayFormulasPositions = this.getArrayFormulasImpactedByChangesOf(positions);
|
|
69201
|
-
|
|
69202
|
-
|
|
69203
|
-
|
|
69204
|
-
this.evaluate(
|
|
69512
|
+
rangesToCompute.addMany(this.getCellsDependingOn(rangesToCompute));
|
|
69513
|
+
rangesToCompute.addMany(arrayFormulasPositions);
|
|
69514
|
+
rangesToCompute.addMany(this.getCellsDependingOn(arrayFormulasPositions));
|
|
69515
|
+
this.evaluate(rangesToCompute);
|
|
69205
69516
|
console.debug("evaluate Cells", performance.now() - start, "ms");
|
|
69206
69517
|
}
|
|
69207
69518
|
getArrayFormulasImpactedByChangesOf(positions) {
|
|
69208
|
-
const
|
|
69519
|
+
const impactedRanges = new RangeSet();
|
|
69209
69520
|
for (const position of positions) {
|
|
69210
69521
|
const content = this.getters.getCell(position)?.content;
|
|
69211
69522
|
const arrayFormulaPosition = this.getArrayFormulaSpreadingOn(position);
|
|
69212
69523
|
if (arrayFormulaPosition !== undefined) {
|
|
69213
69524
|
// take into account new collisions.
|
|
69214
|
-
|
|
69525
|
+
impactedRanges.addPosition(arrayFormulaPosition);
|
|
69215
69526
|
}
|
|
69216
69527
|
if (!content) {
|
|
69217
69528
|
// The previous content could have blocked some array formulas
|
|
69218
|
-
|
|
69529
|
+
impactedRanges.addPosition(position);
|
|
69219
69530
|
}
|
|
69220
69531
|
}
|
|
69221
|
-
const
|
|
69222
|
-
|
|
69223
|
-
for (const zone of zonesBySheetIds[sheetId]) {
|
|
69224
|
-
impactedPositions.addMany(this.getArrayFormulasBlockedBy(sheetId, zone));
|
|
69225
|
-
}
|
|
69532
|
+
for (const range of [...impactedRanges]) {
|
|
69533
|
+
impactedRanges.addMany(this.getArrayFormulasBlockedBy(range.sheetId, range.zone));
|
|
69226
69534
|
}
|
|
69227
|
-
return
|
|
69535
|
+
return impactedRanges;
|
|
69228
69536
|
}
|
|
69229
69537
|
buildDependencyGraph() {
|
|
69230
69538
|
this.blockedArrayFormulas = this.createEmptyPositionSet();
|
|
69231
69539
|
this.spreadingRelations = new SpreadingRelation();
|
|
69232
69540
|
this.formulaDependencies = lazy(() => {
|
|
69233
|
-
const
|
|
69234
|
-
|
|
69235
|
-
.
|
|
69236
|
-
|
|
69237
|
-
|
|
69238
|
-
|
|
69239
|
-
|
|
69240
|
-
|
|
69241
|
-
|
|
69242
|
-
|
|
69541
|
+
const rTreeItems = [];
|
|
69542
|
+
for (const sheetId of this.getters.getSheetIds()) {
|
|
69543
|
+
const cells = this.getters.getCells(sheetId);
|
|
69544
|
+
for (const cellId in cells) {
|
|
69545
|
+
const cell = cells[cellId];
|
|
69546
|
+
if (cell.isFormula) {
|
|
69547
|
+
const directDependencies = cell.compiledFormula.dependencies;
|
|
69548
|
+
for (const range of directDependencies) {
|
|
69549
|
+
if (range.invalidSheetName || range.invalidXc) {
|
|
69550
|
+
continue;
|
|
69551
|
+
}
|
|
69552
|
+
rTreeItems.push({
|
|
69553
|
+
data: {
|
|
69554
|
+
sheetId,
|
|
69555
|
+
zone: positionToZone(this.getters.getCellPosition(cellId)),
|
|
69556
|
+
},
|
|
69557
|
+
boundingBox: { sheetId: range.sheetId, zone: range.zone },
|
|
69558
|
+
});
|
|
69559
|
+
}
|
|
69560
|
+
}
|
|
69561
|
+
}
|
|
69562
|
+
}
|
|
69563
|
+
return new FormulaDependencyGraph(rTreeItems);
|
|
69243
69564
|
});
|
|
69244
69565
|
}
|
|
69245
69566
|
evaluateAllCells() {
|
|
69246
69567
|
const start = performance.now();
|
|
69247
69568
|
this.evaluatedCells = new PositionMap();
|
|
69248
|
-
|
|
69569
|
+
const ranges = [];
|
|
69570
|
+
for (const sheetId of this.getters.getSheetIds()) {
|
|
69571
|
+
const zone = this.getters.getSheetZone(sheetId);
|
|
69572
|
+
ranges.push({ sheetId, zone });
|
|
69573
|
+
}
|
|
69574
|
+
this.evaluate(ranges);
|
|
69249
69575
|
console.debug("evaluate all cells", performance.now() - start, "ms");
|
|
69250
69576
|
}
|
|
69251
69577
|
evaluateFormulaResult(sheetId, formulaString) {
|
|
@@ -69269,48 +69595,47 @@ class Evaluator {
|
|
|
69269
69595
|
return handleError(error, "");
|
|
69270
69596
|
}
|
|
69271
69597
|
}
|
|
69272
|
-
getAllCells() {
|
|
69273
|
-
const positions = this.createEmptyPositionSet();
|
|
69274
|
-
positions.fillAllPositions();
|
|
69275
|
-
return positions;
|
|
69276
|
-
}
|
|
69277
69598
|
/**
|
|
69278
69599
|
* Return the position of formulas blocked by the given positions
|
|
69279
69600
|
* as well as all their dependencies.
|
|
69280
69601
|
*/
|
|
69281
69602
|
getArrayFormulasBlockedBy(sheetId, zone) {
|
|
69282
|
-
const arrayFormulaPositions =
|
|
69603
|
+
const arrayFormulaPositions = new RangeSet();
|
|
69283
69604
|
const arrayFormulas = this.spreadingRelations.searchFormulaPositionsSpreadingOn(sheetId, zone);
|
|
69284
|
-
arrayFormulaPositions.
|
|
69605
|
+
arrayFormulaPositions.addManyPositions(arrayFormulas);
|
|
69285
69606
|
const spilledPositions = [...arrayFormulas].filter((position) => !this.blockedArrayFormulas.has(position));
|
|
69286
69607
|
if (spilledPositions.length) {
|
|
69287
69608
|
// ignore the formula spreading on the position. Keep only the blocked ones
|
|
69288
|
-
arrayFormulaPositions.
|
|
69609
|
+
arrayFormulaPositions.deleteManyPositions(spilledPositions);
|
|
69289
69610
|
}
|
|
69290
69611
|
arrayFormulaPositions.addMany(this.getCellsDependingOn(arrayFormulaPositions));
|
|
69291
69612
|
return arrayFormulaPositions;
|
|
69292
69613
|
}
|
|
69293
|
-
|
|
69614
|
+
nextRangesToUpdate = new RangeSet();
|
|
69294
69615
|
cellsBeingComputed = new Set();
|
|
69295
69616
|
symbolsBeingComputed = new Set();
|
|
69296
|
-
evaluate(
|
|
69617
|
+
evaluate(ranges) {
|
|
69297
69618
|
this.cellsBeingComputed = new Set();
|
|
69298
|
-
this.
|
|
69619
|
+
this.nextRangesToUpdate = new RangeSet(ranges);
|
|
69299
69620
|
let currentIteration = 0;
|
|
69300
|
-
while (!this.
|
|
69621
|
+
while (!this.nextRangesToUpdate.isEmpty() && currentIteration++ < MAX_ITERATION) {
|
|
69301
69622
|
this.updateCompilationParameters();
|
|
69302
|
-
const
|
|
69303
|
-
|
|
69304
|
-
|
|
69305
|
-
|
|
69306
|
-
|
|
69307
|
-
|
|
69308
|
-
|
|
69309
|
-
|
|
69310
|
-
|
|
69311
|
-
|
|
69312
|
-
|
|
69313
|
-
|
|
69623
|
+
const ranges = [...this.nextRangesToUpdate];
|
|
69624
|
+
this.nextRangesToUpdate.clear();
|
|
69625
|
+
this.clearEvaluatedRanges(ranges);
|
|
69626
|
+
for (const range of ranges) {
|
|
69627
|
+
const { left, bottom, right, top } = range.zone;
|
|
69628
|
+
for (let col = left; col <= right; col++) {
|
|
69629
|
+
for (let row = top; row <= bottom; row++) {
|
|
69630
|
+
const position = { sheetId: range.sheetId, col, row };
|
|
69631
|
+
if (this.nextRangesToUpdate.hasPosition(position)) {
|
|
69632
|
+
continue;
|
|
69633
|
+
}
|
|
69634
|
+
const evaluatedCell = this.computeCell(position);
|
|
69635
|
+
if (evaluatedCell !== EMPTY_CELL) {
|
|
69636
|
+
this.evaluatedCells.set(position, evaluatedCell);
|
|
69637
|
+
}
|
|
69638
|
+
}
|
|
69314
69639
|
}
|
|
69315
69640
|
}
|
|
69316
69641
|
onIterationEndEvaluationRegistry.getAll().forEach((callback) => callback(this.getters));
|
|
@@ -69319,6 +69644,16 @@ class Evaluator {
|
|
|
69319
69644
|
console.warn("Maximum iteration reached while evaluating cells");
|
|
69320
69645
|
}
|
|
69321
69646
|
}
|
|
69647
|
+
clearEvaluatedRanges(ranges) {
|
|
69648
|
+
for (const range of ranges) {
|
|
69649
|
+
const { left, bottom, right, top } = range.zone;
|
|
69650
|
+
for (let col = left; col <= right; col++) {
|
|
69651
|
+
for (let row = top; row <= bottom; row++) {
|
|
69652
|
+
this.evaluatedCells.delete({ sheetId: range.sheetId, col, row });
|
|
69653
|
+
}
|
|
69654
|
+
}
|
|
69655
|
+
}
|
|
69656
|
+
}
|
|
69322
69657
|
computeCell(position) {
|
|
69323
69658
|
const evaluation = this.evaluatedCells.get(position);
|
|
69324
69659
|
if (evaluation) {
|
|
@@ -69391,9 +69726,9 @@ class Evaluator {
|
|
|
69391
69726
|
}
|
|
69392
69727
|
invalidatePositionsDependingOnSpread(sheetId, resultZone) {
|
|
69393
69728
|
// the result matrix is split in 2 zones to exclude the array formula position
|
|
69394
|
-
const invalidatedPositions = this.
|
|
69395
|
-
invalidatedPositions.delete({ sheetId,
|
|
69396
|
-
this.
|
|
69729
|
+
const invalidatedPositions = this.getCellsDependingOn(excludeTopLeft(resultZone).map((zone) => ({ sheetId, zone })));
|
|
69730
|
+
invalidatedPositions.delete({ sheetId, zone: resultZone });
|
|
69731
|
+
this.nextRangesToUpdate.addMany(invalidatedPositions);
|
|
69397
69732
|
}
|
|
69398
69733
|
assertSheetHasEnoughSpaceToSpreadFormulaResult({ sheetId, col, row }, matrixResult) {
|
|
69399
69734
|
const numberOfCols = this.getters.getNumberCols(sheetId);
|
|
@@ -69468,7 +69803,7 @@ class Evaluator {
|
|
|
69468
69803
|
}
|
|
69469
69804
|
const sheetId = position.sheetId;
|
|
69470
69805
|
this.invalidatePositionsDependingOnSpread(sheetId, zone);
|
|
69471
|
-
this.
|
|
69806
|
+
this.nextRangesToUpdate.addMany(this.getArrayFormulasBlockedBy(sheetId, zone));
|
|
69472
69807
|
}
|
|
69473
69808
|
/**
|
|
69474
69809
|
* Wraps a GetSymbolValue function to add cycle detection
|
|
@@ -69503,13 +69838,8 @@ class Evaluator {
|
|
|
69503
69838
|
}
|
|
69504
69839
|
return cell.compiledFormula.dependencies;
|
|
69505
69840
|
}
|
|
69506
|
-
getCellsDependingOn(
|
|
69507
|
-
|
|
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);
|
|
69841
|
+
getCellsDependingOn(ranges) {
|
|
69842
|
+
return this.formulaDependencies().getCellsDependingOn(ranges, this.nextRangesToUpdate);
|
|
69513
69843
|
}
|
|
69514
69844
|
}
|
|
69515
69845
|
function forEachSpreadPositionInMatrix(nbColumns, nbRows, callback) {
|
|
@@ -88624,6 +88954,6 @@ exports.tokenColors = tokenColors;
|
|
|
88624
88954
|
exports.tokenize = tokenize;
|
|
88625
88955
|
|
|
88626
88956
|
|
|
88627
|
-
__info__.version = "19.0.
|
|
88628
|
-
__info__.date = "2025-10-
|
|
88629
|
-
__info__.hash = "
|
|
88957
|
+
__info__.version = "19.0.8";
|
|
88958
|
+
__info__.date = "2025-10-30T12:25:04.355Z";
|
|
88959
|
+
__info__.hash = "559e4e5";
|