@odoo/o-spreadsheet 19.0.5 → 19.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,9 +2,9 @@
2
2
  /**
3
3
  * This file is generated by o-spreadsheet build tools. Do not edit it.
4
4
  * @see https://github.com/odoo/o-spreadsheet
5
- * @version 19.0.5
6
- * @date 2025-10-07T10:04:06.400Z
7
- * @hash 86fc442
5
+ * @version 19.0.7
6
+ * @date 2025-10-23T08:19:01.764Z
7
+ * @hash 1c1d1ec
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
- profile.splice(topPredIndex + 1, bottomSuccIndex - topPredIndex - 1, ...newPoints);
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
  */
@@ -4514,7 +4539,17 @@
4514
4539
  return toMatrix(data).map((row) => {
4515
4540
  return row.map((cell) => {
4516
4541
  if (typeof cell.value !== "number") {
4517
- throw new EvaluationError(_t("Function [[FUNCTION_NAME]] expects number values for %s, but got a %s.", argName, typeof cell.value));
4542
+ let message = "";
4543
+ if (typeof cell === "object") {
4544
+ message = _t("Function [[FUNCTION_NAME]] expects number values for %s, but got an empty value.", argName);
4545
+ }
4546
+ else if (typeof cell === "string") {
4547
+ message = _t("Function [[FUNCTION_NAME]] expects number values for %s, but got a string.", argName);
4548
+ }
4549
+ else if (typeof cell === "boolean") {
4550
+ message = _t("Function [[FUNCTION_NAME]] expects number values for %s, but got a boolean.", argName);
4551
+ }
4552
+ throw new EvaluationError(message);
4518
4553
  }
4519
4554
  return cell.value;
4520
4555
  });
@@ -6933,6 +6968,10 @@
6933
6968
  }
6934
6969
  return parts;
6935
6970
  }
6971
+ function positionToBoundedRange(position) {
6972
+ const zone = { left: position.col, top: position.row, right: position.col, bottom: position.row };
6973
+ return { sheetId: position.sheetId, zone };
6974
+ }
6936
6975
  /**
6937
6976
  * Check that a zone is valid regarding the order of top-bottom and left-right.
6938
6977
  * Left should be smaller than right, top should be smaller than bottom.
@@ -9529,7 +9568,7 @@
9529
9568
  pasteCell(origin, target, clipboardOption) {
9530
9569
  const { sheetId, col, row } = target;
9531
9570
  const targetCell = this.getters.getEvaluatedCell(target);
9532
- const originFormat = origin?.format ?? origin.evaluatedCell.format;
9571
+ const originFormat = origin?.format || origin.evaluatedCell.format;
9533
9572
  if (clipboardOption?.pasteOption === "asValue") {
9534
9573
  this.dispatch("UPDATE_CELL", {
9535
9574
  ...target,
@@ -13902,7 +13941,7 @@ stores.inject(MyMetaStore, storeInstance);
13902
13941
  if (knownDataY.length === 0 || knownDataY[0].length === 0) {
13903
13942
  return new EvaluationError(emptyDataErrorMessage("known_data_y"));
13904
13943
  }
13905
- return expM(predictLinearValues(logM(toNumberMatrix(knownDataY, "the first argument (known_data_y)")), toNumberMatrix(knownDataX, "the second argument (known_data_x)"), toNumberMatrix(newDataX, "the third argument (new_data_y)"), toBoolean(b)));
13944
+ return expM(predictLinearValues(logM(toNumberMatrix(knownDataY, "known_data_y")), toNumberMatrix(knownDataX, "known_data_x"), toNumberMatrix(newDataX, "new_data_y"), toBoolean(b)));
13906
13945
  },
13907
13946
  };
13908
13947
  // -----------------------------------------------------------------------------
@@ -13975,7 +14014,7 @@ stores.inject(MyMetaStore, storeInstance);
13975
14014
  if (dataY.length === 0 || dataY[0].length === 0) {
13976
14015
  return new EvaluationError(emptyDataErrorMessage("data_y"));
13977
14016
  }
13978
- return fullLinearRegression(toNumberMatrix(dataX, "the first argument (data_y)"), toNumberMatrix(dataY, "the second argument (data_x)"), toBoolean(calculateB), toBoolean(verbose));
14017
+ return fullLinearRegression(toNumberMatrix(dataX, "data_x"), toNumberMatrix(dataY, "data_y"), toBoolean(calculateB), toBoolean(verbose));
13979
14018
  },
13980
14019
  isExported: true,
13981
14020
  };
@@ -13994,7 +14033,7 @@ stores.inject(MyMetaStore, storeInstance);
13994
14033
  if (dataY.length === 0 || dataY[0].length === 0) {
13995
14034
  return new EvaluationError(emptyDataErrorMessage("data_y"));
13996
14035
  }
13997
- const coeffs = fullLinearRegression(toNumberMatrix(dataX, "the second argument (data_x)"), logM(toNumberMatrix(dataY, "the first argument (data_y)")), toBoolean(calculateB), toBoolean(verbose));
14036
+ const coeffs = fullLinearRegression(toNumberMatrix(dataX, "data_x"), logM(toNumberMatrix(dataY, "data_y")), toBoolean(calculateB), toBoolean(verbose));
13998
14037
  for (let i = 0; i < coeffs.length; i++) {
13999
14038
  coeffs[i][0] = Math.exp(coeffs[i][0]);
14000
14039
  }
@@ -14615,7 +14654,7 @@ stores.inject(MyMetaStore, storeInstance);
14615
14654
  if (knownDataY.length === 0 || knownDataY[0].length === 0) {
14616
14655
  return new EvaluationError(emptyDataErrorMessage("known_data_y"));
14617
14656
  }
14618
- return predictLinearValues(toNumberMatrix(knownDataY, "the first argument (known_data_y)"), toNumberMatrix(knownDataX, "the second argument (known_data_x)"), toNumberMatrix(newDataX, "the third argument (new_data_y)"), toBoolean(b));
14657
+ return predictLinearValues(toNumberMatrix(knownDataY, "known_data_y"), toNumberMatrix(knownDataX, "known_data_x"), toNumberMatrix(newDataX, "new_data_y"), toBoolean(b));
14619
14658
  },
14620
14659
  };
14621
14660
  // -----------------------------------------------------------------------------
@@ -23136,6 +23175,10 @@ stores.inject(MyMetaStore, storeInstance);
23136
23175
  }
23137
23176
  const ctx = chart.ctx;
23138
23177
  ctx.save();
23178
+ const { left, top, height, width } = chart.chartArea;
23179
+ ctx.beginPath();
23180
+ ctx.rect(left, top, width, height);
23181
+ ctx.clip();
23139
23182
  ctx.textAlign = "center";
23140
23183
  ctx.textBaseline = "middle";
23141
23184
  ctx.miterLimit = 1; // Avoid sharp artifacts on strokeText
@@ -24179,7 +24222,7 @@ stores.inject(MyMetaStore, storeInstance);
24179
24222
  this.chart.update();
24180
24223
  }
24181
24224
  hasChartDataChanged() {
24182
- return !deepEquals(this.currentRuntime.chartJsConfig.data, this.chartRuntime.chartJsConfig.data);
24225
+ return !deepEquals(this.getChartDataInRuntime(this.currentRuntime), this.getChartDataInRuntime(this.chartRuntime));
24183
24226
  }
24184
24227
  enableAnimationInChartData(chartData) {
24185
24228
  return {
@@ -24187,6 +24230,17 @@ stores.inject(MyMetaStore, storeInstance);
24187
24230
  options: { ...chartData.options, animation: { animateRotate: true } },
24188
24231
  };
24189
24232
  }
24233
+ getChartDataInRuntime(runtime) {
24234
+ const data = runtime.chartJsConfig.data;
24235
+ return {
24236
+ labels: data.labels,
24237
+ dataset: data.datasets.map((dataset) => ({
24238
+ data: dataset.data,
24239
+ label: dataset.label,
24240
+ tree: dataset.tree,
24241
+ })),
24242
+ };
24243
+ }
24190
24244
  get animationChartId() {
24191
24245
  return this.props.isFullScreen ? this.props.chartId + "-fullscreen" : this.props.chartId;
24192
24246
  }
@@ -25610,6 +25664,7 @@ stores.inject(MyMetaStore, storeInstance);
25610
25664
  parser: luxonFormat,
25611
25665
  displayFormats,
25612
25666
  unit: timeUnit ?? false,
25667
+ tooltipFormat: luxonFormat,
25613
25668
  };
25614
25669
  }
25615
25670
  /**
@@ -26678,6 +26733,7 @@ stores.inject(MyMetaStore, storeInstance);
26678
26733
  };
26679
26734
  Object.assign(scales.x, axis);
26680
26735
  scales.x.ticks.maxTicksLimit = 15;
26736
+ delete scales?.x?.ticks?.callback;
26681
26737
  }
26682
26738
  else if (axisType === "linear") {
26683
26739
  scales.x.type = "linear";
@@ -32363,7 +32419,7 @@ stores.inject(MyMetaStore, storeInstance);
32363
32419
  }
32364
32420
  openContextMenu(ev) {
32365
32421
  this.menuState.isOpen = true;
32366
- this.menuState.anchorRect = { x: ev.clientX, y: ev.clientY, width: 0, height: 0 };
32422
+ this.menuState.anchorRect = getBoundingRectAsPOJO(ev.currentTarget);
32367
32423
  const figureId = this.env.model.getters.getFigureIdFromChartId(this.props.chartId);
32368
32424
  this.menuState.menuItems = getChartMenuActions(figureId, () => { }, this.env);
32369
32425
  }
@@ -32395,6 +32451,7 @@ stores.inject(MyMetaStore, storeInstance);
32395
32451
  onFigureDeleted: Function,
32396
32452
  editFigureStyle: { type: Function, optional: true },
32397
32453
  isFullScreen: { type: Boolean, optional: true },
32454
+ openContextMenu: { type: Function, optional: true },
32398
32455
  };
32399
32456
  static components = { ChartDashboardMenu, MenuPopover };
32400
32457
  carouselTabsRef = owl.useRef("carouselTabs");
@@ -32528,6 +32585,12 @@ stores.inject(MyMetaStore, storeInstance);
32528
32585
  get visibleCarouselItems() {
32529
32586
  return this.carousel.items.filter((item) => item.type === "carouselDataView" && this.props.isFullScreen ? false : true);
32530
32587
  }
32588
+ openContextMenu(event) {
32589
+ const target = event.currentTarget;
32590
+ if (target) {
32591
+ this.props.openContextMenu?.(getBoundingRectAsPOJO(target));
32592
+ }
32593
+ }
32531
32594
  }
32532
32595
 
32533
32596
  class ChartFigure extends owl.Component {
@@ -32537,6 +32600,7 @@ stores.inject(MyMetaStore, storeInstance);
32537
32600
  onFigureDeleted: Function,
32538
32601
  editFigureStyle: { type: Function, optional: true },
32539
32602
  isFullScreen: { type: Boolean, optional: true },
32603
+ openContextMenu: { type: Function, optional: true },
32540
32604
  };
32541
32605
  static components = { ChartDashboardMenu };
32542
32606
  onDoubleClick() {
@@ -32569,6 +32633,7 @@ stores.inject(MyMetaStore, storeInstance);
32569
32633
  figureUI: Object,
32570
32634
  onFigureDeleted: Function,
32571
32635
  editFigureStyle: { type: Function, optional: true },
32636
+ openContextMenu: { type: Function, optional: true },
32572
32637
  };
32573
32638
  static components = {};
32574
32639
  // ---------------------------------------------------------------------------
@@ -34620,8 +34685,11 @@ stores.inject(MyMetaStore, storeInstance);
34620
34685
  }
34621
34686
  const newSelection = this.contentHelper.getCurrentSelection();
34622
34687
  this.props.composerStore.stopComposerRangeSelection();
34623
- this.props.onComposerContentFocused();
34624
- this.props.composerStore.changeComposerCursorSelection(newSelection.start, newSelection.end);
34688
+ const isCurrentlyInactive = this.props.composerStore.editionMode === "inactive";
34689
+ this.props.onComposerContentFocused(newSelection);
34690
+ if (!isCurrentlyInactive) {
34691
+ this.props.composerStore.changeComposerCursorSelection(newSelection.start, newSelection.end);
34692
+ }
34625
34693
  this.processTokenAtCursor();
34626
34694
  }
34627
34695
  onDblClick() {
@@ -35116,13 +35184,6 @@ stores.inject(MyMetaStore, storeInstance);
35116
35184
  }
35117
35185
  }
35118
35186
  startEdition(text, selection) {
35119
- if (selection) {
35120
- const content = text || this.getComposerContent(this.getters.getActivePosition());
35121
- const validSelection = this.isSelectionValid(content.length, selection.start, selection.end);
35122
- if (!validSelection) {
35123
- return;
35124
- }
35125
- }
35126
35187
  const { col, row } = this.getters.getActivePosition();
35127
35188
  this.model.dispatch("SELECT_FIGURE", { figureId: null });
35128
35189
  this.model.dispatch("SCROLL_TO_CELL", { col, row });
@@ -35179,7 +35240,7 @@ stores.inject(MyMetaStore, storeInstance);
35179
35240
  // ---------------------------------------------------------------------------
35180
35241
  get currentContent() {
35181
35242
  if (this.editionMode === "inactive") {
35182
- return this.getComposerContent(this.getters.getActivePosition());
35243
+ return this.getComposerContent(this.getters.getActivePosition()).text;
35183
35244
  }
35184
35245
  return this._currentContent;
35185
35246
  }
@@ -35378,8 +35439,9 @@ stores.inject(MyMetaStore, storeInstance);
35378
35439
  this.sheetId = sheetId;
35379
35440
  this.row = row;
35380
35441
  this.editionMode = "editing";
35381
- this.initialContent = this.getComposerContent({ sheetId, col, row });
35382
- this.setContent(str || this.initialContent, selection);
35442
+ const { text, adjustedSelection } = this.getComposerContent({ sheetId, col, row }, selection);
35443
+ this.initialContent = text;
35444
+ this.setContent(str || this.initialContent, adjustedSelection ?? selection);
35383
35445
  this.colorIndexByRange = {};
35384
35446
  const zone = positionToZone({ col: this.col, row: this.row });
35385
35447
  this.captureSelection(zone, col, row);
@@ -35856,7 +35918,7 @@ stores.inject(MyMetaStore, storeInstance);
35856
35918
  constructor(get, args) {
35857
35919
  super(get);
35858
35920
  this.args = args;
35859
- this._currentContent = this.getComposerContent();
35921
+ this._currentContent = this.getComposerContent().text;
35860
35922
  }
35861
35923
  getAutoCompleteProviders() {
35862
35924
  const providersDefinitions = super.getAutoCompleteProviders();
@@ -35893,7 +35955,7 @@ stores.inject(MyMetaStore, storeInstance);
35893
35955
  })
35894
35956
  .join("");
35895
35957
  }
35896
- return localizeContent(content, this.getters.getLocale());
35958
+ return { text: localizeContent(content, this.getters.getLocale()) };
35897
35959
  }
35898
35960
  stopEdition() {
35899
35961
  this._stopEdition();
@@ -38663,12 +38725,23 @@ stores.inject(MyMetaStore, storeInstance);
38663
38725
  .add("LinkEditor", LinkEditorPopoverBuilder)
38664
38726
  .add("FilterMenu", FilterMenuPopoverBuilder);
38665
38727
 
38666
- const CHART_LIMITS = {
38667
- MAX_PIE_CATEGORIES: 7,
38668
- MAX_PIE_CATEGORIES_NO_TITLE: 6,
38669
- MIN_RADAR_CATEGORIES: 3,
38670
- MAX_RADAR_CATEGORIES: 12,
38671
- PERCENTAGE_THRESHOLD: 100,
38728
+ const DEFAULT_BAR_CHART_CONFIG = {
38729
+ type: "bar",
38730
+ title: {},
38731
+ dataSets: [],
38732
+ legendPosition: "none",
38733
+ dataSetsHaveTitle: false,
38734
+ stacked: false,
38735
+ };
38736
+ const DEFAULT_LINE_CHART_CONFIG = {
38737
+ type: "line",
38738
+ title: {},
38739
+ dataSets: [],
38740
+ legendPosition: "none",
38741
+ dataSetsHaveTitle: false,
38742
+ stacked: false,
38743
+ cumulative: false,
38744
+ labelsAsText: false,
38672
38745
  };
38673
38746
  function getUnboundRange(getters, zone) {
38674
38747
  return zoneToXc(getters.getUnboundedZone(getters.getActiveSheetId(), zone));
@@ -38707,43 +38780,19 @@ stores.inject(MyMetaStore, storeInstance);
38707
38780
  return detectedType;
38708
38781
  }
38709
38782
  function categorizeColumns(zones, getters) {
38710
- const columns = {
38711
- number: [],
38712
- text: [],
38713
- date: [],
38714
- };
38783
+ const columns = [];
38715
38784
  for (const zone of getZonesByColumns(zones)) {
38716
38785
  const cells = getters.getEvaluatedCellsInZone(getters.getActiveSheetId(), zone);
38717
- const type = detectColumnType(cells);
38718
- if (type !== "empty") {
38719
- const targetType = type === "percentage" ? "number" : type;
38720
- columns[targetType].push({ zone, type });
38721
- }
38786
+ columns.push({ zone, type: detectColumnType(cells) });
38722
38787
  }
38723
38788
  return columns;
38724
38789
  }
38725
38790
  function getCellStats(getters, zone) {
38726
38791
  const cells = getters.getEvaluatedCellsInZone(getters.getActiveSheetId(), zone);
38727
- const uniqueValues = new Set();
38728
- let totalCount = 0;
38729
- let percentageSum = 0;
38730
- for (let i = 0; i < cells.length; i++) {
38731
- const { value } = cells[i];
38732
- const str = value?.toString().trim();
38733
- if (!str) {
38734
- continue;
38735
- }
38736
- uniqueValues.add(str);
38737
- totalCount++;
38738
- const num = Number(value);
38739
- if (!isNaN(num)) {
38740
- percentageSum += Math.abs(num) * 100;
38741
- }
38742
- }
38792
+ const values = cells.map((c) => c.value?.toString().trim() || "").filter((s) => s);
38743
38793
  return {
38744
- uniqueCount: uniqueValues.size,
38745
- totalCount,
38746
- percentageSum,
38794
+ uniqueCount: new Set(values).size,
38795
+ totalCount: values.length,
38747
38796
  };
38748
38797
  }
38749
38798
  function isDatasetTitled(getters, column) {
@@ -38754,167 +38803,191 @@ stores.inject(MyMetaStore, storeInstance);
38754
38803
  });
38755
38804
  return ![CellValueType.number, CellValueType.empty].includes(titleCell.type);
38756
38805
  }
38757
- function createBaseChart(type, dataSets, options = {}) {
38758
- return {
38759
- type,
38760
- title: {},
38761
- dataSets,
38762
- legendPosition: "none",
38763
- ...options,
38764
- };
38765
- }
38806
+ /**
38807
+ * Builds a chart definition for a single column selection. The logic to detect the chart type is as follows:
38808
+ * - If the column contains a single cell, create a scorecard.
38809
+ * - If the column type is "percentage", create a pie chart.
38810
+ * - If the column type is "text", create a pie chart
38811
+ * - If the column type is "date", create a line chart.
38812
+ * - Otherwise, create a bar chart.
38813
+ */
38766
38814
  function buildSingleColumnChart(column, getters) {
38767
38815
  const { type, zone } = column;
38768
38816
  const sheetId = getters.getActiveSheetId();
38769
38817
  const dataSetsHaveTitle = isDatasetTitled(getters, column);
38770
38818
  const dataRange = getUnboundRange(getters, zone);
38771
38819
  const titleCell = getters.getEvaluatedCell({ sheetId, col: zone.left, row: zone.top });
38820
+ if (getZoneArea(zone) === 1) {
38821
+ return buildScorecard(zone, getters);
38822
+ }
38772
38823
  switch (type) {
38773
38824
  case "percentage":
38774
- const { percentageSum } = getCellStats(getters, zone);
38775
- return createBaseChart("pie", [{ dataRange }], {
38825
+ return {
38826
+ type: "pie",
38776
38827
  title: dataSetsHaveTitle ? { text: String(titleCell.value) } : {},
38828
+ dataSets: [{ dataRange }],
38829
+ legendPosition: "none",
38777
38830
  dataSetsHaveTitle,
38778
- isDoughnut: percentageSum < CHART_LIMITS.PERCENTAGE_THRESHOLD,
38779
- });
38831
+ };
38780
38832
  case "text":
38781
38833
  const cells = getters.getEvaluatedCellsInZone(sheetId, zone);
38782
38834
  const titleCount = cells.reduce((count, cell) => (cell.value === titleCell.value ? count + 1 : count), 0);
38783
38835
  const hasUniqueTitle = titleCell.value !== null && titleCount === 1;
38784
- return createBaseChart("pie", [{ dataRange }], {
38836
+ return {
38837
+ type: "pie",
38785
38838
  title: hasUniqueTitle ? { text: String(titleCell.value) } : {},
38839
+ dataSets: [{ dataRange }],
38786
38840
  labelRange: dataRange,
38787
38841
  dataSetsHaveTitle: hasUniqueTitle,
38788
- isDoughnut: false,
38789
38842
  aggregated: true,
38790
38843
  legendPosition: "top",
38791
- });
38792
- // TODO: Handle date column with matrix chart when matrix chart is supported
38844
+ };
38793
38845
  case "date":
38794
- return createBaseChart("line", [{ dataRange }], {
38795
- labelRange: dataRange,
38846
+ return {
38847
+ ...DEFAULT_LINE_CHART_CONFIG,
38848
+ type: "line",
38849
+ title: dataSetsHaveTitle ? { text: String(titleCell.value) } : {},
38850
+ dataSets: [{ dataRange }],
38796
38851
  dataSetsHaveTitle,
38797
- cumulative: false,
38798
- labelsAsText: false,
38799
- });
38852
+ };
38800
38853
  }
38801
- return createBaseChart("bar", [{ dataRange }], { dataSetsHaveTitle });
38854
+ return {
38855
+ ...DEFAULT_BAR_CHART_CONFIG,
38856
+ title: dataSetsHaveTitle ? { text: String(titleCell.value) } : {},
38857
+ dataSets: [{ dataRange }],
38858
+ dataSetsHaveTitle,
38859
+ };
38802
38860
  }
38861
+ /**
38862
+ * Builds a chart definition for a selection of two columns. The logic to detect the chart type always consider the
38863
+ * columns left to right, and is as follows:
38864
+ * - any type + percentage columns: pie chart
38865
+ * - number + number columns: scatter chart
38866
+ * - date + number columns: line chart
38867
+ * - text + number columns: treemap if repetition in labels
38868
+ * - any other combination: bar chart
38869
+ */
38803
38870
  function buildTwoColumnChart(columns, getters) {
38804
- const { number: numberColumns, text: textColumns, date: dateColumns } = columns;
38805
- if (numberColumns.length === 2) {
38806
- return createBaseChart("scatter", [{ dataRange: getUnboundRange(getters, numberColumns[1].zone) }], {
38807
- labelRange: getUnboundRange(getters, numberColumns[0].zone),
38808
- dataSetsHaveTitle: isDatasetTitled(getters, numberColumns[1]),
38809
- labelsAsText: false,
38810
- });
38871
+ if (columns.length !== 2) {
38872
+ throw new Error("buildTwoColumnChart expects exactly two columns");
38811
38873
  }
38812
- // TODO: Handle date + number with matrix chart when matrix chart is supported
38813
- if (dateColumns.length === 1 && numberColumns.length === 1) {
38814
- return createBaseChart("line", [{ dataRange: getUnboundRange(getters, numberColumns[0].zone) }], {
38815
- labelRange: getUnboundRange(getters, dateColumns[0].zone),
38816
- dataSetsHaveTitle: isDatasetTitled(getters, numberColumns[0]),
38817
- aggregated: false,
38818
- cumulative: false,
38874
+ if (columns[1].type === "percentage") {
38875
+ return {
38876
+ type: "pie",
38877
+ title: {},
38878
+ dataSets: [{ dataRange: getUnboundRange(getters, columns[1].zone) }],
38879
+ labelRange: getUnboundRange(getters, columns[0].zone),
38880
+ dataSetsHaveTitle: isDatasetTitled(getters, columns[1]),
38881
+ aggregated: true,
38882
+ legendPosition: "none",
38883
+ };
38884
+ }
38885
+ if (columns[0].type === "number" && columns[1].type === "number") {
38886
+ return {
38887
+ type: "scatter",
38888
+ title: {},
38889
+ dataSets: [{ dataRange: getUnboundRange(getters, columns[1].zone) }],
38890
+ labelRange: getUnboundRange(getters, columns[0].zone),
38891
+ dataSetsHaveTitle: isDatasetTitled(getters, columns[1]),
38819
38892
  labelsAsText: false,
38820
- });
38893
+ legendPosition: "none",
38894
+ };
38895
+ }
38896
+ // TODO: Handle date + number with calendar chart when implemented (and change the docstring)
38897
+ if (columns[0].type === "date" && columns[1].type === "number") {
38898
+ return {
38899
+ ...DEFAULT_LINE_CHART_CONFIG,
38900
+ type: "line",
38901
+ dataSets: [{ dataRange: getUnboundRange(getters, columns[1].zone) }],
38902
+ labelRange: getUnboundRange(getters, columns[0].zone),
38903
+ dataSetsHaveTitle: isDatasetTitled(getters, columns[0]),
38904
+ };
38821
38905
  }
38822
- if (textColumns.length === 1 && numberColumns.length === 1) {
38823
- const [textColumn] = textColumns;
38824
- const [numberColumn] = numberColumns;
38906
+ if (columns[0].type === "text" && columns[1].type === "number") {
38907
+ const textColumn = columns[0];
38908
+ const numberColumn = columns[1];
38825
38909
  const { uniqueCount, totalCount } = getCellStats(getters, textColumn.zone);
38826
38910
  const dataSetsHaveTitle = isDatasetTitled(getters, numberColumn);
38827
- const maxCategories = dataSetsHaveTitle
38828
- ? CHART_LIMITS.MAX_PIE_CATEGORIES
38829
- : CHART_LIMITS.MAX_PIE_CATEGORIES_NO_TITLE;
38830
- const labelRange = getUnboundRange(getters, textColumn.zone);
38831
- const dataRange = getUnboundRange(getters, numberColumn.zone);
38832
- if (uniqueCount <= maxCategories) {
38833
- const { percentageSum } = getCellStats(getters, numberColumn.zone);
38834
- return createBaseChart("pie", [{ dataRange }], {
38835
- labelRange,
38836
- dataSetsHaveTitle,
38837
- isDoughnut: numberColumn.type === "percentage" && percentageSum < CHART_LIMITS.PERCENTAGE_THRESHOLD,
38838
- aggregated: true,
38839
- legendPosition: "top",
38840
- });
38841
- }
38842
- // Use treemap when categories repeat, as pie chart would be cluttered
38843
38911
  if (uniqueCount !== totalCount) {
38844
- return createBaseChart("treemap", [{ dataRange: labelRange }], {
38845
- labelRange: dataRange,
38912
+ return {
38913
+ type: "treemap",
38914
+ title: {},
38915
+ dataSets: [{ dataRange: getUnboundRange(getters, textColumn.zone) }],
38916
+ labelRange: getUnboundRange(getters, numberColumn.zone),
38846
38917
  dataSetsHaveTitle,
38847
- });
38918
+ legendPosition: "none",
38919
+ };
38848
38920
  }
38849
- return createBaseChart("bar", [{ dataRange }], {
38850
- labelRange,
38851
- dataSetsHaveTitle,
38852
- });
38853
38921
  }
38854
- const labelColumn = textColumns[0] || dateColumns[0] || numberColumns[0];
38855
- const dataColumn = numberColumns[0] || textColumns[0] || dateColumns[0];
38856
- return createBaseChart("line", [{ dataRange: getUnboundRange(getters, dataColumn.zone) }], {
38857
- labelRange: getUnboundRange(getters, labelColumn.zone),
38858
- dataSetsHaveTitle: isDatasetTitled(getters, dataColumn),
38859
- cumulative: false,
38860
- labelsAsText: true,
38861
- });
38922
+ return {
38923
+ ...DEFAULT_BAR_CHART_CONFIG,
38924
+ dataSets: [{ dataRange: getUnboundRange(getters, columns[1].zone) }],
38925
+ labelRange: getUnboundRange(getters, columns[0].zone),
38926
+ dataSetsHaveTitle: isDatasetTitled(getters, columns[1]),
38927
+ };
38862
38928
  }
38929
+ /**
38930
+ * Builds a chart definition for a selection more than two columns. The logic to detect the chart type always consider
38931
+ * the columns left to right, and is as follows:
38932
+ * - multiple text + single number/percentage columns: sunburst if 3+ text columns, treemap otherwise
38933
+ * - any type + multiple percentage columns: pie chart
38934
+ * - date + multiple number columns: line chart
38935
+ * - any other combination: bar chart
38936
+ */
38863
38937
  function buildMultiColumnChart(columns, getters) {
38864
- const { number: numberColumns, text: textColumns, date: dateColumns } = columns;
38865
- const dataSetsHaveTitle = numberColumns.some((col) => isDatasetTitled(getters, col));
38866
- if (textColumns.length >= 2 && numberColumns.length === 1) {
38867
- const sortedTextColumns = textColumns.sort((colA, colB) => getCellStats(getters, colA.zone).uniqueCount - getCellStats(getters, colB.zone).uniqueCount);
38868
- const dataSets = sortedTextColumns.map(({ zone }) => ({
38938
+ if (columns.length < 3) {
38939
+ throw new Error("buildMultiColumnChart expects at least three columns");
38940
+ }
38941
+ const dataSetsHaveTitle = columns.some((col) => col.type !== "text" && isDatasetTitled(getters, col));
38942
+ const lastColumn = columns[columns.length - 1];
38943
+ const columnsExceptLast = columns.slice(0, columns.length - 1);
38944
+ if ((lastColumn.type === "percentage" || lastColumn.type === "number") &&
38945
+ columnsExceptLast.every((col) => col.type === "text")) {
38946
+ const dataSets = columnsExceptLast.map(({ zone }) => ({
38869
38947
  dataRange: getUnboundRange(getters, zone),
38870
38948
  }));
38871
- return createBaseChart(textColumns.length >= 3 ? "sunburst" : "treemap", dataSets, {
38872
- labelRange: getUnboundRange(getters, numberColumns[0].zone),
38949
+ return {
38950
+ type: columnsExceptLast.length >= 3 ? "sunburst" : "treemap",
38951
+ title: {},
38952
+ dataSets,
38953
+ labelRange: getUnboundRange(getters, lastColumn.zone),
38873
38954
  dataSetsHaveTitle,
38874
- });
38955
+ legendPosition: "none",
38956
+ };
38875
38957
  }
38876
- const dataSets = recomputeZones(numberColumns.map((col) => col.zone)).map((zone) => ({
38958
+ const firstColumn = columns[0];
38959
+ const columnsExceptFirst = columns.slice(1);
38960
+ const rangesOfColumnsExceptFirst = columnsExceptFirst.map(({ zone }) => ({
38877
38961
  dataRange: getUnboundRange(getters, zone),
38878
38962
  }));
38879
- if (dateColumns.length === 1 && numberColumns.length > 1) {
38880
- return createBaseChart("line", dataSets, {
38881
- labelRange: getUnboundRange(getters, dateColumns[0].zone),
38963
+ if (columnsExceptFirst.every((col) => col.type === "percentage")) {
38964
+ return {
38965
+ type: "pie",
38966
+ title: {},
38967
+ dataSets: rangesOfColumnsExceptFirst,
38968
+ labelRange: getUnboundRange(getters, firstColumn.zone),
38882
38969
  dataSetsHaveTitle,
38883
- cumulative: false,
38884
- labelsAsText: false,
38970
+ aggregated: false,
38885
38971
  legendPosition: "top",
38886
- });
38972
+ };
38887
38973
  }
38888
- if (textColumns.length === 1 && numberColumns.length >= 2) {
38889
- const [textColumn] = textColumns;
38890
- const firstCell = getters.getEvaluatedCell({
38891
- sheetId: getters.getActiveSheetId(),
38892
- row: textColumn.zone.top,
38893
- col: textColumn.zone.left,
38894
- });
38895
- const { uniqueCount, totalCount } = getCellStats(getters, textColumn.zone);
38896
- const categoryCount = dataSetsHaveTitle && firstCell.value ? uniqueCount - 1 : uniqueCount;
38897
- const expectedDataCount = categoryCount * numberColumns.length + (dataSetsHaveTitle ? numberColumns.length : 0);
38898
- const actualDataCount = numberColumns.reduce((sum, dataCol) => sum + getCellStats(getters, dataCol.zone).totalCount, 0);
38899
- if (uniqueCount === totalCount &&
38900
- uniqueCount >= CHART_LIMITS.MIN_RADAR_CATEGORIES &&
38901
- uniqueCount <= CHART_LIMITS.MAX_RADAR_CATEGORIES &&
38902
- expectedDataCount === actualDataCount) {
38903
- return createBaseChart("radar", dataSets, {
38904
- title: dataSetsHaveTitle && firstCell.value ? { text: String(firstCell.value) } : {},
38905
- labelRange: getUnboundRange(getters, textColumn.zone),
38906
- dataSetsHaveTitle,
38907
- legendPosition: "top",
38908
- });
38909
- }
38974
+ if (firstColumn.type === "date" && columnsExceptFirst.every((col) => col.type === "number")) {
38975
+ return {
38976
+ ...DEFAULT_LINE_CHART_CONFIG,
38977
+ type: "line",
38978
+ dataSets: rangesOfColumnsExceptFirst,
38979
+ labelRange: getUnboundRange(getters, firstColumn.zone),
38980
+ dataSetsHaveTitle,
38981
+ legendPosition: "top",
38982
+ };
38910
38983
  }
38911
- const labelColumn = textColumns[0] || dateColumns[0] || numberColumns[0];
38912
- return createBaseChart("bar", dataSets, {
38913
- labelRange: dataSets.length ? getUnboundRange(getters, labelColumn.zone) : "",
38984
+ return {
38985
+ ...DEFAULT_BAR_CHART_CONFIG,
38986
+ dataSets: rangesOfColumnsExceptFirst,
38987
+ labelRange: getUnboundRange(getters, firstColumn.zone),
38914
38988
  dataSetsHaveTitle,
38915
- aggregated: true,
38916
38989
  legendPosition: "top",
38917
- });
38990
+ };
38918
38991
  }
38919
38992
  function buildScorecard(zone, getters) {
38920
38993
  const cell = getters.getCell({
@@ -38937,22 +39010,18 @@ stores.inject(MyMetaStore, storeInstance);
38937
39010
  */
38938
39011
  function getSmartChartDefinition(zones, getters) {
38939
39012
  const columns = categorizeColumns(zones, getters);
38940
- const { number: numberColumns, text: textColumns, date: dateColumns } = columns;
38941
- const columnCount = numberColumns.length + textColumns.length + dateColumns.length;
38942
- switch (columnCount) {
38943
- case 0:
38944
- return createBaseChart("bar", [{ dataRange: getUnboundRange(getters, zones[0]) }], {
38945
- dataSetsHaveTitle: false,
38946
- });
39013
+ if (columns.length === 0 || columns.every((col) => col.type === "empty")) {
39014
+ const dataSets = columns.map(({ zone }) => ({ dataRange: getUnboundRange(getters, zone) }));
39015
+ return { ...DEFAULT_BAR_CHART_CONFIG, dataSets };
39016
+ }
39017
+ const nonEmptyColumns = columns.filter((col) => col.type !== "empty");
39018
+ switch (nonEmptyColumns.length) {
38947
39019
  case 1:
38948
- const singleColumn = numberColumns[0] || textColumns[0] || dateColumns[0];
38949
- return getZoneArea(singleColumn.zone) === 1
38950
- ? buildScorecard(singleColumn.zone, getters)
38951
- : buildSingleColumnChart(singleColumn, getters);
39020
+ return buildSingleColumnChart(nonEmptyColumns[0], getters);
38952
39021
  case 2:
38953
- return buildTwoColumnChart(columns, getters);
39022
+ return buildTwoColumnChart(nonEmptyColumns, getters);
38954
39023
  default:
38955
- return buildMultiColumnChart(columns, getters);
39024
+ return buildMultiColumnChart(nonEmptyColumns, getters);
38956
39025
  }
38957
39026
  }
38958
39027
 
@@ -39571,6 +39640,74 @@ stores.inject(MyMetaStore, storeInstance);
39571
39640
  return path2D;
39572
39641
  }
39573
39642
 
39643
+ /**
39644
+ * Get the relative path between two files
39645
+ *
39646
+ * Eg.:
39647
+ * from "folder1/file1.txt" to "folder2/file2.txt" => "../folder2/file2.txt"
39648
+ */
39649
+ function getRelativePath(from, to) {
39650
+ const fromPathParts = from.split("/");
39651
+ const toPathParts = to.split("/");
39652
+ let relPath = "";
39653
+ let startIndex = 0;
39654
+ for (let i = 0; i < fromPathParts.length - 1; i++) {
39655
+ if (fromPathParts[i] === toPathParts[i]) {
39656
+ startIndex++;
39657
+ }
39658
+ else {
39659
+ relPath += "../";
39660
+ }
39661
+ }
39662
+ relPath += toPathParts.slice(startIndex).join("/");
39663
+ return relPath;
39664
+ }
39665
+ /**
39666
+ * Convert an array of element into an object where the objects keys were the elements position in the array.
39667
+ * Can give an offset as argument, and all the array indexes will we shifted by this offset in the returned object.
39668
+ *
39669
+ * eg. : ["a", "b"] => {0:"a", 1:"b"}
39670
+ */
39671
+ function arrayToObject(array, indexOffset = 0) {
39672
+ const obj = {};
39673
+ for (let i = 0; i < array.length; i++) {
39674
+ if (array[i]) {
39675
+ obj[i + indexOffset] = array[i];
39676
+ }
39677
+ }
39678
+ return obj;
39679
+ }
39680
+ /**
39681
+ * In xlsx we can have string with unicode characters with the format _x00fa_.
39682
+ * Replace with characters understandable by JS
39683
+ */
39684
+ function fixXlsxUnicode(str) {
39685
+ return str.replace(/_x([0-9a-zA-Z]{4})_/g, (match, code) => {
39686
+ return String.fromCharCode(parseInt(code, 16));
39687
+ });
39688
+ }
39689
+ /** Get a header in the SheetData. Create the header if it doesn't exist in the SheetData */
39690
+ function getSheetDataHeader(sheetData, dimension, index) {
39691
+ if (dimension === "COL") {
39692
+ if (!sheetData.cols[index]) {
39693
+ sheetData.cols[index] = {};
39694
+ }
39695
+ return sheetData.cols[index];
39696
+ }
39697
+ if (!sheetData.rows[index]) {
39698
+ sheetData.rows[index] = {};
39699
+ }
39700
+ return sheetData.rows[index];
39701
+ }
39702
+ /** Prefix the string by "=" if the string looks like a formula */
39703
+ function prefixFormulaWithEqual(formula) {
39704
+ if (formula[0] === "=") {
39705
+ return formula;
39706
+ }
39707
+ const tokens = tokenize(formula);
39708
+ return tokens.length === 1 && tokens[0].type !== "REFERENCE" ? formula : "=" + formula;
39709
+ }
39710
+
39574
39711
  /**
39575
39712
  * Map of the different types of conversions warnings and their name in error messages
39576
39713
  */
@@ -40093,66 +40230,6 @@ stores.inject(MyMetaStore, storeInstance);
40093
40230
  */
40094
40231
  const DEFAULT_SYSTEM_COLOR = "FF000000";
40095
40232
 
40096
- /**
40097
- * Get the relative path between two files
40098
- *
40099
- * Eg.:
40100
- * from "folder1/file1.txt" to "folder2/file2.txt" => "../folder2/file2.txt"
40101
- */
40102
- function getRelativePath(from, to) {
40103
- const fromPathParts = from.split("/");
40104
- const toPathParts = to.split("/");
40105
- let relPath = "";
40106
- let startIndex = 0;
40107
- for (let i = 0; i < fromPathParts.length - 1; i++) {
40108
- if (fromPathParts[i] === toPathParts[i]) {
40109
- startIndex++;
40110
- }
40111
- else {
40112
- relPath += "../";
40113
- }
40114
- }
40115
- relPath += toPathParts.slice(startIndex).join("/");
40116
- return relPath;
40117
- }
40118
- /**
40119
- * Convert an array of element into an object where the objects keys were the elements position in the array.
40120
- * Can give an offset as argument, and all the array indexes will we shifted by this offset in the returned object.
40121
- *
40122
- * eg. : ["a", "b"] => {0:"a", 1:"b"}
40123
- */
40124
- function arrayToObject(array, indexOffset = 0) {
40125
- const obj = {};
40126
- for (let i = 0; i < array.length; i++) {
40127
- if (array[i]) {
40128
- obj[i + indexOffset] = array[i];
40129
- }
40130
- }
40131
- return obj;
40132
- }
40133
- /**
40134
- * In xlsx we can have string with unicode characters with the format _x00fa_.
40135
- * Replace with characters understandable by JS
40136
- */
40137
- function fixXlsxUnicode(str) {
40138
- return str.replace(/_x([0-9a-zA-Z]{4})_/g, (match, code) => {
40139
- return String.fromCharCode(parseInt(code, 16));
40140
- });
40141
- }
40142
- /** Get a header in the SheetData. Create the header if it doesn't exist in the SheetData */
40143
- function getSheetDataHeader(sheetData, dimension, index) {
40144
- if (dimension === "COL") {
40145
- if (!sheetData.cols[index]) {
40146
- sheetData.cols[index] = {};
40147
- }
40148
- return sheetData.cols[index];
40149
- }
40150
- if (!sheetData.rows[index]) {
40151
- sheetData.rows[index] = {};
40152
- }
40153
- return sheetData.rows[index];
40154
- }
40155
-
40156
40233
  const XLSX_DATE_FORMAT_REGEX = /^(yy|yyyy|m{1,5}|d{1,4}|h{1,2}|s{1,2}|am\/pm|a\/m|\s|-|\/|\.|:)+$/i;
40157
40234
  /**
40158
40235
  * Convert excel format to o_spreadsheet format
@@ -40367,9 +40444,9 @@ stores.inject(MyMetaStore, storeInstance);
40367
40444
  if (!rule.operator || !rule.formula || rule.formula.length === 0)
40368
40445
  continue;
40369
40446
  operator = CF_OPERATOR_TYPE_CONVERSION_MAP[rule.operator];
40370
- values.push(prefixFormula(rule.formula[0]));
40447
+ values.push(prefixFormulaWithEqual(rule.formula[0]));
40371
40448
  if (rule.formula.length === 2) {
40372
- values.push(prefixFormula(rule.formula[1]));
40449
+ values.push(prefixFormulaWithEqual(rule.formula[1]));
40373
40450
  }
40374
40451
  break;
40375
40452
  }
@@ -40527,11 +40604,6 @@ stores.inject(MyMetaStore, storeInstance);
40527
40604
  ? ICON_SETS[iconSet].neutral
40528
40605
  : ICON_SETS[iconSet].good;
40529
40606
  }
40530
- /** Prefix the string by "=" if the string looks like a formula */
40531
- function prefixFormula(formula) {
40532
- const tokens = tokenize(formula);
40533
- return tokens.length === 1 && tokens[0].type !== "REFERENCE" ? formula : "=" + formula;
40534
- }
40535
40607
  // ---------------------------------------------------------------------------
40536
40608
  // Warnings
40537
40609
  // ---------------------------------------------------------------------------
@@ -41007,7 +41079,7 @@ stores.inject(MyMetaStore, storeInstance);
41007
41079
  dvRules.push(decimalRule);
41008
41080
  break;
41009
41081
  case "list":
41010
- const listRule = convertListrule(dvId++, dv);
41082
+ const listRule = convertListRule(dvId++, dv);
41011
41083
  dvRules.push(listRule);
41012
41084
  break;
41013
41085
  case "date":
@@ -41027,9 +41099,9 @@ stores.inject(MyMetaStore, storeInstance);
41027
41099
  return dvRules;
41028
41100
  }
41029
41101
  function convertDecimalRule(id, dv) {
41030
- const values = [dv.formula1.toString()];
41102
+ const values = [prefixFormulaWithEqual(dv.formula1.toString())];
41031
41103
  if (dv.formula2) {
41032
- values.push(dv.formula2.toString());
41104
+ values.push(prefixFormulaWithEqual(dv.formula2.toString()));
41033
41105
  }
41034
41106
  return {
41035
41107
  id: id.toString(),
@@ -41041,7 +41113,7 @@ stores.inject(MyMetaStore, storeInstance);
41041
41113
  },
41042
41114
  };
41043
41115
  }
41044
- function convertListrule(id, dv) {
41116
+ function convertListRule(id, dv) {
41045
41117
  const formula1 = dv.formula1.toString();
41046
41118
  const isRangeRule = rangeReference.test(formula1);
41047
41119
  return {
@@ -41057,9 +41129,9 @@ stores.inject(MyMetaStore, storeInstance);
41057
41129
  }
41058
41130
  function convertDateRule(id, dv) {
41059
41131
  let criterion;
41060
- const values = [dv.formula1.toString()];
41132
+ const values = [prefixFormulaWithEqual(dv.formula1.toString())];
41061
41133
  if (dv.formula2) {
41062
- values.push(dv.formula2.toString());
41134
+ values.push(prefixFormulaWithEqual(dv.formula2.toString()));
41063
41135
  criterion = {
41064
41136
  type: XLSX_DV_DATE_OPERATOR_TO_DV_TYPE_MAPPING[dv.operator],
41065
41137
  values: getDateCriterionFormattedValues(values, DEFAULT_LOCALE),
@@ -41086,7 +41158,7 @@ stores.inject(MyMetaStore, storeInstance);
41086
41158
  isBlocking: dv.errorStyle !== "warning",
41087
41159
  criterion: {
41088
41160
  type: "customFormula",
41089
- values: [`=${dv.formula1.toString()}`],
41161
+ values: [prefixFormulaWithEqual(dv.formula1.toString())],
41090
41162
  },
41091
41163
  };
41092
41164
  }
@@ -43990,6 +44062,7 @@ stores.inject(MyMetaStore, storeInstance);
43990
44062
  return data;
43991
44063
  }
43992
44064
  const figureIds = new Set();
44065
+ const chartIds = new Set();
43993
44066
  const uuidGenerator = new UuidGenerator();
43994
44067
  for (const sheet of data.sheets || []) {
43995
44068
  for (const figure of sheet.figures || []) {
@@ -43997,6 +44070,12 @@ stores.inject(MyMetaStore, storeInstance);
43997
44070
  figure.id += uuidGenerator.smallUuid();
43998
44071
  }
43999
44072
  figureIds.add(figure.id);
44073
+ if (figure.tag === "chart") {
44074
+ if (chartIds.has(figure.data?.chartId)) {
44075
+ figure.data.chartId += uuidGenerator.smallUuid();
44076
+ }
44077
+ chartIds.add(figure.data?.chartId);
44078
+ }
44000
44079
  }
44001
44080
  }
44002
44081
  data.uniqueFigureIds = true;
@@ -49750,39 +49829,63 @@ stores.inject(MyMetaStore, storeInstance);
49750
49829
  this.model.dispatch("AUTOFILL_TABLE_COLUMN", { ...this.currentEditedCell });
49751
49830
  this.setContent("");
49752
49831
  }
49753
- getComposerContent(position) {
49832
+ getComposerContent(position, selection) {
49754
49833
  const locale = this.getters.getLocale();
49755
49834
  const cell = this.getters.getCell(position);
49756
49835
  if (cell?.isFormula) {
49757
49836
  const prettifiedContent = this.getPrettifiedFormula(cell);
49758
- return localizeFormula(prettifiedContent, locale);
49837
+ // when a formula is prettified (multi lines, indented), adapt the cursor position
49838
+ // to take into account line breaks and tabs
49839
+ function adjustCursorIndex(targetIndex) {
49840
+ let adjustedIndex = 0;
49841
+ let originalIndex = 0;
49842
+ while (originalIndex < targetIndex) {
49843
+ adjustedIndex++;
49844
+ const char = prettifiedContent[adjustedIndex];
49845
+ if (char !== "\n" && char !== "\t") {
49846
+ originalIndex++;
49847
+ }
49848
+ }
49849
+ return adjustedIndex;
49850
+ }
49851
+ let adjustedSelection = selection;
49852
+ if (selection) {
49853
+ adjustedSelection = {
49854
+ start: adjustCursorIndex(selection.start),
49855
+ end: adjustCursorIndex(selection.end),
49856
+ };
49857
+ }
49858
+ return {
49859
+ text: localizeFormula(prettifiedContent, locale),
49860
+ adjustedSelection,
49861
+ };
49759
49862
  }
49760
49863
  const spreader = this.model.getters.getArrayFormulaSpreadingOn(position);
49761
49864
  if (spreader) {
49762
- return "";
49865
+ return { text: "" };
49763
49866
  }
49764
49867
  const { format, value, type, formattedValue } = this.getters.getEvaluatedCell(position);
49765
49868
  switch (type) {
49766
49869
  case CellValueType.empty:
49767
- return "";
49870
+ return { text: "" };
49768
49871
  case CellValueType.text:
49769
49872
  case CellValueType.error:
49770
- return value;
49873
+ return { text: value };
49771
49874
  case CellValueType.boolean:
49772
- return formattedValue;
49875
+ return { text: formattedValue };
49773
49876
  case CellValueType.number:
49774
49877
  if (format && isDateTimeFormat(format)) {
49775
49878
  if (parseDateTime(formattedValue, locale) !== null) {
49776
49879
  // formatted string can be parsed again
49777
- return formattedValue;
49880
+ return { text: formattedValue };
49778
49881
  }
49779
49882
  // display a simplified and parsable string otherwise
49780
49883
  const timeFormat = Number.isInteger(value)
49781
49884
  ? locale.dateFormat
49782
49885
  : getDateTimeFormat(locale);
49783
- return formatValue(value, { locale, format: timeFormat });
49886
+ return { text: formatValue(value, { locale, format: timeFormat }) };
49784
49887
  }
49785
- return this.numberComposerContent(value, format, locale);
49888
+ return { text: this.numberComposerContent(value, format, locale) };
49786
49889
  }
49787
49890
  }
49788
49891
  getPrettifiedFormula(cell) {
@@ -49951,8 +50054,9 @@ stores.inject(MyMetaStore, storeInstance);
49951
50054
  },
49952
50055
  focus: this.focus,
49953
50056
  isDefaultFocus: true,
49954
- onComposerContentFocused: () => this.composerFocusStore.focusComposer(this.composerInterface, {
50057
+ onComposerContentFocused: (selection) => this.composerFocusStore.focusComposer(this.composerInterface, {
49955
50058
  focusMode: "contentFocus",
50059
+ selection,
49956
50060
  }),
49957
50061
  onComposerCellFocused: (content) => this.composerFocusStore.focusComposer(this.composerInterface, {
49958
50062
  focusMode: "cellFocus",
@@ -57358,12 +57462,13 @@ stores.inject(MyMetaStore, storeInstance);
57358
57462
  onCloseSidePanel: { type: Function, optional: true },
57359
57463
  };
57360
57464
  state = owl.useState({ rule: this.defaultDataValidationRule, errors: [] });
57465
+ editingSheetId;
57361
57466
  setup() {
57467
+ this.editingSheetId = this.env.model.getters.getActiveSheetId();
57362
57468
  if (this.props.rule) {
57363
- const sheetId = this.env.model.getters.getActiveSheetId();
57364
57469
  this.state.rule = {
57365
57470
  ...this.props.rule,
57366
- ranges: this.props.rule.ranges.map((range) => this.env.model.getters.getRangeString(range, sheetId)),
57471
+ ranges: this.props.rule.ranges.map((range) => this.env.model.getters.getRangeString(range, this.editingSheetId)),
57367
57472
  };
57368
57473
  this.state.rule.criterion.type = this.props.rule.criterion.type;
57369
57474
  }
@@ -57397,7 +57502,6 @@ stores.inject(MyMetaStore, storeInstance);
57397
57502
  const locale = this.env.model.getters.getLocale();
57398
57503
  const criterion = rule.criterion;
57399
57504
  const criterionEvaluator = criterionEvaluatorRegistry.get(criterion.type);
57400
- const sheetId = this.env.model.getters.getActiveSheetId();
57401
57505
  const values = criterion.values
57402
57506
  .slice(0, criterionEvaluator.numberOfValues(criterion))
57403
57507
  .map((value) => value?.trim())
@@ -57405,8 +57509,8 @@ stores.inject(MyMetaStore, storeInstance);
57405
57509
  .map((value) => canonicalizeContent(value, locale));
57406
57510
  rule.criterion = { ...criterion, values };
57407
57511
  return {
57408
- sheetId,
57409
- ranges: this.state.rule.ranges.map((xc) => this.env.model.getters.getRangeDataFromXc(sheetId, xc)),
57512
+ sheetId: this.editingSheetId,
57513
+ ranges: this.state.rule.ranges.map((xc) => this.env.model.getters.getRangeDataFromXc(this.editingSheetId, xc)),
57410
57514
  rule,
57411
57515
  };
57412
57516
  }
@@ -57933,6 +58037,7 @@ stores.inject(MyMetaStore, storeInstance);
57933
58037
  .o-button {
57934
58038
  height: 19px;
57935
58039
  width: 19px;
58040
+ box-sizing: content-box;
57936
58041
  .o-icon {
57937
58042
  height: 14px;
57938
58043
  width: 14px;
@@ -68706,6 +68811,281 @@ stores.inject(MyMetaStore, storeInstance);
68706
68811
  }
68707
68812
  }
68708
68813
 
68814
+ class ZoneSet {
68815
+ profilesStartingPosition = [0];
68816
+ profiles = new Map([[0, []]]);
68817
+ constructor(zones = []) {
68818
+ for (const zone of zones) {
68819
+ this.add(zone);
68820
+ }
68821
+ }
68822
+ isEmpty() {
68823
+ return this.profiles.size === 1 && this.profiles.get(0)?.length === 0;
68824
+ }
68825
+ add(zone) {
68826
+ modifyProfiles(this.profilesStartingPosition, this.profiles, [zone]);
68827
+ }
68828
+ delete(zone) {
68829
+ modifyProfiles(this.profilesStartingPosition, this.profiles, [zone], true);
68830
+ }
68831
+ has(zone) {
68832
+ return profilesContainsZone(this.profilesStartingPosition, this.profiles, zone);
68833
+ }
68834
+ difference(other) {
68835
+ const result = this.copy();
68836
+ for (const zone of other) {
68837
+ result.delete(zone);
68838
+ }
68839
+ return result;
68840
+ }
68841
+ copy() {
68842
+ const result = new ZoneSet();
68843
+ result.profilesStartingPosition = [...this.profilesStartingPosition];
68844
+ result.profiles = new Map();
68845
+ for (const [key, value] of this.profiles) {
68846
+ result.profiles.set(key, [...value]);
68847
+ }
68848
+ return result;
68849
+ }
68850
+ size() {
68851
+ let size = 0;
68852
+ for (const profile of this.profiles.values()) {
68853
+ size += profile.length;
68854
+ }
68855
+ return size / 2;
68856
+ }
68857
+ /**
68858
+ * iterator of all the zones in the ZoneSet
68859
+ */
68860
+ [Symbol.iterator]() {
68861
+ return constructZonesFromProfiles(this.profilesStartingPosition, this.profiles)[Symbol.iterator]();
68862
+ }
68863
+ }
68864
+
68865
+ class RangeSet {
68866
+ setsBySheetId = {};
68867
+ constructor(ranges = []) {
68868
+ for (const range of ranges) {
68869
+ this.add(range);
68870
+ }
68871
+ }
68872
+ add(range) {
68873
+ if (!this.setsBySheetId[range.sheetId]) {
68874
+ this.setsBySheetId[range.sheetId] = new ZoneSet();
68875
+ }
68876
+ this.setsBySheetId[range.sheetId].add(range.zone);
68877
+ }
68878
+ addMany(ranges) {
68879
+ for (const range of ranges) {
68880
+ this.add(range);
68881
+ }
68882
+ }
68883
+ addPosition(position) {
68884
+ this.add(positionToBoundedRange(position));
68885
+ }
68886
+ addManyPositions(positions) {
68887
+ for (const position of positions) {
68888
+ this.addPosition(position);
68889
+ }
68890
+ }
68891
+ has(range) {
68892
+ if (!this.setsBySheetId[range.sheetId]) {
68893
+ return false;
68894
+ }
68895
+ return this.setsBySheetId[range.sheetId].has(range.zone);
68896
+ }
68897
+ hasPosition(position) {
68898
+ return this.has(positionToBoundedRange(position));
68899
+ }
68900
+ delete(range) {
68901
+ if (!this.setsBySheetId[range.sheetId]) {
68902
+ return;
68903
+ }
68904
+ this.setsBySheetId[range.sheetId].delete(range.zone);
68905
+ }
68906
+ deleteMany(ranges) {
68907
+ for (const range of ranges) {
68908
+ this.delete(range);
68909
+ }
68910
+ }
68911
+ deleteManyPositions(positions) {
68912
+ for (const position of positions) {
68913
+ this.delete(positionToBoundedRange(position));
68914
+ }
68915
+ }
68916
+ difference(other) {
68917
+ const result = new RangeSet();
68918
+ for (const sheetId in this.setsBySheetId) {
68919
+ result.setsBySheetId[sheetId] = this.setsBySheetId[sheetId];
68920
+ }
68921
+ for (const sheetId in other.setsBySheetId) {
68922
+ if (result.setsBySheetId[sheetId]) {
68923
+ result.setsBySheetId[sheetId] = result.setsBySheetId[sheetId].difference(other.setsBySheetId[sheetId]);
68924
+ }
68925
+ }
68926
+ return result;
68927
+ }
68928
+ copy() {
68929
+ const result = new RangeSet();
68930
+ for (const sheetId in this.setsBySheetId) {
68931
+ result.setsBySheetId[sheetId] = this.setsBySheetId[sheetId].copy();
68932
+ }
68933
+ return result;
68934
+ }
68935
+ clear() {
68936
+ this.setsBySheetId = {};
68937
+ }
68938
+ size() {
68939
+ let size = 0;
68940
+ for (const sheetId in this.setsBySheetId) {
68941
+ size += this.setsBySheetId[sheetId].size();
68942
+ }
68943
+ return size;
68944
+ }
68945
+ isEmpty() {
68946
+ for (const sheetId in this.setsBySheetId) {
68947
+ if (!this.setsBySheetId[sheetId].isEmpty()) {
68948
+ return false;
68949
+ }
68950
+ }
68951
+ return true;
68952
+ }
68953
+ /**
68954
+ * iterator of all the ranges in the RangeSet
68955
+ */
68956
+ [Symbol.iterator]() {
68957
+ const result = [];
68958
+ for (const sheetId in this.setsBySheetId) {
68959
+ for (const zone of this.setsBySheetId[sheetId]) {
68960
+ result.push({ sheetId: sheetId, zone });
68961
+ }
68962
+ }
68963
+ return result[Symbol.iterator]();
68964
+ }
68965
+ }
68966
+
68967
+ /**
68968
+ * R-Tree of ranges, mapping zones (r-tree bounding boxes) to ranges (data of the r-tree item).
68969
+ * Ranges associated to the exact same bounding box are grouped together
68970
+ * to reduce the number of nodes in the R-tree.
68971
+ */
68972
+ class DependenciesRTree {
68973
+ rTree;
68974
+ constructor(items = []) {
68975
+ const compactedBoxes = groupSameBoundingBoxes(items);
68976
+ this.rTree = new SpreadsheetRTree(compactedBoxes);
68977
+ }
68978
+ insert(item) {
68979
+ const data = this.rTree.search(item.boundingBox);
68980
+ const itemBoundingBox = item.boundingBox;
68981
+ const exactBoundingBox = data.find(({ boundingBox }) => boundingBox.sheetId === itemBoundingBox.sheetId &&
68982
+ boundingBox.zone.left === itemBoundingBox.zone.left &&
68983
+ boundingBox.zone.top === itemBoundingBox.zone.top &&
68984
+ boundingBox.zone.right === itemBoundingBox.zone.right &&
68985
+ boundingBox.zone.bottom === itemBoundingBox.zone.bottom);
68986
+ if (exactBoundingBox) {
68987
+ exactBoundingBox.data.add(item.data);
68988
+ }
68989
+ else {
68990
+ this.rTree.insert({ ...item, data: new RangeSet([item.data]) });
68991
+ }
68992
+ }
68993
+ search({ zone, sheetId }) {
68994
+ const results = new RangeSet();
68995
+ for (const { data } of this.rTree.search({ zone, sheetId })) {
68996
+ results.addMany(data);
68997
+ }
68998
+ return results;
68999
+ }
69000
+ remove(item) {
69001
+ const data = this.rTree.search(item.boundingBox);
69002
+ const itemBoundingBox = item.boundingBox;
69003
+ const exactBoundingBox = data.find(({ boundingBox }) => boundingBox.sheetId === itemBoundingBox.sheetId &&
69004
+ boundingBox.zone.left === itemBoundingBox.zone.left &&
69005
+ boundingBox.zone.top === itemBoundingBox.zone.top &&
69006
+ boundingBox.zone.right === itemBoundingBox.zone.right &&
69007
+ boundingBox.zone.bottom === itemBoundingBox.zone.bottom);
69008
+ if (exactBoundingBox) {
69009
+ exactBoundingBox.data.delete(item.data);
69010
+ }
69011
+ else {
69012
+ this.rTree.remove({ ...item, data: new RangeSet([item.data]) });
69013
+ }
69014
+ }
69015
+ }
69016
+ /**
69017
+ * Group together all formulas pointing to the exact same dependency (bounding box).
69018
+ * The goal is to optimize the following case:
69019
+ * - if any cell in B1:B1000 changes, C1 must be recomputed
69020
+ * - if any cell in B1:B1000 changes, C2 must be recomputed
69021
+ * - if any cell in B1:B1000 changes, C3 must be recomputed
69022
+ * ...
69023
+ * - if any cell in B1:B1000 changes, C1000 must be recomputed
69024
+ *
69025
+ * Instead of having 1000 entries in the R-tree, we want to have a single entry
69026
+ * with B1:B1000 (bounding box) pointing to C1:C1000 (formulas).
69027
+ */
69028
+ function groupSameBoundingBoxes(items) {
69029
+ // Important: this function must be as fast as possible. It is on the evaluation hot path.
69030
+ let maxCol = 0;
69031
+ let maxRow = 0;
69032
+ for (let i = 0; i < items.length; i++) {
69033
+ const zone = items[i].boundingBox.zone;
69034
+ if (zone.right > maxCol) {
69035
+ maxCol = zone.right;
69036
+ }
69037
+ if (zone.bottom > maxRow) {
69038
+ maxRow = zone.bottom;
69039
+ }
69040
+ }
69041
+ maxCol += 1;
69042
+ maxRow += 1;
69043
+ // in most real-world cases, we can use a fast numeric key
69044
+ // but if the zones are too far right or bottom, we fallback to a slower string key
69045
+ const maxPossibleKey = (((maxRow + 1) * maxCol + 1) * maxRow + 1) * maxCol;
69046
+ const useFastKey = maxPossibleKey <= Number.MAX_SAFE_INTEGER;
69047
+ if (!useFastKey) {
69048
+ console.warn("Max col/row size exceeded, using slow zone key");
69049
+ }
69050
+ const groupedByBBox = {};
69051
+ for (const item of items) {
69052
+ const sheetId = item.boundingBox.sheetId;
69053
+ if (!groupedByBBox[sheetId]) {
69054
+ groupedByBBox[sheetId] = {};
69055
+ }
69056
+ const bBox = item.boundingBox.zone;
69057
+ let bBoxKey = 0;
69058
+ if (useFastKey) {
69059
+ bBoxKey =
69060
+ bBox.left +
69061
+ bBox.top * maxCol +
69062
+ bBox.right * maxCol * maxRow +
69063
+ bBox.bottom * maxCol * maxRow * maxCol;
69064
+ }
69065
+ else {
69066
+ bBoxKey = `${bBox.left},${bBox.top},${bBox.right},${bBox.bottom}`;
69067
+ }
69068
+ if (groupedByBBox[sheetId][bBoxKey]) {
69069
+ const ranges = groupedByBBox[sheetId][bBoxKey].data;
69070
+ ranges.add(item.data);
69071
+ }
69072
+ else {
69073
+ groupedByBBox[sheetId][bBoxKey] = {
69074
+ boundingBox: item.boundingBox,
69075
+ data: new RangeSet([item.data]),
69076
+ };
69077
+ }
69078
+ }
69079
+ const result = [];
69080
+ for (const sheetId in groupedByBBox) {
69081
+ const map = groupedByBBox[sheetId];
69082
+ for (const key in map) {
69083
+ result.push(map[key]);
69084
+ }
69085
+ }
69086
+ return result;
69087
+ }
69088
+
68709
69089
  /**
68710
69090
  * Implementation of a dependency Graph.
68711
69091
  * The graph is used to evaluate the cells in the correct
@@ -68714,12 +69094,10 @@ stores.inject(MyMetaStore, storeInstance);
68714
69094
  * It uses an R-Tree data structure to efficiently find dependent cells.
68715
69095
  */
68716
69096
  class FormulaDependencyGraph {
68717
- createEmptyPositionSet;
68718
69097
  dependencies = new PositionMap();
68719
69098
  rTree;
68720
- constructor(createEmptyPositionSet, data = []) {
68721
- this.createEmptyPositionSet = createEmptyPositionSet;
68722
- this.rTree = new SpreadsheetRTree(data);
69099
+ constructor(data = []) {
69100
+ this.rTree = new DependenciesRTree(data);
68723
69101
  }
68724
69102
  removeAllDependencies(formulaPosition) {
68725
69103
  const ranges = this.dependencies.get(formulaPosition);
@@ -68733,7 +69111,10 @@ stores.inject(MyMetaStore, storeInstance);
68733
69111
  }
68734
69112
  addDependencies(formulaPosition, dependencies) {
68735
69113
  const rTreeItems = dependencies.map(({ sheetId, zone }) => ({
68736
- data: formulaPosition,
69114
+ data: {
69115
+ sheetId: formulaPosition.sheetId,
69116
+ zone: positionToZone(formulaPosition),
69117
+ },
68737
69118
  boundingBox: {
68738
69119
  zone,
68739
69120
  sheetId,
@@ -68751,46 +69132,20 @@ stores.inject(MyMetaStore, storeInstance);
68751
69132
  }
68752
69133
  }
68753
69134
  /**
68754
- * Return all the cells that depend on the provided ranges,
68755
- * in the correct order they should be evaluated.
68756
- * This is called a topological ordering (excluding cycles)
69135
+ * Return all the cells that depend on the provided ranges.
68757
69136
  */
68758
- getCellsDependingOn(ranges) {
68759
- const visited = this.createEmptyPositionSet();
69137
+ getCellsDependingOn(ranges, visited = new RangeSet()) {
69138
+ visited = visited.copy();
68760
69139
  const queue = Array.from(ranges).reverse();
68761
69140
  while (queue.length > 0) {
68762
69141
  const range = queue.pop();
68763
- const zone = range.zone;
68764
- const sheetId = range.sheetId;
68765
- for (let col = zone.left; col <= zone.right; col++) {
68766
- for (let row = zone.top; row <= zone.bottom; row++) {
68767
- visited.add({ sheetId, col, row });
68768
- }
68769
- }
68770
- const impactedPositions = this.rTree.search(range).map((dep) => dep.data);
68771
- const nextInQueue = {};
68772
- for (const position of impactedPositions) {
68773
- if (!visited.has(position)) {
68774
- if (!nextInQueue[position.sheetId]) {
68775
- nextInQueue[position.sheetId] = [];
68776
- }
68777
- nextInQueue[position.sheetId].push(positionToZone(position));
68778
- }
68779
- }
68780
- for (const sheetId in nextInQueue) {
68781
- const zones = recomputeZones(nextInQueue[sheetId], []);
68782
- queue.push(...zones.map((zone) => ({ sheetId, zone })));
68783
- }
69142
+ visited.add(range);
69143
+ const impactedRanges = this.rTree.search(range);
69144
+ queue.push(...impactedRanges.difference(visited));
68784
69145
  }
68785
69146
  // remove initial ranges
68786
69147
  for (const range of ranges) {
68787
- const zone = range.zone;
68788
- const sheetId = range.sheetId;
68789
- for (let col = zone.left; col <= zone.right; col++) {
68790
- for (let row = zone.top; row <= zone.bottom; row++) {
68791
- visited.delete({ sheetId, col, row });
68792
- }
68793
- }
69148
+ visited.delete(range);
68794
69149
  }
68795
69150
  return visited;
68796
69151
  }
@@ -69053,7 +69408,7 @@ stores.inject(MyMetaStore, storeInstance);
69053
69408
  getters;
69054
69409
  compilationParams;
69055
69410
  evaluatedCells = new PositionMap();
69056
- formulaDependencies = lazy(new FormulaDependencyGraph(this.createEmptyPositionSet.bind(this)));
69411
+ formulaDependencies = lazy(new FormulaDependencyGraph());
69057
69412
  blockedArrayFormulas = new PositionSet({});
69058
69413
  spreadingRelations = new SpreadingRelation();
69059
69414
  constructor(context, getters) {
@@ -69088,7 +69443,7 @@ stores.inject(MyMetaStore, storeInstance);
69088
69443
  return undefined;
69089
69444
  }
69090
69445
  const arrayFormulas = this.spreadingRelations.searchFormulaPositionsSpreadingOn(position.sheetId, positionToZone(position));
69091
- return Array.from(arrayFormulas).find((position) => !this.blockedArrayFormulas.has(position));
69446
+ return arrayFormulas.find((position) => !this.blockedArrayFormulas.has(position));
69092
69447
  }
69093
69448
  updateDependencies(position) {
69094
69449
  // removing dependencies is slow because it requires
@@ -69132,57 +69487,72 @@ stores.inject(MyMetaStore, storeInstance);
69132
69487
  }
69133
69488
  evaluateCells(positions) {
69134
69489
  const start = performance.now();
69135
- const cellsToCompute = this.createEmptyPositionSet();
69136
- cellsToCompute.addMany(positions);
69490
+ const rangesToCompute = new RangeSet();
69491
+ rangesToCompute.addManyPositions(positions);
69137
69492
  const arrayFormulasPositions = this.getArrayFormulasImpactedByChangesOf(positions);
69138
- cellsToCompute.addMany(this.getCellsDependingOn(positions));
69139
- cellsToCompute.addMany(arrayFormulasPositions);
69140
- cellsToCompute.addMany(this.getCellsDependingOn(arrayFormulasPositions));
69141
- this.evaluate(cellsToCompute);
69493
+ rangesToCompute.addMany(this.getCellsDependingOn(rangesToCompute));
69494
+ rangesToCompute.addMany(arrayFormulasPositions);
69495
+ rangesToCompute.addMany(this.getCellsDependingOn(arrayFormulasPositions));
69496
+ this.evaluate(rangesToCompute);
69142
69497
  console.debug("evaluate Cells", performance.now() - start, "ms");
69143
69498
  }
69144
69499
  getArrayFormulasImpactedByChangesOf(positions) {
69145
- const impactedPositions = this.createEmptyPositionSet();
69500
+ const impactedRanges = new RangeSet();
69146
69501
  for (const position of positions) {
69147
69502
  const content = this.getters.getCell(position)?.content;
69148
69503
  const arrayFormulaPosition = this.getArrayFormulaSpreadingOn(position);
69149
69504
  if (arrayFormulaPosition !== undefined) {
69150
69505
  // take into account new collisions.
69151
- impactedPositions.add(arrayFormulaPosition);
69506
+ impactedRanges.addPosition(arrayFormulaPosition);
69152
69507
  }
69153
69508
  if (!content) {
69154
69509
  // The previous content could have blocked some array formulas
69155
- impactedPositions.add(position);
69510
+ impactedRanges.addPosition(position);
69156
69511
  }
69157
69512
  }
69158
- const zonesBySheetIds = aggregatePositionsToZones(impactedPositions);
69159
- for (const sheetId in zonesBySheetIds) {
69160
- for (const zone of zonesBySheetIds[sheetId]) {
69161
- impactedPositions.addMany(this.getArrayFormulasBlockedBy(sheetId, zone));
69162
- }
69513
+ for (const range of [...impactedRanges]) {
69514
+ impactedRanges.addMany(this.getArrayFormulasBlockedBy(range.sheetId, range.zone));
69163
69515
  }
69164
- return impactedPositions;
69516
+ return impactedRanges;
69165
69517
  }
69166
69518
  buildDependencyGraph() {
69167
69519
  this.blockedArrayFormulas = this.createEmptyPositionSet();
69168
69520
  this.spreadingRelations = new SpreadingRelation();
69169
69521
  this.formulaDependencies = lazy(() => {
69170
- const dependencies = [...this.getAllCells()].flatMap((position) => this.getDirectDependencies(position)
69171
- .filter((range) => !range.invalidSheetName && !range.invalidXc)
69172
- .map((range) => ({
69173
- data: position,
69174
- boundingBox: {
69175
- zone: range.zone,
69176
- sheetId: range.sheetId,
69177
- },
69178
- })));
69179
- return new FormulaDependencyGraph(this.createEmptyPositionSet.bind(this), dependencies);
69522
+ const rTreeItems = [];
69523
+ for (const sheetId of this.getters.getSheetIds()) {
69524
+ const cells = this.getters.getCells(sheetId);
69525
+ for (const cellId in cells) {
69526
+ const cell = cells[cellId];
69527
+ if (cell.isFormula) {
69528
+ const directDependencies = cell.compiledFormula.dependencies;
69529
+ for (const range of directDependencies) {
69530
+ if (range.invalidSheetName || range.invalidXc) {
69531
+ continue;
69532
+ }
69533
+ rTreeItems.push({
69534
+ data: {
69535
+ sheetId,
69536
+ zone: positionToZone(this.getters.getCellPosition(cellId)),
69537
+ },
69538
+ boundingBox: { sheetId: range.sheetId, zone: range.zone },
69539
+ });
69540
+ }
69541
+ }
69542
+ }
69543
+ }
69544
+ return new FormulaDependencyGraph(rTreeItems);
69180
69545
  });
69181
69546
  }
69182
69547
  evaluateAllCells() {
69183
69548
  const start = performance.now();
69184
69549
  this.evaluatedCells = new PositionMap();
69185
- this.evaluate(this.getAllCells());
69550
+ const ranges = [];
69551
+ for (const sheetId of this.getters.getSheetIds()) {
69552
+ const zone = this.getters.getSheetZone(sheetId);
69553
+ ranges.push({ sheetId, zone });
69554
+ }
69555
+ this.evaluate(ranges);
69186
69556
  console.debug("evaluate all cells", performance.now() - start, "ms");
69187
69557
  }
69188
69558
  evaluateFormulaResult(sheetId, formulaString) {
@@ -69206,48 +69576,47 @@ stores.inject(MyMetaStore, storeInstance);
69206
69576
  return handleError(error, "");
69207
69577
  }
69208
69578
  }
69209
- getAllCells() {
69210
- const positions = this.createEmptyPositionSet();
69211
- positions.fillAllPositions();
69212
- return positions;
69213
- }
69214
69579
  /**
69215
69580
  * Return the position of formulas blocked by the given positions
69216
69581
  * as well as all their dependencies.
69217
69582
  */
69218
69583
  getArrayFormulasBlockedBy(sheetId, zone) {
69219
- const arrayFormulaPositions = this.createEmptyPositionSet();
69584
+ const arrayFormulaPositions = new RangeSet();
69220
69585
  const arrayFormulas = this.spreadingRelations.searchFormulaPositionsSpreadingOn(sheetId, zone);
69221
- arrayFormulaPositions.addMany(arrayFormulas);
69586
+ arrayFormulaPositions.addManyPositions(arrayFormulas);
69222
69587
  const spilledPositions = [...arrayFormulas].filter((position) => !this.blockedArrayFormulas.has(position));
69223
69588
  if (spilledPositions.length) {
69224
69589
  // ignore the formula spreading on the position. Keep only the blocked ones
69225
- arrayFormulaPositions.deleteMany(spilledPositions);
69590
+ arrayFormulaPositions.deleteManyPositions(spilledPositions);
69226
69591
  }
69227
69592
  arrayFormulaPositions.addMany(this.getCellsDependingOn(arrayFormulaPositions));
69228
69593
  return arrayFormulaPositions;
69229
69594
  }
69230
- nextPositionsToUpdate = new PositionSet({});
69595
+ nextRangesToUpdate = new RangeSet();
69231
69596
  cellsBeingComputed = new Set();
69232
69597
  symbolsBeingComputed = new Set();
69233
- evaluate(positions) {
69598
+ evaluate(ranges) {
69234
69599
  this.cellsBeingComputed = new Set();
69235
- this.nextPositionsToUpdate = positions;
69600
+ this.nextRangesToUpdate = new RangeSet(ranges);
69236
69601
  let currentIteration = 0;
69237
- while (!this.nextPositionsToUpdate.isEmpty() && currentIteration++ < MAX_ITERATION) {
69602
+ while (!this.nextRangesToUpdate.isEmpty() && currentIteration++ < MAX_ITERATION) {
69238
69603
  this.updateCompilationParameters();
69239
- const positions = this.nextPositionsToUpdate.clear();
69240
- for (let i = 0; i < positions.length; ++i) {
69241
- this.evaluatedCells.delete(positions[i]);
69242
- }
69243
- for (let i = 0; i < positions.length; ++i) {
69244
- const position = positions[i];
69245
- if (this.nextPositionsToUpdate.has(position)) {
69246
- continue;
69247
- }
69248
- const evaluatedCell = this.computeCell(position);
69249
- if (evaluatedCell !== EMPTY_CELL) {
69250
- this.evaluatedCells.set(position, evaluatedCell);
69604
+ const ranges = [...this.nextRangesToUpdate];
69605
+ this.nextRangesToUpdate.clear();
69606
+ this.clearEvaluatedRanges(ranges);
69607
+ for (const range of ranges) {
69608
+ const { left, bottom, right, top } = range.zone;
69609
+ for (let col = left; col <= right; col++) {
69610
+ for (let row = top; row <= bottom; row++) {
69611
+ const position = { sheetId: range.sheetId, col, row };
69612
+ if (this.nextRangesToUpdate.hasPosition(position)) {
69613
+ continue;
69614
+ }
69615
+ const evaluatedCell = this.computeCell(position);
69616
+ if (evaluatedCell !== EMPTY_CELL) {
69617
+ this.evaluatedCells.set(position, evaluatedCell);
69618
+ }
69619
+ }
69251
69620
  }
69252
69621
  }
69253
69622
  onIterationEndEvaluationRegistry.getAll().forEach((callback) => callback(this.getters));
@@ -69256,6 +69625,16 @@ stores.inject(MyMetaStore, storeInstance);
69256
69625
  console.warn("Maximum iteration reached while evaluating cells");
69257
69626
  }
69258
69627
  }
69628
+ clearEvaluatedRanges(ranges) {
69629
+ for (const range of ranges) {
69630
+ const { left, bottom, right, top } = range.zone;
69631
+ for (let col = left; col <= right; col++) {
69632
+ for (let row = top; row <= bottom; row++) {
69633
+ this.evaluatedCells.delete({ sheetId: range.sheetId, col, row });
69634
+ }
69635
+ }
69636
+ }
69637
+ }
69259
69638
  computeCell(position) {
69260
69639
  const evaluation = this.evaluatedCells.get(position);
69261
69640
  if (evaluation) {
@@ -69328,9 +69707,9 @@ stores.inject(MyMetaStore, storeInstance);
69328
69707
  }
69329
69708
  invalidatePositionsDependingOnSpread(sheetId, resultZone) {
69330
69709
  // the result matrix is split in 2 zones to exclude the array formula position
69331
- const invalidatedPositions = this.formulaDependencies().getCellsDependingOn(excludeTopLeft(resultZone).map((zone) => ({ sheetId, zone })));
69332
- invalidatedPositions.delete({ sheetId, col: resultZone.left, row: resultZone.top });
69333
- this.nextPositionsToUpdate.addMany(invalidatedPositions);
69710
+ const invalidatedPositions = this.getCellsDependingOn(excludeTopLeft(resultZone).map((zone) => ({ sheetId, zone })));
69711
+ invalidatedPositions.delete({ sheetId, zone: resultZone });
69712
+ this.nextRangesToUpdate.addMany(invalidatedPositions);
69334
69713
  }
69335
69714
  assertSheetHasEnoughSpaceToSpreadFormulaResult({ sheetId, col, row }, matrixResult) {
69336
69715
  const numberOfCols = this.getters.getNumberCols(sheetId);
@@ -69405,7 +69784,7 @@ stores.inject(MyMetaStore, storeInstance);
69405
69784
  }
69406
69785
  const sheetId = position.sheetId;
69407
69786
  this.invalidatePositionsDependingOnSpread(sheetId, zone);
69408
- this.nextPositionsToUpdate.addMany(this.getArrayFormulasBlockedBy(sheetId, zone));
69787
+ this.nextRangesToUpdate.addMany(this.getArrayFormulasBlockedBy(sheetId, zone));
69409
69788
  }
69410
69789
  /**
69411
69790
  * Wraps a GetSymbolValue function to add cycle detection
@@ -69440,13 +69819,8 @@ stores.inject(MyMetaStore, storeInstance);
69440
69819
  }
69441
69820
  return cell.compiledFormula.dependencies;
69442
69821
  }
69443
- getCellsDependingOn(positions) {
69444
- const ranges = [];
69445
- const zonesBySheetIds = aggregatePositionsToZones(positions);
69446
- for (const sheetId in zonesBySheetIds) {
69447
- ranges.push(...zonesBySheetIds[sheetId].map((zone) => ({ sheetId, zone })));
69448
- }
69449
- return this.formulaDependencies().getCellsDependingOn(ranges);
69822
+ getCellsDependingOn(ranges) {
69823
+ return this.formulaDependencies().getCellsDependingOn(ranges, this.nextRangesToUpdate);
69450
69824
  }
69451
69825
  }
69452
69826
  function forEachSpreadPositionInMatrix(nbColumns, nbRows, callback) {
@@ -70957,7 +71331,8 @@ stores.inject(MyMetaStore, storeInstance);
70957
71331
  const topLeft = { col: unionZone.left, row: unionZone.top, sheetId };
70958
71332
  const parentSpreadingCell = this.getters.getArrayFormulaSpreadingOn(topLeft);
70959
71333
  if (!parentSpreadingCell) {
70960
- return false;
71334
+ const evaluatedCell = this.getters.getEvaluatedCell(topLeft);
71335
+ return (evaluatedCell.value === CellErrorType.SpilledBlocked && !evaluatedCell.errorOriginPosition);
70961
71336
  }
70962
71337
  else if (deepEquals(parentSpreadingCell, topLeft) && getZoneArea(unionZone) === 1) {
70963
71338
  return true;
@@ -82129,6 +82504,7 @@ stores.inject(MyMetaStore, storeInstance);
82129
82504
  static components = { Menu };
82130
82505
  rootItems = topbarMenuRegistry.getMenuItems();
82131
82506
  menuRef = owl.useRef("menu");
82507
+ containerRef = owl.useRef("container");
82132
82508
  state = owl.useState({
82133
82509
  menuItems: this.rootItems,
82134
82510
  title: _t("Menu Bar"),
@@ -82136,6 +82512,7 @@ stores.inject(MyMetaStore, storeInstance);
82136
82512
  });
82137
82513
  setup() {
82138
82514
  owl.useExternalListener(window, "click", this.onExternalClick, { capture: true });
82515
+ owl.onMounted(this.updateShadows);
82139
82516
  }
82140
82517
  onExternalClick(ev) {
82141
82518
  if (!this.menuRef.el?.contains(ev.target)) {
@@ -82148,6 +82525,7 @@ stores.inject(MyMetaStore, storeInstance);
82148
82525
  this.state.parentState = { ...this.state };
82149
82526
  this.state.menuItems = children;
82150
82527
  this.state.title = menu.name(this.env);
82528
+ this.containerRef.el?.scrollTo({ top: 0 });
82151
82529
  }
82152
82530
  else {
82153
82531
  this.state.menuItems = this.rootItems;
@@ -82169,6 +82547,19 @@ stores.inject(MyMetaStore, storeInstance);
82169
82547
  height: `${this.props.height}px`,
82170
82548
  });
82171
82549
  }
82550
+ updateShadows() {
82551
+ if (!this.containerRef.el) {
82552
+ return;
82553
+ }
82554
+ this.containerRef.el.classList.remove("scroll-top", "scroll-bottom");
82555
+ const maxScroll = this.containerRef.el.scrollHeight - this.containerRef.el.clientHeight || 0;
82556
+ if (this.containerRef.el.scrollTop < maxScroll - 1) {
82557
+ this.containerRef.el.classList.add("scroll-bottom");
82558
+ }
82559
+ if (this.containerRef.el.scrollTop > 0) {
82560
+ this.containerRef.el.classList.add("scroll-top");
82561
+ }
82562
+ }
82172
82563
  onClickBack() {
82173
82564
  if (!this.state.parentState) {
82174
82565
  this.props.onClose();
@@ -82177,6 +82568,7 @@ stores.inject(MyMetaStore, storeInstance);
82177
82568
  this.state.menuItems = this.state.parentState.menuItems;
82178
82569
  this.state.title = this.state.parentState.title;
82179
82570
  this.state.parentState = this.state.parentState.parentState;
82571
+ this.containerRef.el?.scrollTo({ top: 0 });
82180
82572
  }
82181
82573
  get backTitle() {
82182
82574
  return this.state.parentState ? _t("Go to previous menu") : _t("Close menu bar");
@@ -82233,7 +82625,9 @@ stores.inject(MyMetaStore, storeInstance);
82233
82625
  : "inactive";
82234
82626
  }
82235
82627
  get showFxIcon() {
82236
- return this.focus === "inactive" && !this.composerStore.currentContent;
82628
+ return (this.focus === "inactive" &&
82629
+ !this.composerStore.currentContent &&
82630
+ !this.composerStore.placeholder);
82237
82631
  }
82238
82632
  get rect() {
82239
82633
  return this.composerRef.el
@@ -82250,8 +82644,9 @@ stores.inject(MyMetaStore, storeInstance);
82250
82644
  },
82251
82645
  focus: this.focus,
82252
82646
  composerStore: this.composerStore,
82253
- onComposerContentFocused: () => this.composerFocusStore.focusComposer(this.composerInterface, {
82647
+ onComposerContentFocused: (selection) => this.composerFocusStore.focusComposer(this.composerInterface, {
82254
82648
  focusMode: "contentFocus",
82649
+ selection,
82255
82650
  }),
82256
82651
  isDefaultFocus: false,
82257
82652
  inputStyle: cssPropertiesToCss({
@@ -82259,6 +82654,7 @@ stores.inject(MyMetaStore, storeInstance);
82259
82654
  "max-height": `130px`,
82260
82655
  }),
82261
82656
  showAssistant: !isIOS(), // Hide assistant on iOS as it breaks visually
82657
+ placeholder: this.composerStore.placeholder,
82262
82658
  };
82263
82659
  }
82264
82660
  get symbols() {
@@ -82321,7 +82717,9 @@ stores.inject(MyMetaStore, storeInstance);
82321
82717
  : "inactive";
82322
82718
  }
82323
82719
  get showFxIcon() {
82324
- return this.focus === "inactive" && !this.composerStore.currentContent;
82720
+ return (this.focus === "inactive" &&
82721
+ !this.composerStore.currentContent &&
82722
+ !this.composerStore.placeholder);
82325
82723
  }
82326
82724
  get composerStyle() {
82327
82725
  const style = {
@@ -88537,9 +88935,9 @@ stores.inject(MyMetaStore, storeInstance);
88537
88935
  exports.tokenize = tokenize;
88538
88936
 
88539
88937
 
88540
- __info__.version = "19.0.5";
88541
- __info__.date = "2025-10-07T10:04:06.400Z";
88542
- __info__.hash = "86fc442";
88938
+ __info__.version = "19.0.7";
88939
+ __info__.date = "2025-10-23T08:19:01.764Z";
88940
+ __info__.hash = "1c1d1ec";
88543
88941
 
88544
88942
 
88545
88943
  })(this.o_spreadsheet = this.o_spreadsheet || {}, owl);