@odoo/o-spreadsheet 17.3.0-alpha.2 → 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.2
7
- * @date 2024-04-05T14:01:07.060Z
8
- * @hash 8c5a229
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
@@ -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) {
@@ -1886,6 +1886,11 @@ const invalidateCFEvaluationCommands = new Set([
1886
1886
  "REMOVE_CONDITIONAL_FORMAT",
1887
1887
  "CHANGE_CONDITIONAL_FORMAT_PRIORITY",
1888
1888
  ]);
1889
+ const invalidateBordersCommands = new Set([
1890
+ "AUTOFILL_CELL",
1891
+ "SET_BORDER",
1892
+ "SET_ZONE_BORDERS",
1893
+ ]);
1889
1894
  const readonlyAllowedCommands = new Set([
1890
1895
  "START",
1891
1896
  "ACTIVATE_SHEET",
@@ -2183,6 +2188,7 @@ const CellErrorType = {
2183
2188
  BadExpression: "#BAD_EXPR",
2184
2189
  CircularDependency: "#CYCLE",
2185
2190
  UnknownFunction: "#NAME?",
2191
+ DivisionByZero: "#DIV/0!",
2186
2192
  GenericError: "#ERROR",
2187
2193
  };
2188
2194
  const errorTypes = new Set(Object.values(CellErrorType));
@@ -2221,9 +2227,9 @@ class UnknownFunctionError extends EvaluationError {
2221
2227
 
2222
2228
  // HELPERS
2223
2229
  const SORT_TYPES_ORDER = ["number", "string", "boolean", "undefined"];
2224
- function assert(condition, message) {
2230
+ function assert(condition, message, value) {
2225
2231
  if (!condition()) {
2226
- throw new EvaluationError(message);
2232
+ throw new EvaluationError(message, value);
2227
2233
  }
2228
2234
  }
2229
2235
  function inferFormat(data) {
@@ -2301,6 +2307,9 @@ function strictToInteger(value, locale) {
2301
2307
  function assertNumberGreaterThanOrEqualToOne(value) {
2302
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()));
2303
2309
  }
2310
+ function assertNotZero(value) {
2311
+ assert(() => value !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."), CellErrorType.DivisionByZero);
2312
+ }
2304
2313
  function toString(data) {
2305
2314
  const value = toValue(data);
2306
2315
  switch (typeof value) {
@@ -3066,6 +3075,9 @@ function applyInternalFormat(value, internalFormat, locale) {
3066
3075
  return formattedValue;
3067
3076
  }
3068
3077
  function applyInternalNumberFormat(value, format, locale) {
3078
+ if (value === Infinity) {
3079
+ return "∞" + (format.isPercent ? "%" : "");
3080
+ }
3069
3081
  if (format.isPercent) {
3070
3082
  value = value * 100;
3071
3083
  }
@@ -3409,6 +3421,46 @@ function roundFormat(format) {
3409
3421
  });
3410
3422
  return convertInternalFormatToFormat(roundedFormat);
3411
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
+ }
3412
3464
  function createLargeNumberFormat(format, magnitude, postFix, locale) {
3413
3465
  const internalFormat = parseFormat(format || "#,##0");
3414
3466
  const largeNumberFormat = [];
@@ -4601,14 +4653,19 @@ function getDefaultCellHeight(ctx, cell, colSize) {
4601
4653
  const textWidthCache = {};
4602
4654
  function computeTextWidth(context, text, style, fontUnit = "pt") {
4603
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;
4604
4664
  if (!textWidthCache[font]) {
4605
4665
  textWidthCache[font] = {};
4606
4666
  }
4607
4667
  if (textWidthCache[font][text] === undefined) {
4608
- context.save();
4609
- context.font = font;
4610
4668
  const textWidth = context.measureText(text).width;
4611
- context.restore();
4612
4669
  textWidthCache[font][text] = textWidth;
4613
4670
  }
4614
4671
  return textWidthCache[font][text];
@@ -4753,6 +4810,42 @@ const pxRegex = /([0-9\.]*)px/;
4753
4810
  function getContextFontSize(font) {
4754
4811
  return Number(font.match(pxRegex)?.[1]);
4755
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
+ }
4756
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
4757
4850
  ) {
4758
4851
  context.fillText(text, position.x, position.y);
@@ -7396,6 +7489,7 @@ const CellIsOperators = {
7396
7489
  };
7397
7490
  const ChartTerms = {
7398
7491
  Series: _t("Series"),
7492
+ BackgroundColor: _t("Background color"),
7399
7493
  Errors: {
7400
7494
  Unexpected: _t("The chart definition is invalid for an unknown reason"),
7401
7495
  // BASIC CHART ERRORS (LINE | BAR | PIE)
@@ -9431,32 +9525,63 @@ function shouldRemoveFirstLabel(labelRange, dataset, dataSetsHaveTitle) {
9431
9525
  }
9432
9526
  return true;
9433
9527
  }
9434
- // ---------------------------------------------------------------------------
9435
- // Scorecard
9436
- // ---------------------------------------------------------------------------
9437
- 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) {
9438
9540
  if (!baseline) {
9439
9541
  return "";
9440
9542
  }
9441
9543
  else if (baselineMode === "text" ||
9442
9544
  keyValue?.type !== CellValueType.number ||
9443
9545
  baseline.type !== CellValueType.number) {
9546
+ if (humanize) {
9547
+ return humanizeNumber(baseline, locale);
9548
+ }
9444
9549
  return baseline.formattedValue;
9445
9550
  }
9551
+ let { value, format } = baseline;
9552
+ if (baselineMode === "progress") {
9553
+ value = keyValue.value / value;
9554
+ format = "0.0%";
9555
+ }
9446
9556
  else {
9447
- let diff = keyValue.value - baseline.value;
9448
- if (baselineMode === "percentage" && diff !== 0) {
9449
- diff = (diff / baseline.value) * 100;
9557
+ value = Math.abs(keyValue.value - value);
9558
+ if (baselineMode === "percentage" && value !== 0) {
9559
+ value = value / baseline.value;
9450
9560
  }
9451
- if (baselineMode !== "percentage" && baseline.format) {
9452
- return formatValue(diff, { format: baseline.format, locale });
9561
+ if (baselineMode === "percentage") {
9562
+ format = "0.0%";
9453
9563
  }
9454
- const baselineStr = Math.abs(parseFloat(diff.toFixed(2))).toLocaleString();
9455
- return baselineMode === "percentage" ? baselineStr + "%" : baselineStr;
9564
+ if (!format) {
9565
+ value = Math.round(value * 100) / 100;
9566
+ }
9567
+ }
9568
+ if (humanize) {
9569
+ return humanizeNumber({ value, format }, locale);
9456
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 ?? "");
9457
9581
  }
9458
9582
  function getBaselineColor(baseline, baselineMode, keyValue, colorUp, colorDown) {
9459
9583
  if (baselineMode === "text" ||
9584
+ baselineMode === "progress" ||
9460
9585
  baseline?.type !== CellValueType.number ||
9461
9586
  keyValue?.type !== CellValueType.number) {
9462
9587
  return undefined;
@@ -9485,17 +9610,6 @@ function getBaselineArrowDirection(baseline, keyValue, baselineMode) {
9485
9610
  }
9486
9611
  return "neutral";
9487
9612
  }
9488
- function getChartPositionAtCenterOfViewport(getters, chartSize) {
9489
- const { x, y } = getters.getMainViewportCoordinates();
9490
- const { scrollX, scrollY } = getters.getActiveSheetScrollInfo();
9491
- const { width, height } = getters.getVisibleRect(getters.getActiveMainViewport());
9492
- const position = {
9493
- x: x + scrollX + Math.max(0, (width - chartSize.width) / 2),
9494
- y: y + scrollY + Math.max(0, (height - chartSize.height) / 2),
9495
- }; // Position at the center of the scrollable viewport
9496
- return position;
9497
- }
9498
-
9499
9613
  function checkKeyValue(definition) {
9500
9614
  return definition.keyValue && !rangeReference.test(definition.keyValue)
9501
9615
  ? "InvalidScorecardKeyValue" /* CommandResult.InvalidScorecardKeyValue */
@@ -9513,10 +9627,12 @@ let ScorecardChart$1 = class ScorecardChart extends AbstractChart {
9513
9627
  baseline;
9514
9628
  baselineMode;
9515
9629
  baselineDescr;
9630
+ progressBar = false;
9516
9631
  background;
9517
9632
  baselineColorUp;
9518
9633
  baselineColorDown;
9519
9634
  fontColor;
9635
+ humanize;
9520
9636
  type = "scorecard";
9521
9637
  constructor(definition, sheetId, getters) {
9522
9638
  super(definition, sheetId, getters);
@@ -9527,6 +9643,7 @@ let ScorecardChart$1 = class ScorecardChart extends AbstractChart {
9527
9643
  this.background = definition.background;
9528
9644
  this.baselineColorUp = definition.baselineColorUp;
9529
9645
  this.baselineColorDown = definition.baselineColorDown;
9646
+ this.humanize = definition.humanize ?? false;
9530
9647
  }
9531
9648
  static validateChartDefinition(validator, definition) {
9532
9649
  return validator.checkValidations(definition, checkKeyValue, checkBaseline);
@@ -9596,6 +9713,7 @@ let ScorecardChart$1 = class ScorecardChart extends AbstractChart {
9596
9713
  keyValue: keyValue
9597
9714
  ? this.getters.getRangeString(keyValue, targetSheetId || this.sheetId)
9598
9715
  : undefined,
9716
+ humanize: this.humanize,
9599
9717
  };
9600
9718
  }
9601
9719
  getDefinitionForExcel() {
@@ -9621,7 +9739,7 @@ function drawScoreChart(structure, canvas) {
9621
9739
  if (structure.title) {
9622
9740
  ctx.font = structure.title.style.font;
9623
9741
  ctx.fillStyle = structure.title.style.color;
9624
- 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);
9625
9743
  }
9626
9744
  if (structure.baseline) {
9627
9745
  ctx.font = structure.baseline.style.font;
@@ -9648,20 +9766,41 @@ function drawScoreChart(structure, canvas) {
9648
9766
  ctx.restore();
9649
9767
  }
9650
9768
  if (structure.baselineDescr) {
9651
- ctx.font = structure.baselineDescr.style.font;
9652
- ctx.fillStyle = structure.baselineDescr.style.color;
9653
- 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
+ }
9654
9775
  }
9655
9776
  if (structure.key) {
9656
9777
  ctx.font = structure.key.style.font;
9657
9778
  ctx.fillStyle = structure.key.style.color;
9658
9779
  drawDecoratedText(ctx, structure.key.text, structure.key.position, structure.key.style.underline, structure.key.style.strikethrough);
9659
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
+ }
9660
9799
  }
9661
9800
  function createScorecardChartRuntime(chart, getters) {
9662
- let keyValue = "";
9663
9801
  let formattedKeyValue = "";
9664
9802
  let keyValueCell;
9803
+ const locale = getters.getLocale();
9665
9804
  if (chart.keyValue) {
9666
9805
  const keyValuePosition = {
9667
9806
  sheetId: chart.keyValue.sheetId,
@@ -9669,31 +9808,33 @@ function createScorecardChartRuntime(chart, getters) {
9669
9808
  row: chart.keyValue.zone.top,
9670
9809
  };
9671
9810
  keyValueCell = getters.getEvaluatedCell(keyValuePosition);
9672
- keyValue = String(keyValueCell.value ?? "");
9673
- formattedKeyValue = keyValueCell.formattedValue;
9811
+ formattedKeyValue = getKeyValueText(keyValueCell, chart.humanize ?? false, locale);
9674
9812
  }
9675
9813
  let baselineCell;
9676
9814
  const baseline = chart.baseline;
9677
9815
  if (baseline) {
9678
9816
  const baselinePosition = {
9679
- sheetId: chart.baseline.sheetId,
9680
- col: chart.baseline.zone.left,
9681
- row: chart.baseline.zone.top,
9817
+ sheetId: baseline.sheetId,
9818
+ col: baseline.zone.left,
9819
+ row: baseline.zone.top,
9682
9820
  };
9683
9821
  baselineCell = getters.getEvaluatedCell(baselinePosition);
9684
9822
  }
9685
9823
  const { background, fontColor } = getters.getStyleOfSingleCellChart(chart.background, chart.keyValue);
9686
- 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;
9687
9828
  return {
9688
9829
  title: _t(chart.title),
9689
- keyValue: formattedKeyValue || keyValue,
9690
- baselineDisplay: getBaselineText(baselineCell, keyValueCell, chart.baselineMode, locale),
9830
+ keyValue: formattedKeyValue,
9831
+ baselineDisplay,
9691
9832
  baselineArrow: getBaselineArrowDirection(baselineCell, keyValueCell, chart.baselineMode),
9692
9833
  baselineColor: getBaselineColor(baselineCell, chart.baselineMode, keyValueCell, chart.baselineColorUp, chart.baselineColorDown),
9693
- baselineDescr: chart.baselineDescr ? _t(chart.baselineDescr) : "",
9834
+ baselineDescr: chart.baselineMode !== "progress" && chart.baselineDescr ? _t(chart.baselineDescr) : "",
9694
9835
  fontColor,
9695
9836
  background,
9696
- baselineStyle: chart.baselineMode !== "percentage" && baseline
9837
+ baselineStyle: chart.baselineMode !== "percentage" && chart.baselineMode !== "progress" && baseline
9697
9838
  ? getters.getCellStyle({
9698
9839
  sheetId: baseline.sheetId,
9699
9840
  col: baseline.zone.left,
@@ -9707,17 +9848,21 @@ function createScorecardChartRuntime(chart, getters) {
9707
9848
  row: chart.keyValue.zone.top,
9708
9849
  })
9709
9850
  : undefined,
9851
+ progressBar: chart.baselineMode === "progress"
9852
+ ? {
9853
+ value: baselineValue,
9854
+ color: baselineValue > 0 ? chart.baselineColorUp : chart.baselineColorDown,
9855
+ }
9856
+ : undefined,
9710
9857
  };
9711
9858
  }
9712
9859
 
9713
9860
  /* Sizes of boxes containing the texts, in percentage of the Chart size */
9714
9861
  const TITLE_FONT_SIZE = 18;
9715
- const BASELINE_BOX_HEIGHT_RATIO = 0.35;
9716
- const KEY_BOX_HEIGHT_RATIO = 0.65;
9717
- /** Baseline description should have a smaller font than the baseline */
9718
- const BASELINE_DESCR_FONT_RATIO = 0.9;
9719
- /* Padding at the border of the chart, in percentage of the chart width */
9720
- 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;
9721
9866
  /**
9722
9867
  * Line height (in em)
9723
9868
  * Having a line heigh =1em (=font size) don't work, the font will overflow.
@@ -9757,31 +9902,37 @@ class ScorecardChartConfigBuilder {
9757
9902
  },
9758
9903
  };
9759
9904
  const style = this.getTextStyles();
9760
- const { height: titleHeight } = this.getTextDimensions(this.title, style.title.font);
9905
+ let titleHeight = 0;
9761
9906
  if (this.title) {
9907
+ ({ height: titleHeight } = this.getFullTextDimensions(this.title, style.title.font));
9762
9908
  structure.title = {
9763
9909
  text: this.title,
9764
9910
  style: style.title,
9765
9911
  position: {
9766
- x: this.chartPadding,
9767
- y: this.chartPadding + titleHeight,
9912
+ x: CHART_PADDING,
9913
+ y: CHART_PADDING / 2 + titleHeight,
9768
9914
  },
9769
9915
  };
9770
9916
  }
9771
9917
  const baselineArrowSize = style.baselineArrow?.size ?? 0;
9772
- const { height: baselineHeight, width: baselineWidth } = this.getTextDimensions(this.baseline, style.baselineValue.font);
9773
- 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;
9774
9925
  structure.baseline = {
9775
9926
  text: this.baseline,
9776
9927
  style: style.baselineValue,
9777
9928
  position: {
9778
9929
  x: (this.width - baselineWidth - baselineDescrWidth + baselineArrowSize) / 2,
9779
9930
  y: this.keyValue
9780
- ? this.height - 2 * this.chartPadding
9781
- : 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,
9782
9933
  },
9783
9934
  };
9784
- if (style.baselineArrow) {
9935
+ if (style.baselineArrow && !this.runtime.progressBar) {
9785
9936
  structure.baselineArrow = {
9786
9937
  direction: this.baselineArrow,
9787
9938
  style: style.baselineArrow,
@@ -9792,23 +9943,68 @@ class ScorecardChartConfigBuilder {
9792
9943
  };
9793
9944
  }
9794
9945
  if (this.baselineDescr) {
9795
- structure.baselineDescr = {
9796
- text: this.baselineDescr,
9797
- 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 = {
9798
9984
  position: {
9799
- x: structure.baseline.position.x + baselineWidth,
9800
- 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,
9801
9996
  },
9802
9997
  };
9803
9998
  }
9804
- 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);
9805
10000
  if (this.keyValue) {
9806
10001
  structure.key = {
9807
10002
  text: this.keyValue,
9808
10003
  style: style.keyValue,
9809
10004
  position: {
9810
10005
  x: (this.width - keyWidth) / 2,
9811
- 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,
9812
10008
  },
9813
10009
  };
9814
10010
  }
@@ -9835,9 +10031,6 @@ class ScorecardChartConfigBuilder {
9835
10031
  get secondaryFontColor() {
9836
10032
  return relativeLuminance(this.backgroundColor) > 0.3 ? "#525252" : "#C8C8C8";
9837
10033
  }
9838
- get chartPadding() {
9839
- return this.width * CHART_PADDING_RATIO;
9840
- }
9841
10034
  getTextDimensions(text, font) {
9842
10035
  this.context.font = font;
9843
10036
  const measure = this.context.measureText(text);
@@ -9846,16 +10039,44 @@ class ScorecardChartConfigBuilder {
9846
10039
  height: measure.actualBoundingBoxAscent + measure.actualBoundingBoxDescent,
9847
10040
  };
9848
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
+ }
9849
10050
  getTextStyles() {
9850
10051
  // If the widest text overflows horizontally, scale it down, and apply the same scaling factors to all the other fonts.
9851
- const maxLineWidth = this.width * (1 - 2 * CHART_PADDING_RATIO);
9852
- const widestElement = this.getWidestElement();
9853
- const baseFontSize = widestElement.getElementMaxFontSize(this.getDrawableHeight(), this);
9854
- const fontSizeMatchingWidth = getFontSizeMatchingWidth(maxLineWidth, baseFontSize, (fontSize) => widestElement.getElementWidth(fontSize, this.context, this));
9855
- let scalingFactor = fontSizeMatchingWidth / baseFontSize;
10052
+ const maxLineWidth = this.width - 2 * CHART_PADDING;
10053
+ const drawableHeight = this.getDrawableHeight();
9856
10054
  // Fonts sizes in px
9857
- const keyFontSize = new KeyValueElement(this.runtime.keyValueStyle).getElementMaxFontSize(this.getDrawableHeight(), this) * scalingFactor;
9858
- 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
+ }
9859
10080
  return {
9860
10081
  title: {
9861
10082
  font: getDefaultContextFont(TITLE_FONT_SIZE),
@@ -9868,7 +10089,7 @@ class ScorecardChartConfigBuilder {
9868
10089
  underline: this.runtime.keyValueStyle?.underline,
9869
10090
  },
9870
10091
  baselineValue: {
9871
- font: getDefaultContextFont(baselineFontSize, this.runtime.baselineStyle?.bold, this.runtime.baselineStyle?.italic),
10092
+ font: getDefaultContextFont(baselineValueFontSize, this.runtime.baselineStyle?.bold, this.runtime.baselineStyle?.italic),
9872
10093
  strikethrough: this.runtime.baselineStyle?.strikethrough,
9873
10094
  underline: this.runtime.baselineStyle?.underline,
9874
10095
  color: this.runtime.baselineStyle?.textColor ||
@@ -9876,33 +10097,25 @@ class ScorecardChartConfigBuilder {
9876
10097
  this.secondaryFontColor,
9877
10098
  },
9878
10099
  baselineDescr: {
9879
- font: getDefaultContextFont(baselineFontSize * BASELINE_DESCR_FONT_RATIO),
10100
+ font: getDefaultContextFont(baselineDescrFontSize),
10101
+ isSplit: isBaselineSplit,
9880
10102
  color: this.secondaryFontColor,
9881
10103
  },
9882
- baselineArrow: this.baselineArrow === "neutral"
10104
+ baselineArrow: this.baselineArrow === "neutral" || this.runtime.progressBar
9883
10105
  ? undefined
9884
10106
  : {
9885
- size: this.keyValue ? 0.8 * baselineFontSize : 0,
10107
+ size: this.keyValue ? 0.8 * baselineValueFontSize : 0,
9886
10108
  color: this.runtime.baselineColor || this.secondaryFontColor,
9887
10109
  },
9888
10110
  };
9889
10111
  }
9890
10112
  /** Get the height of the chart minus all the vertical paddings */
9891
10113
  getDrawableHeight() {
9892
- const verticalPadding = 2 * this.chartPadding;
9893
- let availableHeight = this.height - verticalPadding;
10114
+ const verticalPadding = CHART_PADDING + this.height * BOTTOM_PADDING_RATIO;
10115
+ let availableHeight = this.height - 2 * verticalPadding;
9894
10116
  availableHeight -= this.title ? TITLE_FONT_SIZE * LINE_HEIGHT : 0;
9895
10117
  return availableHeight;
9896
10118
  }
9897
- /** Return the element with he widest text in the chart */
9898
- getWidestElement() {
9899
- const baseline = new BaselineElement(this.runtime.baselineStyle);
9900
- const keyValue = new KeyValueElement(this.runtime.keyValueStyle);
9901
- return baseline.getElementWidth(BASELINE_BOX_HEIGHT_RATIO, this.context, this) >
9902
- keyValue.getElementWidth(KEY_BOX_HEIGHT_RATIO, this.context, this)
9903
- ? baseline
9904
- : keyValue;
9905
- }
9906
10119
  }
9907
10120
  class ScorecardScalableElement {
9908
10121
  style;
@@ -9911,29 +10124,7 @@ class ScorecardScalableElement {
9911
10124
  }
9912
10125
  measureTextWidth(ctx, text, fontSize) {
9913
10126
  ctx.font = getDefaultContextFont(fontSize, this.style.bold, this.style.italic);
9914
- return ctx.measureText(text).width;
9915
- }
9916
- }
9917
- class BaselineElement extends ScorecardScalableElement {
9918
- getElementWidth(fontSize, ctx, chart) {
9919
- if (!chart.runtime) {
9920
- return 0;
9921
- }
9922
- const baselineStr = chart.baseline;
9923
- // Put mock text to simulate the width of the up/down arrow
9924
- const largeText = chart.baselineArrow !== "neutral" ? "A " + baselineStr : baselineStr;
9925
- let textWidth = this.measureTextWidth(ctx, largeText, fontSize);
9926
- // Baseline descr font size should be smaller than baseline font size
9927
- textWidth += this.measureTextWidth(ctx, chart.baselineDescr, fontSize * BASELINE_DESCR_FONT_RATIO);
9928
- return textWidth;
9929
- }
9930
- getElementMaxFontSize(availableHeight, chart) {
9931
- if (!chart.runtime) {
9932
- return 0;
9933
- }
9934
- const haveBaseline = chart.baseline !== "" || chart.baselineDescr;
9935
- const maxHeight = haveBaseline ? BASELINE_BOX_HEIGHT_RATIO * availableHeight : 0;
9936
- return maxHeight / LINE_HEIGHT;
10127
+ return computeCachedTextWidth(ctx, text);
9937
10128
  }
9938
10129
  }
9939
10130
  class KeyValueElement extends ScorecardScalableElement {
@@ -10863,33 +11054,6 @@ var array = /*#__PURE__*/Object.freeze({
10863
11054
  // -----------------------------------------------------------------------------
10864
11055
  // FORMAT.LARGE.NUMBER
10865
11056
  // -----------------------------------------------------------------------------
10866
- function formatLargeNumber(arg, unit, locale) {
10867
- const value = Math.abs(toNumber(arg?.value, locale));
10868
- const format = arg?.format;
10869
- if (unit !== undefined) {
10870
- const postFix = unit?.value;
10871
- switch (postFix) {
10872
- case "k":
10873
- return createLargeNumberFormat(format, 1e3, "k");
10874
- case "m":
10875
- return createLargeNumberFormat(format, 1e6, "m");
10876
- case "b":
10877
- return createLargeNumberFormat(format, 1e9, "b");
10878
- default:
10879
- throw new EvaluationError(_t("The formatting unit should be 'k', 'm' or 'b'."));
10880
- }
10881
- }
10882
- if (value < 1e5) {
10883
- return createLargeNumberFormat(format, 0, "");
10884
- }
10885
- else if (value < 1e8) {
10886
- return createLargeNumberFormat(format, 1e3, "k");
10887
- }
10888
- else if (value < 1e11) {
10889
- return createLargeNumberFormat(format, 1e6, "m");
10890
- }
10891
- return createLargeNumberFormat(format, 1e9, "b");
10892
- }
10893
11057
  const FORMAT_LARGE_NUMBER = {
10894
11058
  description: _t("Apply a large number format"),
10895
11059
  args: [
@@ -11051,7 +11215,7 @@ const ATAN2 = {
11051
11215
  compute: function (x, y) {
11052
11216
  const _x = toNumber(x, this.locale);
11053
11217
  const _y = toNumber(y, this.locale);
11054
- 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);
11055
11219
  return Math.atan2(_y, _x);
11056
11220
  },
11057
11221
  isExported: true,
@@ -11181,7 +11345,7 @@ const COT = {
11181
11345
  returns: ["NUMBER"],
11182
11346
  compute: function (angle) {
11183
11347
  const _angle = toNumber(angle, this.locale);
11184
- assert(() => _angle !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
11348
+ assertNotZero(_angle);
11185
11349
  return 1 / Math.tan(_angle);
11186
11350
  },
11187
11351
  isExported: true,
@@ -11195,7 +11359,7 @@ const COTH = {
11195
11359
  returns: ["NUMBER"],
11196
11360
  compute: function (value) {
11197
11361
  const _value = toNumber(value, this.locale);
11198
- assert(() => _value !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
11362
+ assertNotZero(_value);
11199
11363
  return 1 / Math.tanh(_value);
11200
11364
  },
11201
11365
  isExported: true,
@@ -11323,7 +11487,7 @@ const CSC = {
11323
11487
  returns: ["NUMBER"],
11324
11488
  compute: function (angle) {
11325
11489
  const _angle = toNumber(angle, this.locale);
11326
- assert(() => _angle !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
11490
+ assertNotZero(_angle);
11327
11491
  return 1 / Math.sin(_angle);
11328
11492
  },
11329
11493
  isExported: true,
@@ -11337,7 +11501,7 @@ const CSCH = {
11337
11501
  returns: ["NUMBER"],
11338
11502
  compute: function (value) {
11339
11503
  const _value = toNumber(value, this.locale);
11340
- assert(() => _value !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
11504
+ assertNotZero(_value);
11341
11505
  return 1 / Math.sinh(_value);
11342
11506
  },
11343
11507
  isExported: true,
@@ -11536,7 +11700,7 @@ const LN = {
11536
11700
  // MOD
11537
11701
  // -----------------------------------------------------------------------------
11538
11702
  function mod(dividend, divisor) {
11539
- 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);
11540
11704
  const modulus = dividend % divisor;
11541
11705
  // -42 % 10 = -2 but we want 8, so need the code below
11542
11706
  if ((modulus > 0 && divisor < 0) || (modulus < 0 && divisor > 0)) {
@@ -12105,7 +12269,7 @@ function average(values, locale) {
12105
12269
  count += 1;
12106
12270
  return acc + a;
12107
12271
  }, 0, locale);
12108
- assert(() => count !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
12272
+ assertNotZero(count);
12109
12273
  return sum / count;
12110
12274
  }
12111
12275
  function countNumbers(values, locale) {
@@ -12172,7 +12336,7 @@ function filterAndFlatData(dataY, dataX) {
12172
12336
  function covariance(dataY, dataX, isSample) {
12173
12337
  const { flatDataX, flatDataY } = filterAndFlatData(dataY, dataX);
12174
12338
  const count = flatDataY.length;
12175
- 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);
12176
12340
  let sumY = 0;
12177
12341
  let sumX = 0;
12178
12342
  for (let i = 0; i < count; i++) {
@@ -12195,7 +12359,7 @@ function variance(args, isSample, textAs0, locale) {
12195
12359
  count += 1;
12196
12360
  return acc + a;
12197
12361
  }, 0, locale);
12198
- 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);
12199
12363
  const average = sum / count;
12200
12364
  return (reduceFunction(args, (acc, a) => acc + Math.pow(a - average, 2), 0, locale) /
12201
12365
  (count - (isSample ? 1 : 0)));
@@ -12392,7 +12556,7 @@ const AVEDEV = {
12392
12556
  count += 1;
12393
12557
  return acc + a;
12394
12558
  }, 0, this.locale);
12395
- assert(() => count !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
12559
+ assertNotZero(count);
12396
12560
  const average = sum / count;
12397
12561
  return reduceNumbers(values, (acc, a) => acc + Math.abs(average - a), 0, this.locale) / count;
12398
12562
  },
@@ -12464,7 +12628,7 @@ const AVERAGE_WEIGHTED = {
12464
12628
  }
12465
12629
  }
12466
12630
  }
12467
- assert(() => count !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
12631
+ assertNotZero(count);
12468
12632
  return { value: sum / count, format: inferFormat(args[0]) };
12469
12633
  },
12470
12634
  };
@@ -12484,7 +12648,7 @@ const AVERAGEA = {
12484
12648
  count += 1;
12485
12649
  return acc + a;
12486
12650
  }, 0, this.locale);
12487
- assert(() => count !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
12651
+ assertNotZero(count);
12488
12652
  return {
12489
12653
  value: sum / count,
12490
12654
  format: inferFormat(args[0]),
@@ -12514,7 +12678,7 @@ const AVERAGEIF = {
12514
12678
  sum += value;
12515
12679
  }
12516
12680
  }, this.locale);
12517
- assert(() => count !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
12681
+ assertNotZero(count);
12518
12682
  return sum / count;
12519
12683
  },
12520
12684
  isExported: true,
@@ -12543,7 +12707,7 @@ const AVERAGEIFS = {
12543
12707
  sum += value;
12544
12708
  }
12545
12709
  }, this.locale);
12546
- assert(() => count !== 0, _t("Evaluation of function [[FUNCTION_NAME]] caused a divide by zero error."));
12710
+ assertNotZero(count);
12547
12711
  return sum / count;
12548
12712
  },
12549
12713
  isExported: true,
@@ -17968,7 +18132,7 @@ const DIVIDE = {
17968
18132
  returns: ["NUMBER"],
17969
18133
  compute: function (dividend, divisor) {
17970
18134
  const _divisor = toNumber(divisor, this.locale);
17971
- 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);
17972
18136
  return {
17973
18137
  value: toNumber(dividend, this.locale) / _divisor,
17974
18138
  format: dividend?.format || divisor?.format,
@@ -21013,7 +21177,21 @@ class ScatterChart extends AbstractChart {
21013
21177
  return new ScatterChart(definition, this.sheetId, this.getters);
21014
21178
  }
21015
21179
  getDefinitionForExcel() {
21016
- 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
+ };
21017
21195
  }
21018
21196
  copyForSheetId(sheetId) {
21019
21197
  const dataSets = copyDataSetsWithNewSheetId(this.sheetId, sheetId, this.dataSets);
@@ -27441,13 +27619,30 @@ class ColorPickerWidget extends owl.Component {
27441
27619
  }
27442
27620
  }
27443
27621
 
27444
- class ChartColor extends owl.Component {
27445
- static template = "o-spreadsheet.ChartColor";
27446
- 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 };
27447
27640
  static props = {
27448
27641
  currentColor: { type: String, optional: true },
27642
+ title: { type: String, optional: true },
27449
27643
  onColorPicked: Function,
27450
27644
  };
27645
+ colorPickerButtonRef = owl.useRef("colorPickerButton");
27451
27646
  state;
27452
27647
  setup() {
27453
27648
  this.state = owl.useState({ pickerOpened: false });
@@ -27459,6 +27654,19 @@ class ChartColor extends owl.Component {
27459
27654
  togglePicker() {
27460
27655
  this.state.pickerOpened = !this.state.pickerOpened;
27461
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
+ }
27462
27670
  }
27463
27671
 
27464
27672
  class ChartTitle extends owl.Component {
@@ -27472,7 +27680,7 @@ class ChartTitle extends owl.Component {
27472
27680
 
27473
27681
  class LineBarPieDesignPanel extends owl.Component {
27474
27682
  static template = "o-spreadsheet-LineBarPieDesignPanel";
27475
- static components = { ChartColor, ChartTitle, Section };
27683
+ static components = { RoundColorPicker, ChartTitle, Section };
27476
27684
  static props = {
27477
27685
  figureId: String,
27478
27686
  definition: Object,
@@ -27495,6 +27703,9 @@ class LineBarPieDesignPanel extends owl.Component {
27495
27703
  [attr]: ev.target.value,
27496
27704
  });
27497
27705
  }
27706
+ get backgroundColorTitle() {
27707
+ return ChartTerms.BackgroundColor;
27708
+ }
27498
27709
  }
27499
27710
 
27500
27711
  class BarChartDesignPanel extends LineBarPieDesignPanel {
@@ -27564,6 +27775,10 @@ css /* scss */ `
27564
27775
  line-height: 18px;
27565
27776
  width: 100%;
27566
27777
  }
27778
+ td {
27779
+ box-sizing: border-box;
27780
+ height: 30px;
27781
+ }
27567
27782
  th.o-gauge-color-set-colorPicker {
27568
27783
  width: 8%;
27569
27784
  }
@@ -27586,7 +27801,12 @@ css /* scss */ `
27586
27801
  `;
27587
27802
  class GaugeChartDesignPanel extends owl.Component {
27588
27803
  static template = "o-spreadsheet-GaugeChartDesignPanel";
27589
- static components = { ColorPickerWidget, ChartErrorSection, ChartColor, ChartTitle, Section };
27804
+ static components = {
27805
+ ChartErrorSection,
27806
+ RoundColorPicker,
27807
+ ChartTitle,
27808
+ Section,
27809
+ };
27590
27810
  static props = {
27591
27811
  figureId: String,
27592
27812
  definition: Object,
@@ -27598,9 +27818,6 @@ class GaugeChartDesignPanel extends owl.Component {
27598
27818
  sectionRuleDispatchResult: undefined,
27599
27819
  sectionRule: deepCopy(this.props.definition.sectionRule),
27600
27820
  });
27601
- setup() {
27602
- owl.useExternalListener(window, "click", this.closeMenus);
27603
- }
27604
27821
  get title() {
27605
27822
  return _t(this.props.definition.title);
27606
27823
  }
@@ -27641,27 +27858,22 @@ class GaugeChartDesignPanel extends owl.Component {
27641
27858
  const sectionRule = deepCopy(this.state.sectionRule);
27642
27859
  sectionRule.colors[target] = color;
27643
27860
  this.updateSectionRule(sectionRule);
27644
- this.closeMenus();
27645
- }
27646
- toggleMenu(menu) {
27647
- const isSelected = this.state.openedMenu === menu;
27648
- this.closeMenus();
27649
- if (!isSelected) {
27650
- this.state.openedMenu = menu;
27651
- }
27652
27861
  }
27653
27862
  updateSectionRule(sectionRule) {
27654
27863
  this.state.sectionRuleDispatchResult = this.props.updateChart(this.props.figureId, {
27655
27864
  sectionRule,
27656
27865
  });
27866
+ if (this.state.sectionRuleDispatchResult.isSuccessful) {
27867
+ this.state.sectionRule = deepCopy(sectionRule);
27868
+ }
27657
27869
  }
27658
27870
  canUpdateSectionRule(sectionRule) {
27659
27871
  this.state.sectionRuleDispatchResult = this.props.canUpdateChart(this.props.figureId, {
27660
27872
  sectionRule,
27661
27873
  });
27662
27874
  }
27663
- closeMenus() {
27664
- this.state.openedMenu = undefined;
27875
+ get backgroundColorTitle() {
27876
+ return ChartTerms.BackgroundColor;
27665
27877
  }
27666
27878
  }
27667
27879
 
@@ -27809,39 +28021,36 @@ class ScorecardChartConfigPanel extends owl.Component {
27809
28021
 
27810
28022
  class ScorecardChartDesignPanel extends owl.Component {
27811
28023
  static template = "o-spreadsheet-ScorecardChartDesignPanel";
27812
- static components = { ColorPickerWidget, ChartColor, ChartTitle, Section };
28024
+ static components = { RoundColorPicker, ChartTitle, Section, Checkbox };
27813
28025
  static props = {
27814
28026
  figureId: String,
27815
28027
  definition: Object,
27816
28028
  updateChart: Function,
27817
28029
  canUpdateChart: Function,
27818
28030
  };
27819
- state = owl.useState({
27820
- openedColorPicker: undefined,
27821
- });
27822
- setup() {
27823
- owl.useExternalListener(window, "click", this.closeMenus);
27824
- }
27825
28031
  get title() {
27826
28032
  return _t(this.props.definition.title);
27827
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
+ }
27828
28042
  updateTitle(title) {
27829
28043
  this.props.updateChart(this.props.figureId, { title });
27830
28044
  }
28045
+ updateHumanizeNumbers(humanize) {
28046
+ this.props.updateChart(this.props.figureId, { humanize });
28047
+ }
27831
28048
  translate(term) {
27832
28049
  return _t(term);
27833
28050
  }
27834
28051
  updateBaselineDescr(ev) {
27835
28052
  this.props.updateChart(this.props.figureId, { baselineDescr: ev.target.value });
27836
28053
  }
27837
- toggleColorPicker(colorPickerId) {
27838
- if (this.state.openedColorPicker === colorPickerId) {
27839
- this.state.openedColorPicker = undefined;
27840
- }
27841
- else {
27842
- this.state.openedColorPicker = colorPickerId;
27843
- }
27844
- }
27845
28054
  setColor(color, colorPickerId) {
27846
28055
  switch (colorPickerId) {
27847
28056
  case "backgroundColor":
@@ -27854,10 +28063,9 @@ class ScorecardChartDesignPanel extends owl.Component {
27854
28063
  this.props.updateChart(this.props.figureId, { baselineColorUp: color });
27855
28064
  break;
27856
28065
  }
27857
- this.closeMenus();
27858
28066
  }
27859
- closeMenus() {
27860
- this.state.openedColorPicker = undefined;
28067
+ get backgroundColorTitle() {
28068
+ return ChartTerms.BackgroundColor;
27861
28069
  }
27862
28070
  }
27863
28071
 
@@ -28806,6 +29014,7 @@ class ConditionalFormattingEditor extends owl.Component {
28806
29014
  ColorPickerWidget,
28807
29015
  ConditionalFormatPreviewList,
28808
29016
  Section,
29017
+ RoundColorPicker,
28809
29018
  };
28810
29019
  icons = ICONS;
28811
29020
  cellIsOperators = CellIsOperators;
@@ -32310,6 +32519,7 @@ class Composer extends owl.Component {
32310
32519
  onComposerCellFocused: { type: Function, optional: true },
32311
32520
  onComposerContentFocused: Function,
32312
32521
  isDefaultFocus: { type: Boolean, optional: true },
32522
+ onInputContextMenu: { type: Function, optional: true },
32313
32523
  };
32314
32524
  static components = { TextValueProvider, FunctionDescriptionProvider };
32315
32525
  static defaultProps = {
@@ -32360,6 +32570,9 @@ class Composer extends owl.Component {
32360
32570
  assistantStyle.right = `0px`;
32361
32571
  }
32362
32572
  }
32573
+ else if (this.props.delimitation) {
32574
+ assistantStyle["max-height"] = `${this.props.delimitation.height}px`;
32575
+ }
32363
32576
  return cssPropertiesToCss(assistantStyle);
32364
32577
  }
32365
32578
  // we can't allow input events to be triggered while we remove and add back the content of the composer in processContent
@@ -32646,6 +32859,11 @@ class Composer extends owl.Component {
32646
32859
  }
32647
32860
  }
32648
32861
  }
32862
+ onContextMenu(ev) {
32863
+ if (this.composerStore.editionMode === "inactive") {
32864
+ this.props.onInputContextMenu?.(ev);
32865
+ }
32866
+ }
32649
32867
  // ---------------------------------------------------------------------------
32650
32868
  // Private
32651
32869
  // ---------------------------------------------------------------------------
@@ -32867,6 +33085,7 @@ class GridComposer extends owl.Component {
32867
33085
  static template = "o-spreadsheet-GridComposer";
32868
33086
  static props = {
32869
33087
  gridDims: Object,
33088
+ onInputContextMenu: Function,
32870
33089
  };
32871
33090
  static components = { Composer };
32872
33091
  rect = this.defaultRect;
@@ -32914,6 +33133,7 @@ class GridComposer extends owl.Component {
32914
33133
  isDefaultFocus: true,
32915
33134
  onComposerContentFocused: () => this.composerFocusStore.focusGridComposerContent(),
32916
33135
  onComposerCellFocused: (content) => this.composerFocusStore.focusGridComposerCell(content),
33136
+ onInputContextMenu: this.props.onInputContextMenu,
32917
33137
  };
32918
33138
  }
32919
33139
  get containerStyle() {
@@ -33012,7 +33232,6 @@ class GridCellIcon extends owl.Component {
33012
33232
  cellPosition: Object,
33013
33233
  horizontalAlign: { type: String, optional: true },
33014
33234
  verticalAlign: { type: String, optional: true },
33015
- offset: { type: Object, optional: true },
33016
33235
  slots: Object,
33017
33236
  };
33018
33237
  get iconStyle() {
@@ -33023,8 +33242,8 @@ class GridCellIcon extends owl.Component {
33023
33242
  const x = this.getIconHorizontalPosition(rect, cellPosition);
33024
33243
  const y = this.getIconVerticalPosition(rect, cellPosition);
33025
33244
  return cssPropertiesToCss({
33026
- top: `${y + (this.props.offset?.y || 0)}px`,
33027
- left: `${x + (this.props.offset?.x || 0)}px`,
33245
+ top: `${y}px`,
33246
+ left: `${x}px`,
33028
33247
  });
33029
33248
  }
33030
33249
  getIconVerticalPosition(rect, cellPosition) {
@@ -33064,83 +33283,6 @@ class GridCellIcon extends owl.Component {
33064
33283
  }
33065
33284
  }
33066
33285
 
33067
- css /* scss */ `
33068
- .o-filter-icon {
33069
- color: ${FILTERS_COLOR};
33070
- display: flex;
33071
- align-items: center;
33072
- justify-content: center;
33073
- width: ${GRID_ICON_EDGE_LENGTH}px;
33074
- height: ${GRID_ICON_EDGE_LENGTH}px;
33075
-
33076
- &:hover {
33077
- background: ${FILTERS_COLOR};
33078
- color: #fff;
33079
- }
33080
-
33081
- &.o-high-contrast {
33082
- color: #defade;
33083
- }
33084
- &.o-high-contrast:hover {
33085
- color: ${FILTERS_COLOR};
33086
- background: #fff;
33087
- }
33088
- }
33089
- .o-filter-icon:hover {
33090
- background: ${FILTERS_COLOR};
33091
- color: #fff;
33092
- }
33093
- `;
33094
- class FilterIcon extends owl.Component {
33095
- static template = "o-spreadsheet-FilterIcon";
33096
- static props = {
33097
- cellPosition: Object,
33098
- };
33099
- cellPopovers;
33100
- setup() {
33101
- this.cellPopovers = useStore(CellPopoverStore);
33102
- }
33103
- onClick() {
33104
- const position = this.props.cellPosition;
33105
- const activePopover = this.cellPopovers.persistentCellPopover;
33106
- const { col, row } = position;
33107
- if (activePopover.isOpen &&
33108
- activePopover.col === col &&
33109
- activePopover.row === row &&
33110
- activePopover.type === "FilterMenu") {
33111
- this.cellPopovers.close();
33112
- return;
33113
- }
33114
- this.cellPopovers.open({ col, row }, "FilterMenu");
33115
- }
33116
- get isFilterActive() {
33117
- return this.env.model.getters.isFilterActive(this.props.cellPosition);
33118
- }
33119
- get iconClass() {
33120
- const cellStyle = this.env.model.getters.getCellComputedStyle(this.props.cellPosition);
33121
- const luminance = relativeLuminance(cellStyle.fillColor || "#fff");
33122
- return luminance < 0.45 ? "o-high-contrast" : "";
33123
- }
33124
- }
33125
-
33126
- class FilterIconsOverlay extends owl.Component {
33127
- static template = "o-spreadsheet-FilterIconsOverlay";
33128
- static props = {
33129
- gridPosition: { type: Object, optional: true },
33130
- };
33131
- static components = {
33132
- GridCellIcon,
33133
- FilterIcon,
33134
- };
33135
- static defaultProps = {
33136
- gridPosition: { x: 0, y: 0 },
33137
- };
33138
- getFilterHeadersPositions() {
33139
- const sheetId = this.env.model.getters.getActiveSheetId();
33140
- return this.env.model.getters.getFilterHeaders(sheetId);
33141
- }
33142
- }
33143
-
33144
33286
  const CHECKBOX_WIDTH = 15;
33145
33287
  const MARGIN = (GRID_ICON_EDGE_LENGTH - CHECKBOX_WIDTH) / 2;
33146
33288
  css /* scss */ `
@@ -33808,6 +33950,80 @@ class FiguresContainer extends owl.Component {
33808
33950
  }
33809
33951
  }
33810
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
+
33811
34027
  css /* scss */ `
33812
34028
  .o-grid-add-rows {
33813
34029
  input {
@@ -34021,7 +34237,12 @@ class GridOverlay extends owl.Component {
34021
34237
  onGridMoved: Function,
34022
34238
  gridOverlayDimensions: String,
34023
34239
  };
34024
- static components = { FiguresContainer, DataValidationOverlay, GridAddRowsFooter };
34240
+ static components = {
34241
+ FiguresContainer,
34242
+ DataValidationOverlay,
34243
+ GridAddRowsFooter,
34244
+ FilterIconsOverlay,
34245
+ };
34025
34246
  static defaultProps = {
34026
34247
  onCellHovered: () => { },
34027
34248
  onCellDoubleClicked: () => { },
@@ -34066,7 +34287,7 @@ class GridOverlay extends owl.Component {
34066
34287
  get isPaintingFormat() {
34067
34288
  return this.env.model.getters.isPaintingFormat();
34068
34289
  }
34069
- onMouseDown(ev) {
34290
+ onMouseDown(ev, modifiers) {
34070
34291
  if (ev.button > 0) {
34071
34292
  // not main button, probably a context menu
34072
34293
  return;
@@ -34075,6 +34296,7 @@ class GridOverlay extends owl.Component {
34075
34296
  this.props.onCellClicked(col, row, {
34076
34297
  expandZone: ev.shiftKey,
34077
34298
  addZone: isCtrlKey(ev),
34299
+ closePopover: modifiers?.closePopover ?? true,
34078
34300
  });
34079
34301
  }
34080
34302
  onDoubleClick(ev) {
@@ -35840,7 +36062,6 @@ class Grid extends owl.Component {
35840
36062
  Popover,
35841
36063
  VerticalScrollBar,
35842
36064
  HorizontalScrollBar,
35843
- FilterIconsOverlay,
35844
36065
  };
35845
36066
  HEADER_HEIGHT = HEADER_HEIGHT;
35846
36067
  HEADER_WIDTH = HEADER_WIDTH;
@@ -36141,17 +36362,17 @@ class Grid extends owl.Component {
36141
36362
  // ---------------------------------------------------------------------------
36142
36363
  // Zone selection with mouse
36143
36364
  // ---------------------------------------------------------------------------
36144
- onCellClicked(col, row, { addZone, expandZone }) {
36145
- if (this.cellPopovers.isOpen) {
36365
+ onCellClicked(col, row, modifiers) {
36366
+ if (modifiers.closePopover && this.cellPopovers.isOpen) {
36146
36367
  this.cellPopovers.close();
36147
36368
  }
36148
36369
  if (this.composerStore.editionMode === "editing") {
36149
36370
  this.composerStore.stopEdition();
36150
36371
  }
36151
- if (expandZone) {
36372
+ if (modifiers.expandZone) {
36152
36373
  this.env.model.selection.setAnchorCorner(col, row);
36153
36374
  }
36154
- else if (addZone) {
36375
+ else if (modifiers.addZone) {
36155
36376
  this.env.model.selection.addCellToSelection(col, row);
36156
36377
  }
36157
36378
  else {
@@ -36873,7 +37094,7 @@ const CHART_TYPE_CONVERSION_MAP = {
36873
37094
  line3DChart: undefined,
36874
37095
  stockChart: undefined,
36875
37096
  radarChart: undefined,
36876
- scatterChart: undefined,
37097
+ scatterChart: "scatter",
36877
37098
  pieChart: "pie",
36878
37099
  pie3DChart: undefined,
36879
37100
  doughnutChart: "pie",
@@ -37637,29 +37858,12 @@ function convertWidthFromExcel(width) {
37637
37858
  return width;
37638
37859
  return Math.round((width / WIDTH_FACTOR) * 100) / 100;
37639
37860
  }
37640
- function convertBorderDescr(descr) {
37641
- if (!descr) {
37642
- return undefined;
37643
- }
37644
- return {
37645
- style: descr.style,
37646
- color: { rgb: descr.color },
37647
- };
37648
- }
37649
37861
  function extractStyle(cell, data) {
37650
37862
  let style = {};
37651
37863
  if (cell.style) {
37652
37864
  style = data.styles[cell.style];
37653
37865
  }
37654
37866
  const format = extractFormat(cell, data);
37655
- const exportedBorder = {};
37656
- if (cell.border) {
37657
- const border = data.borders[cell.border];
37658
- exportedBorder.left = convertBorderDescr(border.left);
37659
- exportedBorder.right = convertBorderDescr(border.right);
37660
- exportedBorder.bottom = convertBorderDescr(border.bottom);
37661
- exportedBorder.top = convertBorderDescr(border.top);
37662
- }
37663
37867
  const styles = {
37664
37868
  font: {
37665
37869
  size: style?.fontSize || DEFAULT_FONT_SIZE,
@@ -37673,7 +37877,7 @@ function extractStyle(cell, data) {
37673
37877
  }
37674
37878
  : { reservedAttribute: "none" },
37675
37879
  numFmt: format ? { format: format, id: 0 /* id not used for export */ } : undefined,
37676
- border: exportedBorder || {},
37880
+ border: cell.border || 0,
37677
37881
  alignment: {
37678
37882
  horizontal: style.align,
37679
37883
  vertical: style.verticalAlign
@@ -37695,15 +37899,12 @@ function extractFormat(cell, data) {
37695
37899
  return undefined;
37696
37900
  }
37697
37901
  function normalizeStyle(construct, styles) {
37698
- const { id: fontId } = pushElement(styles["font"], construct.fonts);
37699
- const { id: fillId } = pushElement(styles["fill"], construct.fills);
37700
- const { id: borderId } = pushElement(styles["border"], construct.borders);
37701
37902
  // Normalize this
37702
37903
  const numFmtId = convertFormat(styles["numFmt"], construct.numFmts);
37703
37904
  const style = {
37704
- fontId,
37705
- fillId,
37706
- borderId,
37905
+ fontId: pushElement(styles.font, construct.fonts),
37906
+ fillId: pushElement(styles.fill, construct.fills),
37907
+ borderId: styles.border,
37707
37908
  numFmtId,
37708
37909
  alignment: {
37709
37910
  vertical: styles.alignment.vertical,
@@ -37711,8 +37912,7 @@ function normalizeStyle(construct, styles) {
37711
37912
  wrapText: styles.alignment.wrapText,
37712
37913
  },
37713
37914
  };
37714
- const { id } = pushElement(style, construct.styles);
37715
- return id;
37915
+ return pushElement(style, construct.styles);
37716
37916
  }
37717
37917
  function convertFormat(format, numFmtStructure) {
37718
37918
  if (!format) {
@@ -37720,8 +37920,7 @@ function convertFormat(format, numFmtStructure) {
37720
37920
  }
37721
37921
  let formatId = XLSX_FORMAT_MAP[format.format];
37722
37922
  if (!formatId) {
37723
- const { id } = pushElement(format, numFmtStructure);
37724
- formatId = id + FIRST_NUMFMT_ID;
37923
+ formatId = pushElement(format, numFmtStructure) + FIRST_NUMFMT_ID;
37725
37924
  }
37726
37925
  return formatId;
37727
37926
  }
@@ -37746,20 +37945,15 @@ function addRelsToFile(relsFiles, path, rel) {
37746
37945
  return id;
37747
37946
  }
37748
37947
  function pushElement(property, propertyList) {
37749
- for (let [key, value] of Object.entries(propertyList)) {
37750
- if (JSON.stringify(value) === JSON.stringify(property)) {
37751
- 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;
37752
37953
  }
37753
37954
  }
37754
- let elemId = propertyList.findIndex((elem) => JSON.stringify(elem) === JSON.stringify(property));
37755
- if (elemId === -1) {
37756
- propertyList.push(property);
37757
- elemId = propertyList.length - 1;
37758
- }
37759
- return {
37760
- id: elemId,
37761
- list: propertyList,
37762
- };
37955
+ propertyList[propertyList.length] = property;
37956
+ return propertyList.length - 1;
37763
37957
  }
37764
37958
  const chartIds = [];
37765
37959
  /**
@@ -38554,7 +38748,25 @@ function parseXML(xmlString, mimeType = "text/xml") {
38554
38748
  }
38555
38749
  return document;
38556
38750
  }
38557
- 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];
38558
38770
  return {
38559
38771
  relsFiles: [],
38560
38772
  sharedStrings: [],
@@ -38577,7 +38789,7 @@ function getDefaultXLSXStructure() {
38577
38789
  },
38578
38790
  ],
38579
38791
  fills: [{ reservedAttribute: "none" }, { reservedAttribute: "gray125" }],
38580
- borders: [{}],
38792
+ borders,
38581
38793
  numFmts: [],
38582
38794
  dxfs: [],
38583
38795
  };
@@ -39113,8 +39325,8 @@ class XlsxChartExtractor extends XlsxBaseExtractor {
39113
39325
  return {
39114
39326
  title: chartTitle,
39115
39327
  type: CHART_TYPE_CONVERSION_MAP[chartType],
39116
- dataSets: this.extractChartDatasets(this.querySelector(rootChartElement, `c:${chartType}`)),
39117
- 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`),
39118
39330
  backgroundColor: this.extractChildAttr(rootChartElement, "c:chartSpace > c:spPr a:srgbClr", "val", {
39119
39331
  default: "ffffff",
39120
39332
  }).asString(),
@@ -39143,8 +39355,8 @@ class XlsxChartExtractor extends XlsxBaseExtractor {
39143
39355
  title: chartTitle,
39144
39356
  type: "combo",
39145
39357
  dataSets: [
39146
- ...this.extractChartDatasets(this.querySelector(chartElement, `c:barChart`)),
39147
- ...this.extractChartDatasets(this.querySelector(chartElement, `c:lineChart`)),
39358
+ ...this.extractChartDatasets(this.querySelector(chartElement, `c:barChart`), "comboChart"),
39359
+ ...this.extractChartDatasets(this.querySelector(chartElement, `c:lineChart`), "comboChart"),
39148
39360
  ],
39149
39361
  labelRange: this.extractChildTextContent(chartElement, "c:ser c:cat c:f"),
39150
39362
  backgroundColor: this.extractChildAttr(chartElement, "c:chartSpace > c:spPr a:srgbClr", "val", {
@@ -39162,7 +39374,10 @@ class XlsxChartExtractor extends XlsxBaseExtractor {
39162
39374
  fontColor: "000000",
39163
39375
  };
39164
39376
  }
39165
- extractChartDatasets(chartElement) {
39377
+ extractChartDatasets(chartElement, chartType) {
39378
+ if (chartType === "scatterChart") {
39379
+ return this.extractScatterChartDatasets(chartElement);
39380
+ }
39166
39381
  return this.mapOnElements({ parent: chartElement, query: "c:ser" }, (chartDataElement) => {
39167
39382
  return {
39168
39383
  label: this.extractChildTextContent(chartDataElement, "c:tx c:f"),
@@ -39170,6 +39385,14 @@ class XlsxChartExtractor extends XlsxBaseExtractor {
39170
39385
  };
39171
39386
  });
39172
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
+ }
39173
39396
  /**
39174
39397
  * The chart type in the XML isn't explicitly defined, but there is an XML element that define the
39175
39398
  * chart, and this element tag name tells us which type of chart it is. We just need to find this XML element.
@@ -40563,12 +40786,14 @@ class BasePlugin {
40563
40786
  static getters = [];
40564
40787
  history;
40565
40788
  dispatch;
40566
- constructor(stateObserver, dispatch) {
40789
+ canDispatch;
40790
+ constructor(stateObserver, dispatch, canDispatch) {
40567
40791
  this.history = Object.assign(Object.create(stateObserver), {
40568
40792
  update: stateObserver.addChange.bind(stateObserver, this),
40569
40793
  selectCell: () => { },
40570
40794
  });
40571
40795
  this.dispatch = dispatch;
40796
+ this.canDispatch = canDispatch;
40572
40797
  }
40573
40798
  /**
40574
40799
  * Export for excel should be available for all plugins, even for the UI.
@@ -40647,8 +40872,8 @@ class BasePlugin {
40647
40872
  class CorePlugin extends BasePlugin {
40648
40873
  getters;
40649
40874
  uuidGenerator;
40650
- constructor({ getters, stateObserver, range, dispatch, uuidGenerator }) {
40651
- super(stateObserver, dispatch);
40875
+ constructor({ getters, stateObserver, range, dispatch, canDispatch, uuidGenerator, }) {
40876
+ super(stateObserver, dispatch, canDispatch);
40652
40877
  range.addRangeProvider(this.adaptRanges.bind(this));
40653
40878
  this.getters = getters;
40654
40879
  this.uuidGenerator = uuidGenerator;
@@ -46145,8 +46370,8 @@ class UIPlugin extends BasePlugin {
46145
46370
  getters;
46146
46371
  ui;
46147
46372
  selection;
46148
- constructor({ getters, stateObserver, dispatch, uiActions, selection }) {
46149
- super(stateObserver, dispatch);
46373
+ constructor({ getters, stateObserver, dispatch, canDispatch, uiActions, selection, }) {
46374
+ super(stateObserver, dispatch, canDispatch);
46150
46375
  this.getters = getters;
46151
46376
  this.ui = uiActions;
46152
46377
  this.selection = selection;
@@ -47426,7 +47651,9 @@ class Evaluator {
47426
47651
  this.blockedArrayFormulas = this.createEmptyPositionSet();
47427
47652
  this.spreadingRelations = new SpreadingRelation(this.createEmptyPositionSet.bind(this));
47428
47653
  this.formulaDependencies = lazy(() => {
47429
- 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) => ({
47430
47657
  data: position,
47431
47658
  boundingBox: {
47432
47659
  zone: range.zone,
@@ -51510,6 +51737,7 @@ const invalidateTableStyleCommands = [
51510
51737
  "CREATE_TABLE",
51511
51738
  "UPDATE_TABLE",
51512
51739
  "UPDATE_FILTER",
51740
+ "REMOVE_TABLE",
51513
51741
  ];
51514
51742
  const invalidateTableStyleCommandsSet = new Set(invalidateTableStyleCommands);
51515
51743
  function doesCommandInvalidatesTableStyle(cmd) {
@@ -51537,6 +51765,10 @@ class CellComputedStylePlugin extends UIPlugin {
51537
51765
  this.styles = {};
51538
51766
  return;
51539
51767
  }
51768
+ if (invalidateBordersCommands.has(cmd.type)) {
51769
+ this.borders = {};
51770
+ return;
51771
+ }
51540
51772
  }
51541
51773
  getCellComputedBorder(position) {
51542
51774
  const { sheetId, row, col } = position;
@@ -55477,7 +55709,6 @@ class SpreadsheetDashboard extends owl.Component {
55477
55709
  Popover,
55478
55710
  VerticalScrollBar,
55479
55711
  HorizontalScrollBar,
55480
- FilterIconsOverlay,
55481
55712
  };
55482
55713
  cellPopovers;
55483
55714
  onMouseWheel;
@@ -56364,6 +56595,13 @@ class TopBarComposer extends owl.Component {
56364
56595
  "border-color": SELECTION_BORDER_COLOR,
56365
56596
  });
56366
56597
  }
56598
+ get delimitation() {
56599
+ const { width, height } = this.env.model.getters.getSheetViewDimensionWithHeaders();
56600
+ return {
56601
+ width,
56602
+ height,
56603
+ };
56604
+ }
56367
56605
  onFocus(selection) {
56368
56606
  this.composerFocusStore.focusTopBarComposer(selection);
56369
56607
  }
@@ -58647,6 +58885,9 @@ function createChart(chart, chartSheetIndex, data) {
58647
58885
  case "line":
58648
58886
  plot = addLineChart(chart.data);
58649
58887
  break;
58888
+ case "scatter":
58889
+ plot = addScatterChart(chart.data);
58890
+ break;
58650
58891
  case "pie":
58651
58892
  plot = addDoughnutChart(chart.data, chartSheetIndex, data, { holeSize: 0 });
58652
58893
  break;
@@ -58924,6 +59165,52 @@ function addLineChart(chart) {
58924
59165
  ${addAx(axisPos, "c:valAx", valAxId, catAxId, { fontColor: chart.fontColor })}
58925
59166
  `;
58926
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
+ }
58927
59214
  function addDoughnutChart(chart, chartSheetIndex, data, { holeSize } = { holeSize: 50 }) {
58928
59215
  const colors = new ChartColors();
58929
59216
  const maxLength = largeMax(chart.dataSets.map((ds) => getRangeSize(ds.range, chartSheetIndex, data)));
@@ -59064,8 +59351,7 @@ function addContent(content, sharedStrings, forceString = false) {
59064
59351
  attrs.push(["t", "b"]);
59065
59352
  }
59066
59353
  else if (forceString || !isNumber(value, DEFAULT_LOCALE)) {
59067
- const { id } = pushElement(content, sharedStrings);
59068
- value = id.toString();
59354
+ value = pushElement(content, sharedStrings);
59069
59355
  attrs.push(["t", "s"]);
59070
59356
  }
59071
59357
  return { attrs, node: escapeXml /*xml*/ `<v>${value}</v>` };
@@ -59190,8 +59476,7 @@ function addCellIsRule(cf, rule, dxfs) {
59190
59476
  if (rule.style.fillColor) {
59191
59477
  dxf.fill = { fgColor: { rgb: rule.style.fillColor } };
59192
59478
  }
59193
- const { id } = pushElement(dxf, dxfs);
59194
- ruleAttributes.push(["dxfId", id]);
59479
+ ruleAttributes.push(["dxfId", pushElement(dxf, dxfs)]);
59195
59480
  return escapeXml /*xml*/ `
59196
59481
  <conditionalFormatting sqref="${cf.ranges.join(" ")}">
59197
59482
  <cfRule ${formatAttributes(ruleAttributes)}>
@@ -60021,8 +60306,9 @@ function addSheetViews(sheet) {
60021
60306
  */
60022
60307
  function getXLSX(data) {
60023
60308
  data = fixLengthySheetNames(data);
60309
+ data = purgeSingleRowTables(data);
60024
60310
  const files = [];
60025
- const construct = getDefaultXLSXStructure();
60311
+ const construct = getDefaultXLSXStructure(data);
60026
60312
  files.push(createWorkbook(data, construct));
60027
60313
  files.push(...createWorksheets(data, construct));
60028
60314
  files.push(createStylesSheet(construct));
@@ -60302,6 +60588,16 @@ function fixLengthySheetNames(data) {
60302
60588
  }
60303
60589
  return JSON.parse(stringifiedData);
60304
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
+ }
60305
60601
 
60306
60602
  var Status;
60307
60603
  (function (Status) {
@@ -60555,6 +60851,7 @@ class Model extends EventBus {
60555
60851
  stateObserver: this.state,
60556
60852
  range: this.range,
60557
60853
  dispatch: this.dispatchFromCorePlugin,
60854
+ canDispatch: this.canDispatch,
60558
60855
  uuidGenerator: this.uuidGenerator,
60559
60856
  custom: this.config.custom,
60560
60857
  external: this.config.external,
@@ -60565,6 +60862,7 @@ class Model extends EventBus {
60565
60862
  getters: this.getters,
60566
60863
  stateObserver: this.state,
60567
60864
  dispatch: this.dispatch,
60865
+ canDispatch: this.canDispatch,
60568
60866
  selection: this.selection,
60569
60867
  moveClient: this.session.move.bind(this.session),
60570
60868
  custom: this.config.custom,
@@ -60888,7 +61186,7 @@ const links = {
60888
61186
  const components = {
60889
61187
  Checkbox,
60890
61188
  Section,
60891
- ChartColor,
61189
+ RoundColorPicker,
60892
61190
  ChartDataSeries,
60893
61191
  ChartErrorSection,
60894
61192
  ChartLabelRange,
@@ -60991,6 +61289,6 @@ exports.tokenColors = tokenColors;
60991
61289
  exports.tokenize = tokenize;
60992
61290
 
60993
61291
 
60994
- __info__.version = "17.3.0-alpha.2";
60995
- __info__.date = "2024-04-05T14:01:07.060Z";
60996
- __info__.hash = "8c5a229";
61292
+ __info__.version = "17.3.0-alpha.3";
61293
+ __info__.date = "2024-04-10T12:28:23.658Z";
61294
+ __info__.hash = "80b5056";