@odoo/o-spreadsheet 17.3.0-alpha.1 → 17.3.0-alpha.3

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.
@@ -3,9 +3,9 @@
3
3
  /**
4
4
  * This file is generated by o-spreadsheet build tools. Do not edit it.
5
5
  * @see https://github.com/odoo/o-spreadsheet
6
- * @version 17.3.0-alpha.1
7
- * @date 2024-03-25T09:43:36.072Z
8
- * @hash 4095c41
6
+ * @version 17.3.0-alpha.3
7
+ * @date 2024-04-10T12:28:23.658Z
8
+ * @hash 80b5056
9
9
  */
10
10
 
11
11
  'use strict';
@@ -190,7 +190,7 @@ const DEFAULT_GAUGE_LOWER_COLOR = "#cc0000";
190
190
  const DEFAULT_GAUGE_MIDDLE_COLOR = "#f1c232";
191
191
  const DEFAULT_GAUGE_UPPER_COLOR = "#6aa84f";
192
192
  const DEFAULT_SCORECARD_BASELINE_MODE = "difference";
193
- const DEFAULT_SCORECARD_BASELINE_COLOR_UP = "#6aa84f";
193
+ const DEFAULT_SCORECARD_BASELINE_COLOR_UP = "#6AA84F";
194
194
  const DEFAULT_SCORECARD_BASELINE_COLOR_DOWN = "#E06666";
195
195
  const LINE_FILL_TRANSPARENCY = 0.4;
196
196
  // session
@@ -466,7 +466,7 @@ function getItemId(item, itemsDic) {
466
466
  }
467
467
  // Generate new Id if the item didn't exist in the dictionary
468
468
  const ids = Object.keys(itemsDic);
469
- const maxId = ids.length === 0 ? 0 : Math.max(...ids.map((id) => parseInt(id, 10)));
469
+ const maxId = ids.length === 0 ? 0 : largeMax(ids.map((id) => parseInt(id, 10)));
470
470
  itemsDic[maxId + 1] = item;
471
471
  return maxId + 1;
472
472
  }
@@ -484,7 +484,7 @@ function debounce(func, wait, immediate) {
484
484
  let timeout = undefined;
485
485
  const debounced = function () {
486
486
  const context = this;
487
- const args = arguments;
487
+ const args = Array.from(arguments);
488
488
  function later() {
489
489
  timeout = undefined;
490
490
  if (!immediate) {
@@ -562,7 +562,7 @@ function deepEquals(o1, o2) {
562
562
  if (typeof o1 !== typeof o2)
563
563
  return false;
564
564
  if (typeof o1 !== "object")
565
- return o1 === o2;
565
+ return false;
566
566
  // Objects can have different keys if the values are undefined
567
567
  for (const key in o2) {
568
568
  if (!(key in o1) && o2[key] !== undefined) {
@@ -686,6 +686,34 @@ function getSearchRegex(searchStr, searchOptions) {
686
686
  }
687
687
  return RegExp(searchValue, flags);
688
688
  }
689
+ /**
690
+ * Alternative to Math.max that works with large arrays.
691
+ * Typically useful for arrays bigger than 100k elements.
692
+ */
693
+ function largeMax(array) {
694
+ let len = array.length;
695
+ if (len < 100_000)
696
+ return Math.max(...array);
697
+ let max = -Infinity;
698
+ while (len--) {
699
+ max = array[len] > max ? array[len] : max;
700
+ }
701
+ return max;
702
+ }
703
+ /**
704
+ * Alternative to Math.min that works with large arrays.
705
+ * Typically useful for arrays bigger than 100k elements.
706
+ */
707
+ function largeMin(array) {
708
+ let len = array.length;
709
+ if (len < 100_000)
710
+ return Math.min(...array);
711
+ let min = +Infinity;
712
+ while (len--) {
713
+ min = array[len] < min ? array[len] : min;
714
+ }
715
+ return min;
716
+ }
689
717
 
690
718
  const RBA_REGEX = /rgba?\(|\s+|\)/gi;
691
719
  const HEX_MATCH = /^#([A-F\d]{2}){3,4}$/;
@@ -1858,6 +1886,11 @@ const invalidateCFEvaluationCommands = new Set([
1858
1886
  "REMOVE_CONDITIONAL_FORMAT",
1859
1887
  "CHANGE_CONDITIONAL_FORMAT_PRIORITY",
1860
1888
  ]);
1889
+ const invalidateBordersCommands = new Set([
1890
+ "AUTOFILL_CELL",
1891
+ "SET_BORDER",
1892
+ "SET_ZONE_BORDERS",
1893
+ ]);
1861
1894
  const readonlyAllowedCommands = new Set([
1862
1895
  "START",
1863
1896
  "ACTIVATE_SHEET",
@@ -2088,6 +2121,7 @@ exports.CommandResult = void 0;
2088
2121
  CommandResult["InvalidCopyPasteSelection"] = "InvalidCopyPasteSelection";
2089
2122
  CommandResult["NoChanges"] = "NoChanges";
2090
2123
  CommandResult["InvalidInputId"] = "InvalidInputId";
2124
+ CommandResult["SheetIsHidden"] = "SheetIsHidden";
2091
2125
  })(exports.CommandResult || (exports.CommandResult = {}));
2092
2126
 
2093
2127
  const PLAIN_TEXT_FORMAT = "@"; // see OpenXML spec §18.8.31
@@ -2154,6 +2188,7 @@ const CellErrorType = {
2154
2188
  BadExpression: "#BAD_EXPR",
2155
2189
  CircularDependency: "#CYCLE",
2156
2190
  UnknownFunction: "#NAME?",
2191
+ DivisionByZero: "#DIV/0!",
2157
2192
  GenericError: "#ERROR",
2158
2193
  };
2159
2194
  const errorTypes = new Set(Object.values(CellErrorType));
@@ -2192,9 +2227,9 @@ class UnknownFunctionError extends EvaluationError {
2192
2227
 
2193
2228
  // HELPERS
2194
2229
  const SORT_TYPES_ORDER = ["number", "string", "boolean", "undefined"];
2195
- function assert(condition, message) {
2230
+ function assert(condition, message, value) {
2196
2231
  if (!condition()) {
2197
- throw new EvaluationError(message);
2232
+ throw new EvaluationError(message, value);
2198
2233
  }
2199
2234
  }
2200
2235
  function inferFormat(data) {
@@ -2272,6 +2307,9 @@ function strictToInteger(value, locale) {
2272
2307
  function assertNumberGreaterThanOrEqualToOne(value) {
2273
2308
  assert(() => value >= 1, _t("The function [[FUNCTION_NAME]] expects a number value to be greater than or equal to 1, but receives %s.", value.toString()));
2274
2309
  }
2310
+ function assertNotZero(value) {
2311
+ assert(() => value !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."), CellErrorType.DivisionByZero);
2312
+ }
2275
2313
  function toString(data) {
2276
2314
  const value = toValue(data);
2277
2315
  switch (typeof value) {
@@ -3037,6 +3075,9 @@ function applyInternalFormat(value, internalFormat, locale) {
3037
3075
  return formattedValue;
3038
3076
  }
3039
3077
  function applyInternalNumberFormat(value, format, locale) {
3078
+ if (value === Infinity) {
3079
+ return "∞" + (format.isPercent ? "%" : "");
3080
+ }
3040
3081
  if (format.isPercent) {
3041
3082
  value = value * 100;
3042
3083
  }
@@ -3380,6 +3421,46 @@ function roundFormat(format) {
3380
3421
  });
3381
3422
  return convertInternalFormatToFormat(roundedFormat);
3382
3423
  }
3424
+ function humanizeNumber({ value, format }, locale) {
3425
+ const numberFormat = formatLargeNumber({
3426
+ value,
3427
+ format,
3428
+ }, undefined, locale);
3429
+ return formatValue(value, { format: numberFormat, locale });
3430
+ }
3431
+ function formatLargeNumber(arg, unit, locale) {
3432
+ let value = 0;
3433
+ try {
3434
+ value = Math.abs(toNumber(arg?.value, locale));
3435
+ }
3436
+ catch (e) {
3437
+ return "";
3438
+ }
3439
+ const format = arg?.format;
3440
+ if (unit !== undefined) {
3441
+ const postFix = unit?.value;
3442
+ switch (postFix) {
3443
+ case "k":
3444
+ return createLargeNumberFormat(format, 1e3, "k");
3445
+ case "m":
3446
+ return createLargeNumberFormat(format, 1e6, "m");
3447
+ case "b":
3448
+ return createLargeNumberFormat(format, 1e9, "b");
3449
+ default:
3450
+ throw new EvaluationError(_t("The formatting unit should be 'k', 'm' or 'b'."));
3451
+ }
3452
+ }
3453
+ if (value < 1e5) {
3454
+ return createLargeNumberFormat(format, 0, "");
3455
+ }
3456
+ else if (value < 1e8) {
3457
+ return createLargeNumberFormat(format, 1e3, "k");
3458
+ }
3459
+ else if (value < 1e11) {
3460
+ return createLargeNumberFormat(format, 1e6, "m");
3461
+ }
3462
+ return createLargeNumberFormat(format, 1e9, "b");
3463
+ }
3383
3464
  function createLargeNumberFormat(format, magnitude, postFix, locale) {
3384
3465
  const internalFormat = parseFormat(format || "#,##0");
3385
3466
  const largeNumberFormat = [];
@@ -4559,8 +4640,9 @@ function computeTextLinesHeight(textLineHeight, numberOfLines = 1) {
4559
4640
  * Get the default height of the cell given its style.
4560
4641
  */
4561
4642
  function getDefaultCellHeight(ctx, cell, colSize) {
4562
- if (!cell || !cell.content)
4643
+ if (!cell || (!cell.isFormula && !cell.content)) {
4563
4644
  return DEFAULT_CELL_HEIGHT;
4645
+ }
4564
4646
  const maxWidth = cell.style?.wrapping === "wrap" ? colSize - 2 * MIN_CELL_TEXT_MARGIN : undefined;
4565
4647
  const numberOfLines = cell.isFormula
4566
4648
  ? 1
@@ -4571,14 +4653,19 @@ function getDefaultCellHeight(ctx, cell, colSize) {
4571
4653
  const textWidthCache = {};
4572
4654
  function computeTextWidth(context, text, style, fontUnit = "pt") {
4573
4655
  const font = computeTextFont(style, fontUnit);
4656
+ context.save();
4657
+ context.font = font;
4658
+ const width = computeCachedTextWidth(context, text);
4659
+ context.restore();
4660
+ return width;
4661
+ }
4662
+ function computeCachedTextWidth(context, text) {
4663
+ const font = context.font;
4574
4664
  if (!textWidthCache[font]) {
4575
4665
  textWidthCache[font] = {};
4576
4666
  }
4577
4667
  if (textWidthCache[font][text] === undefined) {
4578
- context.save();
4579
- context.font = font;
4580
4668
  const textWidth = context.measureText(text).width;
4581
- context.restore();
4582
4669
  textWidthCache[font][text] = textWidth;
4583
4670
  }
4584
4671
  return textWidthCache[font][text];
@@ -4723,6 +4810,42 @@ const pxRegex = /([0-9\.]*)px/;
4723
4810
  function getContextFontSize(font) {
4724
4811
  return Number(font.match(pxRegex)?.[1]);
4725
4812
  }
4813
+ // Inspired from https://stackoverflow.com/a/10511598
4814
+ function clipTextWithEllipsis(ctx, text, maxWidth) {
4815
+ let width = computeCachedTextWidth(ctx, text);
4816
+ if (width <= maxWidth) {
4817
+ return text;
4818
+ }
4819
+ const ellipsis = "…";
4820
+ const ellipsisWidth = computeCachedTextWidth(ctx, text);
4821
+ if (width <= ellipsisWidth) {
4822
+ return text;
4823
+ }
4824
+ let len = text.length;
4825
+ while (width >= maxWidth - ellipsisWidth && len-- > 0) {
4826
+ text = text.substring(0, len);
4827
+ width = computeCachedTextWidth(ctx, text);
4828
+ }
4829
+ return text + ellipsis;
4830
+ }
4831
+ function splitTextInTwoLines(text) {
4832
+ let spaces = "";
4833
+ while (text[0] === " ") {
4834
+ spaces += " ";
4835
+ text = text.slice(1);
4836
+ }
4837
+ const length = text.length;
4838
+ const middle = Math.floor(length / 2);
4839
+ const leftSpace = text.substring(0, middle).lastIndexOf(" ");
4840
+ const rightSpace = text.substring(middle).indexOf(" ") + middle;
4841
+ if (leftSpace === -1 && rightSpace === middle - 1) {
4842
+ return [spaces + text, ""];
4843
+ }
4844
+ if (leftSpace > length - rightSpace || rightSpace === middle - 1) {
4845
+ return [spaces + text.slice(0, leftSpace), spaces + text.slice(leftSpace + 1)];
4846
+ }
4847
+ return [spaces + text.slice(0, rightSpace), spaces + text.slice(rightSpace + 1)];
4848
+ }
4726
4849
  function drawDecoratedText(context, text, position, underline = false, strikethrough = false, strokeWidth = getContextFontSize(context.font) / 10 //This value is defined to get a good looking stroke
4727
4850
  ) {
4728
4851
  context.fillText(text, position.x, position.y);
@@ -6994,10 +7117,17 @@ urlRegistry.add("sheet_URL", {
6994
7117
  },
6995
7118
  open(url, env) {
6996
7119
  const sheetId = parseSheetUrl(url);
6997
- env.model.dispatch("ACTIVATE_SHEET", {
7120
+ const result = env.model.dispatch("ACTIVATE_SHEET", {
6998
7121
  sheetIdFrom: env.model.getters.getActiveSheetId(),
6999
7122
  sheetIdTo: sheetId,
7000
7123
  });
7124
+ if (result.isCancelledBecause("SheetIsHidden" /* CommandResult.SheetIsHidden */)) {
7125
+ env.notifyUser({
7126
+ type: "warning",
7127
+ sticky: false,
7128
+ text: _t("Cannot open the link because the linked sheet is hidden."),
7129
+ });
7130
+ }
7001
7131
  },
7002
7132
  sequence: 0,
7003
7133
  });
@@ -7113,7 +7243,7 @@ function textCell(value, format, formattedValue) {
7113
7243
  }
7114
7244
  function numberCell(value, format, formattedValue) {
7115
7245
  return {
7116
- value: value || 0,
7246
+ value: value || 0, // necessary to avoid "-0" and NaN values,
7117
7247
  format,
7118
7248
  formattedValue,
7119
7249
  type: CellValueType.number,
@@ -7359,6 +7489,7 @@ const CellIsOperators = {
7359
7489
  };
7360
7490
  const ChartTerms = {
7361
7491
  Series: _t("Series"),
7492
+ BackgroundColor: _t("Background color"),
7362
7493
  Errors: {
7363
7494
  Unexpected: _t("The chart definition is invalid for an unknown reason"),
7364
7495
  // BASIC CHART ERRORS (LINE | BAR | PIE)
@@ -9394,32 +9525,63 @@ function shouldRemoveFirstLabel(labelRange, dataset, dataSetsHaveTitle) {
9394
9525
  }
9395
9526
  return true;
9396
9527
  }
9397
- // ---------------------------------------------------------------------------
9398
- // Scorecard
9399
- // ---------------------------------------------------------------------------
9400
- function getBaselineText(baseline, keyValue, baselineMode, locale) {
9528
+ function getChartPositionAtCenterOfViewport(getters, chartSize) {
9529
+ const { x, y } = getters.getMainViewportCoordinates();
9530
+ const { scrollX, scrollY } = getters.getActiveSheetScrollInfo();
9531
+ const { width, height } = getters.getVisibleRect(getters.getActiveMainViewport());
9532
+ const position = {
9533
+ x: x + scrollX + Math.max(0, (width - chartSize.width) / 2),
9534
+ y: y + scrollY + Math.max(0, (height - chartSize.height) / 2),
9535
+ }; // Position at the center of the scrollable viewport
9536
+ return position;
9537
+ }
9538
+
9539
+ function getBaselineText(baseline, keyValue, baselineMode, humanize, locale) {
9401
9540
  if (!baseline) {
9402
9541
  return "";
9403
9542
  }
9404
9543
  else if (baselineMode === "text" ||
9405
9544
  keyValue?.type !== CellValueType.number ||
9406
9545
  baseline.type !== CellValueType.number) {
9546
+ if (humanize) {
9547
+ return humanizeNumber(baseline, locale);
9548
+ }
9407
9549
  return baseline.formattedValue;
9408
9550
  }
9551
+ let { value, format } = baseline;
9552
+ if (baselineMode === "progress") {
9553
+ value = keyValue.value / value;
9554
+ format = "0.0%";
9555
+ }
9409
9556
  else {
9410
- let diff = keyValue.value - baseline.value;
9411
- if (baselineMode === "percentage" && diff !== 0) {
9412
- diff = (diff / baseline.value) * 100;
9557
+ value = Math.abs(keyValue.value - value);
9558
+ if (baselineMode === "percentage" && value !== 0) {
9559
+ value = value / baseline.value;
9560
+ }
9561
+ if (baselineMode === "percentage") {
9562
+ format = "0.0%";
9413
9563
  }
9414
- if (baselineMode !== "percentage" && baseline.format) {
9415
- return formatValue(diff, { format: baseline.format, locale });
9564
+ if (!format) {
9565
+ value = Math.round(value * 100) / 100;
9416
9566
  }
9417
- const baselineStr = Math.abs(parseFloat(diff.toFixed(2))).toLocaleString();
9418
- return baselineMode === "percentage" ? baselineStr + "%" : baselineStr;
9419
9567
  }
9568
+ if (humanize) {
9569
+ return humanizeNumber({ value, format }, locale);
9570
+ }
9571
+ return formatValue(value, { format, locale });
9572
+ }
9573
+ function getKeyValueText(keyValueCell, humanize, locale) {
9574
+ if (!keyValueCell) {
9575
+ return "";
9576
+ }
9577
+ if (humanize) {
9578
+ return humanizeNumber(keyValueCell, locale);
9579
+ }
9580
+ return keyValueCell.formattedValue ?? String(keyValueCell.value ?? "");
9420
9581
  }
9421
9582
  function getBaselineColor(baseline, baselineMode, keyValue, colorUp, colorDown) {
9422
9583
  if (baselineMode === "text" ||
9584
+ baselineMode === "progress" ||
9423
9585
  baseline?.type !== CellValueType.number ||
9424
9586
  keyValue?.type !== CellValueType.number) {
9425
9587
  return undefined;
@@ -9448,17 +9610,6 @@ function getBaselineArrowDirection(baseline, keyValue, baselineMode) {
9448
9610
  }
9449
9611
  return "neutral";
9450
9612
  }
9451
- function getChartPositionAtCenterOfViewport(getters, chartSize) {
9452
- const { x, y } = getters.getMainViewportCoordinates();
9453
- const { scrollX, scrollY } = getters.getActiveSheetScrollInfo();
9454
- const { width, height } = getters.getVisibleRect(getters.getActiveMainViewport());
9455
- const position = {
9456
- x: x + scrollX + Math.max(0, (width - chartSize.width) / 2),
9457
- y: y + scrollY + Math.max(0, (height - chartSize.height) / 2),
9458
- }; // Position at the center of the scrollable viewport
9459
- return position;
9460
- }
9461
-
9462
9613
  function checkKeyValue(definition) {
9463
9614
  return definition.keyValue && !rangeReference.test(definition.keyValue)
9464
9615
  ? "InvalidScorecardKeyValue" /* CommandResult.InvalidScorecardKeyValue */
@@ -9476,10 +9627,12 @@ let ScorecardChart$1 = class ScorecardChart extends AbstractChart {
9476
9627
  baseline;
9477
9628
  baselineMode;
9478
9629
  baselineDescr;
9630
+ progressBar = false;
9479
9631
  background;
9480
9632
  baselineColorUp;
9481
9633
  baselineColorDown;
9482
9634
  fontColor;
9635
+ humanize;
9483
9636
  type = "scorecard";
9484
9637
  constructor(definition, sheetId, getters) {
9485
9638
  super(definition, sheetId, getters);
@@ -9490,6 +9643,7 @@ let ScorecardChart$1 = class ScorecardChart extends AbstractChart {
9490
9643
  this.background = definition.background;
9491
9644
  this.baselineColorUp = definition.baselineColorUp;
9492
9645
  this.baselineColorDown = definition.baselineColorDown;
9646
+ this.humanize = definition.humanize ?? false;
9493
9647
  }
9494
9648
  static validateChartDefinition(validator, definition) {
9495
9649
  return validator.checkValidations(definition, checkKeyValue, checkBaseline);
@@ -9559,6 +9713,7 @@ let ScorecardChart$1 = class ScorecardChart extends AbstractChart {
9559
9713
  keyValue: keyValue
9560
9714
  ? this.getters.getRangeString(keyValue, targetSheetId || this.sheetId)
9561
9715
  : undefined,
9716
+ humanize: this.humanize,
9562
9717
  };
9563
9718
  }
9564
9719
  getDefinitionForExcel() {
@@ -9584,7 +9739,7 @@ function drawScoreChart(structure, canvas) {
9584
9739
  if (structure.title) {
9585
9740
  ctx.font = structure.title.style.font;
9586
9741
  ctx.fillStyle = structure.title.style.color;
9587
- ctx.fillText(structure.title.text, structure.title.position.x, structure.title.position.y);
9742
+ ctx.fillText(clipTextWithEllipsis(ctx, structure.title.text, canvas.width - structure.title.position.x), structure.title.position.x, structure.title.position.y);
9588
9743
  }
9589
9744
  if (structure.baseline) {
9590
9745
  ctx.font = structure.baseline.style.font;
@@ -9611,20 +9766,41 @@ function drawScoreChart(structure, canvas) {
9611
9766
  ctx.restore();
9612
9767
  }
9613
9768
  if (structure.baselineDescr) {
9614
- ctx.font = structure.baselineDescr.style.font;
9615
- ctx.fillStyle = structure.baselineDescr.style.color;
9616
- ctx.fillText(structure.baselineDescr.text, structure.baselineDescr.position.x, structure.baselineDescr.position.y);
9769
+ const descr = structure.baselineDescr[0];
9770
+ ctx.font = descr.style.font;
9771
+ ctx.fillStyle = descr.style.color;
9772
+ for (const description of structure.baselineDescr) {
9773
+ ctx.fillText(clipTextWithEllipsis(ctx, description.text, canvas.width - description.position.x), description.position.x, description.position.y);
9774
+ }
9617
9775
  }
9618
9776
  if (structure.key) {
9619
9777
  ctx.font = structure.key.style.font;
9620
9778
  ctx.fillStyle = structure.key.style.color;
9621
9779
  drawDecoratedText(ctx, structure.key.text, structure.key.position, structure.key.style.underline, structure.key.style.strikethrough);
9622
9780
  }
9781
+ if (structure.progressBar) {
9782
+ ctx.fillStyle = structure.progressBar.style.backgroundColor;
9783
+ ctx.beginPath();
9784
+ ctx.roundRect(structure.progressBar.position.x, structure.progressBar.position.y, structure.progressBar.dimension.width, structure.progressBar.dimension.height, structure.progressBar.dimension.height / 2);
9785
+ ctx.fill();
9786
+ ctx.fillStyle = structure.progressBar.style.color;
9787
+ ctx.beginPath();
9788
+ if (structure.progressBar.value > 0) {
9789
+ ctx.roundRect(structure.progressBar.position.x, structure.progressBar.position.y, structure.progressBar.dimension.width *
9790
+ Math.max(0, Math.min(1.0, structure.progressBar.value)), structure.progressBar.dimension.height, structure.progressBar.dimension.height / 2);
9791
+ }
9792
+ else {
9793
+ const width = structure.progressBar.dimension.width *
9794
+ Math.max(0, Math.min(1.0, -structure.progressBar.value));
9795
+ ctx.roundRect(structure.progressBar.position.x + structure.progressBar.dimension.width - width, structure.progressBar.position.y, width, structure.progressBar.dimension.height, structure.progressBar.dimension.height / 2);
9796
+ }
9797
+ ctx.fill();
9798
+ }
9623
9799
  }
9624
9800
  function createScorecardChartRuntime(chart, getters) {
9625
- let keyValue = "";
9626
9801
  let formattedKeyValue = "";
9627
9802
  let keyValueCell;
9803
+ const locale = getters.getLocale();
9628
9804
  if (chart.keyValue) {
9629
9805
  const keyValuePosition = {
9630
9806
  sheetId: chart.keyValue.sheetId,
@@ -9632,31 +9808,33 @@ function createScorecardChartRuntime(chart, getters) {
9632
9808
  row: chart.keyValue.zone.top,
9633
9809
  };
9634
9810
  keyValueCell = getters.getEvaluatedCell(keyValuePosition);
9635
- keyValue = String(keyValueCell.value ?? "");
9636
- formattedKeyValue = keyValueCell.formattedValue;
9811
+ formattedKeyValue = getKeyValueText(keyValueCell, chart.humanize ?? false, locale);
9637
9812
  }
9638
9813
  let baselineCell;
9639
9814
  const baseline = chart.baseline;
9640
9815
  if (baseline) {
9641
9816
  const baselinePosition = {
9642
- sheetId: chart.baseline.sheetId,
9643
- col: chart.baseline.zone.left,
9644
- row: chart.baseline.zone.top,
9817
+ sheetId: baseline.sheetId,
9818
+ col: baseline.zone.left,
9819
+ row: baseline.zone.top,
9645
9820
  };
9646
9821
  baselineCell = getters.getEvaluatedCell(baselinePosition);
9647
9822
  }
9648
9823
  const { background, fontColor } = getters.getStyleOfSingleCellChart(chart.background, chart.keyValue);
9649
- const locale = getters.getLocale();
9824
+ const baselineDisplay = getBaselineText(baselineCell, keyValueCell, chart.baselineMode, chart.humanize ?? false, locale);
9825
+ const baselineValue = chart.baselineMode === "progress" && isNumber(baselineDisplay, locale)
9826
+ ? toNumber(baselineDisplay, locale)
9827
+ : 0;
9650
9828
  return {
9651
9829
  title: _t(chart.title),
9652
- keyValue: formattedKeyValue || keyValue,
9653
- baselineDisplay: getBaselineText(baselineCell, keyValueCell, chart.baselineMode, locale),
9830
+ keyValue: formattedKeyValue,
9831
+ baselineDisplay,
9654
9832
  baselineArrow: getBaselineArrowDirection(baselineCell, keyValueCell, chart.baselineMode),
9655
9833
  baselineColor: getBaselineColor(baselineCell, chart.baselineMode, keyValueCell, chart.baselineColorUp, chart.baselineColorDown),
9656
- baselineDescr: chart.baselineDescr ? _t(chart.baselineDescr) : "",
9834
+ baselineDescr: chart.baselineMode !== "progress" && chart.baselineDescr ? _t(chart.baselineDescr) : "",
9657
9835
  fontColor,
9658
9836
  background,
9659
- baselineStyle: chart.baselineMode !== "percentage" && baseline
9837
+ baselineStyle: chart.baselineMode !== "percentage" && chart.baselineMode !== "progress" && baseline
9660
9838
  ? getters.getCellStyle({
9661
9839
  sheetId: baseline.sheetId,
9662
9840
  col: baseline.zone.left,
@@ -9670,17 +9848,21 @@ function createScorecardChartRuntime(chart, getters) {
9670
9848
  row: chart.keyValue.zone.top,
9671
9849
  })
9672
9850
  : undefined,
9851
+ progressBar: chart.baselineMode === "progress"
9852
+ ? {
9853
+ value: baselineValue,
9854
+ color: baselineValue > 0 ? chart.baselineColorUp : chart.baselineColorDown,
9855
+ }
9856
+ : undefined,
9673
9857
  };
9674
9858
  }
9675
9859
 
9676
9860
  /* Sizes of boxes containing the texts, in percentage of the Chart size */
9677
9861
  const TITLE_FONT_SIZE = 18;
9678
- const BASELINE_BOX_HEIGHT_RATIO = 0.35;
9679
- const KEY_BOX_HEIGHT_RATIO = 0.65;
9680
- /** Baseline description should have a smaller font than the baseline */
9681
- const BASELINE_DESCR_FONT_RATIO = 0.9;
9682
- /* Padding at the border of the chart, in percentage of the chart width */
9683
- const CHART_PADDING_RATIO = 0.02;
9862
+ const KEY_BOX_HEIGHT_RATIO = 0.8;
9863
+ /* Padding at the border of the chart */
9864
+ const CHART_PADDING = 10;
9865
+ const BOTTOM_PADDING_RATIO = 0.05;
9684
9866
  /**
9685
9867
  * Line height (in em)
9686
9868
  * Having a line heigh =1em (=font size) don't work, the font will overflow.
@@ -9720,31 +9902,37 @@ class ScorecardChartConfigBuilder {
9720
9902
  },
9721
9903
  };
9722
9904
  const style = this.getTextStyles();
9723
- const { height: titleHeight } = this.getTextDimensions(this.title, style.title.font);
9905
+ let titleHeight = 0;
9724
9906
  if (this.title) {
9907
+ ({ height: titleHeight } = this.getFullTextDimensions(this.title, style.title.font));
9725
9908
  structure.title = {
9726
9909
  text: this.title,
9727
9910
  style: style.title,
9728
9911
  position: {
9729
- x: this.chartPadding,
9730
- y: this.chartPadding + titleHeight,
9912
+ x: CHART_PADDING,
9913
+ y: CHART_PADDING / 2 + titleHeight,
9731
9914
  },
9732
9915
  };
9733
9916
  }
9734
9917
  const baselineArrowSize = style.baselineArrow?.size ?? 0;
9735
- const { height: baselineHeight, width: baselineWidth } = this.getTextDimensions(this.baseline, style.baselineValue.font);
9736
- const { width: baselineDescrWidth } = this.getTextDimensions(this.baselineDescr, style.baselineDescr.font);
9918
+ let { height: baselineHeight, width: baselineWidth } = this.getTextDimensions(this.baseline, style.baselineValue.font);
9919
+ if (!this.baseline) {
9920
+ baselineHeight = this.getTextDimensions(this.baselineDescr, style.baselineDescr.font).height;
9921
+ }
9922
+ const baselineDescrWidth = style.baselineDescr.isSplit
9923
+ ? Math.max(...splitTextInTwoLines(this.baselineDescr).map((line) => this.getTextDimensions(line, style.baselineDescr.font).width))
9924
+ : this.getTextDimensions(this.baselineDescr, style.baselineDescr.font).width;
9737
9925
  structure.baseline = {
9738
9926
  text: this.baseline,
9739
9927
  style: style.baselineValue,
9740
9928
  position: {
9741
9929
  x: (this.width - baselineWidth - baselineDescrWidth + baselineArrowSize) / 2,
9742
9930
  y: this.keyValue
9743
- ? this.height - 2 * this.chartPadding
9744
- : this.height - (this.height - titleHeight - baselineHeight) / 2 - this.chartPadding,
9931
+ ? this.height * (1 - BOTTOM_PADDING_RATIO * (this.runtime.progressBar ? 1 : 2))
9932
+ : this.height - (this.height - titleHeight - baselineHeight) / 2 - CHART_PADDING,
9745
9933
  },
9746
9934
  };
9747
- if (style.baselineArrow) {
9935
+ if (style.baselineArrow && !this.runtime.progressBar) {
9748
9936
  structure.baselineArrow = {
9749
9937
  direction: this.baselineArrow,
9750
9938
  style: style.baselineArrow,
@@ -9755,23 +9943,68 @@ class ScorecardChartConfigBuilder {
9755
9943
  };
9756
9944
  }
9757
9945
  if (this.baselineDescr) {
9758
- structure.baselineDescr = {
9759
- text: this.baselineDescr,
9760
- style: style.baselineDescr,
9946
+ const position = {
9947
+ x: structure.baseline.position.x + baselineWidth,
9948
+ y: structure.baseline.position.y,
9949
+ };
9950
+ if (style.baselineDescr.isSplit) {
9951
+ const description = splitTextInTwoLines(this.baselineDescr);
9952
+ const measure = this.context.measureText(description[1]);
9953
+ const deltaY = measure.fontBoundingBoxAscent + measure.fontBoundingBoxDescent;
9954
+ structure.baselineDescr = [
9955
+ {
9956
+ text: description[0],
9957
+ style: style.baselineDescr,
9958
+ position: {
9959
+ x: position.x,
9960
+ y: position.y - deltaY,
9961
+ },
9962
+ },
9963
+ {
9964
+ text: description[1],
9965
+ style: style.baselineDescr,
9966
+ position,
9967
+ },
9968
+ ];
9969
+ }
9970
+ else {
9971
+ structure.baselineDescr = [
9972
+ {
9973
+ text: this.baselineDescr,
9974
+ style: style.baselineDescr,
9975
+ position,
9976
+ },
9977
+ ];
9978
+ }
9979
+ }
9980
+ let progressBarHeight = 0;
9981
+ if (this.runtime.progressBar) {
9982
+ progressBarHeight = this.height * 0.05;
9983
+ structure.progressBar = {
9761
9984
  position: {
9762
- x: structure.baseline.position.x + baselineWidth,
9763
- y: structure.baseline.position.y,
9985
+ x: 2 * CHART_PADDING,
9986
+ y: this.height * (1 - 2 * BOTTOM_PADDING_RATIO) - baselineHeight - progressBarHeight,
9987
+ },
9988
+ dimension: {
9989
+ height: progressBarHeight,
9990
+ width: this.width - 4 * CHART_PADDING,
9991
+ },
9992
+ value: this.runtime.progressBar.value,
9993
+ style: {
9994
+ color: this.runtime.progressBar.color,
9995
+ backgroundColor: this.secondaryFontColor,
9764
9996
  },
9765
9997
  };
9766
9998
  }
9767
- const { height: keyHeight, width: keyWidth } = this.getTextDimensions(this.keyValue, style.keyValue.font);
9999
+ const { width: keyWidth, height: keyHeight } = this.getFullTextDimensions(this.keyValue, style.keyValue.font);
9768
10000
  if (this.keyValue) {
9769
10001
  structure.key = {
9770
10002
  text: this.keyValue,
9771
10003
  style: style.keyValue,
9772
10004
  position: {
9773
10005
  x: (this.width - keyWidth) / 2,
9774
- y: (this.height - baselineHeight + titleHeight + keyHeight) / 2 - this.chartPadding,
10006
+ y: this.height * (0.5 - BOTTOM_PADDING_RATIO * 2) +
10007
+ (titleHeight + keyHeight / (this.baseline || this.baselineDescr ? 2 : 1.2)) / 2,
9775
10008
  },
9776
10009
  };
9777
10010
  }
@@ -9798,9 +10031,6 @@ class ScorecardChartConfigBuilder {
9798
10031
  get secondaryFontColor() {
9799
10032
  return relativeLuminance(this.backgroundColor) > 0.3 ? "#525252" : "#C8C8C8";
9800
10033
  }
9801
- get chartPadding() {
9802
- return this.width * CHART_PADDING_RATIO;
9803
- }
9804
10034
  getTextDimensions(text, font) {
9805
10035
  this.context.font = font;
9806
10036
  const measure = this.context.measureText(text);
@@ -9809,16 +10039,44 @@ class ScorecardChartConfigBuilder {
9809
10039
  height: measure.actualBoundingBoxAscent + measure.actualBoundingBoxDescent,
9810
10040
  };
9811
10041
  }
10042
+ getFullTextDimensions(text, font) {
10043
+ this.context.font = font;
10044
+ const measure = this.context.measureText(text);
10045
+ return {
10046
+ width: measure.width,
10047
+ height: measure.fontBoundingBoxAscent + measure.fontBoundingBoxDescent,
10048
+ };
10049
+ }
9812
10050
  getTextStyles() {
9813
10051
  // If the widest text overflows horizontally, scale it down, and apply the same scaling factors to all the other fonts.
9814
- const maxLineWidth = this.width * (1 - 2 * CHART_PADDING_RATIO);
9815
- const widestElement = this.getWidestElement();
9816
- const baseFontSize = widestElement.getElementMaxFontSize(this.getDrawableHeight(), this);
9817
- const fontSizeMatchingWidth = getFontSizeMatchingWidth(maxLineWidth, baseFontSize, (fontSize) => widestElement.getElementWidth(fontSize, this.context, this));
9818
- let scalingFactor = fontSizeMatchingWidth / baseFontSize;
10052
+ const maxLineWidth = this.width - 2 * CHART_PADDING;
10053
+ const drawableHeight = this.getDrawableHeight();
9819
10054
  // Fonts sizes in px
9820
- const keyFontSize = new KeyValueElement(this.runtime.keyValueStyle).getElementMaxFontSize(this.getDrawableHeight(), this) * scalingFactor;
9821
- const baselineFontSize = new BaselineElement(this.runtime.baselineStyle).getElementMaxFontSize(this.getDrawableHeight(), this) * scalingFactor;
10055
+ const keyValueElement = new KeyValueElement(this.runtime.keyValueStyle);
10056
+ const heightFont = keyValueElement.getElementMaxFontSize(drawableHeight, this);
10057
+ const widthFont = getFontSizeMatchingWidth(maxLineWidth, 600, (fontSize) => keyValueElement.getElementWidth(fontSize, this.context, this));
10058
+ const keyFontSize = Math.min(heightFont, widthFont);
10059
+ let baselineValueFontSize = Math.floor(keyFontSize * 0.5);
10060
+ this.context.font = getDefaultContextFont(baselineValueFontSize, this.runtime.baselineStyle?.bold, this.runtime.baselineStyle?.italic);
10061
+ const baselineText = this.baselineArrow !== "neutral" ? "A " + this.baseline : this.baseline;
10062
+ const baselineValueWidth = computeCachedTextWidth(this.context, baselineText);
10063
+ const remainingWidth = maxLineWidth - baselineValueWidth;
10064
+ let baselineDescrFontSize = getFontSizeMatchingWidth(remainingWidth, baselineValueFontSize, (fontSize) => computeTextWidth(this.context, this.baselineDescr, { fontSize }));
10065
+ let isBaselineSplit = false;
10066
+ if (baselineDescrFontSize < baselineValueFontSize / 2.5) {
10067
+ isBaselineSplit = true;
10068
+ baselineDescrFontSize = Math.floor(baselineValueFontSize / 2.5);
10069
+ for (const line of splitTextInTwoLines(this.baselineDescr)) {
10070
+ const lineWidth = getFontSizeMatchingWidth(remainingWidth, baselineValueFontSize, (fontSize) => {
10071
+ this.context.font = getDefaultContextFont(fontSize);
10072
+ return this.context.measureText(line).width;
10073
+ });
10074
+ baselineDescrFontSize = Math.min(baselineDescrFontSize, lineWidth);
10075
+ }
10076
+ }
10077
+ if (this.runtime.progressBar) {
10078
+ baselineValueFontSize /= 1.5;
10079
+ }
9822
10080
  return {
9823
10081
  title: {
9824
10082
  font: getDefaultContextFont(TITLE_FONT_SIZE),
@@ -9831,7 +10089,7 @@ class ScorecardChartConfigBuilder {
9831
10089
  underline: this.runtime.keyValueStyle?.underline,
9832
10090
  },
9833
10091
  baselineValue: {
9834
- font: getDefaultContextFont(baselineFontSize, this.runtime.baselineStyle?.bold, this.runtime.baselineStyle?.italic),
10092
+ font: getDefaultContextFont(baselineValueFontSize, this.runtime.baselineStyle?.bold, this.runtime.baselineStyle?.italic),
9835
10093
  strikethrough: this.runtime.baselineStyle?.strikethrough,
9836
10094
  underline: this.runtime.baselineStyle?.underline,
9837
10095
  color: this.runtime.baselineStyle?.textColor ||
@@ -9839,33 +10097,25 @@ class ScorecardChartConfigBuilder {
9839
10097
  this.secondaryFontColor,
9840
10098
  },
9841
10099
  baselineDescr: {
9842
- font: getDefaultContextFont(baselineFontSize * BASELINE_DESCR_FONT_RATIO),
10100
+ font: getDefaultContextFont(baselineDescrFontSize),
10101
+ isSplit: isBaselineSplit,
9843
10102
  color: this.secondaryFontColor,
9844
10103
  },
9845
- baselineArrow: this.baselineArrow === "neutral"
10104
+ baselineArrow: this.baselineArrow === "neutral" || this.runtime.progressBar
9846
10105
  ? undefined
9847
10106
  : {
9848
- size: this.keyValue ? 0.8 * baselineFontSize : 0,
10107
+ size: this.keyValue ? 0.8 * baselineValueFontSize : 0,
9849
10108
  color: this.runtime.baselineColor || this.secondaryFontColor,
9850
10109
  },
9851
10110
  };
9852
10111
  }
9853
10112
  /** Get the height of the chart minus all the vertical paddings */
9854
10113
  getDrawableHeight() {
9855
- const verticalPadding = 2 * this.chartPadding;
9856
- let availableHeight = this.height - verticalPadding;
10114
+ const verticalPadding = CHART_PADDING + this.height * BOTTOM_PADDING_RATIO;
10115
+ let availableHeight = this.height - 2 * verticalPadding;
9857
10116
  availableHeight -= this.title ? TITLE_FONT_SIZE * LINE_HEIGHT : 0;
9858
10117
  return availableHeight;
9859
10118
  }
9860
- /** Return the element with he widest text in the chart */
9861
- getWidestElement() {
9862
- const baseline = new BaselineElement(this.runtime.baselineStyle);
9863
- const keyValue = new KeyValueElement(this.runtime.keyValueStyle);
9864
- return baseline.getElementWidth(BASELINE_BOX_HEIGHT_RATIO, this.context, this) >
9865
- keyValue.getElementWidth(KEY_BOX_HEIGHT_RATIO, this.context, this)
9866
- ? baseline
9867
- : keyValue;
9868
- }
9869
10119
  }
9870
10120
  class ScorecardScalableElement {
9871
10121
  style;
@@ -9874,29 +10124,7 @@ class ScorecardScalableElement {
9874
10124
  }
9875
10125
  measureTextWidth(ctx, text, fontSize) {
9876
10126
  ctx.font = getDefaultContextFont(fontSize, this.style.bold, this.style.italic);
9877
- return ctx.measureText(text).width;
9878
- }
9879
- }
9880
- class BaselineElement extends ScorecardScalableElement {
9881
- getElementWidth(fontSize, ctx, chart) {
9882
- if (!chart.runtime) {
9883
- return 0;
9884
- }
9885
- const baselineStr = chart.baseline;
9886
- // Put mock text to simulate the width of the up/down arrow
9887
- const largeText = chart.baselineArrow !== "neutral" ? "A " + baselineStr : baselineStr;
9888
- let textWidth = this.measureTextWidth(ctx, largeText, fontSize);
9889
- // Baseline descr font size should be smaller than baseline font size
9890
- textWidth += this.measureTextWidth(ctx, chart.baselineDescr, fontSize * BASELINE_DESCR_FONT_RATIO);
9891
- return textWidth;
9892
- }
9893
- getElementMaxFontSize(availableHeight, chart) {
9894
- if (!chart.runtime) {
9895
- return 0;
9896
- }
9897
- const haveBaseline = chart.baseline !== "" || chart.baselineDescr;
9898
- const maxHeight = haveBaseline ? BASELINE_BOX_HEIGHT_RATIO * availableHeight : 0;
9899
- return maxHeight / LINE_HEIGHT;
10127
+ return computeCachedTextWidth(ctx, text);
9900
10128
  }
9901
10129
  }
9902
10130
  class KeyValueElement extends ScorecardScalableElement {
@@ -9957,11 +10185,11 @@ autoCompleteProviders.add("dataValidation", {
9957
10185
  }
9958
10186
  else {
9959
10187
  const range = this.getters.getRangeFromSheetXC(position.sheetId, rule.criterion.values[0]);
9960
- values = this.getters
10188
+ values = Array.from(new Set(this.getters
9961
10189
  .getRangeValues(range)
9962
10190
  .filter(isNotNull)
9963
10191
  .map((value) => value.toString())
9964
- .filter((val) => val !== "");
10192
+ .filter((val) => val !== "")));
9965
10193
  }
9966
10194
  return values.map((value) => ({ text: value }));
9967
10195
  },
@@ -10826,33 +11054,6 @@ var array = /*#__PURE__*/Object.freeze({
10826
11054
  // -----------------------------------------------------------------------------
10827
11055
  // FORMAT.LARGE.NUMBER
10828
11056
  // -----------------------------------------------------------------------------
10829
- function formatLargeNumber(arg, unit, locale) {
10830
- const value = Math.abs(toNumber(arg?.value, locale));
10831
- const format = arg?.format;
10832
- if (unit !== undefined) {
10833
- const postFix = unit?.value;
10834
- switch (postFix) {
10835
- case "k":
10836
- return createLargeNumberFormat(format, 1e3, "k");
10837
- case "m":
10838
- return createLargeNumberFormat(format, 1e6, "m");
10839
- case "b":
10840
- return createLargeNumberFormat(format, 1e9, "b");
10841
- default:
10842
- throw new EvaluationError(_t("The formatting unit should be 'k', 'm' or 'b'."));
10843
- }
10844
- }
10845
- if (value < 1e5) {
10846
- return createLargeNumberFormat(format, 0, "");
10847
- }
10848
- else if (value < 1e8) {
10849
- return createLargeNumberFormat(format, 1e3, "k");
10850
- }
10851
- else if (value < 1e11) {
10852
- return createLargeNumberFormat(format, 1e6, "m");
10853
- }
10854
- return createLargeNumberFormat(format, 1e9, "b");
10855
- }
10856
11057
  const FORMAT_LARGE_NUMBER = {
10857
11058
  description: _t("Apply a large number format"),
10858
11059
  args: [
@@ -11014,7 +11215,7 @@ const ATAN2 = {
11014
11215
  compute: function (x, y) {
11015
11216
  const _x = toNumber(x, this.locale);
11016
11217
  const _y = toNumber(y, this.locale);
11017
- assert(() => _x !== 0 || _y !== 0, _t("Function [[FUNCTION_NAME]] caused a divide by zero error."));
11218
+ assert(() => _x !== 0 || _y !== 0, _t("Function [[FUNCTION_NAME]] caused a divide by zero error."), CellErrorType.DivisionByZero);
11018
11219
  return Math.atan2(_y, _x);
11019
11220
  },
11020
11221
  isExported: true,
@@ -11144,7 +11345,7 @@ const COT = {
11144
11345
  returns: ["NUMBER"],
11145
11346
  compute: function (angle) {
11146
11347
  const _angle = toNumber(angle, this.locale);
11147
- assert(() => _angle !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
11348
+ assertNotZero(_angle);
11148
11349
  return 1 / Math.tan(_angle);
11149
11350
  },
11150
11351
  isExported: true,
@@ -11158,7 +11359,7 @@ const COTH = {
11158
11359
  returns: ["NUMBER"],
11159
11360
  compute: function (value) {
11160
11361
  const _value = toNumber(value, this.locale);
11161
- assert(() => _value !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
11362
+ assertNotZero(_value);
11162
11363
  return 1 / Math.tanh(_value);
11163
11364
  },
11164
11365
  isExported: true,
@@ -11286,7 +11487,7 @@ const CSC = {
11286
11487
  returns: ["NUMBER"],
11287
11488
  compute: function (angle) {
11288
11489
  const _angle = toNumber(angle, this.locale);
11289
- assert(() => _angle !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
11490
+ assertNotZero(_angle);
11290
11491
  return 1 / Math.sin(_angle);
11291
11492
  },
11292
11493
  isExported: true,
@@ -11300,7 +11501,7 @@ const CSCH = {
11300
11501
  returns: ["NUMBER"],
11301
11502
  compute: function (value) {
11302
11503
  const _value = toNumber(value, this.locale);
11303
- assert(() => _value !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
11504
+ assertNotZero(_value);
11304
11505
  return 1 / Math.sinh(_value);
11305
11506
  },
11306
11507
  isExported: true,
@@ -11499,7 +11700,7 @@ const LN = {
11499
11700
  // MOD
11500
11701
  // -----------------------------------------------------------------------------
11501
11702
  function mod(dividend, divisor) {
11502
- assert(() => divisor !== 0, _t("The divisor must be different from 0."));
11703
+ assert(() => divisor !== 0, _t("The divisor must be different from 0."), CellErrorType.DivisionByZero);
11503
11704
  const modulus = dividend % divisor;
11504
11705
  // -42 % 10 = -2 but we want 8, so need the code below
11505
11706
  if ((modulus > 0 && divisor < 0) || (modulus < 0 && divisor > 0)) {
@@ -12068,7 +12269,7 @@ function average(values, locale) {
12068
12269
  count += 1;
12069
12270
  return acc + a;
12070
12271
  }, 0, locale);
12071
- assert(() => count !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
12272
+ assertNotZero(count);
12072
12273
  return sum / count;
12073
12274
  }
12074
12275
  function countNumbers(values, locale) {
@@ -12135,7 +12336,7 @@ function filterAndFlatData(dataY, dataX) {
12135
12336
  function covariance(dataY, dataX, isSample) {
12136
12337
  const { flatDataX, flatDataY } = filterAndFlatData(dataY, dataX);
12137
12338
  const count = flatDataY.length;
12138
- assert(() => count !== 0 && (!isSample || count !== 1), _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
12339
+ assert(() => count !== 0 && (!isSample || count !== 1), _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."), CellErrorType.DivisionByZero);
12139
12340
  let sumY = 0;
12140
12341
  let sumX = 0;
12141
12342
  for (let i = 0; i < count; i++) {
@@ -12158,7 +12359,7 @@ function variance(args, isSample, textAs0, locale) {
12158
12359
  count += 1;
12159
12360
  return acc + a;
12160
12361
  }, 0, locale);
12161
- assert(() => count !== 0 && (!isSample || count !== 1), _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
12362
+ assert(() => count !== 0 && (!isSample || count !== 1), _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."), CellErrorType.DivisionByZero);
12162
12363
  const average = sum / count;
12163
12364
  return (reduceFunction(args, (acc, a) => acc + Math.pow(a - average, 2), 0, locale) /
12164
12365
  (count - (isSample ? 1 : 0)));
@@ -12355,7 +12556,7 @@ const AVEDEV = {
12355
12556
  count += 1;
12356
12557
  return acc + a;
12357
12558
  }, 0, this.locale);
12358
- assert(() => count !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
12559
+ assertNotZero(count);
12359
12560
  const average = sum / count;
12360
12561
  return reduceNumbers(values, (acc, a) => acc + Math.abs(average - a), 0, this.locale) / count;
12361
12562
  },
@@ -12427,7 +12628,7 @@ const AVERAGE_WEIGHTED = {
12427
12628
  }
12428
12629
  }
12429
12630
  }
12430
- assert(() => count !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
12631
+ assertNotZero(count);
12431
12632
  return { value: sum / count, format: inferFormat(args[0]) };
12432
12633
  },
12433
12634
  };
@@ -12447,7 +12648,7 @@ const AVERAGEA = {
12447
12648
  count += 1;
12448
12649
  return acc + a;
12449
12650
  }, 0, this.locale);
12450
- assert(() => count !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
12651
+ assertNotZero(count);
12451
12652
  return {
12452
12653
  value: sum / count,
12453
12654
  format: inferFormat(args[0]),
@@ -12477,7 +12678,7 @@ const AVERAGEIF = {
12477
12678
  sum += value;
12478
12679
  }
12479
12680
  }, this.locale);
12480
- assert(() => count !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
12681
+ assertNotZero(count);
12481
12682
  return sum / count;
12482
12683
  },
12483
12684
  isExported: true,
@@ -12506,7 +12707,7 @@ const AVERAGEIFS = {
12506
12707
  sum += value;
12507
12708
  }
12508
12709
  }, this.locale);
12509
- assert(() => count !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
12710
+ assertNotZero(count);
12510
12711
  return sum / count;
12511
12712
  },
12512
12713
  isExported: true,
@@ -17931,7 +18132,7 @@ const DIVIDE = {
17931
18132
  returns: ["NUMBER"],
17932
18133
  compute: function (dividend, divisor) {
17933
18134
  const _divisor = toNumber(divisor, this.locale);
17934
- assert(() => _divisor !== 0, _t("The divisor must be different from zero."));
18135
+ assert(() => _divisor !== 0, _t("The divisor must be different from zero."), CellErrorType.DivisionByZero);
17935
18136
  return {
17936
18137
  value: toNumber(dividend, this.locale) / _divisor,
17937
18138
  format: dividend?.format || divisor?.format,
@@ -19408,10 +19609,10 @@ function aggregateDataForLabels(labels, datasets) {
19408
19609
  }
19409
19610
  }
19410
19611
  return {
19411
- labels: Object.keys(labelMap),
19612
+ labels: Array.from(labelSet),
19412
19613
  dataSetsValues: datasets.map((dataset, indexOfDataset) => ({
19413
19614
  ...dataset,
19414
- data: Object.values(labelMap).map((dataOfLabel) => dataOfLabel[indexOfDataset]),
19615
+ data: Array.from(labelSet).map((label) => labelMap[label][indexOfDataset]),
19415
19616
  })),
19416
19617
  };
19417
19618
  }
@@ -19430,8 +19631,8 @@ function truncateLabel(label) {
19430
19631
  function getDefaultChartJsRuntime(chart, labels, fontColor, { format, locale }) {
19431
19632
  const options = {
19432
19633
  // https://www.chartjs.org/docs/latest/general/responsive.html
19433
- responsive: true,
19434
- maintainAspectRatio: false,
19634
+ responsive: true, // will resize when its container is resized
19635
+ maintainAspectRatio: false, // doesn't maintain the aspect ration (width/height =2 by default) so the user has the choice of the exact layout
19435
19636
  layout: {
19436
19637
  padding: { left: 20, right: 20, top: chart.title ? 10 : 25, bottom: 10 },
19437
19638
  },
@@ -19477,7 +19678,7 @@ function getDefaultChartJsRuntime(chart, labels, fontColor, { format, locale })
19477
19678
  labels: labels.map(truncateLabel),
19478
19679
  datasets: [],
19479
19680
  },
19480
- platform: undefined,
19681
+ platform: undefined, // This key is optional and will be set by chart.js
19481
19682
  plugins: [],
19482
19683
  };
19483
19684
  }
@@ -19754,7 +19955,7 @@ function getBarConfiguration(chart, labels, localeFormat) {
19754
19955
  },
19755
19956
  y: {
19756
19957
  position: chart.verticalAxisPosition,
19757
- beginAtZero: true,
19958
+ beginAtZero: true, // the origin of the y axis is always zero
19758
19959
  ticks: {
19759
19960
  color: fontColor,
19760
19961
  callback: (value) => {
@@ -19805,6 +20006,204 @@ function createBarChartRuntime(chart, getters) {
19805
20006
  return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
19806
20007
  }
19807
20008
 
20009
+ class ComboChart extends AbstractChart {
20010
+ useBothYAxis;
20011
+ dataSets;
20012
+ labelRange;
20013
+ background;
20014
+ verticalAxisPosition;
20015
+ legendPosition;
20016
+ aggregated;
20017
+ dataSetsHaveTitle;
20018
+ type = "combo";
20019
+ constructor(definition, sheetId, getters) {
20020
+ super(definition, sheetId, getters);
20021
+ this.dataSets = createDataSets(getters, definition.dataSets, sheetId, definition.dataSetsHaveTitle);
20022
+ this.labelRange = createRange(getters, sheetId, definition.labelRange);
20023
+ this.background = definition.background;
20024
+ this.verticalAxisPosition = definition.verticalAxisPosition;
20025
+ this.legendPosition = definition.legendPosition;
20026
+ this.aggregated = definition.aggregated;
20027
+ this.dataSetsHaveTitle = definition.dataSetsHaveTitle;
20028
+ this.useBothYAxis = definition.useBothYAxis;
20029
+ }
20030
+ static transformDefinition(definition, executed) {
20031
+ return transformChartDefinitionWithDataSetsWithZone(definition, executed);
20032
+ }
20033
+ static validateChartDefinition(validator, definition) {
20034
+ return validator.checkValidations(definition, checkDataset, checkLabelRange);
20035
+ }
20036
+ getContextCreation() {
20037
+ return {
20038
+ background: this.background,
20039
+ title: this.title,
20040
+ range: this.dataSets.map((ds) => this.getters.getRangeString(ds.dataRange, this.sheetId)),
20041
+ auxiliaryRange: this.labelRange
20042
+ ? this.getters.getRangeString(this.labelRange, this.sheetId)
20043
+ : undefined,
20044
+ aggregated: this.aggregated,
20045
+ };
20046
+ }
20047
+ getDefinition() {
20048
+ return this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange);
20049
+ }
20050
+ getDefinitionWithSpecificDataSets(dataSets, labelRange, targetSheetId) {
20051
+ return {
20052
+ type: "combo",
20053
+ dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false,
20054
+ background: this.background,
20055
+ dataSets: dataSets.map((ds) => this.getters.getRangeString(ds.dataRange, targetSheetId || this.sheetId)),
20056
+ legendPosition: this.legendPosition,
20057
+ verticalAxisPosition: this.verticalAxisPosition,
20058
+ labelRange: labelRange
20059
+ ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId)
20060
+ : undefined,
20061
+ title: this.title,
20062
+ aggregated: this.aggregated,
20063
+ useBothYAxis: this.useBothYAxis,
20064
+ };
20065
+ }
20066
+ getDefinitionForExcel() {
20067
+ // Excel does not support aggregating labels
20068
+ if (this.aggregated) {
20069
+ return undefined;
20070
+ }
20071
+ const dataSets = this.dataSets
20072
+ .map((ds) => toExcelDataset(this.getters, ds))
20073
+ .filter((ds) => ds.range !== "" && ds.range !== CellErrorType.InvalidReference);
20074
+ const labelRange = toExcelLabelRange(this.getters, this.labelRange, shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], this.dataSetsHaveTitle));
20075
+ return {
20076
+ ...this.getDefinition(),
20077
+ backgroundColor: toXlsxHexColor(this.background || BACKGROUND_CHART_COLOR),
20078
+ fontColor: toXlsxHexColor(chartFontColor(this.background)),
20079
+ dataSets,
20080
+ labelRange,
20081
+ };
20082
+ }
20083
+ updateRanges(applyChange) {
20084
+ const { dataSets, labelRange, isStale } = updateChartRangesWithDataSets(this.getters, applyChange, this.dataSets, this.labelRange);
20085
+ if (!isStale) {
20086
+ return this;
20087
+ }
20088
+ const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange);
20089
+ return new ComboChart(definition, this.sheetId, this.getters);
20090
+ }
20091
+ static getDefinitionFromContextCreation(context) {
20092
+ return {
20093
+ background: context.background,
20094
+ dataSets: context.range ? context.range : [],
20095
+ dataSetsHaveTitle: false,
20096
+ aggregated: context.aggregated,
20097
+ legendPosition: "top",
20098
+ title: context.title || "",
20099
+ verticalAxisPosition: "left",
20100
+ labelRange: context.auxiliaryRange || undefined,
20101
+ type: "combo",
20102
+ useBothYAxis: false,
20103
+ };
20104
+ }
20105
+ copyForSheetId(sheetId) {
20106
+ const dataSets = copyDataSetsWithNewSheetId(this.sheetId, sheetId, this.dataSets);
20107
+ const labelRange = copyLabelRangeWithNewSheetId(this.sheetId, sheetId, this.labelRange);
20108
+ const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange, sheetId);
20109
+ return new ComboChart(definition, sheetId, this.getters);
20110
+ }
20111
+ copyInSheetId(sheetId) {
20112
+ const definition = this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange, sheetId);
20113
+ return new ComboChart(definition, sheetId, this.getters);
20114
+ }
20115
+ }
20116
+ function createComboChartRuntime(chart, getters) {
20117
+ const dataSetFormat = getChartDatasetFormat(getters, chart.dataSets);
20118
+ const locale = getters.getLocale();
20119
+ const labelValues = getChartLabelValues(getters, chart.dataSets, chart.labelRange);
20120
+ let labels = labelValues.formattedValues;
20121
+ let dataSetsValues = getChartDatasetValues(getters, chart.dataSets);
20122
+ if (chart.dataSetsHaveTitle &&
20123
+ dataSetsValues[0] &&
20124
+ labels.length > dataSetsValues[0].data.length) {
20125
+ labels.shift();
20126
+ }
20127
+ ({ labels, dataSetsValues } = filterEmptyDataPoints(labels, dataSetsValues));
20128
+ if (chart.aggregated) {
20129
+ ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));
20130
+ }
20131
+ const localeFormat = { format: dataSetFormat, locale };
20132
+ const fontColor = chartFontColor(chart.background);
20133
+ const config = getDefaultChartJsRuntime(chart, labels, fontColor, localeFormat);
20134
+ const legend = {
20135
+ labels: { color: fontColor },
20136
+ };
20137
+ if ((!chart.labelRange && chart.dataSets.length === 1) || chart.legendPosition === "none") {
20138
+ legend.display = false;
20139
+ }
20140
+ else {
20141
+ legend.position = chart.legendPosition;
20142
+ }
20143
+ config.options.plugins.legend = { ...config.options.plugins?.legend, ...legend };
20144
+ config.options.layout = {
20145
+ padding: { left: 20, right: 20, top: chart.title ? 10 : 25, bottom: 10 },
20146
+ };
20147
+ config.options.scales = {
20148
+ x: {
20149
+ ticks: {
20150
+ padding: 5,
20151
+ color: fontColor,
20152
+ },
20153
+ },
20154
+ };
20155
+ const verticalAxis = {
20156
+ beginAtZero: true, // the origin of the y axis is always zero
20157
+ ticks: {
20158
+ color: fontColor,
20159
+ callback: (value) => {
20160
+ value = Number(value);
20161
+ if (isNaN(value))
20162
+ return value;
20163
+ const { locale, format } = localeFormat;
20164
+ return formatValue(value, {
20165
+ locale,
20166
+ format: !format && Math.abs(value) >= 1000 ? "#,##" : format,
20167
+ });
20168
+ },
20169
+ },
20170
+ };
20171
+ if (chart.useBothYAxis) {
20172
+ config.options.scales.y = {
20173
+ ...verticalAxis,
20174
+ position: "left",
20175
+ };
20176
+ config.options.scales.y1 = {
20177
+ ...verticalAxis,
20178
+ position: "right",
20179
+ grid: {
20180
+ display: false,
20181
+ },
20182
+ };
20183
+ }
20184
+ else {
20185
+ config.options.scales.y = {
20186
+ ...verticalAxis,
20187
+ position: chart.verticalAxisPosition,
20188
+ };
20189
+ }
20190
+ const colors = new ChartColors();
20191
+ for (let [index, { label, data }] of dataSetsValues.entries()) {
20192
+ const color = colors.next();
20193
+ const dataset = {
20194
+ label,
20195
+ data,
20196
+ borderColor: color,
20197
+ backgroundColor: color,
20198
+ yAxisID: index > 0 && chart.useBothYAxis ? "y1" : "y",
20199
+ type: index === 0 ? "bar" : "line",
20200
+ order: -index,
20201
+ };
20202
+ config.data.datasets.push(dataset);
20203
+ }
20204
+ return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
20205
+ }
20206
+
19808
20207
  function isDataRangeValid(definition) {
19809
20208
  return definition.dataRange && !rangeReference.test(definition.dataRange)
19810
20209
  ? "InvalidGaugeDataRange" /* CommandResult.InvalidGaugeDataRange */
@@ -20143,7 +20542,7 @@ function getBestTimeUnitForScale(labels, format, locale) {
20143
20542
  return undefined;
20144
20543
  }
20145
20544
  const labelsTimestamps = labelDates.map((date) => date.getTime());
20146
- const period = Math.max(...labelsTimestamps) - Math.min(...labelsTimestamps);
20545
+ const period = largeMax(labelsTimestamps) - largeMin(labelsTimestamps);
20147
20546
  const minUnit = getFormatMinDisplayUnit(format);
20148
20547
  if (UNIT_LENGTH.second >= UNIT_LENGTH[minUnit] && Milliseconds.inSeconds(period) < 180) {
20149
20548
  return "second";
@@ -20275,7 +20674,7 @@ function getLineOrScatterConfiguration(chart, labels, localeFormat) {
20275
20674
  },
20276
20675
  y: {
20277
20676
  position: chart.verticalAxisPosition,
20278
- beginAtZero: true,
20677
+ beginAtZero: true, // the origin of the y axis is always zero
20279
20678
  ticks: {
20280
20679
  color: fontColor,
20281
20680
  callback: (value) => {
@@ -20361,7 +20760,7 @@ function createLineOrScatterChartRuntime(chart, getters) {
20361
20760
  const dataset = {
20362
20761
  label,
20363
20762
  data,
20364
- tension: 0,
20763
+ tension: 0, // 0 -> render straight lines, which is much faster
20365
20764
  borderColor: color,
20366
20765
  backgroundColor,
20367
20766
  pointBackgroundColor: color,
@@ -20583,7 +20982,7 @@ class PieChart extends AbstractChart {
20583
20982
  ...this.getDefinition(),
20584
20983
  backgroundColor: toXlsxHexColor(this.background || BACKGROUND_CHART_COLOR),
20585
20984
  fontColor: toXlsxHexColor(chartFontColor(this.background)),
20586
- verticalAxisPosition: "left",
20985
+ verticalAxisPosition: "left", //TODO ExcelChartDefinition should be adapted, but can be done later
20587
20986
  dataSets,
20588
20987
  labelRange,
20589
20988
  };
@@ -20631,7 +21030,7 @@ function getPieConfiguration(chart, labels, localeFormat) {
20631
21030
  }
20632
21031
  function getPieColors(colors, dataSetsValues) {
20633
21032
  const pieColors = [];
20634
- const maxLength = Math.max(...dataSetsValues.map((ds) => ds.data.length));
21033
+ const maxLength = largeMax(dataSetsValues.map((ds) => ds.data.length));
20635
21034
  for (let i = 0; i <= maxLength; i++) {
20636
21035
  pieColors.push(colors.next());
20637
21036
  }
@@ -20778,7 +21177,21 @@ class ScatterChart extends AbstractChart {
20778
21177
  return new ScatterChart(definition, this.sheetId, this.getters);
20779
21178
  }
20780
21179
  getDefinitionForExcel() {
20781
- return undefined; // TODO
21180
+ // Excel does not support aggregating labels
21181
+ if (this.aggregated) {
21182
+ return undefined;
21183
+ }
21184
+ const dataSets = this.dataSets
21185
+ .map((ds) => toExcelDataset(this.getters, ds))
21186
+ .filter((ds) => ds.range !== "");
21187
+ const labelRange = toExcelLabelRange(this.getters, this.labelRange, shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], this.dataSetsHaveTitle));
21188
+ return {
21189
+ ...this.getDefinition(),
21190
+ backgroundColor: toXlsxHexColor(this.background || BACKGROUND_CHART_COLOR),
21191
+ fontColor: toXlsxHexColor(chartFontColor(this.background)),
21192
+ dataSets,
21193
+ labelRange,
21194
+ };
20782
21195
  }
20783
21196
  copyForSheetId(sheetId) {
20784
21197
  const dataSets = copyDataSetsWithNewSheetId(this.sheetId, sheetId, this.dataSets);
@@ -20800,7 +21213,7 @@ function createScatterChartRuntime(chart, getters) {
20800
21213
  configOptions.elements = {
20801
21214
  point: {
20802
21215
  radius: 3,
20803
- hoverRadius: 3,
21216
+ hoverRadius: 3, // chartJS seems bugged, the point starts with the "hoverRadius" value
20804
21217
  hitRadius: 8,
20805
21218
  },
20806
21219
  };
@@ -20840,6 +21253,16 @@ chartRegistry.add("bar", {
20840
21253
  name: _t("Bar"),
20841
21254
  sequence: 10,
20842
21255
  });
21256
+ chartRegistry.add("combo", {
21257
+ match: (type) => type === "combo",
21258
+ createChart: (definition, sheetId, getters) => new ComboChart(definition, sheetId, getters),
21259
+ getChartRuntime: createComboChartRuntime,
21260
+ validateChartDefinition: (validator, definition) => ComboChart.validateChartDefinition(validator, definition),
21261
+ transformDefinition: (definition, executed) => ComboChart.transformDefinition(definition, executed),
21262
+ getChartDefinitionFromContextCreation: (context) => ComboChart.getDefinitionFromContextCreation(context),
21263
+ name: _t("Combo"),
21264
+ sequence: 15,
21265
+ });
20843
21266
  chartRegistry.add("line", {
20844
21267
  match: (type) => type === "line",
20845
21268
  createChart: (definition, sheetId, getters) => new LineChart(definition, sheetId, getters),
@@ -20893,6 +21316,7 @@ chartRegistry.add("scatter", {
20893
21316
  const chartComponentRegistry = new Registry();
20894
21317
  chartComponentRegistry.add("line", ChartJsComponent);
20895
21318
  chartComponentRegistry.add("bar", ChartJsComponent);
21319
+ chartComponentRegistry.add("combo", ChartJsComponent);
20896
21320
  chartComponentRegistry.add("pie", ChartJsComponent);
20897
21321
  chartComponentRegistry.add("gauge", GaugeChartComponent);
20898
21322
  chartComponentRegistry.add("scatter", ChartJsComponent);
@@ -23151,7 +23575,7 @@ const lightTemplateWithHeader = (colorSet) => ({
23151
23575
  style: { fillColor: colorSet.highlight, textColor: "#FFFFFF" },
23152
23576
  border: { bottom: { color: colorSet.highlight, style: "thin" } },
23153
23577
  },
23154
- totalRow: { border: { top: { color: colorSet.highlight, style: "medium" } } },
23578
+ totalRow: { border: { top: { color: colorSet.highlight, style: "medium" } } }, // @compatibility: should be double line
23155
23579
  firstRowStripe: { border: { bottom: { color: colorSet.highlight, style: "thin" } } },
23156
23580
  secondRowStripe: { border: { bottom: { color: colorSet.highlight, style: "thin" } } },
23157
23581
  });
@@ -23169,7 +23593,7 @@ const lightTemplateAllBorders = (colorSet) => ({
23169
23593
  },
23170
23594
  },
23171
23595
  headerRow: { border: { bottom: { color: colorSet.highlight, style: "medium" } } },
23172
- totalRow: { border: { top: { color: colorSet.highlight, style: "medium" } } },
23596
+ totalRow: { border: { top: { color: colorSet.highlight, style: "medium" } } }, // @compatibility: should be double line
23173
23597
  firstRowStripe: { style: { fillColor: colorSet.light } },
23174
23598
  firstColumnStripe: { style: { fillColor: colorSet.light } },
23175
23599
  });
@@ -23188,7 +23612,7 @@ const mediumTemplateBandedBorders = (colorSet) => ({
23188
23612
  headerRow: {
23189
23613
  style: { fillColor: colorSet.highlight, textColor: "#FFFFFF" },
23190
23614
  },
23191
- totalRow: { border: { top: { color: colorSet.highlight, style: "medium" } } },
23615
+ totalRow: { border: { top: { color: colorSet.highlight, style: "medium" } } }, // @compatibility: should be double line
23192
23616
  firstRowStripe: { style: { fillColor: colorSet.light } },
23193
23617
  firstColumnStripe: { style: { fillColor: colorSet.light } },
23194
23618
  });
@@ -23224,7 +23648,7 @@ const mediumTemplateMinimalBorders = (colorSet) => ({
23224
23648
  bottom: { color: "#000000", style: "medium" },
23225
23649
  },
23226
23650
  },
23227
- totalRow: { border: { top: { color: "#000000", style: "medium" } } },
23651
+ totalRow: { border: { top: { color: "#000000", style: "medium" } } }, // @compatibility: should be double line
23228
23652
  headerRow: {
23229
23653
  style: { fillColor: colorSet.highlight, textColor: "#FFFFFF" },
23230
23654
  border: { bottom: { color: "#000000", style: "medium" } },
@@ -23248,7 +23672,7 @@ const mediumTemplateAllBorders = (colorSet) => ({
23248
23672
  },
23249
23673
  style: { fillColor: colorSet.light },
23250
23674
  },
23251
- totalRow: { border: { top: { color: colorSet.highlight, style: "medium" } } },
23675
+ totalRow: { border: { top: { color: colorSet.highlight, style: "medium" } } }, // @compatibility: should be double line
23252
23676
  firstRowStripe: { style: { fillColor: colorSet.medium } },
23253
23677
  firstColumnStripe: { style: { fillColor: colorSet.medium } },
23254
23678
  });
@@ -23279,7 +23703,7 @@ const darkTemplateNoBorders = (colorSet) => ({
23279
23703
  category: "dark",
23280
23704
  colorName: colorSet.name,
23281
23705
  wholeTable: { style: { fillColor: colorSet.light } },
23282
- totalRow: { border: { top: { color: "#000000", style: "medium" } } },
23706
+ totalRow: { border: { top: { color: "#000000", style: "medium" } } }, // @compatibility: should be double line
23283
23707
  headerRow: { style: { fillColor: colorSet.highlight, textColor: "#FFFFFF" } },
23284
23708
  firstRowStripe: { style: { fillColor: colorSet.medium } },
23285
23709
  firstColumnStripe: { style: { fillColor: colorSet.medium } },
@@ -23444,8 +23868,8 @@ const DELETE_CONTENT_ROWS_NAME = (env) => {
23444
23868
  let last;
23445
23869
  const activesRows = env.model.getters.getActiveRows();
23446
23870
  if (activesRows.size !== 0) {
23447
- first = Math.min(...activesRows);
23448
- last = Math.max(...activesRows);
23871
+ first = largeMin([...activesRows]);
23872
+ last = largeMax([...activesRows]);
23449
23873
  }
23450
23874
  else {
23451
23875
  const zone = env.model.getters.getSelectedZones()[0];
@@ -23473,8 +23897,8 @@ const DELETE_CONTENT_COLUMNS_NAME = (env) => {
23473
23897
  let last;
23474
23898
  const activeCols = env.model.getters.getActiveCols();
23475
23899
  if (activeCols.size !== 0) {
23476
- first = Math.min(...activeCols);
23477
- last = Math.max(...activeCols);
23900
+ first = largeMin([...activeCols]);
23901
+ last = largeMax([...activeCols]);
23478
23902
  }
23479
23903
  else {
23480
23904
  const zone = env.model.getters.getSelectedZones()[0];
@@ -23502,8 +23926,8 @@ const REMOVE_ROWS_NAME = (env) => {
23502
23926
  let last;
23503
23927
  const activesRows = env.model.getters.getActiveRows();
23504
23928
  if (activesRows.size !== 0) {
23505
- first = Math.min(...activesRows);
23506
- last = Math.max(...activesRows);
23929
+ first = largeMin([...activesRows]);
23930
+ last = largeMax([...activesRows]);
23507
23931
  }
23508
23932
  else {
23509
23933
  const zone = env.model.getters.getSelectedZones()[0];
@@ -23544,8 +23968,8 @@ const REMOVE_COLUMNS_NAME = (env) => {
23544
23968
  let last;
23545
23969
  const activeCols = env.model.getters.getActiveCols();
23546
23970
  if (activeCols.size !== 0) {
23547
- first = Math.min(...activeCols);
23548
- last = Math.max(...activeCols);
23971
+ first = largeMin([...activeCols]);
23972
+ last = largeMax([...activeCols]);
23549
23973
  }
23550
23974
  else {
23551
23975
  const zone = env.model.getters.getSelectedZones()[0];
@@ -23586,7 +24010,7 @@ const INSERT_ROWS_BEFORE_ACTION = (env) => {
23586
24010
  let row;
23587
24011
  let quantity;
23588
24012
  if (activeRows.size) {
23589
- row = Math.min(...activeRows);
24013
+ row = largeMin([...activeRows]);
23590
24014
  quantity = activeRows.size;
23591
24015
  }
23592
24016
  else {
@@ -23607,7 +24031,7 @@ const INSERT_ROWS_AFTER_ACTION = (env) => {
23607
24031
  let row;
23608
24032
  let quantity;
23609
24033
  if (activeRows.size) {
23610
- row = Math.max(...activeRows);
24034
+ row = largeMax([...activeRows]);
23611
24035
  quantity = activeRows.size;
23612
24036
  }
23613
24037
  else {
@@ -23628,7 +24052,7 @@ const INSERT_COLUMNS_BEFORE_ACTION = (env) => {
23628
24052
  let column;
23629
24053
  let quantity;
23630
24054
  if (activeCols.size) {
23631
- column = Math.min(...activeCols);
24055
+ column = largeMin([...activeCols]);
23632
24056
  quantity = activeCols.size;
23633
24057
  }
23634
24058
  else {
@@ -23649,7 +24073,7 @@ const INSERT_COLUMNS_AFTER_ACTION = (env) => {
23649
24073
  let column;
23650
24074
  let quantity;
23651
24075
  if (activeCols.size) {
23652
- column = Math.max(...activeCols);
24076
+ column = largeMax([...activeCols]);
23653
24077
  quantity = activeCols.size;
23654
24078
  }
23655
24079
  else {
@@ -27195,13 +27619,30 @@ class ColorPickerWidget extends owl.Component {
27195
27619
  }
27196
27620
  }
27197
27621
 
27198
- class ChartColor extends owl.Component {
27199
- static template = "o-spreadsheet.ChartColor";
27200
- static components = { ColorPickerWidget, Section };
27622
+ const TRANSPARENT_BACKGROUND_SVG = /*xml*/ `
27623
+ <svg xmlns="http://www.w3.org/2000/svg" width="10" height="10">
27624
+ <path fill="#d9d9d9" d="M5 5h5v5H5zH0V0h5"/>
27625
+ </svg>
27626
+ `;
27627
+ css /* scss */ `
27628
+ .o-round-color-picker-button {
27629
+ width: 15px;
27630
+ height: 15px;
27631
+ cursor: pointer;
27632
+ border: 1px solid #aaa;
27633
+ background-position: 1px 1px;
27634
+ background-image: url("data:image/svg+xml,${encodeURIComponent(TRANSPARENT_BACKGROUND_SVG)}");
27635
+ }
27636
+ `;
27637
+ class RoundColorPicker extends owl.Component {
27638
+ static template = "o-spreadsheet.RoundColorPicker";
27639
+ static components = { ColorPickerWidget, Section, ColorPicker };
27201
27640
  static props = {
27202
27641
  currentColor: { type: String, optional: true },
27642
+ title: { type: String, optional: true },
27203
27643
  onColorPicked: Function,
27204
27644
  };
27645
+ colorPickerButtonRef = owl.useRef("colorPickerButton");
27205
27646
  state;
27206
27647
  setup() {
27207
27648
  this.state = owl.useState({ pickerOpened: false });
@@ -27213,6 +27654,19 @@ class ChartColor extends owl.Component {
27213
27654
  togglePicker() {
27214
27655
  this.state.pickerOpened = !this.state.pickerOpened;
27215
27656
  }
27657
+ onColorPicked(color) {
27658
+ this.props.onColorPicked(color);
27659
+ this.state.pickerOpened = false;
27660
+ }
27661
+ get colorPickerAnchorRect() {
27662
+ const button = this.colorPickerButtonRef.el;
27663
+ return getBoundingRectAsPOJO(button);
27664
+ }
27665
+ get buttonStyle() {
27666
+ return cssPropertiesToCss({
27667
+ background: this.props.currentColor,
27668
+ });
27669
+ }
27216
27670
  }
27217
27671
 
27218
27672
  class ChartTitle extends owl.Component {
@@ -27226,7 +27680,7 @@ class ChartTitle extends owl.Component {
27226
27680
 
27227
27681
  class LineBarPieDesignPanel extends owl.Component {
27228
27682
  static template = "o-spreadsheet-LineBarPieDesignPanel";
27229
- static components = { ChartColor, ChartTitle, Section };
27683
+ static components = { RoundColorPicker, ChartTitle, Section };
27230
27684
  static props = {
27231
27685
  figureId: String,
27232
27686
  definition: Object,
@@ -27249,12 +27703,31 @@ class LineBarPieDesignPanel extends owl.Component {
27249
27703
  [attr]: ev.target.value,
27250
27704
  });
27251
27705
  }
27706
+ get backgroundColorTitle() {
27707
+ return ChartTerms.BackgroundColor;
27708
+ }
27252
27709
  }
27253
27710
 
27254
27711
  class BarChartDesignPanel extends LineBarPieDesignPanel {
27255
27712
  static template = "o-spreadsheet-BarChartDesignPanel";
27256
27713
  }
27257
27714
 
27715
+ class ComboChartConfigPanel extends LineBarPieConfigPanel {
27716
+ static template = "o-spreadsheet-ComboChartConfigPanel";
27717
+ get shouldUseRightAxis() {
27718
+ return _t("Use right axis for line series");
27719
+ }
27720
+ onUpdateUseRightAxis(useBothYAxis) {
27721
+ this.props.updateChart(this.props.figureId, {
27722
+ useBothYAxis,
27723
+ });
27724
+ }
27725
+ }
27726
+
27727
+ class ComboChartDesignPanel extends LineBarPieDesignPanel {
27728
+ static template = "o-spreadsheet-ComboChartDesignPanel";
27729
+ }
27730
+
27258
27731
  class GaugeChartConfigPanel extends owl.Component {
27259
27732
  static template = "o-spreadsheet-GaugeChartConfigPanel";
27260
27733
  static components = { ChartErrorSection, ChartDataSeries };
@@ -27302,6 +27775,10 @@ css /* scss */ `
27302
27775
  line-height: 18px;
27303
27776
  width: 100%;
27304
27777
  }
27778
+ td {
27779
+ box-sizing: border-box;
27780
+ height: 30px;
27781
+ }
27305
27782
  th.o-gauge-color-set-colorPicker {
27306
27783
  width: 8%;
27307
27784
  }
@@ -27324,7 +27801,12 @@ css /* scss */ `
27324
27801
  `;
27325
27802
  class GaugeChartDesignPanel extends owl.Component {
27326
27803
  static template = "o-spreadsheet-GaugeChartDesignPanel";
27327
- static components = { ColorPickerWidget, ChartErrorSection, ChartColor, ChartTitle, Section };
27804
+ static components = {
27805
+ ChartErrorSection,
27806
+ RoundColorPicker,
27807
+ ChartTitle,
27808
+ Section,
27809
+ };
27328
27810
  static props = {
27329
27811
  figureId: String,
27330
27812
  definition: Object,
@@ -27336,9 +27818,6 @@ class GaugeChartDesignPanel extends owl.Component {
27336
27818
  sectionRuleDispatchResult: undefined,
27337
27819
  sectionRule: deepCopy(this.props.definition.sectionRule),
27338
27820
  });
27339
- setup() {
27340
- owl.useExternalListener(window, "click", this.closeMenus);
27341
- }
27342
27821
  get title() {
27343
27822
  return _t(this.props.definition.title);
27344
27823
  }
@@ -27379,27 +27858,22 @@ class GaugeChartDesignPanel extends owl.Component {
27379
27858
  const sectionRule = deepCopy(this.state.sectionRule);
27380
27859
  sectionRule.colors[target] = color;
27381
27860
  this.updateSectionRule(sectionRule);
27382
- this.closeMenus();
27383
- }
27384
- toggleMenu(menu) {
27385
- const isSelected = this.state.openedMenu === menu;
27386
- this.closeMenus();
27387
- if (!isSelected) {
27388
- this.state.openedMenu = menu;
27389
- }
27390
27861
  }
27391
27862
  updateSectionRule(sectionRule) {
27392
27863
  this.state.sectionRuleDispatchResult = this.props.updateChart(this.props.figureId, {
27393
27864
  sectionRule,
27394
27865
  });
27866
+ if (this.state.sectionRuleDispatchResult.isSuccessful) {
27867
+ this.state.sectionRule = deepCopy(sectionRule);
27868
+ }
27395
27869
  }
27396
27870
  canUpdateSectionRule(sectionRule) {
27397
27871
  this.state.sectionRuleDispatchResult = this.props.canUpdateChart(this.props.figureId, {
27398
27872
  sectionRule,
27399
27873
  });
27400
27874
  }
27401
- closeMenus() {
27402
- this.state.openedMenu = undefined;
27875
+ get backgroundColorTitle() {
27876
+ return ChartTerms.BackgroundColor;
27403
27877
  }
27404
27878
  }
27405
27879
 
@@ -27547,39 +28021,36 @@ class ScorecardChartConfigPanel extends owl.Component {
27547
28021
 
27548
28022
  class ScorecardChartDesignPanel extends owl.Component {
27549
28023
  static template = "o-spreadsheet-ScorecardChartDesignPanel";
27550
- static components = { ColorPickerWidget, ChartColor, ChartTitle, Section };
28024
+ static components = { RoundColorPicker, ChartTitle, Section, Checkbox };
27551
28025
  static props = {
27552
28026
  figureId: String,
27553
28027
  definition: Object,
27554
28028
  updateChart: Function,
27555
28029
  canUpdateChart: Function,
27556
28030
  };
27557
- state = owl.useState({
27558
- openedColorPicker: undefined,
27559
- });
27560
- setup() {
27561
- owl.useExternalListener(window, "click", this.closeMenus);
27562
- }
27563
28031
  get title() {
27564
28032
  return _t(this.props.definition.title);
27565
28033
  }
28034
+ get colorsSectionTitle() {
28035
+ return this.props.definition.baselineMode === "progress"
28036
+ ? _t("Progress bar colors")
28037
+ : _t("Baseline colors");
28038
+ }
28039
+ get humanizeNumbersLabel() {
28040
+ return _t("Humanize numbers");
28041
+ }
27566
28042
  updateTitle(title) {
27567
28043
  this.props.updateChart(this.props.figureId, { title });
27568
28044
  }
28045
+ updateHumanizeNumbers(humanize) {
28046
+ this.props.updateChart(this.props.figureId, { humanize });
28047
+ }
27569
28048
  translate(term) {
27570
28049
  return _t(term);
27571
28050
  }
27572
28051
  updateBaselineDescr(ev) {
27573
28052
  this.props.updateChart(this.props.figureId, { baselineDescr: ev.target.value });
27574
28053
  }
27575
- toggleColorPicker(colorPickerId) {
27576
- if (this.state.openedColorPicker === colorPickerId) {
27577
- this.state.openedColorPicker = undefined;
27578
- }
27579
- else {
27580
- this.state.openedColorPicker = colorPickerId;
27581
- }
27582
- }
27583
28054
  setColor(color, colorPickerId) {
27584
28055
  switch (colorPickerId) {
27585
28056
  case "backgroundColor":
@@ -27592,10 +28063,9 @@ class ScorecardChartDesignPanel extends owl.Component {
27592
28063
  this.props.updateChart(this.props.figureId, { baselineColorUp: color });
27593
28064
  break;
27594
28065
  }
27595
- this.closeMenus();
27596
28066
  }
27597
- closeMenus() {
27598
- this.state.openedColorPicker = undefined;
28067
+ get backgroundColorTitle() {
28068
+ return ChartTerms.BackgroundColor;
27599
28069
  }
27600
28070
  }
27601
28071
 
@@ -27612,6 +28082,10 @@ chartSidePanelComponentRegistry
27612
28082
  .add("bar", {
27613
28083
  configuration: BarConfigPanel,
27614
28084
  design: BarChartDesignPanel,
28085
+ })
28086
+ .add("combo", {
28087
+ configuration: ComboChartConfigPanel,
28088
+ design: ComboChartDesignPanel,
27615
28089
  })
27616
28090
  .add("pie", {
27617
28091
  configuration: LineBarPieConfigPanel,
@@ -28540,6 +29014,7 @@ class ConditionalFormattingEditor extends owl.Component {
28540
29014
  ColorPickerWidget,
28541
29015
  ConditionalFormatPreviewList,
28542
29016
  Section,
29017
+ RoundColorPicker,
28543
29018
  };
28544
29019
  icons = ICONS;
28545
29020
  cellIsOperators = CellIsOperators;
@@ -30252,7 +30727,11 @@ class SplitIntoColumnsPanel extends owl.Component {
30252
30727
  const composerStore = useStore(ComposerStore);
30253
30728
  // The feature makes no sense if we are editing a cell, because then the selection isn't active
30254
30729
  // Stop the edition when the panel is mounted, and close the panel if the user start editing a cell
30255
- owl.useEffect(this.props.onCloseSidePanel, () => [composerStore.editionMode]);
30730
+ owl.useEffect((editionMode) => {
30731
+ if (editionMode !== "inactive") {
30732
+ this.props.onCloseSidePanel();
30733
+ }
30734
+ }, () => [composerStore.editionMode]);
30256
30735
  owl.onMounted(() => {
30257
30736
  composerStore.stopEdition();
30258
30737
  });
@@ -31961,6 +32440,9 @@ class FunctionDescriptionProvider extends owl.Component {
31961
32440
  this.assistantState.allowCellSelectionBehind = false;
31962
32441
  }, 2000);
31963
32442
  }
32443
+ get formulaArgSeparator() {
32444
+ return this.env.model.getters.getLocale().formulaArgSeparator + " ";
32445
+ }
31964
32446
  }
31965
32447
 
31966
32448
  const functions$2 = functionRegistry.content;
@@ -32037,6 +32519,7 @@ class Composer extends owl.Component {
32037
32519
  onComposerCellFocused: { type: Function, optional: true },
32038
32520
  onComposerContentFocused: Function,
32039
32521
  isDefaultFocus: { type: Boolean, optional: true },
32522
+ onInputContextMenu: { type: Function, optional: true },
32040
32523
  };
32041
32524
  static components = { TextValueProvider, FunctionDescriptionProvider };
32042
32525
  static defaultProps = {
@@ -32087,6 +32570,9 @@ class Composer extends owl.Component {
32087
32570
  assistantStyle.right = `0px`;
32088
32571
  }
32089
32572
  }
32573
+ else if (this.props.delimitation) {
32574
+ assistantStyle["max-height"] = `${this.props.delimitation.height}px`;
32575
+ }
32090
32576
  return cssPropertiesToCss(assistantStyle);
32091
32577
  }
32092
32578
  // we can't allow input events to be triggered while we remove and add back the content of the composer in processContent
@@ -32120,6 +32606,12 @@ class Composer extends owl.Component {
32120
32606
  owl.useEffect(() => {
32121
32607
  this.processContent();
32122
32608
  });
32609
+ owl.onPatched(() => {
32610
+ // Required because typing '=SUM' and double-clicking another cell leaves ShowProvider/ShowDescription true
32611
+ if (this.composerStore.editionMode === "inactive") {
32612
+ this.processTokenAtCursor();
32613
+ }
32614
+ });
32123
32615
  }
32124
32616
  // ---------------------------------------------------------------------------
32125
32617
  // Handlers
@@ -32367,6 +32859,11 @@ class Composer extends owl.Component {
32367
32859
  }
32368
32860
  }
32369
32861
  }
32862
+ onContextMenu(ev) {
32863
+ if (this.composerStore.editionMode === "inactive") {
32864
+ this.props.onInputContextMenu?.(ev);
32865
+ }
32866
+ }
32370
32867
  // ---------------------------------------------------------------------------
32371
32868
  // Private
32372
32869
  // ---------------------------------------------------------------------------
@@ -32588,6 +33085,7 @@ class GridComposer extends owl.Component {
32588
33085
  static template = "o-spreadsheet-GridComposer";
32589
33086
  static props = {
32590
33087
  gridDims: Object,
33088
+ onInputContextMenu: Function,
32591
33089
  };
32592
33090
  static components = { Composer };
32593
33091
  rect = this.defaultRect;
@@ -32635,6 +33133,7 @@ class GridComposer extends owl.Component {
32635
33133
  isDefaultFocus: true,
32636
33134
  onComposerContentFocused: () => this.composerFocusStore.focusGridComposerContent(),
32637
33135
  onComposerCellFocused: (content) => this.composerFocusStore.focusGridComposerCell(content),
33136
+ onInputContextMenu: this.props.onInputContextMenu,
32638
33137
  };
32639
33138
  }
32640
33139
  get containerStyle() {
@@ -32733,7 +33232,6 @@ class GridCellIcon extends owl.Component {
32733
33232
  cellPosition: Object,
32734
33233
  horizontalAlign: { type: String, optional: true },
32735
33234
  verticalAlign: { type: String, optional: true },
32736
- offset: { type: Object, optional: true },
32737
33235
  slots: Object,
32738
33236
  };
32739
33237
  get iconStyle() {
@@ -32744,8 +33242,8 @@ class GridCellIcon extends owl.Component {
32744
33242
  const x = this.getIconHorizontalPosition(rect, cellPosition);
32745
33243
  const y = this.getIconVerticalPosition(rect, cellPosition);
32746
33244
  return cssPropertiesToCss({
32747
- top: `${y + (this.props.offset?.y || 0)}px`,
32748
- left: `${x + (this.props.offset?.x || 0)}px`,
33245
+ top: `${y}px`,
33246
+ left: `${x}px`,
32749
33247
  });
32750
33248
  }
32751
33249
  getIconVerticalPosition(rect, cellPosition) {
@@ -32785,83 +33283,6 @@ class GridCellIcon extends owl.Component {
32785
33283
  }
32786
33284
  }
32787
33285
 
32788
- css /* scss */ `
32789
- .o-filter-icon {
32790
- color: ${FILTERS_COLOR};
32791
- display: flex;
32792
- align-items: center;
32793
- justify-content: center;
32794
- width: ${GRID_ICON_EDGE_LENGTH}px;
32795
- height: ${GRID_ICON_EDGE_LENGTH}px;
32796
-
32797
- &:hover {
32798
- background: ${FILTERS_COLOR};
32799
- color: #fff;
32800
- }
32801
-
32802
- &.o-high-contrast {
32803
- color: #defade;
32804
- }
32805
- &.o-high-contrast:hover {
32806
- color: ${FILTERS_COLOR};
32807
- background: #fff;
32808
- }
32809
- }
32810
- .o-filter-icon:hover {
32811
- background: ${FILTERS_COLOR};
32812
- color: #fff;
32813
- }
32814
- `;
32815
- class FilterIcon extends owl.Component {
32816
- static template = "o-spreadsheet-FilterIcon";
32817
- static props = {
32818
- cellPosition: Object,
32819
- };
32820
- cellPopovers;
32821
- setup() {
32822
- this.cellPopovers = useStore(CellPopoverStore);
32823
- }
32824
- onClick() {
32825
- const position = this.props.cellPosition;
32826
- const activePopover = this.cellPopovers.persistentCellPopover;
32827
- const { col, row } = position;
32828
- if (activePopover.isOpen &&
32829
- activePopover.col === col &&
32830
- activePopover.row === row &&
32831
- activePopover.type === "FilterMenu") {
32832
- this.cellPopovers.close();
32833
- return;
32834
- }
32835
- this.cellPopovers.open({ col, row }, "FilterMenu");
32836
- }
32837
- get isFilterActive() {
32838
- return this.env.model.getters.isFilterActive(this.props.cellPosition);
32839
- }
32840
- get iconClass() {
32841
- const cellStyle = this.env.model.getters.getCellComputedStyle(this.props.cellPosition);
32842
- const luminance = relativeLuminance(cellStyle.fillColor || "#fff");
32843
- return luminance < 0.45 ? "o-high-contrast" : "";
32844
- }
32845
- }
32846
-
32847
- class FilterIconsOverlay extends owl.Component {
32848
- static template = "o-spreadsheet-FilterIconsOverlay";
32849
- static props = {
32850
- gridPosition: { type: Object, optional: true },
32851
- };
32852
- static components = {
32853
- GridCellIcon,
32854
- FilterIcon,
32855
- };
32856
- static defaultProps = {
32857
- gridPosition: { x: 0, y: 0 },
32858
- };
32859
- getFilterHeadersPositions() {
32860
- const sheetId = this.env.model.getters.getActiveSheetId();
32861
- return this.env.model.getters.getFilterHeaders(sheetId);
32862
- }
32863
- }
32864
-
32865
33286
  const CHECKBOX_WIDTH = 15;
32866
33287
  const MARGIN = (GRID_ICON_EDGE_LENGTH - CHECKBOX_WIDTH) / 2;
32867
33288
  css /* scss */ `
@@ -33529,6 +33950,80 @@ class FiguresContainer extends owl.Component {
33529
33950
  }
33530
33951
  }
33531
33952
 
33953
+ css /* scss */ `
33954
+ .o-filter-icon {
33955
+ color: ${FILTERS_COLOR};
33956
+ display: flex;
33957
+ align-items: center;
33958
+ justify-content: center;
33959
+ width: ${GRID_ICON_EDGE_LENGTH}px;
33960
+ height: ${GRID_ICON_EDGE_LENGTH}px;
33961
+
33962
+ &:hover {
33963
+ background: ${FILTERS_COLOR};
33964
+ color: #fff;
33965
+ }
33966
+
33967
+ &.o-high-contrast {
33968
+ color: #defade;
33969
+ }
33970
+ &.o-high-contrast:hover {
33971
+ color: ${FILTERS_COLOR};
33972
+ background: #fff;
33973
+ }
33974
+ }
33975
+ .o-filter-icon:hover {
33976
+ background: ${FILTERS_COLOR};
33977
+ color: #fff;
33978
+ }
33979
+ `;
33980
+ class FilterIcon extends owl.Component {
33981
+ static template = "o-spreadsheet-FilterIcon";
33982
+ static props = {
33983
+ cellPosition: Object,
33984
+ };
33985
+ cellPopovers;
33986
+ setup() {
33987
+ this.cellPopovers = useStore(CellPopoverStore);
33988
+ }
33989
+ onClick() {
33990
+ const position = this.props.cellPosition;
33991
+ const activePopover = this.cellPopovers.persistentCellPopover;
33992
+ const { col, row } = position;
33993
+ if (activePopover.isOpen &&
33994
+ activePopover.col === col &&
33995
+ activePopover.row === row &&
33996
+ activePopover.type === "FilterMenu") {
33997
+ this.cellPopovers.close();
33998
+ return;
33999
+ }
34000
+ this.cellPopovers.open({ col, row }, "FilterMenu");
34001
+ }
34002
+ get isFilterActive() {
34003
+ return this.env.model.getters.isFilterActive(this.props.cellPosition);
34004
+ }
34005
+ get iconClass() {
34006
+ const cellStyle = this.env.model.getters.getCellComputedStyle(this.props.cellPosition);
34007
+ const luminance = relativeLuminance(cellStyle.fillColor || "#fff");
34008
+ return luminance < 0.45 ? "o-high-contrast" : "";
34009
+ }
34010
+ }
34011
+
34012
+ class FilterIconsOverlay extends owl.Component {
34013
+ static template = "o-spreadsheet-FilterIconsOverlay";
34014
+ static props = {
34015
+ onMouseDown: Function,
34016
+ };
34017
+ static components = {
34018
+ GridCellIcon,
34019
+ FilterIcon,
34020
+ };
34021
+ getFilterHeadersPositions() {
34022
+ const sheetId = this.env.model.getters.getActiveSheetId();
34023
+ return this.env.model.getters.getFilterHeaders(sheetId);
34024
+ }
34025
+ }
34026
+
33532
34027
  css /* scss */ `
33533
34028
  .o-grid-add-rows {
33534
34029
  input {
@@ -33742,7 +34237,12 @@ class GridOverlay extends owl.Component {
33742
34237
  onGridMoved: Function,
33743
34238
  gridOverlayDimensions: String,
33744
34239
  };
33745
- static components = { FiguresContainer, DataValidationOverlay, GridAddRowsFooter };
34240
+ static components = {
34241
+ FiguresContainer,
34242
+ DataValidationOverlay,
34243
+ GridAddRowsFooter,
34244
+ FilterIconsOverlay,
34245
+ };
33746
34246
  static defaultProps = {
33747
34247
  onCellHovered: () => { },
33748
34248
  onCellDoubleClicked: () => { },
@@ -33787,7 +34287,7 @@ class GridOverlay extends owl.Component {
33787
34287
  get isPaintingFormat() {
33788
34288
  return this.env.model.getters.isPaintingFormat();
33789
34289
  }
33790
- onMouseDown(ev) {
34290
+ onMouseDown(ev, modifiers) {
33791
34291
  if (ev.button > 0) {
33792
34292
  // not main button, probably a context menu
33793
34293
  return;
@@ -33796,6 +34296,7 @@ class GridOverlay extends owl.Component {
33796
34296
  this.props.onCellClicked(col, row, {
33797
34297
  expandZone: ev.shiftKey,
33798
34298
  addZone: isCtrlKey(ev),
34299
+ closePopover: modifiers?.closePopover ?? true,
33799
34300
  });
33800
34301
  }
33801
34302
  onDoubleClick(ev) {
@@ -34099,11 +34600,6 @@ css /* scss */ `
34099
34600
  height: 10000px;
34100
34601
  background-color: ${SELECTION_BORDER_COLOR};
34101
34602
  }
34102
- .o-unhide-buttons {
34103
- width: fit-content;
34104
- gap: 5px;
34105
- transform: translate(-50%, 0);
34106
- }
34107
34603
  .o-unhide:hover {
34108
34604
  z-index: ${ComponentsImportance.Grid + 1};
34109
34605
  background-color: lightgrey;
@@ -34265,10 +34761,6 @@ css /* scss */ `
34265
34761
  height: 1px;
34266
34762
  background-color: ${SELECTION_BORDER_COLOR};
34267
34763
  }
34268
- .o-unhide-buttons {
34269
- height: fit-content;
34270
- transform: translate(0, -50%);
34271
- }
34272
34764
  .o-unhide:hover {
34273
34765
  z-index: ${ComponentsImportance.Grid + 1};
34274
34766
  background-color: lightgrey;
@@ -35476,7 +35968,7 @@ class VerticalScrollBar extends owl.Component {
35476
35968
  onScroll(offset) {
35477
35969
  const { scrollX } = this.env.model.getters.getActiveSheetDOMScrollInfo();
35478
35970
  this.env.model.dispatch("SET_VIEWPORT_OFFSET", {
35479
- offsetX: scrollX,
35971
+ offsetX: scrollX, // offsetX is the same
35480
35972
  offsetY: offset,
35481
35973
  });
35482
35974
  }
@@ -35570,7 +36062,6 @@ class Grid extends owl.Component {
35570
36062
  Popover,
35571
36063
  VerticalScrollBar,
35572
36064
  HorizontalScrollBar,
35573
- FilterIconsOverlay,
35574
36065
  };
35575
36066
  HEADER_HEIGHT = HEADER_HEIGHT;
35576
36067
  HEADER_WIDTH = HEADER_WIDTH;
@@ -35764,8 +36255,8 @@ class Grid extends owl.Component {
35764
36255
  "Ctrl+Shift+L": () => this.setHorizontalAlign("left"),
35765
36256
  "Ctrl+Shift+R": () => this.setHorizontalAlign("right"),
35766
36257
  "Ctrl+Shift+V": () => PASTE_AS_VALUE_ACTION(this.env),
35767
- "Ctrl+Shift+<": () => this.clearFormatting(),
35768
- "Ctrl+<": () => this.clearFormatting(),
36258
+ "Ctrl+Shift+<": () => this.clearFormatting(), // for qwerty
36259
+ "Ctrl+<": () => this.clearFormatting(), // for azerty
35769
36260
  "Ctrl+Shift+ ": () => {
35770
36261
  this.env.model.selection.selectAll();
35771
36262
  },
@@ -35871,17 +36362,17 @@ class Grid extends owl.Component {
35871
36362
  // ---------------------------------------------------------------------------
35872
36363
  // Zone selection with mouse
35873
36364
  // ---------------------------------------------------------------------------
35874
- onCellClicked(col, row, { addZone, expandZone }) {
35875
- if (this.cellPopovers.isOpen) {
36365
+ onCellClicked(col, row, modifiers) {
36366
+ if (modifiers.closePopover && this.cellPopovers.isOpen) {
35876
36367
  this.cellPopovers.close();
35877
36368
  }
35878
36369
  if (this.composerStore.editionMode === "editing") {
35879
36370
  this.composerStore.stopEdition();
35880
36371
  }
35881
- if (expandZone) {
36372
+ if (modifiers.expandZone) {
35882
36373
  this.env.model.selection.setAnchorCorner(col, row);
35883
36374
  }
35884
- else if (addZone) {
36375
+ else if (modifiers.addZone) {
35885
36376
  this.env.model.selection.addCellToSelection(col, row);
35886
36377
  }
35887
36378
  else {
@@ -36201,6 +36692,7 @@ const XLSX_CHART_TYPES = [
36201
36692
  "surfaceChart",
36202
36693
  "surface3DChart",
36203
36694
  "bubbleChart",
36695
+ "comboChart",
36204
36696
  ];
36205
36697
 
36206
36698
  /** In XLSX color format (no #) */
@@ -36532,10 +37024,10 @@ function convertCFCellIsOperator(xlsxCfOperator) {
36532
37024
  const CF_TYPE_CONVERSION_MAP = {
36533
37025
  aboveAverage: undefined,
36534
37026
  expression: undefined,
36535
- cellIs: undefined,
36536
- colorScale: undefined,
37027
+ cellIs: undefined, // exist but isn't an operator in o_spreadsheet
37028
+ colorScale: undefined, // exist but isn't an operator in o_spreadsheet
36537
37029
  dataBar: undefined,
36538
- iconSet: undefined,
37030
+ iconSet: undefined, // exist but isn't an operator in o_spreadsheet
36539
37031
  top10: undefined,
36540
37032
  uniqueValues: undefined,
36541
37033
  duplicateValues: undefined,
@@ -36602,7 +37094,7 @@ const CHART_TYPE_CONVERSION_MAP = {
36602
37094
  line3DChart: undefined,
36603
37095
  stockChart: undefined,
36604
37096
  radarChart: undefined,
36605
- scatterChart: undefined,
37097
+ scatterChart: "scatter",
36606
37098
  pieChart: "pie",
36607
37099
  pie3DChart: undefined,
36608
37100
  doughnutChart: "pie",
@@ -36612,6 +37104,7 @@ const CHART_TYPE_CONVERSION_MAP = {
36612
37104
  surfaceChart: undefined,
36613
37105
  surface3DChart: undefined,
36614
37106
  bubbleChart: undefined,
37107
+ comboChart: "combo",
36615
37108
  };
36616
37109
  /** Conversion map for the SUBTOTAL(index, formula) function in xlsx, index <=> actual function*/
36617
37110
  const SUBTOTAL_FUNCTION_CONVERSION_MAP = {
@@ -36770,7 +37263,7 @@ const XLSX_INDEXED_COLORS = {
36770
37263
  61: "993366",
36771
37264
  62: "333399",
36772
37265
  63: "333333",
36773
- 64: "000000",
37266
+ 64: "000000", // system foreground
36774
37267
  65: "FFFFFF", // system background
36775
37268
  };
36776
37269
  const IMAGE_MIMETYPE_TO_EXTENSION_MAPPING = {
@@ -37365,29 +37858,12 @@ function convertWidthFromExcel(width) {
37365
37858
  return width;
37366
37859
  return Math.round((width / WIDTH_FACTOR) * 100) / 100;
37367
37860
  }
37368
- function convertBorderDescr(descr) {
37369
- if (!descr) {
37370
- return undefined;
37371
- }
37372
- return {
37373
- style: descr.style,
37374
- color: { rgb: descr.color },
37375
- };
37376
- }
37377
37861
  function extractStyle(cell, data) {
37378
37862
  let style = {};
37379
37863
  if (cell.style) {
37380
37864
  style = data.styles[cell.style];
37381
37865
  }
37382
37866
  const format = extractFormat(cell, data);
37383
- const exportedBorder = {};
37384
- if (cell.border) {
37385
- const border = data.borders[cell.border];
37386
- exportedBorder.left = convertBorderDescr(border.left);
37387
- exportedBorder.right = convertBorderDescr(border.right);
37388
- exportedBorder.bottom = convertBorderDescr(border.bottom);
37389
- exportedBorder.top = convertBorderDescr(border.top);
37390
- }
37391
37867
  const styles = {
37392
37868
  font: {
37393
37869
  size: style?.fontSize || DEFAULT_FONT_SIZE,
@@ -37401,7 +37877,7 @@ function extractStyle(cell, data) {
37401
37877
  }
37402
37878
  : { reservedAttribute: "none" },
37403
37879
  numFmt: format ? { format: format, id: 0 /* id not used for export */ } : undefined,
37404
- border: exportedBorder || {},
37880
+ border: cell.border || 0,
37405
37881
  alignment: {
37406
37882
  horizontal: style.align,
37407
37883
  vertical: style.verticalAlign
@@ -37423,15 +37899,12 @@ function extractFormat(cell, data) {
37423
37899
  return undefined;
37424
37900
  }
37425
37901
  function normalizeStyle(construct, styles) {
37426
- const { id: fontId } = pushElement(styles["font"], construct.fonts);
37427
- const { id: fillId } = pushElement(styles["fill"], construct.fills);
37428
- const { id: borderId } = pushElement(styles["border"], construct.borders);
37429
37902
  // Normalize this
37430
37903
  const numFmtId = convertFormat(styles["numFmt"], construct.numFmts);
37431
37904
  const style = {
37432
- fontId,
37433
- fillId,
37434
- borderId,
37905
+ fontId: pushElement(styles.font, construct.fonts),
37906
+ fillId: pushElement(styles.fill, construct.fills),
37907
+ borderId: styles.border,
37435
37908
  numFmtId,
37436
37909
  alignment: {
37437
37910
  vertical: styles.alignment.vertical,
@@ -37439,8 +37912,7 @@ function normalizeStyle(construct, styles) {
37439
37912
  wrapText: styles.alignment.wrapText,
37440
37913
  },
37441
37914
  };
37442
- const { id } = pushElement(style, construct.styles);
37443
- return id;
37915
+ return pushElement(style, construct.styles);
37444
37916
  }
37445
37917
  function convertFormat(format, numFmtStructure) {
37446
37918
  if (!format) {
@@ -37448,8 +37920,7 @@ function convertFormat(format, numFmtStructure) {
37448
37920
  }
37449
37921
  let formatId = XLSX_FORMAT_MAP[format.format];
37450
37922
  if (!formatId) {
37451
- const { id } = pushElement(format, numFmtStructure);
37452
- formatId = id + FIRST_NUMFMT_ID;
37923
+ formatId = pushElement(format, numFmtStructure) + FIRST_NUMFMT_ID;
37453
37924
  }
37454
37925
  return formatId;
37455
37926
  }
@@ -37474,20 +37945,15 @@ function addRelsToFile(relsFiles, path, rel) {
37474
37945
  return id;
37475
37946
  }
37476
37947
  function pushElement(property, propertyList) {
37477
- for (let [key, value] of Object.entries(propertyList)) {
37478
- if (JSON.stringify(value) === JSON.stringify(property)) {
37479
- return { id: parseInt(key, 10), list: propertyList };
37948
+ let len = propertyList.length;
37949
+ const operator = typeof property === "object" ? deepEquals : (a, b) => a === b;
37950
+ for (let i = 0; i < len; i++) {
37951
+ if (operator(property, propertyList[i])) {
37952
+ return i;
37480
37953
  }
37481
37954
  }
37482
- let elemId = propertyList.findIndex((elem) => JSON.stringify(elem) === JSON.stringify(property));
37483
- if (elemId === -1) {
37484
- propertyList.push(property);
37485
- elemId = propertyList.length - 1;
37486
- }
37487
- return {
37488
- id: elemId,
37489
- list: propertyList,
37490
- };
37955
+ propertyList[propertyList.length] = property;
37956
+ return propertyList.length - 1;
37491
37957
  }
37492
37958
  const chartIds = [];
37493
37959
  /**
@@ -37962,7 +38428,7 @@ function convertHyperlink(link, cellValue, warningManager) {
37962
38428
  function getSheetDims(sheet) {
37963
38429
  const dims = [0, 0];
37964
38430
  for (let row of sheet.rows) {
37965
- dims[0] = Math.max(dims[0], ...row.cells.map((cell) => toCartesian(cell.xc).col));
38431
+ dims[0] = Math.max(dims[0], largeMax(row.cells.map((cell) => toCartesian(cell.xc).col)));
37966
38432
  dims[1] = Math.max(dims[1], row.index);
37967
38433
  }
37968
38434
  dims[0] = Math.max(dims[0], EXCEL_IMPORT_DEFAULT_NUMBER_OF_COLS);
@@ -38282,7 +38748,25 @@ function parseXML(xmlString, mimeType = "text/xml") {
38282
38748
  }
38283
38749
  return document;
38284
38750
  }
38285
- function getDefaultXLSXStructure() {
38751
+ function convertBorderDescr(descr) {
38752
+ if (!descr) {
38753
+ return undefined;
38754
+ }
38755
+ return {
38756
+ style: descr.style,
38757
+ color: { rgb: descr.color },
38758
+ };
38759
+ }
38760
+ function getDefaultXLSXStructure(data) {
38761
+ const xlsxBorders = Object.values(data.borders).map((border) => {
38762
+ return {
38763
+ left: convertBorderDescr(border.left),
38764
+ right: convertBorderDescr(border.right),
38765
+ bottom: convertBorderDescr(border.bottom),
38766
+ top: convertBorderDescr(border.top),
38767
+ };
38768
+ });
38769
+ const borders = [{}, ...xlsxBorders];
38286
38770
  return {
38287
38771
  relsFiles: [],
38288
38772
  sharedStrings: [],
@@ -38305,7 +38789,7 @@ function getDefaultXLSXStructure() {
38305
38789
  },
38306
38790
  ],
38307
38791
  fills: [{ reservedAttribute: "none" }, { reservedAttribute: "gray125" }],
38308
- borders: [{}],
38792
+ borders,
38309
38793
  numFmts: [],
38310
38794
  dxfs: [],
38311
38795
  };
@@ -38828,6 +39312,9 @@ class XlsxChartExtractor extends XlsxBaseExtractor {
38828
39312
  if (!CHART_TYPE_CONVERSION_MAP[chartType]) {
38829
39313
  throw new Error(`Unsupported chart type ${chartType}`);
38830
39314
  }
39315
+ if (CHART_TYPE_CONVERSION_MAP[chartType] === "combo") {
39316
+ return this.extractComboChart(rootChartElement);
39317
+ }
38831
39318
  // Title can be separated into multiple xml elements (for styling and such), we only import the text
38832
39319
  const chartTitle = this.mapOnElements({ parent: rootChartElement, query: "c:title a:t" }, (textElement) => {
38833
39320
  return textElement.textContent || "";
@@ -38838,8 +39325,8 @@ class XlsxChartExtractor extends XlsxBaseExtractor {
38838
39325
  return {
38839
39326
  title: chartTitle,
38840
39327
  type: CHART_TYPE_CONVERSION_MAP[chartType],
38841
- dataSets: this.extractChartDatasets(this.querySelector(rootChartElement, `c:${chartType}`)),
38842
- labelRange: this.extractChildTextContent(rootChartElement, "c:ser c:cat c:f"),
39328
+ dataSets: this.extractChartDatasets(this.querySelector(rootChartElement, `c:${chartType}`), chartType),
39329
+ labelRange: this.extractChildTextContent(rootChartElement, `c:ser ${chartType === "scatterChart" ? "c:numRef" : "c:cat"} c:f`),
38843
39330
  backgroundColor: this.extractChildAttr(rootChartElement, "c:chartSpace > c:spPr a:srgbClr", "val", {
38844
39331
  default: "ffffff",
38845
39332
  }).asString(),
@@ -38856,7 +39343,41 @@ class XlsxChartExtractor extends XlsxBaseExtractor {
38856
39343
  };
38857
39344
  })[0];
38858
39345
  }
38859
- extractChartDatasets(chartElement) {
39346
+ extractComboChart(chartElement) {
39347
+ // Title can be separated into multiple xml elements (for styling and such), we only import the text
39348
+ const chartTitle = this.mapOnElements({ parent: chartElement, query: "c:title a:t" }, (textElement) => {
39349
+ return textElement.textContent || "";
39350
+ }).join("");
39351
+ const barChartGrouping = this.extractChildAttr(chartElement, "c:grouping", "val", {
39352
+ default: "clustered",
39353
+ }).asString();
39354
+ return {
39355
+ title: chartTitle,
39356
+ type: "combo",
39357
+ dataSets: [
39358
+ ...this.extractChartDatasets(this.querySelector(chartElement, `c:barChart`), "comboChart"),
39359
+ ...this.extractChartDatasets(this.querySelector(chartElement, `c:lineChart`), "comboChart"),
39360
+ ],
39361
+ labelRange: this.extractChildTextContent(chartElement, "c:ser c:cat c:f"),
39362
+ backgroundColor: this.extractChildAttr(chartElement, "c:chartSpace > c:spPr a:srgbClr", "val", {
39363
+ default: "ffffff",
39364
+ }).asString(),
39365
+ verticalAxisPosition: this.extractChildAttr(chartElement, "c:valAx > c:axPos", "val", {
39366
+ default: "l",
39367
+ }).asString() === "r"
39368
+ ? "right"
39369
+ : "left",
39370
+ legendPosition: DRAWING_LEGEND_POSITION_CONVERSION_MAP[this.extractChildAttr(chartElement, "c:legendPos", "val", {
39371
+ default: "b",
39372
+ }).asString()],
39373
+ stacked: barChartGrouping === "stacked",
39374
+ fontColor: "000000",
39375
+ };
39376
+ }
39377
+ extractChartDatasets(chartElement, chartType) {
39378
+ if (chartType === "scatterChart") {
39379
+ return this.extractScatterChartDatasets(chartElement);
39380
+ }
38860
39381
  return this.mapOnElements({ parent: chartElement, query: "c:ser" }, (chartDataElement) => {
38861
39382
  return {
38862
39383
  label: this.extractChildTextContent(chartDataElement, "c:tx c:f"),
@@ -38864,6 +39385,14 @@ class XlsxChartExtractor extends XlsxBaseExtractor {
38864
39385
  };
38865
39386
  });
38866
39387
  }
39388
+ extractScatterChartDatasets(chartElement) {
39389
+ return this.mapOnElements({ parent: chartElement, query: "c:ser" }, (chartDataElement) => {
39390
+ return {
39391
+ label: this.extractChildTextContent(chartDataElement, "c:xVal c:f", { required: false }),
39392
+ range: this.extractChildTextContent(chartDataElement, "c:yVal c:f", { required: true }),
39393
+ };
39394
+ });
39395
+ }
38867
39396
  /**
38868
39397
  * The chart type in the XML isn't explicitly defined, but there is an XML element that define the
38869
39398
  * chart, and this element tag name tells us which type of chart it is. We just need to find this XML element.
@@ -38873,12 +39402,21 @@ class XlsxChartExtractor extends XlsxBaseExtractor {
38873
39402
  if (!plotAreaElement) {
38874
39403
  throw new Error("Missing plot area in the chart definition.");
38875
39404
  }
39405
+ let globalTag = undefined;
38876
39406
  for (let child of plotAreaElement.children) {
38877
39407
  const tag = removeTagEscapedNamespaces(child.tagName);
38878
39408
  if (XLSX_CHART_TYPES.some((chartType) => chartType === tag)) {
38879
- return tag;
39409
+ if (!globalTag) {
39410
+ globalTag = tag;
39411
+ }
39412
+ else if (globalTag !== tag) {
39413
+ globalTag = "comboChart";
39414
+ }
38880
39415
  }
38881
39416
  }
39417
+ if (globalTag) {
39418
+ return globalTag;
39419
+ }
38882
39420
  throw new Error("Unknown chart type");
38883
39421
  }
38884
39422
  }
@@ -40248,12 +40786,14 @@ class BasePlugin {
40248
40786
  static getters = [];
40249
40787
  history;
40250
40788
  dispatch;
40251
- constructor(stateObserver, dispatch) {
40789
+ canDispatch;
40790
+ constructor(stateObserver, dispatch, canDispatch) {
40252
40791
  this.history = Object.assign(Object.create(stateObserver), {
40253
40792
  update: stateObserver.addChange.bind(stateObserver, this),
40254
40793
  selectCell: () => { },
40255
40794
  });
40256
40795
  this.dispatch = dispatch;
40796
+ this.canDispatch = canDispatch;
40257
40797
  }
40258
40798
  /**
40259
40799
  * Export for excel should be available for all plugins, even for the UI.
@@ -40332,8 +40872,8 @@ class BasePlugin {
40332
40872
  class CorePlugin extends BasePlugin {
40333
40873
  getters;
40334
40874
  uuidGenerator;
40335
- constructor({ getters, stateObserver, range, dispatch, uuidGenerator }) {
40336
- super(stateObserver, dispatch);
40875
+ constructor({ getters, stateObserver, range, dispatch, canDispatch, uuidGenerator, }) {
40876
+ super(stateObserver, dispatch, canDispatch);
40337
40877
  range.addRangeProvider(this.adaptRanges.bind(this));
40338
40878
  this.getters = getters;
40339
40879
  this.uuidGenerator = uuidGenerator;
@@ -42469,6 +43009,9 @@ class DataValidationPlugin extends CorePlugin {
42469
43009
  if (newRule.criterion.type === "isBoolean") {
42470
43010
  this.setCenterStyleToBooleanCells(newRule);
42471
43011
  }
43012
+ else if (newRule.criterion.type === "isValueInList") {
43013
+ newRule.criterion.values = Array.from(new Set(newRule.criterion.values));
43014
+ }
42472
43015
  const adaptedRules = this.removeRangesFromRules(sheetId, newRule.ranges, rules);
42473
43016
  const ruleIndex = adaptedRules.findIndex((rule) => rule.id === newRule.id);
42474
43017
  if (ruleIndex !== -1) {
@@ -42865,7 +43408,7 @@ class HeaderVisibilityPlugin extends CorePlugin {
42865
43408
  if (hiddenElements.size >= elements) {
42866
43409
  return "TooManyHiddenElements" /* CommandResult.TooManyHiddenElements */;
42867
43410
  }
42868
- else if (Math.min(...cmd.elements) < 0 || Math.max(...cmd.elements) > elements) {
43411
+ else if (largeMin(cmd.elements) < 0 || largeMax(cmd.elements) > elements) {
42869
43412
  return "InvalidHeaderIndex" /* CommandResult.InvalidHeaderIndex */;
42870
43413
  }
42871
43414
  else {
@@ -43194,7 +43737,6 @@ class MergePlugin extends CorePlugin {
43194
43737
  "isInSameMerge",
43195
43738
  "isMergeHidden",
43196
43739
  "getMainCellPosition",
43197
- "getBottomLeftCell",
43198
43740
  "expandZone",
43199
43741
  "doesIntersectMerge",
43200
43742
  "doesColumnsHaveCommonMerges",
@@ -43376,13 +43918,6 @@ class MergePlugin extends CorePlugin {
43376
43918
  const mergeTopLeftPos = this.getMerge(position).topLeft;
43377
43919
  return { sheetId: position.sheetId, col: mergeTopLeftPos.col, row: mergeTopLeftPos.row };
43378
43920
  }
43379
- getBottomLeftCell(position) {
43380
- if (!this.isInMerge(position)) {
43381
- return position;
43382
- }
43383
- const { bottom, left } = this.getMerge(position);
43384
- return { sheetId: position.sheetId, col: left, row: bottom };
43385
- }
43386
43921
  isMergeHidden(sheetId, merge) {
43387
43922
  const hiddenColsGroups = this.getters.getHiddenColsGroups(sheetId);
43388
43923
  const hiddenRowsGroups = this.getters.getHiddenRowsGroups(sheetId);
@@ -43683,8 +44218,8 @@ class RangeAdapter {
43683
44218
  let newRange = range;
43684
44219
  let changeType = "NONE";
43685
44220
  for (let group of groups) {
43686
- const min = Math.min(...group);
43687
- const max = Math.max(...group);
44221
+ const min = largeMin(group);
44222
+ const max = largeMax(group);
43688
44223
  if (range.zone[start] <= min && min <= range.zone[end]) {
43689
44224
  const toRemove = Math.min(range.zone[end], max) - min + 1;
43690
44225
  changeType = "RESIZE";
@@ -44066,7 +44601,6 @@ class SheetPlugin extends CorePlugin {
44066
44601
  "getSheetIds",
44067
44602
  "getVisibleSheetIds",
44068
44603
  "isSheetVisible",
44069
- "getEvaluationSheets",
44070
44604
  "doesHeaderExist",
44071
44605
  "doesHeadersExist",
44072
44606
  "getCell",
@@ -44133,8 +44667,8 @@ class SheetPlugin extends CorePlugin {
44133
44667
  }
44134
44668
  return "Success" /* CommandResult.Success */;
44135
44669
  case "REMOVE_COLUMNS_ROWS": {
44136
- const min = Math.min(...cmd.elements);
44137
- const max = Math.max(...cmd.elements);
44670
+ const min = largeMin(cmd.elements);
44671
+ const max = largeMax(cmd.elements);
44138
44672
  if (min < 0 || !this.doesHeaderExist(cmd.sheetId, cmd.dimension, max)) {
44139
44673
  return "InvalidHeaderIndex" /* CommandResult.InvalidHeaderIndex */;
44140
44674
  }
@@ -44325,9 +44859,6 @@ class SheetPlugin extends CorePlugin {
44325
44859
  getVisibleSheetIds() {
44326
44860
  return this.orderedSheetIds.filter(this.isSheetVisible.bind(this));
44327
44861
  }
44328
- getEvaluationSheets() {
44329
- return this.sheets;
44330
- }
44331
44862
  doesHeaderExist(sheetId, dimension, index) {
44332
44863
  return dimension === "COL"
44333
44864
  ? index >= 0 && index < this.getNumberCols(sheetId)
@@ -44336,13 +44867,6 @@ class SheetPlugin extends CorePlugin {
44336
44867
  doesHeadersExist(sheetId, dimension, headerIndexes) {
44337
44868
  return headerIndexes.every((index) => this.doesHeaderExist(sheetId, dimension, index));
44338
44869
  }
44339
- getRow(sheetId, index) {
44340
- const row = this.getSheet(sheetId).rows[index];
44341
- if (!row) {
44342
- throw new Error(`Row ${row} not found.`);
44343
- }
44344
- return row;
44345
- }
44346
44870
  getCell({ sheetId, col, row }) {
44347
44871
  const sheet = this.tryGetSheet(sheetId);
44348
44872
  const cellId = sheet?.rows[row]?.cells[col];
@@ -45846,8 +46370,8 @@ class UIPlugin extends BasePlugin {
45846
46370
  getters;
45847
46371
  ui;
45848
46372
  selection;
45849
- constructor({ getters, stateObserver, dispatch, uiActions, selection }) {
45850
- super(stateObserver, dispatch);
46373
+ constructor({ getters, stateObserver, dispatch, canDispatch, uiActions, selection, }) {
46374
+ super(stateObserver, dispatch, canDispatch);
45851
46375
  this.getters = getters;
45852
46376
  this.ui = uiActions;
45853
46377
  this.selection = selection;
@@ -45911,12 +46435,6 @@ class CompilationParametersBuilder {
45911
46435
  : _t("Function %s expects its parameters to be single values or single cell references, not ranges.", functionName.toString()));
45912
46436
  }
45913
46437
  const position = { sheetId: range.sheetId, col: range.zone.left, row: range.zone.top };
45914
- return this.readCell(position);
45915
- }
45916
- readCell(position) {
45917
- if (!this.getters.tryGetSheet(position.sheetId)) {
45918
- throw new EvaluationError(_t("Invalid sheet name"));
45919
- }
45920
46438
  return this.computeCell(position);
45921
46439
  }
45922
46440
  /**
@@ -45952,7 +46470,7 @@ class CompilationParametersBuilder {
45952
46470
  matrix[colIndex] = new Array(height);
45953
46471
  for (let row = _zone.top; row <= _zone.bottom; row++) {
45954
46472
  const rowIndex = row - _zone.top;
45955
- matrix[colIndex][rowIndex] = this.readCell({ sheetId, col, row });
46473
+ matrix[colIndex][rowIndex] = this.computeCell({ sheetId, col, row });
45956
46474
  }
45957
46475
  }
45958
46476
  this.rangeCache[cacheKey] = matrix;
@@ -47060,15 +47578,15 @@ class Evaluator {
47060
47578
  getEvaluatedCell(position) {
47061
47579
  return this.evaluatedCells.get(position) || EMPTY_CELL;
47062
47580
  }
47063
- getSpreadPositionsOf(position) {
47581
+ getSpreadZone(position) {
47064
47582
  if (!this.spreadingRelations.isArrayFormula(position)) {
47065
- return [];
47583
+ return undefined;
47066
47584
  }
47067
47585
  if (this.evaluatedCells.get(position)?.type === CellValueType.error) {
47068
- return [position];
47586
+ return positionToZone(position);
47069
47587
  }
47070
47588
  const spreadPositions = Array.from(this.spreadingRelations.getArrayResultPositions(position));
47071
- return [position, ...spreadPositions];
47589
+ return union(positionToZone(position), unionPositionsToZone(spreadPositions));
47072
47590
  }
47073
47591
  getEvaluatedPositions() {
47074
47592
  return this.evaluatedCells.keys();
@@ -47133,7 +47651,9 @@ class Evaluator {
47133
47651
  this.blockedArrayFormulas = this.createEmptyPositionSet();
47134
47652
  this.spreadingRelations = new SpreadingRelation(this.createEmptyPositionSet.bind(this));
47135
47653
  this.formulaDependencies = lazy(() => {
47136
- const dependencies = [...this.getAllCells()].flatMap((position) => this.getDirectDependencies(position).map((range) => ({
47654
+ const dependencies = [...this.getAllCells()].flatMap((position) => this.getDirectDependencies(position)
47655
+ .filter((range) => !range.invalidSheetName && !range.invalidXc)
47656
+ .map((range) => ({
47137
47657
  data: position,
47138
47658
  boundingBox: {
47139
47659
  zone: range.zone,
@@ -47473,7 +47993,7 @@ class EvaluationPlugin extends UIPlugin {
47473
47993
  "getEvaluatedCell",
47474
47994
  "getEvaluatedCells",
47475
47995
  "getEvaluatedCellsInZone",
47476
- "getSpreadPositionsOf",
47996
+ "getSpreadZone",
47477
47997
  "getArrayFormulaSpreadingOn",
47478
47998
  "isEmpty",
47479
47999
  ];
@@ -47579,8 +48099,11 @@ class EvaluationPlugin extends UIPlugin {
47579
48099
  getEvaluatedCellsInZone(sheetId, zone) {
47580
48100
  return positions(zone).map(({ col, row }) => this.getters.getEvaluatedCell({ sheetId, col, row }));
47581
48101
  }
47582
- getSpreadPositionsOf(position) {
47583
- return this.evaluator.getSpreadPositionsOf(position);
48102
+ /**
48103
+ * Return the spread zone the position is part of, if any
48104
+ */
48105
+ getSpreadZone(position) {
48106
+ return this.evaluator.getSpreadZone(position);
47584
48107
  }
47585
48108
  getArrayFormulaSpreadingOn(position) {
47586
48109
  return this.evaluator.getArrayFormulaSpreadingOn(position);
@@ -47620,7 +48143,7 @@ class EvaluationPlugin extends UIPlugin {
47620
48143
  ? getItemId(newFormat, data.formats)
47621
48144
  : exportedCellData.format;
47622
48145
  let content;
47623
- if (formulaCell instanceof FormulaCellWithDependencies) {
48146
+ if (isFormula && formulaCell instanceof FormulaCellWithDependencies) {
47624
48147
  content = formulaCell.contentWithFixedReferences;
47625
48148
  }
47626
48149
  else {
@@ -47668,17 +48191,17 @@ function isBadExpression(tokens) {
47668
48191
  */
47669
48192
  function sortWithClusters(colorsToSort) {
47670
48193
  const clusters = [
47671
- { leadColor: rgba(255, 0, 0), colors: [] },
47672
- { leadColor: rgba(255, 128, 0), colors: [] },
47673
- { leadColor: rgba(128, 128, 0), colors: [] },
47674
- { leadColor: rgba(128, 255, 0), colors: [] },
47675
- { leadColor: rgba(0, 255, 0), colors: [] },
47676
- { leadColor: rgba(0, 255, 128), colors: [] },
47677
- { leadColor: rgba(0, 255, 255), colors: [] },
47678
- { leadColor: rgba(0, 127, 255), colors: [] },
47679
- { leadColor: rgba(0, 0, 255), colors: [] },
47680
- { leadColor: rgba(127, 0, 255), colors: [] },
47681
- { leadColor: rgba(128, 0, 128), colors: [] },
48194
+ { leadColor: rgba(255, 0, 0), colors: [] }, // red
48195
+ { leadColor: rgba(255, 128, 0), colors: [] }, // orange
48196
+ { leadColor: rgba(128, 128, 0), colors: [] }, // yellow
48197
+ { leadColor: rgba(128, 255, 0), colors: [] }, // chartreuse
48198
+ { leadColor: rgba(0, 255, 0), colors: [] }, // green
48199
+ { leadColor: rgba(0, 255, 128), colors: [] }, // spring green
48200
+ { leadColor: rgba(0, 255, 255), colors: [] }, // cyan
48201
+ { leadColor: rgba(0, 127, 255), colors: [] }, // azure
48202
+ { leadColor: rgba(0, 0, 255), colors: [] }, // blue
48203
+ { leadColor: rgba(127, 0, 255), colors: [] }, // violet
48204
+ { leadColor: rgba(128, 0, 128), colors: [] }, // magenta
47682
48205
  { leadColor: rgba(255, 0, 128), colors: [] }, // rose
47683
48206
  ];
47684
48207
  for (const color of colorsToSort.map(colorToRGBA)) {
@@ -48069,13 +48592,13 @@ class EvaluationConditionalFormatPlugin extends UIPlugin {
48069
48592
  .map((cell) => cell.value);
48070
48593
  switch (threshold.type) {
48071
48594
  case "value":
48072
- const result = functionName === "max" ? Math.max(...rangeValues) : Math.min(...rangeValues);
48595
+ const result = functionName === "max" ? largeMax(rangeValues) : largeMin(rangeValues);
48073
48596
  return result;
48074
48597
  case "number":
48075
48598
  return Number(threshold.value);
48076
48599
  case "percentage":
48077
- const min = Math.min(...rangeValues);
48078
- const max = Math.max(...rangeValues);
48600
+ const min = largeMin(rangeValues);
48601
+ const max = largeMax(rangeValues);
48079
48602
  const delta = max - min;
48080
48603
  return min + (delta * Number(threshold.value)) / 100;
48081
48604
  case "percentile":
@@ -48542,8 +49065,8 @@ class DynamicTablesPlugin extends UIPlugin {
48542
49065
  else if (deepEquals(parentSpreadingCell, topLeft) && getZoneArea(unionZone) === 1) {
48543
49066
  return true;
48544
49067
  }
48545
- const spreadPositions = this.getters.getSpreadPositionsOf(parentSpreadingCell);
48546
- return deepEquals(unionZone, unionPositionsToZone(spreadPositions));
49068
+ const zone = this.getters.getSpreadZone(parentSpreadingCell);
49069
+ return deepEquals(unionZone, zone);
48547
49070
  }
48548
49071
  coreTableToTable(sheetId, table) {
48549
49072
  if (table.type !== "dynamic") {
@@ -48551,8 +49074,7 @@ class DynamicTablesPlugin extends UIPlugin {
48551
49074
  }
48552
49075
  const tableZone = table.range.zone;
48553
49076
  const tablePosition = { sheetId, col: tableZone.left, row: tableZone.top };
48554
- const spreadPositions = this.getters.getSpreadPositionsOf(tablePosition);
48555
- const zone = spreadPositions.length ? unionPositionsToZone(spreadPositions) : table.range.zone;
49077
+ const zone = this.getters.getSpreadZone(tablePosition) ?? table.range.zone;
48556
49078
  const range = this.getters.getRangeFromZone(sheetId, zone);
48557
49079
  const filters = this.getDynamicTableFilters(sheetId, table, zone);
48558
49080
  return { id: table.id, range, filters, config: table.config };
@@ -49002,8 +49524,7 @@ class AutofillPlugin extends UIPlugin {
49002
49524
  let row = zone.bottom;
49003
49525
  if (col > 0) {
49004
49526
  let leftPosition = { sheetId, col: col - 1, row };
49005
- while (this.getters.getCorrespondingFormulaCell(leftPosition) ||
49006
- this.getters.getCell(leftPosition)?.content) {
49527
+ while (this.getters.getEvaluatedCell(leftPosition).type !== CellValueType.empty) {
49007
49528
  row += 1;
49008
49529
  leftPosition = { sheetId, col: col - 1, row };
49009
49530
  }
@@ -49012,8 +49533,7 @@ class AutofillPlugin extends UIPlugin {
49012
49533
  col = zone.right;
49013
49534
  if (col <= this.getters.getNumberCols(sheetId)) {
49014
49535
  let rightPosition = { sheetId, col: col + 1, row };
49015
- while (this.getters.getCorrespondingFormulaCell(rightPosition) ||
49016
- this.getters.getCell(rightPosition)?.content) {
49536
+ while (this.getters.getEvaluatedCell(rightPosition).type !== CellValueType.empty) {
49017
49537
  row += 1;
49018
49538
  rightPosition = { sheetId, col: col + 1, row };
49019
49539
  }
@@ -49323,13 +49843,13 @@ class AutomaticSumPlugin extends UIPlugin {
49323
49843
  const cells = this.getters.getEvaluatedCellsInZone(sheet.id, zone);
49324
49844
  const cellPositions = range(end, -1, -1);
49325
49845
  const invalidCells = cellPositions.filter((position) => cells[position] && !cells[position].isAutoSummable);
49326
- const maxValidPosition = Math.max(...invalidCells);
49846
+ const maxValidPosition = largeMax(invalidCells);
49327
49847
  const numberSequences = groupConsecutive(cellPositions.filter((position) => this.isNumber(cells[position])));
49328
49848
  const firstSequence = numberSequences[0] || [];
49329
- if (Math.max(...firstSequence) < maxValidPosition) {
49849
+ if (largeMax(firstSequence) < maxValidPosition) {
49330
49850
  return Infinity;
49331
49851
  }
49332
- return Math.min(...firstSequence);
49852
+ return largeMin(firstSequence);
49333
49853
  }
49334
49854
  shouldFindData(sheetId, zone) {
49335
49855
  return this.getters.isEmpty(sheetId, zone) || this.getters.isSingleCellOrMerge(sheetId, zone);
@@ -50904,8 +51424,6 @@ class SheetUIPlugin extends UIPlugin {
50904
51424
  static getters = [
50905
51425
  "doesCellHaveGridIcon",
50906
51426
  "getCellWidth",
50907
- "getCellComputedBorder",
50908
- "getCellComputedStyle",
50909
51427
  "getTextWidth",
50910
51428
  "getCellText",
50911
51429
  "getCellMultiLineText",
@@ -50949,7 +51467,7 @@ class SheetUIPlugin extends UIPlugin {
50949
51467
  // Getters
50950
51468
  // ---------------------------------------------------------------------------
50951
51469
  getCellWidth(position) {
50952
- const style = this.getCellComputedStyle(position);
51470
+ const style = this.getters.getCellComputedStyle(position);
50953
51471
  let contentWidth = 0;
50954
51472
  const content = this.getters.getEvaluatedCell(position).formattedValue;
50955
51473
  if (content) {
@@ -51042,35 +51560,12 @@ class SheetUIPlugin extends UIPlugin {
51042
51560
  */
51043
51561
  isCellEmpty(position) {
51044
51562
  const mainPosition = this.getters.getMainCellPosition(position);
51045
- return !(this.getters.getCorrespondingFormulaCell(mainPosition) ||
51046
- this.getters.getCell(mainPosition)?.content);
51047
- }
51048
- getCellComputedBorder(position) {
51049
- const cellBorder = this.getters.getCellBorder(position) || {};
51050
- const cellTableBorder = this.getters.getCellTableBorder(position) || {};
51051
- // Use removeFalsyAttributes to avoid overwriting borders with undefined values
51052
- const border = { ...cellTableBorder, ...removeFalsyAttributes(cellBorder) };
51053
- return isObjectEmptyRecursive(border) ? null : border;
51054
- }
51055
- getCellComputedStyle(position) {
51056
- const cell = this.getters.getCell(position);
51057
- const cfStyle = this.getters.getCellConditionalFormatStyle(position);
51058
- const tableStyle = this.getters.getCellTableStyle(position);
51059
- const computedStyle = {
51060
- ...removeFalsyAttributes(tableStyle),
51061
- ...removeFalsyAttributes(cell?.style),
51062
- ...removeFalsyAttributes(cfStyle),
51063
- };
51064
- const evaluatedCell = this.getters.getEvaluatedCell(position);
51065
- if (evaluatedCell.link && !computedStyle.textColor) {
51066
- computedStyle.textColor = LINK_COLOR;
51067
- }
51068
- return computedStyle;
51563
+ return this.getters.getEvaluatedCell(mainPosition).type === CellValueType.empty;
51069
51564
  }
51070
51565
  getColMaxWidth(sheetId, index) {
51071
51566
  const cellsPositions = positions(this.getters.getColsZone(sheetId, index, index));
51072
51567
  const sizes = cellsPositions.map((position) => this.getCellWidth({ sheetId, ...position }));
51073
- return Math.max(0, ...sizes);
51568
+ return Math.max(0, largeMax(sizes));
51074
51569
  }
51075
51570
  /**
51076
51571
  * Check that any "sheetId" in the command matches an existing
@@ -51099,6 +51594,241 @@ class SheetUIPlugin extends UIPlugin {
51099
51594
  }
51100
51595
  }
51101
51596
 
51597
+ class TableStylePlugin extends UIPlugin {
51598
+ static getters = ["getCellTableStyle", "getCellTableBorder"];
51599
+ tableStyles = {};
51600
+ handle(cmd) {
51601
+ if (invalidateEvaluationCommands.has(cmd.type) ||
51602
+ (cmd.type === "UPDATE_CELL" && "content" in cmd) ||
51603
+ cmd.type === "EVALUATE_CELLS") {
51604
+ this.tableStyles = {};
51605
+ return;
51606
+ }
51607
+ if (doesCommandInvalidatesTableStyle(cmd)) {
51608
+ delete this.tableStyles[cmd.sheetId];
51609
+ return;
51610
+ }
51611
+ }
51612
+ finalize() {
51613
+ for (const sheetId of this.getters.getSheetIds()) {
51614
+ if (!this.tableStyles[sheetId]) {
51615
+ this.tableStyles[sheetId] = {};
51616
+ }
51617
+ for (const table of this.getters.getTables(sheetId)) {
51618
+ if (!this.tableStyles[sheetId][table.id]) {
51619
+ this.tableStyles[sheetId][table.id] = this.computeTableStyle(sheetId, table);
51620
+ }
51621
+ }
51622
+ }
51623
+ }
51624
+ getCellTableStyle(position) {
51625
+ const table = this.getters.getTable(position);
51626
+ if (!table) {
51627
+ return undefined;
51628
+ }
51629
+ return this.tableStyles[position.sheetId][table.id]().styles[position.col]?.[position.row];
51630
+ }
51631
+ getCellTableBorder(position) {
51632
+ const table = this.getters.getTable(position);
51633
+ if (!table) {
51634
+ return undefined;
51635
+ }
51636
+ return this.tableStyles[position.sheetId][table.id]().borders[position.col]?.[position.row];
51637
+ }
51638
+ computeTableStyle(sheetId, table) {
51639
+ return lazy(() => {
51640
+ const { config, numberOfCols, numberOfRows } = this.getTableRuntimeConfig(sheetId, table);
51641
+ const relativeTableStyle = getComputedTableStyle(config, numberOfCols, numberOfRows);
51642
+ // Return the style with sheet coordinates instead of tables coordinates
51643
+ const mapping = this.getTableMapping(sheetId, table);
51644
+ const absoluteTableStyle = { borders: {}, styles: {} };
51645
+ for (let col = 0; col < numberOfCols; col++) {
51646
+ const colInSheet = mapping.colMapping[col];
51647
+ absoluteTableStyle.borders[colInSheet] = {};
51648
+ absoluteTableStyle.styles[colInSheet] = {};
51649
+ for (let row = 0; row < numberOfRows; row++) {
51650
+ const rowInSheet = mapping.rowMapping[row];
51651
+ absoluteTableStyle.borders[colInSheet][rowInSheet] = relativeTableStyle.borders[col][row];
51652
+ absoluteTableStyle.styles[colInSheet][rowInSheet] = relativeTableStyle.styles[col][row];
51653
+ }
51654
+ }
51655
+ return absoluteTableStyle;
51656
+ });
51657
+ }
51658
+ /**
51659
+ * Get the actual table config that will be used to compute the table style. It is different from
51660
+ * the config of the table because of hidden rows and columns in the sheet. For example remove the
51661
+ * hidden rows from config.numberOfHeaders.
51662
+ */
51663
+ getTableRuntimeConfig(sheetId, table) {
51664
+ const tableZone = table.range.zone;
51665
+ const config = { ...table.config };
51666
+ let numberOfCols = tableZone.right - tableZone.left + 1;
51667
+ let numberOfRows = tableZone.bottom - tableZone.top + 1;
51668
+ for (let row = tableZone.top; row <= tableZone.bottom; row++) {
51669
+ if (!this.getters.isRowHidden(sheetId, row)) {
51670
+ continue;
51671
+ }
51672
+ numberOfRows--;
51673
+ if (row - tableZone.top < table.config.numberOfHeaders) {
51674
+ config.numberOfHeaders--;
51675
+ if (config.numberOfHeaders < 0) {
51676
+ config.numberOfHeaders = 0;
51677
+ }
51678
+ }
51679
+ if (row === tableZone.bottom) {
51680
+ config.totalRow = false;
51681
+ }
51682
+ }
51683
+ for (let col = tableZone.left; col <= tableZone.right; col++) {
51684
+ if (!this.getters.isColHidden(sheetId, col)) {
51685
+ continue;
51686
+ }
51687
+ numberOfCols--;
51688
+ if (col === tableZone.left) {
51689
+ config.firstColumn = false;
51690
+ }
51691
+ if (col === tableZone.right) {
51692
+ config.lastColumn = false;
51693
+ }
51694
+ }
51695
+ return {
51696
+ config,
51697
+ numberOfCols,
51698
+ numberOfRows,
51699
+ };
51700
+ }
51701
+ /**
51702
+ * Get a mapping: relative col/row position in the table <=> col/row in the sheet
51703
+ */
51704
+ getTableMapping(sheetId, table) {
51705
+ const colMapping = {};
51706
+ const rowMapping = {};
51707
+ let colOffset = 0;
51708
+ let rowOffset = 0;
51709
+ const tableZone = table.range.zone;
51710
+ for (let col = tableZone.left; col <= tableZone.right; col++) {
51711
+ if (this.getters.isColHidden(sheetId, col)) {
51712
+ continue;
51713
+ }
51714
+ colMapping[colOffset] = col;
51715
+ colOffset++;
51716
+ for (let row = tableZone.top; row <= tableZone.bottom; row++) {
51717
+ if (this.getters.isRowHidden(sheetId, row)) {
51718
+ continue;
51719
+ }
51720
+ rowMapping[rowOffset] = row;
51721
+ rowOffset++;
51722
+ }
51723
+ }
51724
+ return {
51725
+ colMapping,
51726
+ rowMapping,
51727
+ };
51728
+ }
51729
+ }
51730
+ const invalidateTableStyleCommands = [
51731
+ "HIDE_COLUMNS_ROWS",
51732
+ "UNHIDE_COLUMNS_ROWS",
51733
+ "UNFOLD_HEADER_GROUP",
51734
+ "FOLD_HEADER_GROUP",
51735
+ "FOLD_ALL_HEADER_GROUPS",
51736
+ "UNFOLD_ALL_HEADER_GROUPS",
51737
+ "CREATE_TABLE",
51738
+ "UPDATE_TABLE",
51739
+ "UPDATE_FILTER",
51740
+ "REMOVE_TABLE",
51741
+ ];
51742
+ const invalidateTableStyleCommandsSet = new Set(invalidateTableStyleCommands);
51743
+ function doesCommandInvalidatesTableStyle(cmd) {
51744
+ return invalidateTableStyleCommandsSet.has(cmd.type);
51745
+ }
51746
+
51747
+ class CellComputedStylePlugin extends UIPlugin {
51748
+ static getters = ["getCellComputedBorder", "getCellComputedStyle"];
51749
+ styles = {};
51750
+ borders = {};
51751
+ handle(cmd) {
51752
+ if (invalidateEvaluationCommands.has(cmd.type) ||
51753
+ cmd.type === "UPDATE_CELL" ||
51754
+ cmd.type === "EVALUATE_CELLS") {
51755
+ this.styles = {};
51756
+ this.borders = {};
51757
+ return;
51758
+ }
51759
+ if (doesCommandInvalidatesTableStyle(cmd)) {
51760
+ delete this.styles[cmd.sheetId];
51761
+ delete this.borders[cmd.sheetId];
51762
+ return;
51763
+ }
51764
+ if (invalidateCFEvaluationCommands.has(cmd.type)) {
51765
+ this.styles = {};
51766
+ return;
51767
+ }
51768
+ if (invalidateBordersCommands.has(cmd.type)) {
51769
+ this.borders = {};
51770
+ return;
51771
+ }
51772
+ }
51773
+ getCellComputedBorder(position) {
51774
+ const { sheetId, row, col } = position;
51775
+ if (this.borders[sheetId]?.[row]?.[col] !== undefined) {
51776
+ return this.borders[sheetId][row][col];
51777
+ }
51778
+ if (!this.borders[sheetId]) {
51779
+ this.borders[sheetId] = {};
51780
+ }
51781
+ if (!this.borders[sheetId][row]) {
51782
+ this.borders[sheetId][row] = {};
51783
+ }
51784
+ if (!this.borders[sheetId][row][col]) {
51785
+ this.borders[sheetId][row][col] = this.computeCellBorder(position);
51786
+ }
51787
+ return this.borders[sheetId][row][col];
51788
+ }
51789
+ getCellComputedStyle(position) {
51790
+ const { sheetId, row, col } = position;
51791
+ if (this.styles[sheetId]?.[row]?.[col] !== undefined) {
51792
+ return this.styles[sheetId][row][col];
51793
+ }
51794
+ if (!this.styles[sheetId]) {
51795
+ this.styles[sheetId] = {};
51796
+ }
51797
+ if (!this.styles[sheetId][row]) {
51798
+ this.styles[sheetId][row] = {};
51799
+ }
51800
+ if (!this.styles[sheetId][row][col]) {
51801
+ this.styles[sheetId][row][col] = this.computeCellStyle(position);
51802
+ }
51803
+ return this.styles[sheetId][row][col];
51804
+ }
51805
+ computeCellBorder(position) {
51806
+ const cellBorder = this.getters.getCellBorder(position) || {};
51807
+ const cellTableBorder = this.getters.getCellTableBorder(position) || {};
51808
+ // Use removeFalsyAttributes to avoid overwriting borders with undefined values
51809
+ const border = {
51810
+ ...removeFalsyAttributes(cellTableBorder),
51811
+ ...removeFalsyAttributes(cellBorder),
51812
+ };
51813
+ return isObjectEmptyRecursive(border) ? null : border;
51814
+ }
51815
+ computeCellStyle(position) {
51816
+ const cell = this.getters.getCell(position);
51817
+ const cfStyle = this.getters.getCellConditionalFormatStyle(position);
51818
+ const tableStyle = this.getters.getCellTableStyle(position);
51819
+ const computedStyle = {
51820
+ ...removeFalsyAttributes(tableStyle),
51821
+ ...removeFalsyAttributes(cell?.style),
51822
+ ...removeFalsyAttributes(cfStyle),
51823
+ };
51824
+ const evaluatedCell = this.getters.getEvaluatedCell(position);
51825
+ if (evaluatedCell.link && !computedStyle.textColor) {
51826
+ computedStyle.textColor = LINK_COLOR;
51827
+ }
51828
+ return computedStyle;
51829
+ }
51830
+ }
51831
+
51102
51832
  const genericRepeatsTransforms = [
51103
51833
  repeatSheetDependantCommand,
51104
51834
  repeatTargetDependantCommand,
@@ -51676,148 +52406,6 @@ class TableAutofillPlugin extends UIPlugin {
51676
52406
  }
51677
52407
  }
51678
52408
 
51679
- class TableStylePlugin extends UIPlugin {
51680
- static getters = ["getCellTableStyle", "getCellTableBorder"];
51681
- tableStyles = {};
51682
- handle(cmd) {
51683
- if (invalidateEvaluationCommands.has(cmd.type) ||
51684
- (cmd.type === "UPDATE_CELL" && "content" in cmd) ||
51685
- cmd.type === "EVALUATE_CELLS") {
51686
- this.tableStyles = {};
51687
- return;
51688
- }
51689
- switch (cmd.type) {
51690
- case "HIDE_COLUMNS_ROWS":
51691
- case "UNHIDE_COLUMNS_ROWS":
51692
- case "UNFOLD_HEADER_GROUP":
51693
- case "FOLD_HEADER_GROUP":
51694
- case "FOLD_ALL_HEADER_GROUPS":
51695
- case "UNFOLD_ALL_HEADER_GROUPS":
51696
- case "UPDATE_TABLE":
51697
- case "UPDATE_FILTER":
51698
- delete this.tableStyles[cmd.sheetId];
51699
- break;
51700
- }
51701
- }
51702
- finalize() {
51703
- for (const sheetId of this.getters.getSheetIds()) {
51704
- if (!this.tableStyles[sheetId]) {
51705
- this.tableStyles[sheetId] = {};
51706
- }
51707
- for (const table of this.getters.getTables(sheetId)) {
51708
- if (!this.tableStyles[sheetId][table.id]) {
51709
- this.tableStyles[sheetId][table.id] = this.computeTableStyle(sheetId, table);
51710
- }
51711
- }
51712
- }
51713
- }
51714
- getCellTableStyle(position) {
51715
- const table = this.getters.getTable(position);
51716
- if (!table) {
51717
- return undefined;
51718
- }
51719
- return this.tableStyles[position.sheetId][table.id]().styles[position.col]?.[position.row];
51720
- }
51721
- getCellTableBorder(position) {
51722
- const table = this.getters.getTable(position);
51723
- if (!table) {
51724
- return undefined;
51725
- }
51726
- return this.tableStyles[position.sheetId][table.id]().borders[position.col]?.[position.row];
51727
- }
51728
- computeTableStyle(sheetId, table) {
51729
- return lazy(() => {
51730
- const { config, numberOfCols, numberOfRows } = this.getTableRuntimeConfig(sheetId, table);
51731
- const relativeTableStyle = getComputedTableStyle(config, numberOfCols, numberOfRows);
51732
- // Return the style with sheet coordinates instead of tables coordinates
51733
- const mapping = this.getTableMapping(sheetId, table);
51734
- const absoluteTableStyle = { borders: {}, styles: {} };
51735
- for (let col = 0; col < numberOfCols; col++) {
51736
- const colInSheet = mapping.colMapping[col];
51737
- absoluteTableStyle.borders[colInSheet] = {};
51738
- absoluteTableStyle.styles[colInSheet] = {};
51739
- for (let row = 0; row < numberOfRows; row++) {
51740
- const rowInSheet = mapping.rowMapping[row];
51741
- absoluteTableStyle.borders[colInSheet][rowInSheet] = relativeTableStyle.borders[col][row];
51742
- absoluteTableStyle.styles[colInSheet][rowInSheet] = relativeTableStyle.styles[col][row];
51743
- }
51744
- }
51745
- return absoluteTableStyle;
51746
- });
51747
- }
51748
- /**
51749
- * Get the actual table config that will be used to compute the table style. It is different from
51750
- * the config of the table because of hidden rows and columns in the sheet. For example remove the
51751
- * hidden rows from config.numberOfHeaders.
51752
- */
51753
- getTableRuntimeConfig(sheetId, table) {
51754
- const tableZone = table.range.zone;
51755
- const config = { ...table.config };
51756
- let numberOfCols = tableZone.right - tableZone.left + 1;
51757
- let numberOfRows = tableZone.bottom - tableZone.top + 1;
51758
- for (let row = tableZone.top; row <= tableZone.bottom; row++) {
51759
- if (!this.getters.isRowHidden(sheetId, row)) {
51760
- continue;
51761
- }
51762
- numberOfRows--;
51763
- if (row - tableZone.top < table.config.numberOfHeaders) {
51764
- config.numberOfHeaders--;
51765
- if (config.numberOfHeaders < 0) {
51766
- config.numberOfHeaders = 0;
51767
- }
51768
- }
51769
- if (row === tableZone.bottom) {
51770
- config.totalRow = false;
51771
- }
51772
- }
51773
- for (let col = tableZone.left; col <= tableZone.right; col++) {
51774
- if (!this.getters.isColHidden(sheetId, col)) {
51775
- continue;
51776
- }
51777
- numberOfCols--;
51778
- if (col === tableZone.left) {
51779
- config.firstColumn = false;
51780
- }
51781
- if (col === tableZone.right) {
51782
- config.lastColumn = false;
51783
- }
51784
- }
51785
- return {
51786
- config,
51787
- numberOfCols,
51788
- numberOfRows,
51789
- };
51790
- }
51791
- /**
51792
- * Get a mapping: relative col/row position in the table <=> col/row in the sheet
51793
- */
51794
- getTableMapping(sheetId, table) {
51795
- const colMapping = {};
51796
- const rowMapping = {};
51797
- let colOffset = 0;
51798
- let rowOffset = 0;
51799
- const tableZone = table.range.zone;
51800
- for (let col = tableZone.left; col <= tableZone.right; col++) {
51801
- if (this.getters.isColHidden(sheetId, col)) {
51802
- continue;
51803
- }
51804
- colMapping[colOffset] = col;
51805
- colOffset++;
51806
- for (let row = tableZone.top; row <= tableZone.bottom; row++) {
51807
- if (this.getters.isRowHidden(sheetId, row)) {
51808
- continue;
51809
- }
51810
- rowMapping[rowOffset] = row;
51811
- rowOffset++;
51812
- }
51813
- }
51814
- return {
51815
- colMapping,
51816
- rowMapping,
51817
- };
51818
- }
51819
- }
51820
-
51821
52409
  /**
51822
52410
  * Clipboard Plugin
51823
52411
  *
@@ -52527,38 +53115,6 @@ class FilterEvaluationPlugin extends UIPlugin {
52527
53115
  }
52528
53116
  }
52529
53117
 
52530
- const selectionStatisticFunctions = [
52531
- {
52532
- name: _t("Sum"),
52533
- types: [CellValueType.number],
52534
- compute: (values, locale) => sum([[values]], locale),
52535
- },
52536
- {
52537
- name: _t("Avg"),
52538
- types: [CellValueType.number],
52539
- compute: (values, locale) => average([[values]], locale),
52540
- },
52541
- {
52542
- name: _t("Min"),
52543
- types: [CellValueType.number],
52544
- compute: (values, locale) => min([[values]], locale),
52545
- },
52546
- {
52547
- name: _t("Max"),
52548
- types: [CellValueType.number],
52549
- compute: (values, locale) => max([[values]], locale),
52550
- },
52551
- {
52552
- name: _t("Count"),
52553
- types: [CellValueType.number, CellValueType.text, CellValueType.boolean, CellValueType.error],
52554
- compute: (values) => countAny([[values]]),
52555
- },
52556
- {
52557
- name: _t("Count Numbers"),
52558
- types: [CellValueType.number, CellValueType.text, CellValueType.boolean, CellValueType.error],
52559
- compute: (values, locale) => countNumbers([[values]], locale),
52560
- },
52561
- ];
52562
53118
  /**
52563
53119
  * SelectionPlugin
52564
53120
  */
@@ -52574,8 +53130,6 @@ class GridSelectionPlugin extends UIPlugin {
52574
53130
  "getSelectedZones",
52575
53131
  "getSelectedZone",
52576
53132
  "getSelectedCells",
52577
- "getStatisticFnResults",
52578
- "getAggregate",
52579
53133
  "getSelectedFigureId",
52580
53134
  "getSelection",
52581
53135
  "getActivePosition",
@@ -52610,7 +53164,10 @@ class GridSelectionPlugin extends UIPlugin {
52610
53164
  switch (cmd.type) {
52611
53165
  case "ACTIVATE_SHEET":
52612
53166
  try {
52613
- this.getters.getSheet(cmd.sheetIdTo);
53167
+ const sheet = this.getters.getSheet(cmd.sheetIdTo);
53168
+ if (!sheet.isVisible) {
53169
+ return "SheetIsHidden" /* CommandResult.SheetIsHidden */;
53170
+ }
52614
53171
  break;
52615
53172
  }
52616
53173
  catch (error) {
@@ -52859,52 +53416,6 @@ class GridSelectionPlugin extends UIPlugin {
52859
53416
  : this.getters.getNextVisibleCellPosition({ sheetId, col: 0, row: 0 });
52860
53417
  }
52861
53418
  }
52862
- getStatisticFnResults() {
52863
- const sheetId = this.getters.getActiveSheetId();
52864
- const cells = new Set();
52865
- for (const zone of this.gridSelection.zones) {
52866
- for (const { col, row } of positions(zone)) {
52867
- if (this.getters.isRowHidden(sheetId, row) || this.getters.isColHidden(sheetId, col)) {
52868
- continue; // Skip hidden cells
52869
- }
52870
- const evaluatedCell = this.getters.getEvaluatedCell({ sheetId, col, row });
52871
- if (evaluatedCell.type !== CellValueType.empty) {
52872
- cells.add(evaluatedCell);
52873
- }
52874
- }
52875
- }
52876
- const locale = this.getters.getLocale();
52877
- let statisticFnResults = {};
52878
- for (let fn of selectionStatisticFunctions) {
52879
- // We don't want to display statistical information when there is no interest:
52880
- // We set the statistical result to undefined if the data handled by the selection
52881
- // does not match the data handled by the function.
52882
- // Ex: if there are only texts in the selection, we prefer that the SUM result
52883
- // be displayed as undefined rather than 0.
52884
- let fnResult = undefined;
52885
- const evaluatedCells = [...cells].filter((c) => fn.types.includes(c.type));
52886
- if (evaluatedCells.length) {
52887
- fnResult = fn.compute(evaluatedCells, locale);
52888
- }
52889
- statisticFnResults[fn.name] = fnResult;
52890
- }
52891
- return statisticFnResults;
52892
- }
52893
- getAggregate() {
52894
- let aggregate = 0;
52895
- let n = 0;
52896
- const sheetId = this.getters.getActiveSheetId();
52897
- const cellPositions = this.gridSelection.zones.map(positions).flat();
52898
- for (const { col, row } of cellPositions) {
52899
- const cell = this.getters.getEvaluatedCell({ sheetId, col, row });
52900
- if (cell.type === CellValueType.number) {
52901
- n++;
52902
- aggregate += cell.value;
52903
- }
52904
- }
52905
- const locale = this.getters.getLocale();
52906
- return n < 2 ? null : formatValue(aggregate, { locale });
52907
- }
52908
53419
  isSelected(zone) {
52909
53420
  return !!this.getters.getSelectedZones().find((z) => isEqual(z, zone));
52910
53421
  }
@@ -52946,9 +53457,6 @@ class GridSelectionPlugin extends UIPlugin {
52946
53457
  // Other
52947
53458
  // ---------------------------------------------------------------------------
52948
53459
  activateSheet(sheetIdFrom, sheetIdTo) {
52949
- if (!this.getters.isSheetVisible(sheetIdTo)) {
52950
- this.dispatch("SHOW_SHEET", { sheetId: sheetIdTo });
52951
- }
52952
53460
  this.setActiveSheet(sheetIdTo);
52953
53461
  this.sheetsData[sheetIdFrom] = {
52954
53462
  gridSelection: deepCopy(this.gridSelection),
@@ -53955,7 +54463,7 @@ class SheetViewPlugin extends UIPlugin {
53955
54463
  * column of the current viewport
53956
54464
  */
53957
54465
  getColDimensionsInViewport(sheetId, col) {
53958
- const left = Math.min(...this.getters.getSheetViewVisibleCols());
54466
+ const left = largeMin(this.getters.getSheetViewVisibleCols());
53959
54467
  const start = this.getters.getColRowOffsetInViewport("COL", left, col);
53960
54468
  const size = this.getters.getColSize(sheetId, col);
53961
54469
  const isColHidden = this.getters.isColHidden(sheetId, col);
@@ -53970,7 +54478,7 @@ class SheetViewPlugin extends UIPlugin {
53970
54478
  * of the current viewport
53971
54479
  */
53972
54480
  getRowDimensionsInViewport(sheetId, row) {
53973
- const top = Math.min(...this.getters.getSheetViewVisibleRows());
54481
+ const top = largeMin(this.getters.getSheetViewVisibleRows());
53974
54482
  const start = this.getters.getColRowOffsetInViewport("ROW", top, row);
53975
54483
  const size = this.getters.getRowSize(sheetId, row);
53976
54484
  const isRowHidden = this.getters.isRowHidden(sheetId, row);
@@ -54322,6 +54830,7 @@ const statefulUIPluginRegistry = new Registry()
54322
54830
  .add("evaluation_filter", FilterEvaluationPlugin)
54323
54831
  .add("header_visibility_ui", HeaderVisibilityUIPlugin)
54324
54832
  .add("table_style", TableStylePlugin)
54833
+ .add("cell_computed_style", CellComputedStylePlugin)
54325
54834
  .add("header_positions", HeaderPositionsUIPlugin)
54326
54835
  .add("viewport", SheetViewPlugin)
54327
54836
  .add("clipboard", ClipboardPlugin);
@@ -54382,6 +54891,38 @@ class ImageProvider {
54382
54891
  }
54383
54892
  }
54384
54893
 
54894
+ class ArrayFormulaHighlight extends SpreadsheetStore {
54895
+ highlightStore = this.get(HighlightStore);
54896
+ constructor(get) {
54897
+ super(get);
54898
+ this.highlightStore.register(this);
54899
+ }
54900
+ get highlights() {
54901
+ const zone = this.getHighlightZone();
54902
+ if (!zone) {
54903
+ return [];
54904
+ }
54905
+ const sheetId = this.model.getters.getActiveSheetId();
54906
+ return [
54907
+ {
54908
+ sheetId,
54909
+ zone,
54910
+ color: "#17A2B8",
54911
+ noFill: true,
54912
+ thinLine: true,
54913
+ },
54914
+ ];
54915
+ }
54916
+ getHighlightZone() {
54917
+ const position = this.model.getters.getActivePosition();
54918
+ const spreader = this.model.getters.getArrayFormulaSpreadingOn(position);
54919
+ const spreadZone = spreader
54920
+ ? this.model.getters.getSpreadZone(spreader)
54921
+ : this.model.getters.getSpreadZone(position);
54922
+ return spreadZone;
54923
+ }
54924
+ }
54925
+
54385
54926
  const RIPPLE_KEY_FRAMES = [
54386
54927
  { transform: "scale(0)" },
54387
54928
  { transform: "scale(0.8)", offset: 0.33 },
@@ -54684,12 +55225,14 @@ class BottomBarSheet extends owl.Component {
54684
55225
  this.editionState = "initializing";
54685
55226
  }
54686
55227
  stopEdition() {
54687
- if (!this.state.isEditing)
55228
+ const input = this.sheetNameRef.el;
55229
+ if (!this.state.isEditing || !input)
54688
55230
  return;
54689
55231
  this.state.isEditing = false;
54690
55232
  this.editionState = "initializing";
54691
- this.sheetNameRef.el?.blur();
55233
+ input.blur();
54692
55234
  const inputValue = this.getInputContent() || "";
55235
+ input.innerText = inputValue;
54693
55236
  interactiveRenameSheet(this.env, this.props.sheetId, inputValue, () => this.startEdition());
54694
55237
  }
54695
55238
  cancelEdition() {
@@ -54733,6 +55276,115 @@ class BottomBarSheet extends owl.Component {
54733
55276
  }
54734
55277
  }
54735
55278
 
55279
+ const selectionStatisticFunctions = [
55280
+ {
55281
+ name: _t("Sum"),
55282
+ types: [CellValueType.number],
55283
+ compute: (values, locale) => sum([[values]], locale),
55284
+ },
55285
+ {
55286
+ name: _t("Avg"),
55287
+ types: [CellValueType.number],
55288
+ compute: (values, locale) => average([[values]], locale),
55289
+ },
55290
+ {
55291
+ name: _t("Min"),
55292
+ types: [CellValueType.number],
55293
+ compute: (values, locale) => min([[values]], locale),
55294
+ },
55295
+ {
55296
+ name: _t("Max"),
55297
+ types: [CellValueType.number],
55298
+ compute: (values, locale) => max([[values]], locale),
55299
+ },
55300
+ {
55301
+ name: _t("Count"),
55302
+ types: [CellValueType.number, CellValueType.text, CellValueType.boolean, CellValueType.error],
55303
+ compute: (values) => countAny([[values]]),
55304
+ },
55305
+ {
55306
+ name: _t("Count Numbers"),
55307
+ types: [CellValueType.number, CellValueType.text, CellValueType.boolean, CellValueType.error],
55308
+ compute: (values, locale) => countNumbers([[values]], locale),
55309
+ },
55310
+ ];
55311
+ class AggregateStatisticsStore extends SpreadsheetStore {
55312
+ statisticFnResults = this._computeStatisticFnResults();
55313
+ isDirty = false;
55314
+ constructor(get) {
55315
+ super(get);
55316
+ this.model.selection.observe(this, {
55317
+ handleEvent: this.handleEvent.bind(this),
55318
+ });
55319
+ }
55320
+ handle(cmd) {
55321
+ if (invalidateEvaluationCommands.has(cmd.type) ||
55322
+ (cmd.type === "UPDATE_CELL" && "content" in cmd)) {
55323
+ this.isDirty = true;
55324
+ }
55325
+ switch (cmd.type) {
55326
+ case "HIDE_COLUMNS_ROWS":
55327
+ case "UNHIDE_COLUMNS_ROWS":
55328
+ case "GROUP_HEADERS":
55329
+ case "UNGROUP_HEADERS":
55330
+ case "ACTIVATE_SHEET":
55331
+ case "ACTIVATE_NEXT_SHEET":
55332
+ case "ACTIVATE_PREVIOUS_SHEET":
55333
+ case "EVALUATE_CELLS":
55334
+ case "UNDO":
55335
+ case "REDO":
55336
+ this.isDirty = true;
55337
+ }
55338
+ }
55339
+ finalize() {
55340
+ if (this.isDirty) {
55341
+ this.isDirty = false;
55342
+ this.statisticFnResults = this._computeStatisticFnResults();
55343
+ }
55344
+ }
55345
+ handleEvent() {
55346
+ if (this.getters.isGridSelectionActive()) {
55347
+ this.statisticFnResults = this._computeStatisticFnResults();
55348
+ }
55349
+ }
55350
+ _computeStatisticFnResults() {
55351
+ const getters = this.getters;
55352
+ const sheetId = getters.getActiveSheetId();
55353
+ const cells = new Set();
55354
+ const zones = getters.getSelectedZones();
55355
+ for (const zone of zones) {
55356
+ for (let col = zone.left; col <= zone.right; col++) {
55357
+ for (let row = zone.top; row <= zone.bottom; row++) {
55358
+ if (getters.isRowHidden(sheetId, row) || getters.isColHidden(sheetId, col)) {
55359
+ continue; // Skip hidden cells
55360
+ }
55361
+ const evaluatedCell = getters.getEvaluatedCell({ sheetId, col, row });
55362
+ if (evaluatedCell.type !== CellValueType.empty) {
55363
+ cells.add(evaluatedCell);
55364
+ }
55365
+ }
55366
+ }
55367
+ }
55368
+ const locale = getters.getLocale();
55369
+ let statisticFnResults = {};
55370
+ const cellsArray = [...cells];
55371
+ for (let fn of selectionStatisticFunctions) {
55372
+ // We don't want to display statistical information when there is no interest:
55373
+ // We set the statistical result to undefined if the data handled by the selection
55374
+ // does not match the data handled by the function.
55375
+ // Ex: if there are only texts in the selection, we prefer that the SUM result
55376
+ // be displayed as undefined rather than 0.
55377
+ let fnResult = undefined;
55378
+ const evaluatedCells = cellsArray.filter((c) => fn.types.includes(c.type));
55379
+ if (evaluatedCells.length) {
55380
+ fnResult = fn.compute(evaluatedCells, locale);
55381
+ }
55382
+ statisticFnResults[fn.name] = fnResult;
55383
+ }
55384
+ return statisticFnResults;
55385
+ }
55386
+ }
55387
+
54736
55388
  // -----------------------------------------------------------------------------
54737
55389
  // SpreadSheet
54738
55390
  // -----------------------------------------------------------------------------
@@ -54748,40 +55400,38 @@ css /* scss */ `
54748
55400
  }
54749
55401
  `;
54750
55402
  class BottomBarStatistic extends owl.Component {
54751
- static template = "o-spreadsheet-BottomBarStatisic";
55403
+ static template = "o-spreadsheet-BottomBarStatistic";
54752
55404
  static props = {
54753
55405
  openContextMenu: Function,
54754
55406
  closeContextMenu: Function,
54755
55407
  };
54756
55408
  static components = { Ripple };
54757
55409
  selectedStatisticFn = "";
54758
- statisticFnResults = {};
55410
+ store;
54759
55411
  setup() {
54760
- this.statisticFnResults = this.env.model.getters.getStatisticFnResults();
55412
+ this.store = useStore(AggregateStatisticsStore);
54761
55413
  owl.onWillUpdateProps(() => {
54762
- const newStatisticFnResults = this.env.model.getters.getStatisticFnResults();
54763
- if (!deepEquals(newStatisticFnResults, this.statisticFnResults)) {
55414
+ if (Object.values(this.store.statisticFnResults).every((result) => result === undefined)) {
54764
55415
  this.props.closeContextMenu();
54765
55416
  }
54766
- this.statisticFnResults = newStatisticFnResults;
54767
55417
  });
54768
55418
  }
54769
55419
  getSelectedStatistic() {
54770
55420
  // don't display button if no function has a result
54771
- if (Object.values(this.statisticFnResults).every((result) => result === undefined)) {
55421
+ if (Object.values(this.store.statisticFnResults).every((result) => result === undefined)) {
54772
55422
  return undefined;
54773
55423
  }
54774
55424
  if (this.selectedStatisticFn === "") {
54775
- this.selectedStatisticFn = Object.keys(this.statisticFnResults)[0];
55425
+ this.selectedStatisticFn = Object.keys(this.store.statisticFnResults)[0];
54776
55426
  }
54777
- return this.getComposedFnName(this.selectedStatisticFn, this.statisticFnResults[this.selectedStatisticFn]);
55427
+ return this.getComposedFnName(this.selectedStatisticFn);
54778
55428
  }
54779
55429
  listSelectionStatistics(ev) {
54780
55430
  const registry = new MenuItemRegistry();
54781
55431
  let i = 0;
54782
- for (let [fnName, fnValue] of Object.entries(this.statisticFnResults)) {
55432
+ for (let [fnName] of Object.entries(this.store.statisticFnResults)) {
54783
55433
  registry.add(fnName, {
54784
- name: this.getComposedFnName(fnName, fnValue),
55434
+ name: () => this.getComposedFnName(fnName),
54785
55435
  sequence: i,
54786
55436
  isReadonlyAllowed: true,
54787
55437
  execute: () => {
@@ -54794,8 +55444,9 @@ class BottomBarStatistic extends owl.Component {
54794
55444
  const { top, left, width } = target.getBoundingClientRect();
54795
55445
  this.props.openContextMenu(left + width, top, registry);
54796
55446
  }
54797
- getComposedFnName(fnName, fnValue) {
55447
+ getComposedFnName(fnName) {
54798
55448
  const locale = this.env.model.getters.getLocale();
55449
+ const fnValue = this.store.statisticFnResults[fnName];
54799
55450
  return fnName + ": " + (fnValue !== undefined ? formatValue(fnValue, { locale }) : "__");
54800
55451
  }
54801
55452
  }
@@ -54904,10 +55555,14 @@ class BottomBar extends owl.Component {
54904
55555
  name: sheet.name,
54905
55556
  sequence: i,
54906
55557
  isReadonlyAllowed: true,
54907
- textColor: sheet.isVisible ? undefined : "grey",
55558
+ textColor: sheet.isVisible ? undefined : "#808080",
54908
55559
  execute: (env) => {
55560
+ if (!this.env.model.getters.isSheetVisible(sheetId)) {
55561
+ this.env.model.dispatch("SHOW_SHEET", { sheetId });
55562
+ }
54909
55563
  env.model.dispatch("ACTIVATE_SHEET", { sheetIdFrom: from, sheetIdTo: sheetId });
54910
55564
  },
55565
+ isEnabled: (env) => (env.model.getters.isReadonly() ? sheet.isVisible : true),
54911
55566
  });
54912
55567
  i++;
54913
55568
  }
@@ -54980,7 +55635,7 @@ class BottomBar extends owl.Component {
54980
55635
  this.sheetListRef.el.scrollTo({ top: 0, left: scroll, behavior: "smooth" });
54981
55636
  }
54982
55637
  onSheetMouseDown(sheetId, event) {
54983
- if (event.button !== 0)
55638
+ if (event.button !== 0 || this.env.model.getters.isReadonly())
54984
55639
  return;
54985
55640
  this.closeMenu();
54986
55641
  const visibleSheets = this.getVisibleSheets();
@@ -55016,7 +55671,7 @@ class BottomBar extends owl.Component {
55016
55671
  .map((sheetEl) => sheetEl.getBoundingClientRect())
55017
55672
  .map((rect) => ({
55018
55673
  x: rect.x,
55019
- width: rect.width - 1,
55674
+ width: rect.width - 1, // -1 to compensate negative margin
55020
55675
  y: rect.y,
55021
55676
  height: rect.height,
55022
55677
  }));
@@ -55054,7 +55709,6 @@ class SpreadsheetDashboard extends owl.Component {
55054
55709
  Popover,
55055
55710
  VerticalScrollBar,
55056
55711
  HorizontalScrollBar,
55057
- FilterIconsOverlay,
55058
55712
  };
55059
55713
  cellPopovers;
55060
55714
  onMouseWheel;
@@ -55243,7 +55897,7 @@ class RowGroup extends AbstractHeaderGroup {
55243
55897
  }
55244
55898
  return cssPropertiesToCss({
55245
55899
  top: `${groupBox.headerRect.height / 2}px`,
55246
- left: `calc(50% - 1px)`,
55900
+ left: `calc(50% - 1px)`, // -1px: we want the border to be on the center
55247
55901
  width: `30%`,
55248
55902
  height: `calc(100% - ${groupBox.headerRect.height / 2}px)`,
55249
55903
  "border-left": `1px solid ${HEADER_GROUPING_BORDER_COLOR}`,
@@ -55295,7 +55949,7 @@ class ColGroup extends AbstractHeaderGroup {
55295
55949
  return "";
55296
55950
  }
55297
55951
  return cssPropertiesToCss({
55298
- top: `calc(50% - 1px)`,
55952
+ top: `calc(50% - 1px)`, // -1px: we want the border to be on the center
55299
55953
  left: `${groupBox.headerRect.width / 2}px`,
55300
55954
  width: `calc(100% - ${groupBox.headerRect.width / 2}px)`,
55301
55955
  height: `30%`,
@@ -55941,6 +56595,13 @@ class TopBarComposer extends owl.Component {
55941
56595
  "border-color": SELECTION_BORDER_COLOR,
55942
56596
  });
55943
56597
  }
56598
+ get delimitation() {
56599
+ const { width, height } = this.env.model.getters.getSheetViewDimensionWithHeaders();
56600
+ return {
56601
+ width,
56602
+ height,
56603
+ };
56604
+ }
55944
56605
  onFocus(selection) {
55945
56606
  this.composerFocusStore.focusTopBarComposer(selection);
55946
56607
  }
@@ -56386,7 +57047,7 @@ css /* scss */ `
56386
57047
  }
56387
57048
  .o-disabled {
56388
57049
  opacity: 0.4;
56389
- pointer: default;
57050
+ cursor: default;
56390
57051
  pointer-events: none;
56391
57052
  }
56392
57053
 
@@ -56552,7 +57213,7 @@ css /* scss */ `
56552
57213
 
56553
57214
  .o-number-input {
56554
57215
  /* Remove number input arrows */
56555
- -moz-appearance: textfield;
57216
+ appearance: textfield;
56556
57217
  &::-webkit-outer-spin-button,
56557
57218
  &::-webkit-inner-spin-button {
56558
57219
  -webkit-appearance: none;
@@ -56595,6 +57256,7 @@ class Spreadsheet extends owl.Component {
56595
57256
  this.notificationStore = useStore(NotificationStore);
56596
57257
  this.composerFocusStore = useStore(ComposerFocusStore);
56597
57258
  this.sidePanel = useStore(SidePanelStore);
57259
+ useStore(ArrayFormulaHighlight);
56598
57260
  this.keyDownMapping = {
56599
57261
  "CTRL+H": () => this.sidePanel.toggle("FindAndReplace", {}),
56600
57262
  "CTRL+F": () => this.sidePanel.toggle("FindAndReplace", {}),
@@ -56697,7 +57359,7 @@ class Spreadsheet extends owl.Component {
56697
57359
  const gridColSize = GROUP_LAYER_WIDTH * this.rowLayers.length;
56698
57360
  const gridRowSize = GROUP_LAYER_WIDTH * this.colLayers.length;
56699
57361
  return cssPropertiesToCss({
56700
- "grid-template-columns": `${gridColSize ? gridColSize + 2 : 0}px auto`,
57362
+ "grid-template-columns": `${gridColSize ? gridColSize + 2 : 0}px auto`, // +2: margins
56701
57363
  "grid-template-rows": `${gridRowSize ? gridRowSize + 2 : 0}px auto`,
56702
57364
  });
56703
57365
  }
@@ -57429,14 +58091,6 @@ class SelectiveHistory {
57429
58091
  this.revertBefore(operationId);
57430
58092
  this.tree.drop(operationId);
57431
58093
  }
57432
- getRevertedExecution() {
57433
- const data = [];
57434
- const operations = this.tree.revertedExecution(this.HEAD_BRANCH);
57435
- for (const { operation } of operations) {
57436
- data.push(operation.data);
57437
- }
57438
- return data;
57439
- }
57440
58094
  /**
57441
58095
  * Revert the state as it was *before* the given operation was executed.
57442
58096
  */
@@ -58225,9 +58879,15 @@ function createChart(chart, chartSheetIndex, data) {
58225
58879
  case "bar":
58226
58880
  plot = addBarChart(chart.data);
58227
58881
  break;
58882
+ case "combo":
58883
+ plot = addComboChart(chart.data);
58884
+ break;
58228
58885
  case "line":
58229
58886
  plot = addLineChart(chart.data);
58230
58887
  break;
58888
+ case "scatter":
58889
+ plot = addScatterChart(chart.data);
58890
+ break;
58231
58891
  case "pie":
58232
58892
  plot = addDoughnutChart(chart.data, chartSheetIndex, data, { holeSize: 0 });
58233
58893
  break;
@@ -58387,6 +59047,79 @@ function addBarChart(chart) {
58387
59047
  ${addAx(axisPos, "c:valAx", valAxId, catAxId, { fontColor: chart.fontColor })}
58388
59048
  `;
58389
59049
  }
59050
+ function addComboChart(chart) {
59051
+ // gapWitdh and overlap that define the space between clusters (in %) and the overlap between datasets (from -100: completely scattered to 100, completely overlapped)
59052
+ // see gapWidth : https://c-rex.net/projects/samples/ooxml/e1/Part4/OOXML_P4_DOCX_gapWidth_topic_ID0EFVEQB.html#topic_ID0EFVEQB
59053
+ // see overlap : https://c-rex.net/projects/samples/ooxml/e1/Part4/OOXML_P4_DOCX_overlap_topic_ID0ELYQQB.html#topic_ID0ELYQQB
59054
+ //
59055
+ // overlap and gapWitdh seems to be by default at -20 and 20 in chart.js.
59056
+ // See https://www.chartjs.org/docs/latest/charts/bar.html and https://www.chartjs.org/docs/latest/charts/bar.html#barpercentage-vs-categorypercentage
59057
+ const colors = new ChartColors();
59058
+ const dataSetsNodes = [];
59059
+ for (const [dsIndex, dataset] of Object.entries(chart.dataSets)) {
59060
+ const color = toXlsxHexColor(colors.next());
59061
+ const dataShapeProperty = shapeProperty({
59062
+ backgroundColor: color,
59063
+ line: { color },
59064
+ });
59065
+ dataSetsNodes.push(dsIndex === "0"
59066
+ ? escapeXml /*xml*/ `
59067
+ <c:ser>
59068
+ <c:idx val="${dsIndex}"/>
59069
+ <c:order val="${dsIndex}"/>
59070
+ ${dataset.label ? escapeXml /*xml*/ `<c:tx>${stringRef(dataset.label)}</c:tx>` : ""}
59071
+ ${dataShapeProperty}
59072
+ ${chart.labelRange ? escapeXml /*xml*/ `<c:cat>${stringRef(chart.labelRange)}</c:cat>` : ""} <!-- x-coordinate values -->
59073
+ <c:val> <!-- x-coordinate values -->
59074
+ ${numberRef(dataset.range)}
59075
+ </c:val>
59076
+ </c:ser>
59077
+ `
59078
+ : escapeXml /*xml*/ `
59079
+ <c:ser>
59080
+ <c:idx val="${dsIndex}"/>
59081
+ <c:order val="${dsIndex}"/>
59082
+ <c:smooth val="0"/>
59083
+ <c:marker>
59084
+ <c:symbol val="circle" />
59085
+ <c:size val="5"/>
59086
+ </c:marker>
59087
+ ${dataset.label ? escapeXml `<c:tx>${stringRef(dataset.label)}</c:tx>` : ""}
59088
+ ${dataShapeProperty}
59089
+ ${chart.labelRange ? escapeXml `<c:cat>${stringRef(chart.labelRange)}</c:cat>` : ""} <!-- x-coordinate values -->
59090
+ <c:val> <!-- x-coordinate values -->
59091
+ ${numberRef(dataset.range)}
59092
+ </c:val>
59093
+ </c:ser>
59094
+ `);
59095
+ }
59096
+ // Excel does not support this feature
59097
+ const axisPos = chart.verticalAxisPosition === "left" ? "l" : "r";
59098
+ const overlap = chart.stacked ? 100 : -20;
59099
+ return escapeXml /*xml*/ `
59100
+ <c:barChart>
59101
+ <c:barDir val="col"/>
59102
+ <c:grouping val="clustered"/>
59103
+ <c:overlap val="${overlap}"/>
59104
+ <c:gapWidth val="70"/>
59105
+ <!-- each data marker in the series does not have a different color -->
59106
+ <c:varyColors val="0"/>
59107
+ ${dataSetsNodes[0]}
59108
+ <c:axId val="${catAxId}" />
59109
+ <c:axId val="${valAxId}" />
59110
+ </c:barChart>
59111
+ <c:lineChart>
59112
+ <c:grouping val="standard"/>
59113
+ <!-- each data marker in the series does not have a different color -->
59114
+ <c:varyColors val="0"/>
59115
+ ${joinXmlNodes(dataSetsNodes.slice(1))}
59116
+ <c:axId val="${catAxId}" />
59117
+ <c:axId val="${valAxId}" />
59118
+ </c:lineChart>
59119
+ ${addAx("b", "c:catAx", catAxId, valAxId, { fontColor: chart.fontColor })}
59120
+ ${addAx(axisPos, "c:valAx", valAxId, catAxId, { fontColor: chart.fontColor })}
59121
+ `;
59122
+ }
58390
59123
  function addLineChart(chart) {
58391
59124
  const colors = new ChartColors();
58392
59125
  const dataSetsNodes = [];
@@ -58432,9 +59165,55 @@ function addLineChart(chart) {
58432
59165
  ${addAx(axisPos, "c:valAx", valAxId, catAxId, { fontColor: chart.fontColor })}
58433
59166
  `;
58434
59167
  }
59168
+ function addScatterChart(chart) {
59169
+ const colors = new ChartColors();
59170
+ const dataSetsNodes = [];
59171
+ for (const [dsIndex, dataset] of Object.entries(chart.dataSets)) {
59172
+ dataSetsNodes.push(escapeXml /*xml*/ `
59173
+ <c:ser>
59174
+ <c:idx val="${dsIndex}"/>
59175
+ <c:order val="${dsIndex}"/>
59176
+ <c:smooth val="0"/>
59177
+ <c:spPr>
59178
+ <a:ln w="19050" cap="rnd">
59179
+ <a:noFill/>
59180
+ <a:round/>
59181
+ </a:ln>
59182
+ <a:effectLst/>
59183
+ </c:spPr>
59184
+ <c:marker>
59185
+ <c:symbol val="circle" />
59186
+ <c:size val="5"/>
59187
+ ${shapeProperty({ backgroundColor: toXlsxHexColor(colors.next()) })}
59188
+ </c:marker>
59189
+ ${chart.labelRange
59190
+ ? escapeXml /*xml*/ `<c:xVal> <!-- x-coordinate values -->
59191
+ ${numberRef(chart.labelRange)}
59192
+ </c:xVal>`
59193
+ : ""}
59194
+ <c:yVal> <!-- y-coordinate values -->
59195
+ ${numberRef(dataset.range)}
59196
+ </c:yVal>
59197
+ </c:ser>
59198
+ `);
59199
+ }
59200
+ const axisPos = chart.verticalAxisPosition === "left" ? "l" : "r";
59201
+ return escapeXml /*xml*/ `
59202
+ <c:scatterChart>
59203
+ <!-- each data marker in the series does not have a different color -->
59204
+ <c:varyColors val="0"/>
59205
+ <c:scatterStyle val="lineMarker"/>
59206
+ ${joinXmlNodes(dataSetsNodes)}
59207
+ <c:axId val="${catAxId}" />
59208
+ <c:axId val="${valAxId}" />
59209
+ </c:scatterChart>
59210
+ ${addAx("b", "c:valAx", catAxId, valAxId, { fontColor: chart.fontColor })}
59211
+ ${addAx(axisPos, "c:valAx", valAxId, catAxId, { fontColor: chart.fontColor })}
59212
+ `;
59213
+ }
58435
59214
  function addDoughnutChart(chart, chartSheetIndex, data, { holeSize } = { holeSize: 50 }) {
58436
59215
  const colors = new ChartColors();
58437
- const maxLength = Math.max(...chart.dataSets.map((ds) => getRangeSize(ds.range, chartSheetIndex, data)));
59216
+ const maxLength = largeMax(chart.dataSets.map((ds) => getRangeSize(ds.range, chartSheetIndex, data)));
58438
59217
  const doughnutColors = range(0, maxLength).map(() => toXlsxHexColor(colors.next()));
58439
59218
  const dataSetsNodes = [];
58440
59219
  for (const [dsIndex, dataset] of Object.entries(chart.dataSets).reverse()) {
@@ -58572,8 +59351,7 @@ function addContent(content, sharedStrings, forceString = false) {
58572
59351
  attrs.push(["t", "b"]);
58573
59352
  }
58574
59353
  else if (forceString || !isNumber(value, DEFAULT_LOCALE)) {
58575
- const { id } = pushElement(content, sharedStrings);
58576
- value = id.toString();
59354
+ value = pushElement(content, sharedStrings);
58577
59355
  attrs.push(["t", "s"]);
58578
59356
  }
58579
59357
  return { attrs, node: escapeXml /*xml*/ `<v>${value}</v>` };
@@ -58698,8 +59476,7 @@ function addCellIsRule(cf, rule, dxfs) {
58698
59476
  if (rule.style.fillColor) {
58699
59477
  dxf.fill = { fgColor: { rgb: rule.style.fillColor } };
58700
59478
  }
58701
- const { id } = pushElement(dxf, dxfs);
58702
- ruleAttributes.push(["dxfId", id]);
59479
+ ruleAttributes.push(["dxfId", pushElement(dxf, dxfs)]);
58703
59480
  return escapeXml /*xml*/ `
58704
59481
  <conditionalFormatting sqref="${cf.ranges.join(" ")}">
58705
59482
  <cfRule ${formatAttributes(ruleAttributes)}>
@@ -59312,7 +60089,7 @@ function addTableColumns(table, sheetData) {
59312
60089
  const colHeaderXc = toXC(tableZone.left + i, tableZone.top);
59313
60090
  const colName = sheetData.cells[colHeaderXc]?.content || `col${i}`;
59314
60091
  const colAttributes = [
59315
- ["id", i + 1],
60092
+ ["id", i + 1], // id cannot be 0
59316
60093
  ["name", colName],
59317
60094
  ];
59318
60095
  columns.push(escapeXml /*xml*/ `<tableColumn ${formatAttributes(colAttributes)}/>`);
@@ -59528,8 +60305,10 @@ function addSheetViews(sheet) {
59528
60305
  * https://www.ecma-international.org/publications-and-standards/standards/ecma-376/
59529
60306
  */
59530
60307
  function getXLSX(data) {
60308
+ data = fixLengthySheetNames(data);
60309
+ data = purgeSingleRowTables(data);
59531
60310
  const files = [];
59532
- const construct = getDefaultXLSXStructure();
60311
+ const construct = getDefaultXLSXStructure(data);
59533
60312
  files.push(createWorkbook(data, construct));
59534
60313
  files.push(...createWorksheets(data, construct));
59535
60314
  files.push(createStylesSheet(construct));
@@ -59775,6 +60554,50 @@ function createRelRoot() {
59775
60554
  `;
59776
60555
  return createXMLFile(parseXML(xml), "_rels/.rels");
59777
60556
  }
60557
+ /**
60558
+ * Excel sheet names are maximum 31 characters while o-spreadsheet do not have this limit.
60559
+ * This method converts the sheet names to be within the 31 characters limit.
60560
+ * The cells/charts referencing this sheet will be updated accordingly.
60561
+ */
60562
+ function fixLengthySheetNames(data) {
60563
+ const nameMapping = {};
60564
+ const newNames = new Set();
60565
+ for (const sheet of data.sheets) {
60566
+ let newName = sheet.name.slice(0, 31);
60567
+ let i = 1;
60568
+ while (newNames.has(newName)) {
60569
+ newName = newName.slice(0, 31 - String(i).length) + i++;
60570
+ }
60571
+ newNames.add(newName);
60572
+ if (newName !== sheet.name) {
60573
+ nameMapping[sheet.name] = newName;
60574
+ sheet.name = newName;
60575
+ }
60576
+ }
60577
+ if (!Object.keys(nameMapping).length) {
60578
+ return data;
60579
+ }
60580
+ const sheetWithNewNames = Object.keys(nameMapping).sort((a, b) => b.length - a.length);
60581
+ let stringifiedData = JSON.stringify(data);
60582
+ for (const sheetName of sheetWithNewNames) {
60583
+ const regex = new RegExp(`'?${escapeRegExp(sheetName)}'?!`, "g");
60584
+ stringifiedData = stringifiedData.replaceAll(regex, (match) => {
60585
+ const newName = nameMapping[sheetName];
60586
+ return match.replace(sheetName, newName);
60587
+ });
60588
+ }
60589
+ return JSON.parse(stringifiedData);
60590
+ }
60591
+ /** Excel files do not support tables with a single row the defined range
60592
+ * Since those tables are not really useful (no filtering/limited styling)
60593
+ * This function filters out all tables with a single row.
60594
+ */
60595
+ function purgeSingleRowTables(data) {
60596
+ for (const sheet of data.sheets) {
60597
+ sheet.tables = sheet.tables.filter((table) => zoneToDimension(toZone(table.range)).numberOfRows > 1);
60598
+ }
60599
+ return data;
60600
+ }
59778
60601
 
59779
60602
  var Status;
59780
60603
  (function (Status) {
@@ -60028,6 +60851,7 @@ class Model extends EventBus {
60028
60851
  stateObserver: this.state,
60029
60852
  range: this.range,
60030
60853
  dispatch: this.dispatchFromCorePlugin,
60854
+ canDispatch: this.canDispatch,
60031
60855
  uuidGenerator: this.uuidGenerator,
60032
60856
  custom: this.config.custom,
60033
60857
  external: this.config.external,
@@ -60038,6 +60862,7 @@ class Model extends EventBus {
60038
60862
  getters: this.getters,
60039
60863
  stateObserver: this.state,
60040
60864
  dispatch: this.dispatch,
60865
+ canDispatch: this.canDispatch,
60041
60866
  selection: this.selection,
60042
60867
  moveClient: this.session.move.bind(this.session),
60043
60868
  custom: this.config.custom,
@@ -60361,7 +61186,7 @@ const links = {
60361
61186
  const components = {
60362
61187
  Checkbox,
60363
61188
  Section,
60364
- ChartColor,
61189
+ RoundColorPicker,
60365
61190
  ChartDataSeries,
60366
61191
  ChartErrorSection,
60367
61192
  ChartLabelRange,
@@ -60464,6 +61289,6 @@ exports.tokenColors = tokenColors;
60464
61289
  exports.tokenize = tokenize;
60465
61290
 
60466
61291
 
60467
- __info__.version = "17.3.0-alpha.1";
60468
- __info__.date = "2024-03-25T09:43:36.072Z";
60469
- __info__.hash = "4095c41";
61292
+ __info__.version = "17.3.0-alpha.3";
61293
+ __info__.date = "2024-04-10T12:28:23.658Z";
61294
+ __info__.hash = "80b5056";