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