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