@odoo/o-spreadsheet 18.1.0-alpha.6 → 18.1.0-alpha.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,9 +2,9 @@
2
2
  /**
3
3
  * This file is generated by o-spreadsheet build tools. Do not edit it.
4
4
  * @see https://github.com/odoo/o-spreadsheet
5
- * @version 18.1.0-alpha.6
6
- * @date 2024-11-28T09:06:59.527Z
7
- * @hash 875c901
5
+ * @version 18.1.0-alpha.8
6
+ * @date 2024-12-19T07:49:54.421Z
7
+ * @hash 87a1567
8
8
  */
9
9
 
10
10
  (function (exports, owl) {
@@ -171,10 +171,13 @@
171
171
  const ALERT_INFO_BORDER = "#98DBE2";
172
172
  const ALERT_INFO_TEXT_COLOR = "#09414A";
173
173
  const BADGE_SELECTED_COLOR = "#E6F2F3";
174
- const DEFAULT_CHART_PADDING = 20;
175
- const DEFAULT_CHART_FONT_SIZE = 22;
176
- const SCORECARD_GAUGE_CHART_PADDING = 10;
177
- const SCORECARD_GAUGE_CHART_FONT_SIZE = 14;
174
+ const CHART_PADDING$1 = 20;
175
+ const CHART_PADDING_BOTTOM = 10;
176
+ const CHART_PADDING_TOP = 15;
177
+ const CHART_TITLE_FONT_SIZE = 16;
178
+ const CHART_AXIS_TITLE_FONT_SIZE = 12;
179
+ const SCORECARD_CHART_TITLE_FONT_SIZE = 14;
180
+ const PIVOT_TOKEN_COLOR = "#F28C28";
178
181
  // Color picker defaults as upper case HEX to match `toHex`helper
179
182
  const COLOR_PICKER_DEFAULTS = [
180
183
  "#000000",
@@ -263,7 +266,6 @@
263
266
  const MIN_COL_WIDTH = 5;
264
267
  const HEADER_HEIGHT = 26;
265
268
  const HEADER_WIDTH = 48;
266
- const TOPBAR_HEIGHT = 63;
267
269
  const TOPBAR_TOOLBAR_HEIGHT = 34;
268
270
  const BOTTOMBAR_HEIGHT = 36;
269
271
  const DEFAULT_CELL_WIDTH = 96;
@@ -334,8 +336,8 @@
334
336
  const DEBOUNCE_TIME = 200;
335
337
  const MESSAGE_VERSION = 1;
336
338
  // Sheets
337
- const FORBIDDEN_SHEET_CHARS = ["'", "*", "?", "/", "\\", "[", "]"];
338
- const FORBIDDEN_IN_EXCEL_REGEX = /'|\*|\?|\/|\\|\[|\]/;
339
+ const FORBIDDEN_SHEETNAME_CHARS = ["'", "*", "?", "/", "\\", "[", "]"];
340
+ const FORBIDDEN_SHEETNAME_CHARS_IN_EXCEL_REGEX = /'|\*|\?|\/|\\|\[|\]/;
339
341
  // Cells
340
342
  const FORMULA_REF_IDENTIFIER = "|";
341
343
  // Components
@@ -388,6 +390,7 @@
388
390
  //------------------------------------------------------------------------------
389
391
  // Miscellaneous
390
392
  //------------------------------------------------------------------------------
393
+ const sanitizeSheetNameRegex = new RegExp(FORBIDDEN_SHEETNAME_CHARS_IN_EXCEL_REGEX, "g");
391
394
  /**
392
395
  * Remove quotes from a quoted string
393
396
  * ```js
@@ -483,6 +486,10 @@
483
486
  }
484
487
  return symbolName;
485
488
  }
489
+ /** Replace the excel-excluded characters of a sheetName */
490
+ function sanitizeSheetName(sheetName, replacementChar = " ") {
491
+ return sheetName.replace(sanitizeSheetNameRegex, replacementChar);
492
+ }
486
493
  function clip(val, min, max) {
487
494
  return val < min ? min : val > max ? max : val;
488
495
  }
@@ -782,6 +789,7 @@
782
789
  * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Character_Classes
783
790
  */
784
791
  const whiteSpaceSpecialCharacters = [
792
+ " ",
785
793
  "\t",
786
794
  "\f",
787
795
  "\v",
@@ -796,17 +804,15 @@
796
804
  String.fromCharCode(parseInt("3000", 16)),
797
805
  String.fromCharCode(parseInt("feff", 16)),
798
806
  ];
799
- const whiteSpaceRegexp = new RegExp(whiteSpaceSpecialCharacters.join("|") + "|(\r\n|\r|\n)", "g");
807
+ const whiteSpaceRegexp = new RegExp(whiteSpaceSpecialCharacters.join("|"), "g");
808
+ const newLineRegexp = /(\r\n|\r)/g;
800
809
  /**
801
- * Replace all the special spaces in a string (non-breaking, tabs, ...) by normal spaces, and all the
802
- * different newlines types by \n.
810
+ * Replace all different newlines characters by \n
803
811
  */
804
- function replaceSpecialSpaces(text) {
812
+ function replaceNewLines(text) {
805
813
  if (!text)
806
814
  return "";
807
- if (!whiteSpaceRegexp.test(text))
808
- return text;
809
- return text.replace(whiteSpaceRegexp, (match, newLine) => (newLine ? NEWLINE : " "));
815
+ return text.replace(newLineRegexp, NEWLINE);
810
816
  }
811
817
  /**
812
818
  * Determine if the numbers are consecutive.
@@ -972,7 +978,7 @@
972
978
 
973
979
  const RBA_REGEX = /rgba?\(|\s+|\)/gi;
974
980
  const HEX_MATCH = /^#([A-F\d]{2}){3,4}$/;
975
- const colors$1 = [
981
+ const colors = [
976
982
  "#eb6d00",
977
983
  "#0074d9",
978
984
  "#ad8e00",
@@ -995,6 +1001,12 @@
995
1001
  function colorNumberString(color) {
996
1002
  return toHex(color.toString(16).padStart(6, "0"));
997
1003
  }
1004
+ function colorToNumber(color) {
1005
+ if (typeof color === "number") {
1006
+ return color;
1007
+ }
1008
+ return Number.parseInt(toHex(color).slice(1), 16);
1009
+ }
998
1010
  /**
999
1011
  * Converts any CSS color value to a standardized hex6 value.
1000
1012
  * Accepts: hex3, hex6, hex8, rgb[1] and rgba[1].
@@ -1345,9 +1357,9 @@
1345
1357
  "#056BD9", // Blue #3
1346
1358
  "#155193", // Blue #4
1347
1359
  "#A76DBC", // Violet #1
1348
- "#7F4295", // Violet #1
1349
- "#6D2387", // Violet #1
1350
- "#4F1565", // Violet #1
1360
+ "#7F4295", // Violet #2
1361
+ "#6D2387", // Violet #3
1362
+ "#4F1565", // Violet #4
1351
1363
  "#EA6175", // Red #1
1352
1364
  "#CE4257", // Red #2
1353
1365
  "#982738", // Red #3
@@ -1373,6 +1385,81 @@
1373
1385
  "#C08A16", // Yellow #3
1374
1386
  "#936A12", // Yellow #4
1375
1387
  ];
1388
+ // Same as above but with alternating colors
1389
+ const ALTERNATING_COLORS_MD = [
1390
+ "#4EA7F2", // Blue #1
1391
+ "#43C5B1", // Teal #1
1392
+ "#EA6175", // Red #1
1393
+ "#F4A261", // Orange #1
1394
+ "#8481DD", // Purple #1
1395
+ "#FFD86D", // Yellow #1
1396
+ "#3188E6", // Blue #2
1397
+ "#00A78D", // Teal #2
1398
+ "#CE4257", // Red #2
1399
+ "#F48935", // Orange #2
1400
+ "#5752D1", // Purple #2
1401
+ "#FFBC2C", // Yellow #2
1402
+ ];
1403
+ const ALTERNATING_COLORS_LG = [
1404
+ "#4EA7F2", // Blue #1
1405
+ "#A76DBC", // Violet #1
1406
+ "#EA6175", // Red #1
1407
+ "#43C5B1", // Teal #1
1408
+ "#F4A261", // Orange #1
1409
+ "#8481DD", // Purple #1
1410
+ "#A4A8B6", // Gray #1
1411
+ "#FFD86D", // Yellow #1
1412
+ "#3188E6", // Blue #2
1413
+ "#7F4295", // Violet #2
1414
+ "#CE4257", // Red #2
1415
+ "#00A78D", // Teal #2
1416
+ "#F48935", // Orange #2
1417
+ "#5752D1", // Purple #2
1418
+ "#7E8290", // Gray #2
1419
+ "#FFBC2C", // Yellow #2
1420
+ "#056BD9", // Blue #3
1421
+ "#6D2387", // Violet #3
1422
+ "#982738", // Red #3
1423
+ "#0E8270", // Teal #3
1424
+ "#BE5D10", // Orange #3
1425
+ "#3A3580", // Purple #3
1426
+ "#545B70", // Gray #3
1427
+ "#C08A16", // Yellow #3
1428
+ ];
1429
+ const ALTERNATING_COLORS_XL = [
1430
+ "#4EA7F2", // Blue #1
1431
+ "#A76DBC", // Violet #1
1432
+ "#EA6175", // Red #1
1433
+ "#43C5B1", // Teal #1
1434
+ "#F4A261", // Orange #1
1435
+ "#8481DD", // Purple #1
1436
+ "#A4A8B6", // Grey #1
1437
+ "#FFD86D", // Yellow #1
1438
+ "#3188E6", // Blue #2
1439
+ "#7F4295", // Violet #2
1440
+ "#CE4257", // Red #2
1441
+ "#00A78D", // Teal #2
1442
+ "#F48935", // Orange #2
1443
+ "#5752D1", // Purple #2
1444
+ "#7E8290", // Grey #2
1445
+ "#FFBC2C", // Yellow #2
1446
+ "#056BD9", // Blue #3
1447
+ "#6D2387", // Violet #3
1448
+ "#982738", // Red #3
1449
+ "#0E8270", // Teal #3
1450
+ "#BE5D10", // Orange #3
1451
+ "#3A3580", // Purple #3
1452
+ "#545B70", // Grey #3
1453
+ "#C08A16", // Yellow #3
1454
+ "#155193", // Blue #4
1455
+ "#4F1565", // Violet #4
1456
+ "#791B29", // Red #4
1457
+ "#105F53", // Teal #4
1458
+ "#7D380D", // Orange #4
1459
+ "#26235F", // Purple #4
1460
+ "#3F4250", // Grey #4
1461
+ "#936A12", // Yellow #4
1462
+ ];
1376
1463
  function getNthColor(index, palette) {
1377
1464
  return palette[index % palette.length];
1378
1465
  }
@@ -1390,6 +1477,20 @@
1390
1477
  return COLORS_XL;
1391
1478
  }
1392
1479
  }
1480
+ function getAlternatingColorsPalette(quantity) {
1481
+ if (quantity <= 6) {
1482
+ return COLORS_SM;
1483
+ }
1484
+ else if (quantity <= 12) {
1485
+ return ALTERNATING_COLORS_MD;
1486
+ }
1487
+ else if (quantity <= 24) {
1488
+ return ALTERNATING_COLORS_LG;
1489
+ }
1490
+ else {
1491
+ return ALTERNATING_COLORS_XL;
1492
+ }
1493
+ }
1393
1494
  class ColorGenerator {
1394
1495
  preferredColors;
1395
1496
  currentColorIndex = 0;
@@ -1404,6 +1505,62 @@
1404
1505
  : getNthColor(this.currentColorIndex++, this.palette);
1405
1506
  }
1406
1507
  }
1508
+ class AlternatingColorGenerator extends ColorGenerator {
1509
+ constructor(paletteSize, preferredColors = []) {
1510
+ super(paletteSize, preferredColors);
1511
+ this.palette = getAlternatingColorsPalette(paletteSize).filter((c) => !preferredColors.includes(c));
1512
+ }
1513
+ }
1514
+ /**
1515
+ * Returns a function that maps a value to a color using a color scale defined by the given
1516
+ * color/threshold values pairs.
1517
+ */
1518
+ function getColorScale(colorScalePoints) {
1519
+ if (colorScalePoints.length < 2) {
1520
+ throw new Error("Color scale must have at least 2 points");
1521
+ }
1522
+ const sortedColorScalePoints = [...colorScalePoints.sort((a, b) => a.value - b.value)];
1523
+ const thresholds = [];
1524
+ for (let i = 1; i < sortedColorScalePoints.length; i++) {
1525
+ const minColor = colorToNumber(sortedColorScalePoints[i - 1].color);
1526
+ const maxColor = colorToNumber(sortedColorScalePoints[i].color);
1527
+ thresholds.push({
1528
+ min: sortedColorScalePoints[i - 1].value,
1529
+ max: sortedColorScalePoints[i].value,
1530
+ minColor,
1531
+ maxColor,
1532
+ colorDiff: computeColorDiffUnits(sortedColorScalePoints[i - 1].value, sortedColorScalePoints[i].value, minColor, maxColor),
1533
+ });
1534
+ }
1535
+ return (value) => {
1536
+ if (value < thresholds[0].min) {
1537
+ return colorNumberString(thresholds[0].minColor);
1538
+ }
1539
+ for (const threshold of thresholds) {
1540
+ if (value >= threshold.min && value <= threshold.max) {
1541
+ return colorNumberString(colorCell(value, threshold.min, threshold.minColor, threshold.colorDiff));
1542
+ }
1543
+ }
1544
+ return colorNumberString(thresholds[thresholds.length - 1].maxColor);
1545
+ };
1546
+ }
1547
+ function computeColorDiffUnits(minValue, maxValue, minColor, maxColor) {
1548
+ const deltaValue = maxValue - minValue;
1549
+ const deltaColorR = ((minColor >> 16) % 256) - ((maxColor >> 16) % 256);
1550
+ const deltaColorG = ((minColor >> 8) % 256) - ((maxColor >> 8) % 256);
1551
+ const deltaColorB = (minColor % 256) - (maxColor % 256);
1552
+ const colorDiffUnitR = deltaColorR / deltaValue;
1553
+ const colorDiffUnitG = deltaColorG / deltaValue;
1554
+ const colorDiffUnitB = deltaColorB / deltaValue;
1555
+ return [colorDiffUnitR, colorDiffUnitG, colorDiffUnitB];
1556
+ }
1557
+ function colorCell(value, minValue, minColor, colorDiffUnit) {
1558
+ const [colorDiffUnitR, colorDiffUnitG, colorDiffUnitB] = colorDiffUnit;
1559
+ const r = Math.round(((minColor >> 16) % 256) - colorDiffUnitR * (value - minValue));
1560
+ const g = Math.round(((minColor >> 8) % 256) - colorDiffUnitG * (value - minValue));
1561
+ const b = Math.round((minColor % 256) - colorDiffUnitB * (value - minValue));
1562
+ return (r << 16) | (g << 8) | b;
1563
+ }
1407
1564
 
1408
1565
  //------------------------------------------------------------------------------
1409
1566
  // Coordinate
@@ -3275,6 +3432,7 @@
3275
3432
  ]);
3276
3433
  const invalidateChartEvaluationCommands = new Set([
3277
3434
  "EVALUATE_CELLS",
3435
+ "EVALUATE_CHARTS",
3278
3436
  "UPDATE_CELL",
3279
3437
  "UNHIDE_COLUMNS_ROWS",
3280
3438
  "HIDE_COLUMNS_ROWS",
@@ -3311,6 +3469,7 @@
3311
3469
  "RESIZE_SHEETVIEW",
3312
3470
  "SET_VIEWPORT_OFFSET",
3313
3471
  "EVALUATE_CELLS",
3472
+ "EVALUATE_CHARTS",
3314
3473
  "SET_FORMULA_VISIBILITY",
3315
3474
  "UPDATE_FILTER",
3316
3475
  ]);
@@ -6515,7 +6674,7 @@
6515
6674
  const POSTFIX_UNARY_OPERATORS = ["%"];
6516
6675
  const OPERATORS = "+,-,*,/,:,=,<>,>=,>,<=,<,^,&".split(",").concat(POSTFIX_UNARY_OPERATORS);
6517
6676
  function tokenize(str, locale = DEFAULT_LOCALE) {
6518
- str = replaceSpecialSpaces(str);
6677
+ str = replaceNewLines(str);
6519
6678
  const chars = new TokenizingChars(str);
6520
6679
  const result = [];
6521
6680
  while (!chars.isOver()) {
@@ -6662,12 +6821,12 @@
6662
6821
  if (length) {
6663
6822
  return { type: "SPACE", value: NEWLINE.repeat(length) };
6664
6823
  }
6665
- while (chars.current === " ") {
6666
- length++;
6667
- chars.shift();
6824
+ let spaces = "";
6825
+ while (chars.current && chars.current.match(whiteSpaceRegexp)) {
6826
+ spaces += chars.shift();
6668
6827
  }
6669
- if (length) {
6670
- return { type: "SPACE", value: " ".repeat(length) };
6828
+ if (spaces) {
6829
+ return { type: "SPACE", value: spaces };
6671
6830
  }
6672
6831
  return null;
6673
6832
  }
@@ -7526,6 +7685,16 @@
7526
7685
  }
7527
7686
  return domainToString([...domain.slice(0, index), ...domain.slice(index + 1)]);
7528
7687
  }
7688
+ function sortPivotTree(tree, baseDomain, sortFn) {
7689
+ const sortedTree = [...tree];
7690
+ const domain = [...baseDomain];
7691
+ sortedTree.sort((node1, node2) => sortFn([...domain, node1], [...domain, node2]));
7692
+ for (const node of tree) {
7693
+ const children = sortPivotTree(node.children, [...domain, node], sortFn);
7694
+ node.children = children;
7695
+ }
7696
+ return sortedTree;
7697
+ }
7529
7698
 
7530
7699
  const pivotTimeAdapterRegistry = new Registry();
7531
7700
  function pivotTimeAdapter(granularity) {
@@ -8036,6 +8205,29 @@
8036
8205
  format: `${" ".repeat(indent)}${format}* `,
8037
8206
  };
8038
8207
  }
8208
+ function isSortedColumnValid(sortedColumn, pivot) {
8209
+ try {
8210
+ if (!pivot.getMeasure(sortedColumn.measure)) {
8211
+ return false;
8212
+ }
8213
+ const columns = pivot.definition.columns;
8214
+ for (let i = 0; i < sortedColumn.domain.length; i++) {
8215
+ if (columns[i].nameWithGranularity !== sortedColumn.domain[i].field) {
8216
+ return false;
8217
+ }
8218
+ const possibleValues = pivot
8219
+ .getPossibleFieldValues(columns[i])
8220
+ .map((v) => v.value);
8221
+ if (!possibleValues.includes(sortedColumn.domain[i].value)) {
8222
+ return false;
8223
+ }
8224
+ }
8225
+ return true;
8226
+ }
8227
+ catch (e) {
8228
+ return false;
8229
+ }
8230
+ }
8039
8231
 
8040
8232
  class CellClipboardHandler extends AbstractCellClipboardHandler {
8041
8233
  isCutAllowed(data) {
@@ -9515,6 +9707,12 @@ stores.inject(MyMetaStore, storeInstance);
9515
9707
  }
9516
9708
  return relativeLuminance(backgroundColor) < 0.3 ? "#FFFFFF" : "#000000";
9517
9709
  }
9710
+ function chartMutedFontColor(backgroundColor) {
9711
+ if (!backgroundColor) {
9712
+ return "#666666";
9713
+ }
9714
+ return relativeLuminance(backgroundColor) < 0.3 ? "#C8C8C8" : "#666666";
9715
+ }
9518
9716
  function checkDataset(definition) {
9519
9717
  if (definition.dataSets) {
9520
9718
  const invalidRanges = definition.dataSets.find((range) => !rangeReference.test(range.dataRange)) !== undefined;
@@ -9650,8 +9848,8 @@ stores.inject(MyMetaStore, storeInstance);
9650
9848
  const yMin = chart.chartArea.top;
9651
9849
  const textsPositions = {};
9652
9850
  for (const dataset of chart._metasets) {
9653
- if (dataset.xAxisID === TREND_LINE_XAXIS_ID) {
9654
- return; // ignore trend lines
9851
+ if (dataset.xAxisID === TREND_LINE_XAXIS_ID || dataset.hidden) {
9852
+ continue;
9655
9853
  }
9656
9854
  for (let i = 0; i < dataset._parsed.length; i++) {
9657
9855
  const parsedValue = dataset._parsed[i];
@@ -10098,7 +10296,7 @@ stores.inject(MyMetaStore, storeInstance);
10098
10296
  function drawScoreChart(structure, canvas) {
10099
10297
  const ctx = canvas.getContext("2d");
10100
10298
  canvas.width = structure.canvas.width;
10101
- const availableWidth = canvas.width - DEFAULT_CHART_PADDING;
10299
+ const availableWidth = canvas.width - CHART_PADDING$1 * 2;
10102
10300
  canvas.height = structure.canvas.height;
10103
10301
  ctx.fillStyle = structure.canvas.backgroundColor;
10104
10302
  ctx.fillRect(0, 0, structure.canvas.width, structure.canvas.height);
@@ -10233,10 +10431,9 @@ stores.inject(MyMetaStore, storeInstance);
10233
10431
  }
10234
10432
 
10235
10433
  /* Padding at the border of the chart */
10236
- const CHART_PADDING = SCORECARD_GAUGE_CHART_PADDING;
10434
+ const CHART_PADDING = 10;
10237
10435
  const BOTTOM_PADDING_RATIO = 0.05;
10238
10436
  /* Maximum font sizes of each element */
10239
- const CHART_TITLE_FONT_SIZE = SCORECARD_GAUGE_CHART_FONT_SIZE;
10240
10437
  const KEY_VALUE_FONT_SIZE = 32;
10241
10438
  const BASELINE_MAX_FONT_SIZE = 16;
10242
10439
  function formatBaselineDescr(baselineDescr, baseline) {
@@ -10308,7 +10505,7 @@ stores.inject(MyMetaStore, storeInstance);
10308
10505
  : this.height - (this.height - titleHeight - baselineHeight) / 2 - CHART_PADDING,
10309
10506
  },
10310
10507
  };
10311
- const minimalBaselinePosition = baselineArrowSize + DEFAULT_CHART_PADDING;
10508
+ const minimalBaselinePosition = baselineArrowSize + CHART_PADDING * 2;
10312
10509
  if (structure.baseline.position.x < minimalBaselinePosition) {
10313
10510
  structure.baseline.position.x = minimalBaselinePosition;
10314
10511
  }
@@ -10388,7 +10585,7 @@ stores.inject(MyMetaStore, storeInstance);
10388
10585
  return this.runtime.background;
10389
10586
  }
10390
10587
  get secondaryFontColor() {
10391
- return relativeLuminance(this.backgroundColor) > 0.3 ? "#525252" : "#C8C8C8";
10588
+ return chartMutedFontColor(this.backgroundColor);
10392
10589
  }
10393
10590
  getTextDimensions(text, font) {
10394
10591
  this.context.font = font;
@@ -10414,7 +10611,7 @@ stores.inject(MyMetaStore, storeInstance);
10414
10611
  }
10415
10612
  return {
10416
10613
  title: {
10417
- font: getDefaultContextFont(CHART_TITLE_FONT_SIZE, this.runtime.title.bold, this.runtime.title.italic),
10614
+ font: getDefaultContextFont(this.runtime.title.fontSize ?? SCORECARD_CHART_TITLE_FONT_SIZE, this.runtime.title.bold, this.runtime.title.italic),
10418
10615
  color: this.runtime.title.color ?? this.secondaryFontColor,
10419
10616
  },
10420
10617
  keyValue: {
@@ -14639,7 +14836,7 @@ stores.inject(MyMetaStore, storeInstance);
14639
14836
  CellValueType.boolean,
14640
14837
  ];
14641
14838
  function cellsSortingCriterion(sortingOrder) {
14642
- const inverse = sortingOrder === "ascending" ? 1 : -1;
14839
+ const inverse = sortingOrder === "asc" ? 1 : -1;
14643
14840
  return (left, right) => {
14644
14841
  if (left.type === CellValueType.empty) {
14645
14842
  return right.type === CellValueType.empty ? 0 : 1;
@@ -14731,7 +14928,7 @@ stores.inject(MyMetaStore, storeInstance);
14731
14928
  const sortColumns = [];
14732
14929
  const nRows = matrix.length;
14733
14930
  for (let i = 0; i < criteria.length; i += 2) {
14734
- sortingOrders.push(toBoolean(toScalar(criteria[i + 1])?.value) ? "ascending" : "descending");
14931
+ sortingOrders.push(toBoolean(toScalar(criteria[i + 1])?.value) ? "asc" : "desc");
14735
14932
  const sortColumn = criteria[i];
14736
14933
  if (isMatrix(sortColumn) && (sortColumn.length > 1 || sortColumn[0].length > 1)) {
14737
14934
  assert(() => sortColumn.length === 1 && sortColumn[0].length === nRows, _t("Wrong size for %s. Expected a range of size 1x%s. Got %sx%s.", `sort_column${i + 1}`, nRows, sortColumn.length, sortColumn[0].length));
@@ -14748,12 +14945,12 @@ stores.inject(MyMetaStore, storeInstance);
14748
14945
  if (sortColumns.length === 0) {
14749
14946
  for (let i = 0; i < matrix[0].length; i++) {
14750
14947
  sortColumns.push(matrix.map((row) => row[i].value));
14751
- sortingOrders.push("ascending");
14948
+ sortingOrders.push("asc");
14752
14949
  }
14753
14950
  }
14754
14951
  const sortingCriteria = {
14755
- descending: cellsSortingCriterion("descending"),
14756
- ascending: cellsSortingCriterion("ascending"),
14952
+ desc: cellsSortingCriterion("desc"),
14953
+ asc: cellsSortingCriterion("asc"),
14757
14954
  };
14758
14955
  const indexes = range(0, matrix.length);
14759
14956
  indexes.sort((a, b) => {
@@ -20477,20 +20674,8 @@ stores.inject(MyMetaStore, storeInstance);
20477
20674
  replaceSelectedRange(zone) {
20478
20675
  const ref = this.getZoneReference(zone);
20479
20676
  const currentToken = this.tokenAtCursor;
20480
- let replaceStart = this.selectionStart;
20481
- if (currentToken?.type === "REFERENCE") {
20482
- replaceStart = currentToken.start;
20483
- }
20484
- else if (currentToken?.type === "RIGHT_PAREN") {
20485
- // match left parenthesis
20486
- const leftParenthesisIndex = this.currentTokens.findIndex((token) => token.type === "LEFT_PAREN" && token.parenthesesCode === currentToken.parenthesesCode);
20487
- const functionToken = this.currentTokens[leftParenthesisIndex - 1];
20488
- if (functionToken === undefined) {
20489
- return;
20490
- }
20491
- replaceStart = functionToken.start;
20492
- }
20493
- this.replaceText(ref, replaceStart, this.selectionEnd);
20677
+ const start = currentToken?.type === "REFERENCE" ? currentToken.start : this.selectionStart;
20678
+ this.replaceText(ref, start, this.selectionEnd);
20494
20679
  }
20495
20680
  /**
20496
20681
  * Replace the reference of the old zone by the new one.
@@ -20523,17 +20708,6 @@ stores.inject(MyMetaStore, storeInstance);
20523
20708
  getZoneReference(zone) {
20524
20709
  const inputSheetId = this.sheetId;
20525
20710
  const sheetId = this.getters.getActiveSheetId();
20526
- if (zone.top === zone.bottom && zone.left === zone.right) {
20527
- const position = { sheetId, col: zone.left, row: zone.top };
20528
- const pivotId = this.getters.getPivotIdFromPosition(position);
20529
- const pivotCell = this.getters.getPivotCellFromPosition(position);
20530
- const cell = this.getters.getCell(position);
20531
- if (pivotId && pivotCell.type !== "EMPTY" && !cell?.isFormula) {
20532
- const formulaPivotId = this.getters.getPivotFormulaId(pivotId);
20533
- const formula = createPivotFormula(formulaPivotId, pivotCell);
20534
- return localizeFormula(formula, this.getters.getLocale()).slice(1); // strip leading =
20535
- }
20536
- }
20537
20711
  const range = this.getters.getRangeFromZone(sheetId, zone);
20538
20712
  return this.getters.getSelectionRangeString(range, inputSheetId);
20539
20713
  }
@@ -20704,37 +20878,21 @@ stores.inject(MyMetaStore, storeInstance);
20704
20878
  const editionSheetId = this.sheetId;
20705
20879
  const rangeColor = (rangeString) => {
20706
20880
  const colorIndex = this.colorIndexByRange[rangeString];
20707
- return colors$1[colorIndex % colors$1.length];
20881
+ return colors[colorIndex % colors.length];
20708
20882
  };
20709
- const highlights = [];
20710
- for (const range of this.getReferencedRanges()) {
20883
+ return this.getReferencedRanges().map((range) => {
20711
20884
  const rangeString = this.getters.getRangeString(range, editionSheetId);
20712
20885
  const { numberOfRows, numberOfCols } = zoneToDimension(range.zone);
20713
20886
  const zone = numberOfRows * numberOfCols === 1
20714
20887
  ? this.getters.expandZone(range.sheetId, range.zone)
20715
20888
  : range.zone;
20716
- highlights.push({
20889
+ return {
20717
20890
  zone,
20718
20891
  color: rangeColor(rangeString),
20719
20892
  sheetId: range.sheetId,
20720
20893
  interactive: true,
20721
- });
20722
- }
20723
- const activeSheetId = this.getters.getActiveSheetId();
20724
- const selectionZone = this.model.selection.getAnchor().zone;
20725
- const isSelectionHightlighted = highlights.find((highlight) => highlight.sheetId === activeSheetId && isEqual(highlight.zone, selectionZone));
20726
- if (this.editionMode === "selecting" && !isSelectionHightlighted) {
20727
- highlights.push({
20728
- zone: selectionZone,
20729
- color: "#445566",
20730
- sheetId: activeSheetId,
20731
- dashed: true,
20732
- interactive: false,
20733
- noFill: true,
20734
- thinLine: true,
20735
- });
20736
- }
20737
- return highlights;
20894
+ };
20895
+ });
20738
20896
  }
20739
20897
  /**
20740
20898
  * Return ranges currently referenced in the composer
@@ -20962,6 +21120,7 @@ stores.inject(MyMetaStore, storeInstance);
20962
21120
  return error;
20963
21121
  },
20964
21122
  isBadExpression: true,
21123
+ normalizedFormula: tokens.map((t) => t.value).join(""),
20965
21124
  };
20966
21125
  }
20967
21126
  }
@@ -21089,6 +21248,7 @@ stores.inject(MyMetaStore, storeInstance);
21089
21248
  symbols,
21090
21249
  tokens,
21091
21250
  isBadExpression: false,
21251
+ normalizedFormula: cacheKey,
21092
21252
  };
21093
21253
  return compiledFormula;
21094
21254
  }
@@ -21235,7 +21395,7 @@ stores.inject(MyMetaStore, storeInstance);
21235
21395
 
21236
21396
  const PIVOT_FUNCTIONS = ["PIVOT.VALUE", "PIVOT.HEADER", "PIVOT"];
21237
21397
  /**
21238
- * Create a proposal entry for the compose autowcomplete
21398
+ * Create a proposal entry for the composer autocomplete
21239
21399
  * to insert a field name string in a formula.
21240
21400
  */
21241
21401
  function makeFieldProposal(field, granularity) {
@@ -21353,6 +21513,7 @@ stores.inject(MyMetaStore, storeInstance);
21353
21513
  description: definition.name,
21354
21514
  htmlContent: [{ value: str, color: tokenColors.NUMBER }],
21355
21515
  fuzzySearchKey: str + definition.name,
21516
+ alwaysExpanded: true,
21356
21517
  };
21357
21518
  })
21358
21519
  .filter(isDefined);
@@ -22018,14 +22179,8 @@ stores.inject(MyMetaStore, storeInstance);
22018
22179
  const GAUGE_LABELS_FONT_SIZE = 12;
22019
22180
  const GAUGE_DEFAULT_VALUE_FONT_SIZE = 80;
22020
22181
  const GAUGE_BACKGROUND_COLOR = "#F3F2F1";
22021
- const GAUGE_TEXT_COLOR = "#666666";
22022
- const GAUGE_TEXT_COLOR_HIGH_CONTRAST = "#C8C8C8";
22023
- const GAUGE_INFLECTION_MARKER_COLOR = "#666666aa";
22024
22182
  const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
22025
22183
  const GAUGE_TITLE_SECTION_HEIGHT = 25;
22026
- const GAUGE_TITLE_FONT_SIZE = SCORECARD_GAUGE_CHART_FONT_SIZE;
22027
- const GAUGE_TITLE_PADDING_LEFT = SCORECARD_GAUGE_CHART_PADDING;
22028
- const GAUGE_TITLE_PADDING_TOP = SCORECARD_GAUGE_CHART_PADDING;
22029
22184
  function drawGaugeChart(canvas, runtime) {
22030
22185
  const canvasBoundingRect = canvas.getBoundingClientRect();
22031
22186
  canvas.width = canvasBoundingRect.width;
@@ -22084,7 +22239,7 @@ stores.inject(MyMetaStore, storeInstance);
22084
22239
  ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment
22085
22240
  ctx.rotate(Math.PI / 2 - inflectionValue.rotation);
22086
22241
  ctx.lineWidth = 2;
22087
- ctx.strokeStyle = GAUGE_INFLECTION_MARKER_COLOR;
22242
+ ctx.strokeStyle = chartMutedFontColor(config.backgroundColor) + "aa";
22088
22243
  ctx.beginPath();
22089
22244
  ctx.moveTo(0, -(height - config.gauge.arcWidth));
22090
22245
  ctx.lineTo(0, -height - 3);
@@ -22138,22 +22293,22 @@ stores.inject(MyMetaStore, storeInstance);
22138
22293
  x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
22139
22294
  y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
22140
22295
  };
22141
- const textColor = getContrastedTextColor(runtime.background);
22296
+ const textColor = chartMutedFontColor(runtime.background);
22142
22297
  const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);
22143
22298
  let x = 0, titleWidth = 0, titleHeight = 0;
22144
22299
  if (runtime.title.text) {
22145
- ({ width: titleWidth, height: titleHeight } = computeTextDimension(ctx, runtime.title.text, { ...runtime.title, fontSize: GAUGE_TITLE_FONT_SIZE }, "px"));
22300
+ ({ width: titleWidth, height: titleHeight } = computeTextDimension(ctx, runtime.title.text, { fontSize: CHART_TITLE_FONT_SIZE, ...runtime.title }, "px"));
22146
22301
  }
22147
22302
  switch (runtime.title.align) {
22148
22303
  case "right":
22149
- x = boundingRect.width - titleWidth - GAUGE_TITLE_PADDING_LEFT;
22304
+ x = boundingRect.width - titleWidth - CHART_PADDING$1;
22150
22305
  break;
22151
22306
  case "center":
22152
22307
  x = (boundingRect.width - titleWidth) / 2;
22153
22308
  break;
22154
22309
  case "left":
22155
22310
  default:
22156
- x = GAUGE_TITLE_PADDING_LEFT;
22311
+ x = CHART_PADDING$1;
22157
22312
  break;
22158
22313
  }
22159
22314
  return {
@@ -22161,10 +22316,10 @@ stores.inject(MyMetaStore, storeInstance);
22161
22316
  height: boundingRect.height,
22162
22317
  title: {
22163
22318
  label: runtime.title.text ?? "",
22164
- fontSize: GAUGE_TITLE_FONT_SIZE,
22319
+ fontSize: runtime.title.fontSize ?? CHART_TITLE_FONT_SIZE,
22165
22320
  textPosition: {
22166
22321
  x,
22167
- y: GAUGE_TITLE_PADDING_TOP + titleHeight / 2,
22322
+ y: CHART_PADDING_TOP + titleHeight / 2,
22168
22323
  },
22169
22324
  color: runtime.title.color ?? textColor,
22170
22325
  bold: runtime.title.bold,
@@ -22281,11 +22436,6 @@ stores.inject(MyMetaStore, storeInstance);
22281
22436
  }
22282
22437
  return runtime.colors.at(-1);
22283
22438
  }
22284
- function getContrastedTextColor(backgroundColor) {
22285
- return relativeLuminance(backgroundColor) > 0.3
22286
- ? GAUGE_TEXT_COLOR
22287
- : GAUGE_TEXT_COLOR_HIGH_CONTRAST;
22288
- }
22289
22439
  function getSegmentsOfRectangle(rectangle) {
22290
22440
  return [
22291
22441
  { start: rectangle.topLeft, end: rectangle.topRight },
@@ -26999,13 +27149,12 @@ stores.inject(MyMetaStore, storeInstance);
26999
27149
  versionFrom: "7",
27000
27150
  migrate(data) {
27001
27151
  const namesTaken = [];
27002
- const globalForbiddenInExcel = new RegExp(FORBIDDEN_IN_EXCEL_REGEX, "g");
27003
27152
  for (let sheet of data.sheets || []) {
27004
27153
  if (!sheet.name) {
27005
27154
  continue;
27006
27155
  }
27007
27156
  const oldName = sheet.name;
27008
- const escapedName = oldName.replace(globalForbiddenInExcel, "_");
27157
+ const escapedName = sanitizeSheetName(oldName, "_");
27009
27158
  let i = 1;
27010
27159
  let newName = escapedName;
27011
27160
  while (namesTaken.includes(newName)) {
@@ -27758,6 +27907,19 @@ stores.inject(MyMetaStore, storeInstance);
27758
27907
  ["GaugeLowerInflectionPointNaN" /* CommandResult.GaugeLowerInflectionPointNaN */]: _t("The lower inflection point value must be a number"),
27759
27908
  ["GaugeUpperInflectionPointNaN" /* CommandResult.GaugeUpperInflectionPointNaN */]: _t("The upper inflection point value must be a number"),
27760
27909
  },
27910
+ GeoChart: {
27911
+ ColorScales: {
27912
+ blues: _t("Blues"),
27913
+ cividis: _t("Cividis"),
27914
+ greens: _t("Greens"),
27915
+ greys: _t("Greys"),
27916
+ oranges: _t("Oranges"),
27917
+ purples: _t("Purples"),
27918
+ rainbow: _t("Rainbow"),
27919
+ reds: _t("Reds"),
27920
+ viridis: _t("Viridis"),
27921
+ },
27922
+ },
27761
27923
  };
27762
27924
  const CustomCurrencyTerms = {
27763
27925
  Custom: _t("Custom"),
@@ -28161,6 +28323,26 @@ stores.inject(MyMetaStore, storeInstance);
28161
28323
  locale: getters.getLocale(),
28162
28324
  };
28163
28325
  }
28326
+ function getGeoChartData(definition, dataSets, labelRange, getters) {
28327
+ const labelValues = getChartLabelValues(getters, dataSets, labelRange);
28328
+ let labels = labelValues.formattedValues;
28329
+ if (definition.dataSetsHaveTitle) {
28330
+ labels.shift();
28331
+ }
28332
+ let dataSetsValues = getChartDatasetValues(getters, dataSets);
28333
+ ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));
28334
+ const format = getChartDatasetFormat(getters, dataSets, "left") ||
28335
+ getChartDatasetFormat(getters, dataSets, "right");
28336
+ return {
28337
+ dataSetsValues,
28338
+ axisFormats: { y: format },
28339
+ labels,
28340
+ locale: getters.getLocale(),
28341
+ availableRegions: getters.getGeoChartAvailableRegions(),
28342
+ geoFeatureNameToId: getters.geoFeatureNameToId,
28343
+ getGeoJsonFeatures: getters.getGeoJsonFeatures,
28344
+ };
28345
+ }
28164
28346
  function getTrendDatasetForBarChart(config, data) {
28165
28347
  const filteredValues = [];
28166
28348
  const filteredLabels = [];
@@ -28241,37 +28423,45 @@ stores.inject(MyMetaStore, storeInstance);
28241
28423
  const labelRange = labelMax - labelMin;
28242
28424
  const normalizedLabels = labels.map((v) => (v - labelMin) / labelRange);
28243
28425
  const normalizedNewLabels = newLabels.map((v) => (v - labelMin) / labelRange);
28244
- switch (config.type) {
28245
- case "polynomial": {
28246
- const order = config.order ?? 2;
28247
- if (order === 1) {
28248
- return predictLinearValues([values], [normalizedLabels], [normalizedNewLabels], true)[0];
28249
- }
28250
- const coeffs = polynomialRegression(values, normalizedLabels, order, true).flat();
28251
- return normalizedNewLabels.map((v) => evaluatePolynomial(coeffs, v, order));
28252
- }
28253
- case "exponential": {
28254
- const positiveLogValues = [];
28255
- const filteredLabels = [];
28256
- for (let i = 0; i < values.length; i++) {
28257
- if (values[i] > 0) {
28258
- positiveLogValues.push(Math.log(values[i]));
28259
- filteredLabels.push(normalizedLabels[i]);
28426
+ try {
28427
+ switch (config.type) {
28428
+ case "polynomial": {
28429
+ const order = config.order;
28430
+ if (!order) {
28431
+ return Array.from({ length: newLabels.length }, () => NaN);
28432
+ }
28433
+ if (order === 1) {
28434
+ return predictLinearValues([values], [normalizedLabels], [normalizedNewLabels], true)[0];
28435
+ }
28436
+ const coeffs = polynomialRegression(values, normalizedLabels, order, true).flat();
28437
+ return normalizedNewLabels.map((v) => evaluatePolynomial(coeffs, v, order));
28438
+ }
28439
+ case "exponential": {
28440
+ const positiveLogValues = [];
28441
+ const filteredLabels = [];
28442
+ for (let i = 0; i < values.length; i++) {
28443
+ if (values[i] > 0) {
28444
+ positiveLogValues.push(Math.log(values[i]));
28445
+ filteredLabels.push(normalizedLabels[i]);
28446
+ }
28447
+ }
28448
+ if (!filteredLabels.length) {
28449
+ return Array.from({ length: newLabels.length }, () => NaN);
28260
28450
  }
28451
+ return expM(predictLinearValues([positiveLogValues], [filteredLabels], [normalizedNewLabels], true))[0];
28261
28452
  }
28262
- if (!filteredLabels.length) {
28263
- return [];
28453
+ case "logarithmic": {
28454
+ return predictLinearValues([values], logM([normalizedLabels]), logM([normalizedNewLabels]), true)[0];
28264
28455
  }
28265
- return expM(predictLinearValues([positiveLogValues], [filteredLabels], [normalizedNewLabels], true))[0];
28266
- }
28267
- case "logarithmic": {
28268
- return predictLinearValues([values], logM([normalizedLabels]), logM([normalizedNewLabels]), true)[0];
28269
- }
28270
- case "trailingMovingAverage": {
28271
- return getMovingAverageValues(values, config.window);
28456
+ case "trailingMovingAverage": {
28457
+ return getMovingAverageValues(values, config.window);
28458
+ }
28459
+ default:
28460
+ return Array.from({ length: newLabels.length }, () => NaN);
28272
28461
  }
28273
- default:
28274
- return [];
28462
+ }
28463
+ catch (e) {
28464
+ return Array.from({ length: newLabels.length }, () => NaN);
28275
28465
  }
28276
28466
  }
28277
28467
  function getChartAxisType(chart, labelRange, getters) {
@@ -28705,6 +28895,43 @@ stores.inject(MyMetaStore, storeInstance);
28705
28895
  }
28706
28896
  return datasets;
28707
28897
  }
28898
+ function getGeoChartDatasets(definition, args) {
28899
+ const { availableRegions, dataSetsValues, labels } = args;
28900
+ const regionName = definition.region || availableRegions[0]?.id;
28901
+ const features = regionName ? args.getGeoJsonFeatures(regionName) : undefined;
28902
+ const dataset = {
28903
+ outline: features,
28904
+ showOutline: !!features,
28905
+ data: [],
28906
+ };
28907
+ if (features && regionName) {
28908
+ const labelsAndValues = {};
28909
+ if (dataSetsValues[0]) {
28910
+ for (let i = 0; i < dataSetsValues[0].data.length; i++) {
28911
+ if (!labels[i] || dataSetsValues[0].data[i] === undefined) {
28912
+ continue;
28913
+ }
28914
+ const featureId = args.geoFeatureNameToId(regionName, labels[i]);
28915
+ if (featureId) {
28916
+ labelsAndValues[featureId] = { value: dataSetsValues[0].data[i], label: labels[i] };
28917
+ }
28918
+ }
28919
+ }
28920
+ for (const feature of features) {
28921
+ if (!feature.id) {
28922
+ continue;
28923
+ }
28924
+ dataset.data.push({
28925
+ feature: {
28926
+ ...feature,
28927
+ properties: { name: labelsAndValues[feature.id]?.label },
28928
+ },
28929
+ value: labelsAndValues[feature.id]?.value,
28930
+ });
28931
+ }
28932
+ }
28933
+ return [dataset];
28934
+ }
28708
28935
  function getTrendingLineDataSet(dataset, config, data) {
28709
28936
  const defaultBorderColor = colorToRGBA(dataset.backgroundColor);
28710
28937
  defaultBorderColor.a = 1;
@@ -28742,54 +28969,16 @@ stores.inject(MyMetaStore, storeInstance);
28742
28969
  return new ColorGenerator(dataSetsSize, definition.dataSets?.map((ds) => ds.backgroundColor) || []);
28743
28970
  }
28744
28971
 
28745
- function getCommonChartLayout(definition) {
28746
- // TODO FIXME: this is unused ATM. All the charts should probably use this instead oh whatever padding they are using now
28747
- // also look into how DEFAULT_CHART_PADDING is used in scorecards, it look strange
28972
+ function getChartLayout(definition) {
28748
28973
  return {
28749
28974
  padding: {
28750
- left: DEFAULT_CHART_PADDING,
28751
- right: DEFAULT_CHART_PADDING,
28752
- top: definition.title?.text ? DEFAULT_CHART_PADDING / 2 : DEFAULT_CHART_PADDING + 5,
28753
- bottom: DEFAULT_CHART_PADDING,
28975
+ left: CHART_PADDING$1,
28976
+ right: CHART_PADDING$1,
28977
+ top: CHART_PADDING_TOP,
28978
+ bottom: CHART_PADDING_BOTTOM,
28754
28979
  },
28755
28980
  };
28756
28981
  }
28757
- function getBarChartLayout(definition) {
28758
- return {
28759
- padding: computeChartPadding({
28760
- displayTitle: !!definition.title?.text,
28761
- displayLegend: definition.legendPosition === "top",
28762
- }),
28763
- };
28764
- }
28765
- function getLineChartLayout(definition) {
28766
- return {
28767
- padding: computeChartPadding({
28768
- displayTitle: !!definition.title?.text,
28769
- displayLegend: definition.legendPosition === "top",
28770
- }),
28771
- };
28772
- }
28773
- function getPieChartLayout(definition) {
28774
- return {
28775
- padding: { left: 20, right: 20, top: definition.title ? 10 : 25, bottom: 10 },
28776
- };
28777
- }
28778
- function getWaterfallChartLayout(definition) {
28779
- return {
28780
- padding: { left: 20, right: 20, top: definition.title ? 10 : 25, bottom: 10 },
28781
- };
28782
- }
28783
- function computeChartPadding({ displayTitle, displayLegend, }) {
28784
- let top = 25;
28785
- if (displayTitle) {
28786
- top = 0;
28787
- }
28788
- else if (displayLegend) {
28789
- top = 10;
28790
- }
28791
- return { left: 20, right: 20, top, bottom: 10 };
28792
- }
28793
28982
 
28794
28983
  function getLegendDisplayOptions(definition, args) {
28795
28984
  return {
@@ -28847,11 +29036,12 @@ stores.inject(MyMetaStore, storeInstance);
28847
29036
  return {
28848
29037
  ...INTERACTIVE_LEGEND_CONFIG,
28849
29038
  ...getLegendDisplayOptions(definition),
28850
- labels: {
28851
- color: chartFontColor(definition.background),
28852
- boxHeight: 6,
28853
- usePointStyle: true,
28854
- },
29039
+ ...getCustomLegendLabels(chartFontColor(definition.background), {
29040
+ pointStyle: "circle",
29041
+ // the stroke is the border around the circle, so increasing its size with the chart's color reduce the size of the circle
29042
+ strokeStyle: definition.background || "#ffffff",
29043
+ lineWidth: 8,
29044
+ }),
28855
29045
  };
28856
29046
  }
28857
29047
  function getComboChartLegend(definition, args) {
@@ -28940,10 +29130,10 @@ stores.inject(MyMetaStore, storeInstance);
28940
29130
  target.style.cursor = "default";
28941
29131
  },
28942
29132
  onClick: (event, legendItem, legend) => {
28943
- if (!legend.legendItems) {
29133
+ const index = legendItem.datasetIndex;
29134
+ if (!legend.legendItems || index === undefined) {
28944
29135
  return;
28945
29136
  }
28946
- const index = legend.legendItems.indexOf(legendItem);
28947
29137
  if (legend.chart.isDatasetVisible(index)) {
28948
29138
  legend.chart.hide(index);
28949
29139
  }
@@ -28959,15 +29149,29 @@ stores.inject(MyMetaStore, storeInstance);
28959
29149
  labels: {
28960
29150
  color: fontColor,
28961
29151
  usePointStyle: true,
28962
- generateLabels: (chart) => chart.data.datasets.map((dataset, index) => ({
28963
- text: dataset.label ?? "",
28964
- fontColor,
28965
- strokeStyle: dataset.borderColor,
28966
- fillStyle: dataset.backgroundColor,
28967
- hidden: !chart.isDatasetVisible(index),
28968
- pointStyle: dataset.type === "line" ? "line" : "rect",
28969
- ...legendLabelConfig,
28970
- })),
29152
+ generateLabels: (chart) => chart.data.datasets.map((dataset, index) => {
29153
+ if (dataset["xAxisID"] === TREND_LINE_XAXIS_ID) {
29154
+ return {
29155
+ text: dataset.label ?? "",
29156
+ fontColor,
29157
+ strokeStyle: dataset.borderColor,
29158
+ hidden: !chart.isDatasetVisible(index),
29159
+ pointStyle: "line",
29160
+ datasetIndex: index,
29161
+ lineWidth: 3,
29162
+ };
29163
+ }
29164
+ return {
29165
+ text: dataset.label ?? "",
29166
+ fontColor,
29167
+ strokeStyle: dataset.borderColor,
29168
+ fillStyle: dataset.backgroundColor,
29169
+ hidden: !chart.isDatasetVisible(index),
29170
+ pointStyle: dataset.type === "line" ? "line" : "rect",
29171
+ datasetIndex: index,
29172
+ ...legendLabelConfig,
29173
+ };
29174
+ }),
28971
29175
  },
28972
29176
  };
28973
29177
  }
@@ -29105,6 +29309,44 @@ stores.inject(MyMetaStore, storeInstance);
29105
29309
  },
29106
29310
  };
29107
29311
  }
29312
+ function getGeoChartScales(definition, args) {
29313
+ const { locale, axisFormats, availableRegions } = args;
29314
+ const geoLegendPosition = legendPositionToGeoLegendPosition(definition.legendPosition);
29315
+ const region = definition.region
29316
+ ? availableRegions.find((r) => r.id === definition.region)
29317
+ : availableRegions[0];
29318
+ const format = axisFormats?.y || axisFormats?.y1;
29319
+ return {
29320
+ projection: {
29321
+ // projection: region?.defaultProjection,
29322
+ projection: getGeoChartProjection(region?.defaultProjection || "mercator"),
29323
+ axis: "x",
29324
+ },
29325
+ color: {
29326
+ axis: "x",
29327
+ display: definition.legendPosition !== "none",
29328
+ border: { color: GRAY_300 },
29329
+ grid: { color: GRAY_300 },
29330
+ ticks: {
29331
+ color: chartFontColor(definition.background),
29332
+ callback: formatTickValue({ locale, format }),
29333
+ },
29334
+ legend: {
29335
+ position: geoLegendPosition,
29336
+ align: geoLegendPosition.includes("right") ? "left" : "right",
29337
+ margin: getLegendMargin(definition),
29338
+ },
29339
+ interpolate: getRuntimeColorScale(definition),
29340
+ missing: definition.missingValueColor || "#ffffff",
29341
+ },
29342
+ };
29343
+ }
29344
+ function getGeoChartProjection(projection) {
29345
+ if (projection === "conicConformal") {
29346
+ return window.ChartGeo.geoConicConformal().rotate([100, 0]); // Centered on the US
29347
+ }
29348
+ return projection;
29349
+ }
29108
29350
  function getChartAxisTitleRuntime(design) {
29109
29351
  if (design?.title?.text) {
29110
29352
  const { text, color, align, italic, bold } = design.title;
@@ -29115,6 +29357,7 @@ stores.inject(MyMetaStore, storeInstance);
29115
29357
  font: {
29116
29358
  style: italic ? "italic" : "normal",
29117
29359
  weight: bold ? "bold" : "normal",
29360
+ size: design.title.fontSize ?? CHART_AXIS_TITLE_FONT_SIZE,
29118
29361
  },
29119
29362
  align: align === "left" ? "start" : align === "right" ? "end" : "center",
29120
29363
  };
@@ -29167,6 +29410,44 @@ stores.inject(MyMetaStore, storeInstance);
29167
29410
  };
29168
29411
  }
29169
29412
  }
29413
+ function getRuntimeColorScale(definition) {
29414
+ if (!definition.colorScale || typeof definition.colorScale === "string") {
29415
+ return definition.colorScale || "oranges";
29416
+ }
29417
+ const scaleColors = [{ value: 0, color: definition.colorScale.minColor }];
29418
+ if (definition.colorScale.midColor) {
29419
+ scaleColors.push({ value: 0.5, color: definition.colorScale.midColor });
29420
+ }
29421
+ scaleColors.push({ value: 1, color: definition.colorScale.maxColor });
29422
+ return getColorScale(scaleColors);
29423
+ }
29424
+ function getLegendMargin(definition) {
29425
+ switch (definition.legendPosition) {
29426
+ case "top":
29427
+ case "right":
29428
+ const hasTitle = !!definition.title.text;
29429
+ const topMargin = hasTitle ? CHART_PADDING_TOP + 30 : CHART_PADDING_TOP;
29430
+ return { top: topMargin, left: CHART_PADDING$1, right: CHART_PADDING$1 };
29431
+ case "bottom":
29432
+ case "left":
29433
+ case "none":
29434
+ return { left: CHART_PADDING$1, right: CHART_PADDING$1, bottom: CHART_PADDING_BOTTOM };
29435
+ }
29436
+ }
29437
+ function legendPositionToGeoLegendPosition(position) {
29438
+ switch (position) {
29439
+ case "top":
29440
+ return "top-left";
29441
+ case "right":
29442
+ return "top-right";
29443
+ case "bottom":
29444
+ return "bottom-right";
29445
+ case "left":
29446
+ return "bottom-left";
29447
+ case "none":
29448
+ return "bottom-left";
29449
+ }
29450
+ }
29170
29451
 
29171
29452
  function getChartShowValues(definition, args) {
29172
29453
  const { axisFormats, locale } = args;
@@ -29180,17 +29461,22 @@ stores.inject(MyMetaStore, storeInstance);
29180
29461
 
29181
29462
  function getChartTitle(definition) {
29182
29463
  const chartTitle = definition.title;
29183
- const fontColor = chartFontColor(definition.background);
29464
+ const fontColor = chartMutedFontColor(definition.background);
29184
29465
  return {
29185
29466
  display: !!chartTitle.text,
29186
29467
  text: _t(chartTitle.text),
29187
29468
  color: chartTitle?.color ?? fontColor,
29188
29469
  align: chartTitle.align === "center" ? "center" : chartTitle.align === "right" ? "end" : "start",
29189
29470
  font: {
29190
- size: DEFAULT_CHART_FONT_SIZE,
29471
+ size: definition.title.fontSize ?? CHART_TITLE_FONT_SIZE,
29191
29472
  weight: chartTitle.bold ? "bold" : "normal",
29192
29473
  style: chartTitle.italic ? "italic" : "normal",
29193
29474
  },
29475
+ padding: {
29476
+ // Disable title top/left/right padding to use the chart padding instead.
29477
+ // The legend already has a top padding, so bottom padding is useless for the title there.
29478
+ bottom: definition.legendPosition === "top" ? 0 : CHART_PADDING$1,
29479
+ },
29194
29480
  };
29195
29481
  }
29196
29482
 
@@ -29321,6 +29607,25 @@ stores.inject(MyMetaStore, storeInstance);
29321
29607
  },
29322
29608
  };
29323
29609
  }
29610
+ function getGeoChartTooltip(definition, args) {
29611
+ const { locale, axisFormats } = args;
29612
+ const format = axisFormats?.y || axisFormats?.y1;
29613
+ return {
29614
+ filter: function (tooltipItem) {
29615
+ return tooltipItem.raw.value !== undefined;
29616
+ },
29617
+ callbacks: {
29618
+ label: function (tooltipItem) {
29619
+ const rawItem = tooltipItem.raw;
29620
+ const xLabel = rawItem.feature.properties.name;
29621
+ const yLabel = rawItem.value;
29622
+ const toolTipFormat = !format && Math.abs(yLabel) >= 1000 ? "#,##" : format;
29623
+ const yLabelStr = formatValue(yLabel, { format: toolTipFormat, locale });
29624
+ return xLabel ? `${xLabel}: ${yLabelStr}` : yLabelStr;
29625
+ },
29626
+ },
29627
+ };
29628
+ }
29324
29629
  function calculatePercentage(dataset, dataIndex) {
29325
29630
  const numericData = dataset.filter((value) => typeof value === "number");
29326
29631
  const total = numericData.reduce((sum, value) => sum + value, 0);
@@ -29337,26 +29642,27 @@ stores.inject(MyMetaStore, storeInstance);
29337
29642
  canChartParseLabels: canChartParseLabels,
29338
29643
  getBarChartData: getBarChartData,
29339
29644
  getBarChartDatasets: getBarChartDatasets,
29340
- getBarChartLayout: getBarChartLayout,
29341
29645
  getBarChartLegend: getBarChartLegend,
29342
29646
  getBarChartScales: getBarChartScales,
29343
29647
  getBarChartTooltip: getBarChartTooltip,
29344
29648
  getChartLabelFormat: getChartLabelFormat,
29649
+ getChartLayout: getChartLayout,
29345
29650
  getChartShowValues: getChartShowValues,
29346
29651
  getChartTitle: getChartTitle,
29347
29652
  getComboChartDatasets: getComboChartDatasets,
29348
29653
  getComboChartLegend: getComboChartLegend,
29349
- getCommonChartLayout: getCommonChartLayout,
29350
29654
  getData: getData,
29655
+ getGeoChartData: getGeoChartData,
29656
+ getGeoChartDatasets: getGeoChartDatasets,
29657
+ getGeoChartScales: getGeoChartScales,
29658
+ getGeoChartTooltip: getGeoChartTooltip,
29351
29659
  getLineChartData: getLineChartData,
29352
29660
  getLineChartDatasets: getLineChartDatasets,
29353
- getLineChartLayout: getLineChartLayout,
29354
29661
  getLineChartLegend: getLineChartLegend,
29355
29662
  getLineChartScales: getLineChartScales,
29356
29663
  getLineChartTooltip: getLineChartTooltip,
29357
29664
  getPieChartData: getPieChartData,
29358
29665
  getPieChartDatasets: getPieChartDatasets,
29359
- getPieChartLayout: getPieChartLayout,
29360
29666
  getPieChartLegend: getPieChartLegend,
29361
29667
  getPieChartTooltip: getPieChartTooltip,
29362
29668
  getPyramidChartData: getPyramidChartData,
@@ -29372,7 +29678,6 @@ stores.inject(MyMetaStore, storeInstance);
29372
29678
  getScatterChartScales: getScatterChartScales,
29373
29679
  getTrendDatasetForBarChart: getTrendDatasetForBarChart,
29374
29680
  getTrendDatasetForLineChart: getTrendDatasetForLineChart,
29375
- getWaterfallChartLayout: getWaterfallChartLayout,
29376
29681
  getWaterfallChartLegend: getWaterfallChartLegend,
29377
29682
  getWaterfallChartScales: getWaterfallChartScales,
29378
29683
  getWaterfallChartTooltip: getWaterfallChartTooltip,
@@ -29520,7 +29825,7 @@ stores.inject(MyMetaStore, storeInstance);
29520
29825
  options: {
29521
29826
  ...CHART_COMMON_OPTIONS,
29522
29827
  indexAxis: chart.horizontal ? "y" : "x",
29523
- layout: getBarChartLayout(definition),
29828
+ layout: getChartLayout(),
29524
29829
  scales: getBarChartScales(definition, chartData),
29525
29830
  plugins: {
29526
29831
  title: getChartTitle(definition),
@@ -29672,7 +29977,7 @@ stores.inject(MyMetaStore, storeInstance);
29672
29977
  },
29673
29978
  options: {
29674
29979
  ...CHART_COMMON_OPTIONS,
29675
- layout: getBarChartLayout(definition),
29980
+ layout: getChartLayout(),
29676
29981
  scales: getBarChartScales(definition, chartData),
29677
29982
  plugins: {
29678
29983
  title: getChartTitle(definition),
@@ -29924,6 +30229,133 @@ stores.inject(MyMetaStore, storeInstance);
29924
30229
  return clip(value, minValue, maxValue);
29925
30230
  }
29926
30231
 
30232
+ class GeoChart extends AbstractChart {
30233
+ dataSets;
30234
+ labelRange;
30235
+ background;
30236
+ legendPosition;
30237
+ type = "geo";
30238
+ dataSetsHaveTitle;
30239
+ dataSetDesign;
30240
+ colorScale;
30241
+ missingValueColor;
30242
+ region;
30243
+ constructor(definition, sheetId, getters) {
30244
+ super(definition, sheetId, getters);
30245
+ this.dataSets = createDataSets(getters, definition.dataSets, sheetId, definition.dataSetsHaveTitle);
30246
+ this.labelRange = createValidRange(getters, sheetId, definition.labelRange);
30247
+ this.background = definition.background;
30248
+ this.legendPosition = definition.legendPosition;
30249
+ this.dataSetsHaveTitle = definition.dataSetsHaveTitle;
30250
+ this.dataSetDesign = definition.dataSets;
30251
+ this.colorScale = definition.colorScale;
30252
+ this.missingValueColor = definition.missingValueColor;
30253
+ this.region = definition.region;
30254
+ }
30255
+ static transformDefinition(definition, executed) {
30256
+ return transformChartDefinitionWithDataSetsWithZone(definition, executed);
30257
+ }
30258
+ static validateChartDefinition(validator, definition) {
30259
+ return validator.checkValidations(definition, checkDataset, checkLabelRange);
30260
+ }
30261
+ static getDefinitionFromContextCreation(context) {
30262
+ return {
30263
+ background: context.background,
30264
+ dataSets: context.range ?? [],
30265
+ dataSetsHaveTitle: context.dataSetsHaveTitle ?? false,
30266
+ legendPosition: context.legendPosition ?? "top",
30267
+ title: context.title || { text: "" },
30268
+ type: "geo",
30269
+ labelRange: context.auxiliaryRange || undefined,
30270
+ aggregated: context.aggregated,
30271
+ };
30272
+ }
30273
+ getContextCreation() {
30274
+ const range = [];
30275
+ for (const [i, dataSet] of this.dataSets.entries()) {
30276
+ range.push({
30277
+ ...this.dataSetDesign?.[i],
30278
+ dataRange: this.getters.getRangeString(dataSet.dataRange, this.sheetId),
30279
+ });
30280
+ }
30281
+ return {
30282
+ ...this,
30283
+ range,
30284
+ auxiliaryRange: this.labelRange
30285
+ ? this.getters.getRangeString(this.labelRange, this.sheetId)
30286
+ : undefined,
30287
+ };
30288
+ }
30289
+ copyForSheetId(sheetId) {
30290
+ const dataSets = copyDataSetsWithNewSheetId(this.sheetId, sheetId, this.dataSets);
30291
+ const labelRange = copyLabelRangeWithNewSheetId(this.sheetId, sheetId, this.labelRange);
30292
+ const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange, sheetId);
30293
+ return new GeoChart(definition, sheetId, this.getters);
30294
+ }
30295
+ copyInSheetId(sheetId) {
30296
+ const definition = this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange, sheetId);
30297
+ return new GeoChart(definition, sheetId, this.getters);
30298
+ }
30299
+ getDefinition() {
30300
+ return this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange);
30301
+ }
30302
+ getDefinitionWithSpecificDataSets(dataSets, labelRange, targetSheetId) {
30303
+ const ranges = [];
30304
+ for (const [i, dataSet] of dataSets.entries()) {
30305
+ ranges.push({
30306
+ ...this.dataSetDesign?.[i],
30307
+ dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId),
30308
+ });
30309
+ }
30310
+ return {
30311
+ type: "geo",
30312
+ dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false,
30313
+ background: this.background,
30314
+ dataSets: ranges,
30315
+ legendPosition: this.legendPosition,
30316
+ labelRange: labelRange
30317
+ ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId)
30318
+ : undefined,
30319
+ title: this.title,
30320
+ colorScale: this.colorScale,
30321
+ missingValueColor: this.missingValueColor,
30322
+ region: this.region,
30323
+ };
30324
+ }
30325
+ getDefinitionForExcel() {
30326
+ return undefined;
30327
+ }
30328
+ updateRanges(applyChange) {
30329
+ const { dataSets, labelRange, isStale } = updateChartRangesWithDataSets(this.getters, applyChange, this.dataSets, this.labelRange);
30330
+ if (!isStale) {
30331
+ return this;
30332
+ }
30333
+ const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange);
30334
+ return new GeoChart(definition, this.sheetId, this.getters);
30335
+ }
30336
+ }
30337
+ function createGeoChartRuntime(chart, getters) {
30338
+ const definition = chart.getDefinition();
30339
+ const chartData = getGeoChartData(definition, chart.dataSets, chart.labelRange, getters);
30340
+ const config = {
30341
+ type: "choropleth",
30342
+ data: {
30343
+ datasets: getGeoChartDatasets(definition, chartData),
30344
+ },
30345
+ options: {
30346
+ ...CHART_COMMON_OPTIONS,
30347
+ layout: getChartLayout(),
30348
+ scales: getGeoChartScales(definition, chartData),
30349
+ plugins: {
30350
+ title: getChartTitle(definition),
30351
+ tooltip: getGeoChartTooltip(definition, chartData),
30352
+ legend: { display: false },
30353
+ },
30354
+ },
30355
+ };
30356
+ return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
30357
+ }
30358
+
29927
30359
  class LineChart extends AbstractChart {
29928
30360
  dataSets;
29929
30361
  labelRange;
@@ -30073,7 +30505,7 @@ stores.inject(MyMetaStore, storeInstance);
30073
30505
  },
30074
30506
  options: {
30075
30507
  ...CHART_COMMON_OPTIONS,
30076
- layout: getLineChartLayout(definition),
30508
+ layout: getChartLayout(),
30077
30509
  scales: getLineChartScales(definition, chartData),
30078
30510
  plugins: {
30079
30511
  title: getChartTitle(definition),
@@ -30208,7 +30640,7 @@ stores.inject(MyMetaStore, storeInstance);
30208
30640
  },
30209
30641
  options: {
30210
30642
  ...CHART_COMMON_OPTIONS,
30211
- layout: getPieChartLayout(definition),
30643
+ layout: getChartLayout(),
30212
30644
  plugins: {
30213
30645
  title: getChartTitle(definition),
30214
30646
  legend: getPieChartLegend(definition, chartData),
@@ -30345,7 +30777,7 @@ stores.inject(MyMetaStore, storeInstance);
30345
30777
  options: {
30346
30778
  ...CHART_COMMON_OPTIONS,
30347
30779
  indexAxis: "y",
30348
- layout: getBarChartLayout(definition),
30780
+ layout: getChartLayout(),
30349
30781
  scales: getPyramidChartScales(definition, chartData),
30350
30782
  plugins: {
30351
30783
  title: getChartTitle(definition),
@@ -30494,7 +30926,7 @@ stores.inject(MyMetaStore, storeInstance);
30494
30926
  },
30495
30927
  options: {
30496
30928
  ...CHART_COMMON_OPTIONS,
30497
- layout: getBarChartLayout(definition),
30929
+ layout: getChartLayout(),
30498
30930
  scales: getRadarChartScales(definition, chartData),
30499
30931
  plugins: {
30500
30932
  title: getChartTitle(definition),
@@ -30647,7 +31079,7 @@ stores.inject(MyMetaStore, storeInstance);
30647
31079
  },
30648
31080
  options: {
30649
31081
  ...CHART_COMMON_OPTIONS,
30650
- layout: getLineChartLayout(definition),
31082
+ layout: getChartLayout(),
30651
31083
  scales: getScatterChartScales(definition, chartData),
30652
31084
  plugins: {
30653
31085
  title: getChartTitle(definition),
@@ -30808,7 +31240,7 @@ stores.inject(MyMetaStore, storeInstance);
30808
31240
  },
30809
31241
  options: {
30810
31242
  ...CHART_COMMON_OPTIONS,
30811
- layout: getWaterfallChartLayout(definition),
31243
+ layout: getChartLayout(),
30812
31244
  scales: getWaterfallChartScales(definition, chartData),
30813
31245
  plugins: {
30814
31246
  title: getChartTitle(definition),
@@ -30917,6 +31349,15 @@ stores.inject(MyMetaStore, storeInstance);
30917
31349
  getChartDefinitionFromContextCreation: RadarChart.getDefinitionFromContextCreation,
30918
31350
  sequence: 80,
30919
31351
  });
31352
+ chartRegistry.add("geo", {
31353
+ match: (type) => type === "geo",
31354
+ createChart: (definition, sheetId, getters) => new GeoChart(definition, sheetId, getters),
31355
+ getChartRuntime: createGeoChartRuntime,
31356
+ validateChartDefinition: GeoChart.validateChartDefinition,
31357
+ transformDefinition: GeoChart.transformDefinition,
31358
+ getChartDefinitionFromContextCreation: GeoChart.getDefinitionFromContextCreation,
31359
+ sequence: 90,
31360
+ });
30920
31361
  const chartComponentRegistry = new Registry();
30921
31362
  chartComponentRegistry.add("line", ChartJsComponent);
30922
31363
  chartComponentRegistry.add("bar", ChartJsComponent);
@@ -30928,6 +31369,7 @@ stores.inject(MyMetaStore, storeInstance);
30928
31369
  chartComponentRegistry.add("waterfall", ChartJsComponent);
30929
31370
  chartComponentRegistry.add("pyramid", ChartJsComponent);
30930
31371
  chartComponentRegistry.add("radar", ChartJsComponent);
31372
+ chartComponentRegistry.add("geo", ChartJsComponent);
30931
31373
  const chartCategories = {
30932
31374
  line: _t("Line"),
30933
31375
  column: _t("Column"),
@@ -31087,6 +31529,13 @@ stores.inject(MyMetaStore, storeInstance);
31087
31529
  subtypeDefinition: { fillArea: true },
31088
31530
  category: "misc",
31089
31531
  preview: "o-spreadsheet-ChartPreview.FILLED_RADAR_CHART",
31532
+ })
31533
+ .add("geo", {
31534
+ displayName: _t("Geo Chart"),
31535
+ chartSubtype: "geo",
31536
+ chartType: "geo",
31537
+ category: "misc",
31538
+ preview: "o-spreadsheet-ChartPreview.GEO_CHART",
31090
31539
  });
31091
31540
 
31092
31541
  /**
@@ -31937,18 +32386,66 @@ stores.inject(MyMetaStore, storeInstance);
31937
32386
  },
31938
32387
  };
31939
32388
 
31940
- css /*SCSS*/ `
31941
- .o-filter-menu-value {
31942
- padding: 4px;
31943
- line-height: 20px;
31944
- height: 28px;
31945
- .o-filter-menu-value-checked {
31946
- width: 20px;
32389
+ const CHECK_SVG = /*xml*/ `
32390
+ <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'>
32391
+ <path fill='none' stroke='#FFF' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/>
32392
+ </svg>
32393
+ `;
32394
+ const CHECKBOX_WIDTH = 14;
32395
+ css /* scss */ `
32396
+ label.o-checkbox {
32397
+ input {
32398
+ appearance: none;
32399
+ -webkit-appearance: none;
32400
+ -moz-appearance: none;
32401
+ border-radius: 0;
32402
+ width: ${CHECKBOX_WIDTH}px;
32403
+ height: ${CHECKBOX_WIDTH}px;
32404
+ vertical-align: top;
32405
+ box-sizing: border-box;
32406
+ outline: none;
32407
+ border: 1px solid ${GRAY_300};
32408
+ cursor: pointer;
32409
+
32410
+ &:hover {
32411
+ border-color: ${ACTION_COLOR};
32412
+ }
32413
+
32414
+ &:checked {
32415
+ background: url("data:image/svg+xml,${encodeURIComponent(CHECK_SVG)}");
32416
+ background-color: ${ACTION_COLOR};
32417
+ border-color: ${ACTION_COLOR};
32418
+ }
32419
+
32420
+ &:focus {
32421
+ outline: none;
32422
+ box-shadow: 0 0 0 0.25rem rgba(113, 75, 103, 0.25);
32423
+ border-color: ${ACTION_COLOR};
32424
+ }
31947
32425
  }
31948
32426
  }
31949
32427
  `;
32428
+ class Checkbox extends owl.Component {
32429
+ static template = "o-spreadsheet.Checkbox";
32430
+ static props = {
32431
+ label: { type: String, optional: true },
32432
+ value: { type: Boolean, optional: true },
32433
+ className: { type: String, optional: true },
32434
+ name: { type: String, optional: true },
32435
+ title: { type: String, optional: true },
32436
+ disabled: { type: Boolean, optional: true },
32437
+ onChange: Function,
32438
+ };
32439
+ static defaultProps = { value: false };
32440
+ onChange(ev) {
32441
+ const value = ev.target.checked;
32442
+ this.props.onChange(value);
32443
+ }
32444
+ }
32445
+
31950
32446
  class FilterMenuValueItem extends owl.Component {
31951
32447
  static template = "o-spreadsheet-FilterMenuValueItem";
32448
+ static components = { Checkbox };
31952
32449
  static props = {
31953
32450
  value: String,
31954
32451
  isChecked: Boolean,
@@ -31986,8 +32483,6 @@ stores.inject(MyMetaStore, storeInstance);
31986
32483
  .o-filter-menu-item {
31987
32484
  display: flex;
31988
32485
  box-sizing: border-box;
31989
- height: ${MENU_ITEM_HEIGHT}px;
31990
- padding: 4px 4px 4px 0px;
31991
32486
  cursor: pointer;
31992
32487
  user-select: none;
31993
32488
 
@@ -31996,14 +32491,6 @@ stores.inject(MyMetaStore, storeInstance);
31996
32491
  }
31997
32492
  }
31998
32493
 
31999
- input {
32000
- box-sizing: border-box;
32001
- margin-bottom: 5px;
32002
- border: 1px solid #949494;
32003
- height: 24px;
32004
- padding-right: 28px;
32005
- }
32006
-
32007
32494
  .o-search-icon {
32008
32495
  right: 5px;
32009
32496
  top: 3px;
@@ -32020,19 +32507,12 @@ stores.inject(MyMetaStore, storeInstance);
32020
32507
  display: flex;
32021
32508
  flex-direction: row;
32022
32509
  margin-bottom: 4px;
32023
-
32024
- .o-filter-menu-action-text {
32025
- cursor: pointer;
32026
- margin-right: 10px;
32027
- color: blue;
32028
- text-decoration: underline;
32029
- }
32030
32510
  }
32031
32511
 
32032
32512
  .o-filter-menu-list {
32033
32513
  flex: auto;
32034
32514
  overflow-y: auto;
32035
- border: 1px solid #949494;
32515
+ border: 1px solid ${GRAY_300};
32036
32516
 
32037
32517
  .o-filter-menu-no-values {
32038
32518
  color: #949494;
@@ -33118,6 +33598,7 @@ stores.inject(MyMetaStore, storeInstance);
33118
33598
  adaptChartRange: adaptChartRange,
33119
33599
  chartFactory: chartFactory,
33120
33600
  chartFontColor: chartFontColor,
33601
+ chartMutedFontColor: chartMutedFontColor,
33121
33602
  chartRuntimeFactory: chartRuntimeFactory,
33122
33603
  chartToImage: chartToImage,
33123
33604
  checkDataset: checkDataset,
@@ -34223,6 +34704,21 @@ stores.inject(MyMetaStore, storeInstance);
34223
34704
  isReadonlyAllowed: true,
34224
34705
  icon: "o-spreadsheet-Icon.PIVOT",
34225
34706
  };
34707
+ const pivotSortingAsc = {
34708
+ name: _t("Ascending"),
34709
+ execute: (env) => sortPivot(env, "asc"),
34710
+ isActive: (env) => isPivotSortMenuItemActive(env, "asc"),
34711
+ };
34712
+ const pivotSortingDesc = {
34713
+ name: _t("Descending"),
34714
+ execute: (env) => sortPivot(env, "desc"),
34715
+ isActive: (env) => isPivotSortMenuItemActive(env, "desc"),
34716
+ };
34717
+ const noPivotSorting = {
34718
+ name: _t("No sorting"),
34719
+ execute: (env) => sortPivot(env, "none"),
34720
+ isActive: (env) => isPivotSortMenuItemActive(env, "none"),
34721
+ };
34226
34722
  const FIX_FORMULAS = {
34227
34723
  name: _t("Convert to individual formulas"),
34228
34724
  execute(env) {
@@ -34259,6 +34755,66 @@ stores.inject(MyMetaStore, storeInstance);
34259
34755
  },
34260
34756
  icon: "o-spreadsheet-Icon.PIVOT",
34261
34757
  };
34758
+ function canSortPivot(env) {
34759
+ const position = env.model.getters.getActivePosition();
34760
+ const pivotId = env.model.getters.getPivotIdFromPosition(position);
34761
+ if (!pivotId ||
34762
+ !env.model.getters.isExistingPivot(pivotId) ||
34763
+ !env.model.getters.isSpillPivotFormula(position)) {
34764
+ return false;
34765
+ }
34766
+ const pivot = env.model.getters.getPivot(pivotId);
34767
+ if (!pivot.isValid()) {
34768
+ return false;
34769
+ }
34770
+ const pivotCell = env.model.getters.getPivotCellFromPosition(position);
34771
+ return pivotCell.type === "VALUE" || pivotCell.type === "MEASURE_HEADER";
34772
+ }
34773
+ function sortPivot(env, order) {
34774
+ const position = env.model.getters.getActivePosition();
34775
+ const pivotId = env.model.getters.getPivotIdFromPosition(position);
34776
+ const pivotCell = env.model.getters.getPivotCellFromPosition(position);
34777
+ if (pivotCell.type === "EMPTY" || pivotCell.type === "HEADER" || !pivotId) {
34778
+ return;
34779
+ }
34780
+ if (order === "none") {
34781
+ env.model.dispatch("UPDATE_PIVOT", {
34782
+ pivotId: pivotId,
34783
+ pivot: {
34784
+ ...env.model.getters.getPivotCoreDefinition(pivotId),
34785
+ sortedColumn: undefined,
34786
+ },
34787
+ });
34788
+ return;
34789
+ }
34790
+ const pivot = env.model.getters.getPivot(pivotId);
34791
+ const colDomain = domainToColRowDomain(pivot, pivotCell.domain).colDomain;
34792
+ env.model.dispatch("UPDATE_PIVOT", {
34793
+ pivotId: pivotId,
34794
+ pivot: {
34795
+ ...env.model.getters.getPivotCoreDefinition(pivotId),
34796
+ sortedColumn: { domain: colDomain, order, measure: pivotCell.measure },
34797
+ },
34798
+ });
34799
+ }
34800
+ function isPivotSortMenuItemActive(env, order) {
34801
+ const position = env.model.getters.getActivePosition();
34802
+ const pivotId = env.model.getters.getPivotIdFromPosition(position);
34803
+ const pivotCell = env.model.getters.getPivotCellFromPosition(position);
34804
+ if (pivotCell.type === "EMPTY" || pivotCell.type === "HEADER" || !pivotId) {
34805
+ return false;
34806
+ }
34807
+ const pivot = env.model.getters.getPivot(pivotId);
34808
+ const colDomain = domainToColRowDomain(pivot, pivotCell.domain).colDomain;
34809
+ const sortedColumn = pivot.definition.sortedColumn;
34810
+ if (order === "none") {
34811
+ return !sortedColumn;
34812
+ }
34813
+ if (!sortedColumn || sortedColumn.order !== order) {
34814
+ return false;
34815
+ }
34816
+ return sortedColumn.measure === pivotCell.measure && deepEquals(sortedColumn.domain, colDomain);
34817
+ }
34262
34818
 
34263
34819
  //------------------------------------------------------------------------------
34264
34820
  // Context Menu Registry
@@ -34357,14 +34913,33 @@ stores.inject(MyMetaStore, storeInstance);
34357
34913
  name: INSERT_LINK_NAME,
34358
34914
  sequence: 150,
34359
34915
  separator: true,
34916
+ })
34917
+ .add("pivot_sorting", {
34918
+ name: _t("Sort pivot"),
34919
+ sequence: 155,
34920
+ icon: "o-spreadsheet-Icon.SORT_RANGE",
34921
+ isVisible: canSortPivot,
34360
34922
  })
34361
34923
  .add("pivot_fix_formulas", {
34362
34924
  ...FIX_FORMULAS,
34363
- sequence: 155,
34925
+ sequence: 160,
34364
34926
  })
34365
34927
  .add("pivot_properties", {
34366
34928
  ...pivotProperties,
34367
34929
  sequence: 170,
34930
+ separator: true,
34931
+ })
34932
+ .addChild("pivot_sorting_asc", ["pivot_sorting"], {
34933
+ ...pivotSortingAsc,
34934
+ sequence: 10,
34935
+ })
34936
+ .addChild("pivot_sorting_desc", ["pivot_sorting"], {
34937
+ ...pivotSortingDesc,
34938
+ sequence: 20,
34939
+ })
34940
+ .addChild("pivot_sorting_none", ["pivot_sorting"], {
34941
+ ...noPivotSorting,
34942
+ sequence: 30,
34368
34943
  });
34369
34944
 
34370
34945
  const sortRange = {
@@ -34377,7 +34952,7 @@ stores.inject(MyMetaStore, storeInstance);
34377
34952
  execute: (env) => {
34378
34953
  const { anchor, zones } = env.model.getters.getSelection();
34379
34954
  const sheetId = env.model.getters.getActiveSheetId();
34380
- interactiveSortSelection(env, sheetId, anchor.cell, zones[0], "ascending");
34955
+ interactiveSortSelection(env, sheetId, anchor.cell, zones[0], "asc");
34381
34956
  },
34382
34957
  icon: "o-spreadsheet-Icon.SORT_ASCENDING",
34383
34958
  };
@@ -34405,7 +34980,7 @@ stores.inject(MyMetaStore, storeInstance);
34405
34980
  execute: (env) => {
34406
34981
  const { anchor, zones } = env.model.getters.getSelection();
34407
34982
  const sheetId = env.model.getters.getActiveSheetId();
34408
- interactiveSortSelection(env, sheetId, anchor.cell, zones[0], "descending");
34983
+ interactiveSortSelection(env, sheetId, anchor.cell, zones[0], "desc");
34409
34984
  },
34410
34985
  icon: "o-spreadsheet-Icon.SORT_DESCENDING",
34411
34986
  };
@@ -34888,6 +35463,227 @@ stores.inject(MyMetaStore, storeInstance);
34888
35463
  }
34889
35464
  }
34890
35465
 
35466
+ class PositionMap {
35467
+ map = {};
35468
+ constructor(entries = []) {
35469
+ for (const [position, value] of entries) {
35470
+ this.set(position, value);
35471
+ }
35472
+ }
35473
+ set({ sheetId, col, row }, value) {
35474
+ const map = this.map;
35475
+ if (!map[sheetId]) {
35476
+ map[sheetId] = {};
35477
+ }
35478
+ if (!map[sheetId][col]) {
35479
+ map[sheetId][col] = {};
35480
+ }
35481
+ map[sheetId][col][row] = value;
35482
+ }
35483
+ get({ sheetId, col, row }) {
35484
+ return this.map[sheetId]?.[col]?.[row];
35485
+ }
35486
+ getSheet(sheetId) {
35487
+ return this.map[sheetId];
35488
+ }
35489
+ has({ sheetId, col, row }) {
35490
+ return this.map[sheetId]?.[col]?.[row] !== undefined;
35491
+ }
35492
+ delete({ sheetId, col, row }) {
35493
+ delete this.map[sheetId]?.[col]?.[row];
35494
+ }
35495
+ keys() {
35496
+ const map = this.map;
35497
+ const keys = [];
35498
+ for (const sheetId in map) {
35499
+ for (const col in map[sheetId]) {
35500
+ for (const row in map[sheetId][col]) {
35501
+ keys.push({ sheetId, col: parseInt(col), row: parseInt(row) });
35502
+ }
35503
+ }
35504
+ }
35505
+ return keys;
35506
+ }
35507
+ keysForSheet(sheetId) {
35508
+ const map = this.map[sheetId];
35509
+ if (!map) {
35510
+ return [];
35511
+ }
35512
+ const keys = [];
35513
+ for (const col in map) {
35514
+ for (const row in map[col]) {
35515
+ keys.push({ sheetId, col: parseInt(col), row: parseInt(row) });
35516
+ }
35517
+ }
35518
+ return keys;
35519
+ }
35520
+ *entries() {
35521
+ const map = this.map;
35522
+ for (const position of this.keys()) {
35523
+ const { sheetId, col, row } = position;
35524
+ yield [position, map[sheetId][col][row]];
35525
+ }
35526
+ }
35527
+ }
35528
+
35529
+ class FormulaFingerprintStore extends SpreadsheetStore {
35530
+ mutators = ["enable", "disable"];
35531
+ isInvalidated = false;
35532
+ fingerprintColors = {
35533
+ [DATA_FINGERPRINT]: "#D9D9D9",
35534
+ };
35535
+ isEnabled = false;
35536
+ colors = new PositionMap();
35537
+ handle(cmd) {
35538
+ if (isCoreCommand(cmd) && this.isEnabled) {
35539
+ this.isInvalidated = true;
35540
+ }
35541
+ switch (cmd.type) {
35542
+ case "UNDO":
35543
+ case "REDO":
35544
+ case "ACTIVATE_SHEET":
35545
+ if (this.isEnabled) {
35546
+ this.isInvalidated = true;
35547
+ }
35548
+ break;
35549
+ }
35550
+ }
35551
+ finalize() {
35552
+ if (this.isInvalidated) {
35553
+ this.isInvalidated = false;
35554
+ this.computeFingerprints();
35555
+ }
35556
+ }
35557
+ enable() {
35558
+ this.isEnabled = true;
35559
+ this.computeFingerprints();
35560
+ }
35561
+ disable() {
35562
+ this.isEnabled = false;
35563
+ this.colors = new PositionMap();
35564
+ }
35565
+ computeFingerprints() {
35566
+ this.colors = new PositionMap();
35567
+ const fingerprints = new PositionMap();
35568
+ const allFingerprints = new Set();
35569
+ const activeSheetId = this.getters.getActiveSheetId();
35570
+ const cells = this.getters.getCells(activeSheetId);
35571
+ for (const cellId in cells) {
35572
+ const fingerprint = this.computeFingerprint(cells[cellId]);
35573
+ if (!fingerprint) {
35574
+ continue;
35575
+ }
35576
+ allFingerprints.add(fingerprint);
35577
+ const position = this.getters.getCellPosition(cellId);
35578
+ fingerprints.set(position, fingerprint);
35579
+ }
35580
+ this.assignColors(allFingerprints);
35581
+ for (const [position, fingerprint] of fingerprints.entries()) {
35582
+ const color = this.fingerprintColors[fingerprint];
35583
+ this.colors.set(position, color);
35584
+ this.colorSpreadZone(position, color);
35585
+ }
35586
+ }
35587
+ colorSpreadZone(position, fingerprintColor) {
35588
+ const spreadZone = this.getters.getSpreadZone(position);
35589
+ if (!spreadZone) {
35590
+ return;
35591
+ }
35592
+ const sheetId = position.sheetId;
35593
+ for (let row = spreadZone.top; row <= spreadZone.bottom; row++) {
35594
+ for (let col = spreadZone.left; col <= spreadZone.right; col++) {
35595
+ const spreadPosition = { sheetId, col, row };
35596
+ this.colors.set(spreadPosition, fingerprintColor);
35597
+ }
35598
+ }
35599
+ }
35600
+ assignColors(fingerprints) {
35601
+ const colors = new AlternatingColorGenerator(fingerprints.size);
35602
+ Object.keys(this.fingerprintColors).forEach(() => colors.next());
35603
+ for (const fingerprint of fingerprints) {
35604
+ if (!this.fingerprintColors[fingerprint]) {
35605
+ this.fingerprintColors[fingerprint] = setColorAlpha(colors.next(), 0.8);
35606
+ }
35607
+ }
35608
+ }
35609
+ computeFingerprint(cell) {
35610
+ const position = this.getters.getCellPosition(cell.id);
35611
+ if (cell.isFormula) {
35612
+ return this.computeFormulaFingerprint(position, cell);
35613
+ }
35614
+ else {
35615
+ return this.getLiteralFingerprint(position);
35616
+ }
35617
+ }
35618
+ computeFormulaFingerprint(position, cell) {
35619
+ const dependencies = cell.compiledFormula.dependencies;
35620
+ const colCellOffset = position.col;
35621
+ const rowCellOffset = position.row;
35622
+ const positionSheetIndex = this.getters.getSheetIds().indexOf(position.sheetId);
35623
+ // As an optimization, we do not build each reference vector individually
35624
+ // to sum them up, but instead we directly add each component to the resulting
35625
+ // vector. This is equivalent to summing up all reference vectors.
35626
+ const fingerprintVector = {
35627
+ dx: 0,
35628
+ dy: 0,
35629
+ dSheet: 0,
35630
+ };
35631
+ for (const range of dependencies) {
35632
+ const zone = range.zone;
35633
+ const [left, right] = range.parts;
35634
+ const rangeSheetIndex = this.getters.getSheetIds().indexOf(range.sheetId);
35635
+ fingerprintVector.dSheet = rangeSheetIndex - positionSheetIndex;
35636
+ // in relative mode, we offset the col and row by the cell's position
35637
+ // in absolute mode, we offset the col and row relative to the sheet
35638
+ const isLeftUnbounded = range.isFullRow && !range.unboundedZone.hasHeader;
35639
+ const isTopUnbounded = range.isFullCol && !range.unboundedZone.hasHeader;
35640
+ const leftOffset = isLeftUnbounded || left?.colFixed ? 0 : colCellOffset;
35641
+ const topOffset = isTopUnbounded || left?.rowFixed ? 0 : rowCellOffset;
35642
+ const isRightFixed = (!right && left?.colFixed) || right?.colFixed;
35643
+ const isBottomFixed = (!right && left.rowFixed) || right?.rowFixed;
35644
+ const isRightUnbounded = range.unboundedZone.right === undefined;
35645
+ const isBottomUnbounded = range.unboundedZone.bottom === undefined;
35646
+ const rightOffset = isRightUnbounded || isRightFixed ? 0 : colCellOffset;
35647
+ const bottomOffset = isBottomUnbounded || isBottomFixed ? 0 : rowCellOffset;
35648
+ const referenceZone = reorderZone({
35649
+ top: zone.top - topOffset,
35650
+ left: zone.left - leftOffset,
35651
+ right: zone.right - rightOffset,
35652
+ bottom: zone.bottom - bottomOffset,
35653
+ });
35654
+ for (let dy = referenceZone.top; dy <= referenceZone.bottom; dy++) {
35655
+ for (let dx = referenceZone.left; dx <= referenceZone.right; dx++) {
35656
+ fingerprintVector.dx += dx;
35657
+ fingerprintVector.dy += dy;
35658
+ }
35659
+ }
35660
+ }
35661
+ // removes the index placeholders from the normalized formula
35662
+ // =|N0|+|N1|+|N0| -> =|N|+|N|+|N|
35663
+ const normalizedFormula = cell.compiledFormula.normalizedFormula.replace(/(|\w)(\d)(|)/g, "$1$3");
35664
+ return hash(fingerprintVector) + normalizedFormula;
35665
+ }
35666
+ getLiteralFingerprint(position) {
35667
+ const evaluatedCell = this.getters.getEvaluatedCell(position);
35668
+ switch (evaluatedCell.type) {
35669
+ case CellValueType.number:
35670
+ case CellValueType.boolean:
35671
+ return DATA_FINGERPRINT;
35672
+ case CellValueType.text:
35673
+ case CellValueType.empty:
35674
+ case CellValueType.error:
35675
+ return undefined;
35676
+ }
35677
+ }
35678
+ }
35679
+ function hash(vector) {
35680
+ return Object.entries(vector)
35681
+ .sort(([a], [b]) => a.localeCompare(b))
35682
+ .map(([_, value]) => value)
35683
+ .join(",");
35684
+ }
35685
+ const DATA_FINGERPRINT = "DATA_FINGERPRINT";
35686
+
34891
35687
  const hideCols = {
34892
35688
  name: HIDE_COLUMNS_NAME,
34893
35689
  execute: (env) => {
@@ -35059,6 +35855,19 @@ stores.inject(MyMetaStore, storeInstance);
35059
35855
  return env.model.getters.getGridLinesVisibility(sheetId);
35060
35856
  },
35061
35857
  };
35858
+ const irregularityMap = {
35859
+ name: _t("Irregularity map"),
35860
+ execute: (env) => {
35861
+ const fingerprintStore = env.getStore(FormulaFingerprintStore);
35862
+ if (fingerprintStore.isEnabled) {
35863
+ fingerprintStore.disable();
35864
+ }
35865
+ else {
35866
+ fingerprintStore.enable();
35867
+ }
35868
+ },
35869
+ icon: "o-spreadsheet-Icon.IRREGULARITY_MAP",
35870
+ };
35062
35871
  const viewFormulas = {
35063
35872
  name: _t("Formulas"),
35064
35873
  isActive: (env) => env.model.getters.shouldShowFormulas(),
@@ -35676,6 +36485,11 @@ stores.inject(MyMetaStore, storeInstance);
35676
36485
  .addChild("view_formulas", ["view", "show"], {
35677
36486
  ...viewFormulas,
35678
36487
  sequence: 10,
36488
+ })
36489
+ .addChild("view_irregularity_map", ["view"], {
36490
+ ...irregularityMap,
36491
+ sequence: 40,
36492
+ separator: true,
35679
36493
  })
35680
36494
  // ---------------------------------------------------------------------
35681
36495
  // INSERT MENU ITEMS
@@ -35978,62 +36792,6 @@ stores.inject(MyMetaStore, storeInstance);
35978
36792
  }
35979
36793
  const otRegistry = new OTRegistry();
35980
36794
 
35981
- const CHECK_SVG = /*xml*/ `
35982
- <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'>
35983
- <path fill='none' stroke='#FFF' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/>
35984
- </svg>
35985
- `;
35986
- const CHECKBOX_WIDTH = 14;
35987
- css /* scss */ `
35988
- label.o-checkbox {
35989
- input {
35990
- appearance: none;
35991
- -webkit-appearance: none;
35992
- -moz-appearance: none;
35993
- border-radius: 0;
35994
- width: ${CHECKBOX_WIDTH}px;
35995
- height: ${CHECKBOX_WIDTH}px;
35996
- vertical-align: top;
35997
- box-sizing: border-box;
35998
- outline: none;
35999
- border: 1px solid ${GRAY_300};
36000
-
36001
- &:hover {
36002
- border-color: ${ACTION_COLOR};
36003
- }
36004
-
36005
- &:checked {
36006
- background: url("data:image/svg+xml,${encodeURIComponent(CHECK_SVG)}");
36007
- background-color: ${ACTION_COLOR};
36008
- border-color: ${ACTION_COLOR};
36009
- }
36010
-
36011
- &:focus {
36012
- outline: none;
36013
- box-shadow: 0 0 0 0.25rem rgba(113, 75, 103, 0.25);
36014
- border-color: ${ACTION_COLOR};
36015
- }
36016
- }
36017
- }
36018
- `;
36019
- class Checkbox extends owl.Component {
36020
- static template = "o-spreadsheet.Checkbox";
36021
- static props = {
36022
- label: { type: String, optional: true },
36023
- value: { type: Boolean, optional: true },
36024
- className: { type: String, optional: true },
36025
- name: { type: String, optional: true },
36026
- title: { type: String, optional: true },
36027
- disabled: { type: Boolean, optional: true },
36028
- onChange: Function,
36029
- };
36030
- static defaultProps = { value: false };
36031
- onChange(ev) {
36032
- const value = ev.target.checked;
36033
- this.props.onChange(value);
36034
- }
36035
- }
36036
-
36037
36795
  class Section extends owl.Component {
36038
36796
  static template = "o_spreadsheet.Section";
36039
36797
  static props = {
@@ -37410,6 +38168,95 @@ stores.inject(MyMetaStore, storeInstance);
37410
38168
  }
37411
38169
  }
37412
38170
 
38171
+ css /* scss */ `
38172
+ .o-font-size-editor {
38173
+ height: calc(100% - 4px);
38174
+ input.o-font-size {
38175
+ outline-color: ${SELECTION_BORDER_COLOR};
38176
+ height: 20px;
38177
+ width: 23px;
38178
+ }
38179
+ }
38180
+ .o-text-options > div {
38181
+ cursor: pointer;
38182
+ line-height: 26px;
38183
+ padding: 3px 12px;
38184
+ &:hover {
38185
+ background-color: rgba(0, 0, 0, 0.08);
38186
+ }
38187
+ }
38188
+ `;
38189
+ class FontSizeEditor extends owl.Component {
38190
+ static template = "o-spreadsheet-FontSizeEditor";
38191
+ static props = {
38192
+ currentFontSize: Number,
38193
+ onFontSizeChanged: Function,
38194
+ onToggle: { type: Function, optional: true },
38195
+ class: String,
38196
+ };
38197
+ static components = { Popover };
38198
+ fontSizes = FONT_SIZES;
38199
+ dropdown = owl.useState({ isOpen: false });
38200
+ inputRef = owl.useRef("inputFontSize");
38201
+ rootEditorRef = owl.useRef("FontSizeEditor");
38202
+ fontSizeListRef = owl.useRef("fontSizeList");
38203
+ setup() {
38204
+ owl.useExternalListener(window, "click", this.onExternalClick, { capture: true });
38205
+ }
38206
+ get popoverProps() {
38207
+ const { x, y, width, height } = this.rootEditorRef.el.getBoundingClientRect();
38208
+ return {
38209
+ anchorRect: { x, y, width, height },
38210
+ positioning: "BottomLeft",
38211
+ verticalOffset: 0,
38212
+ };
38213
+ }
38214
+ onExternalClick(ev) {
38215
+ if (!isChildEvent(this.fontSizeListRef.el, ev) && !isChildEvent(this.rootEditorRef.el, ev)) {
38216
+ this.closeFontList();
38217
+ }
38218
+ }
38219
+ toggleFontList() {
38220
+ const isOpen = this.dropdown.isOpen;
38221
+ if (!isOpen) {
38222
+ this.props.onToggle?.();
38223
+ this.inputRef.el.focus();
38224
+ }
38225
+ else {
38226
+ this.closeFontList();
38227
+ }
38228
+ }
38229
+ closeFontList() {
38230
+ this.dropdown.isOpen = false;
38231
+ }
38232
+ setSize(fontSizeStr) {
38233
+ const fontSize = clip(Math.floor(parseFloat(fontSizeStr)), 1, 400);
38234
+ this.props.onFontSizeChanged(fontSize);
38235
+ this.closeFontList();
38236
+ }
38237
+ setSizeFromInput(ev) {
38238
+ this.setSize(ev.target.value);
38239
+ }
38240
+ setSizeFromList(fontSizeStr) {
38241
+ this.setSize(fontSizeStr);
38242
+ }
38243
+ onInputFocused(ev) {
38244
+ this.dropdown.isOpen = true;
38245
+ ev.target.select();
38246
+ }
38247
+ onInputKeydown(ev) {
38248
+ if (ev.key === "Enter" || ev.key === "Escape") {
38249
+ this.closeFontList();
38250
+ const target = ev.target;
38251
+ // In the case of a ESCAPE key, we get the previous font size back
38252
+ if (ev.key === "Escape") {
38253
+ target.value = `${this.props.currentFontSize}`;
38254
+ }
38255
+ this.props.onToggle?.();
38256
+ }
38257
+ }
38258
+ }
38259
+
37413
38260
  css /* scss */ `
37414
38261
  .o-chart-title-designer {
37415
38262
  > span {
@@ -37444,7 +38291,7 @@ stores.inject(MyMetaStore, storeInstance);
37444
38291
  `;
37445
38292
  class ChartTitle extends owl.Component {
37446
38293
  static template = "o-spreadsheet.ChartTitle";
37447
- static components = { Section, ColorPickerWidget };
38294
+ static components = { Section, ColorPickerWidget, FontSizeEditor };
37448
38295
  static props = {
37449
38296
  title: { type: String, optional: true },
37450
38297
  updateTitle: Function,
@@ -37453,7 +38300,8 @@ stores.inject(MyMetaStore, storeInstance);
37453
38300
  toggleBold: { type: Function, optional: true },
37454
38301
  updateAlignment: { type: Function, optional: true },
37455
38302
  updateColor: { type: Function, optional: true },
37456
- style: { type: Object, optional: true },
38303
+ style: Object,
38304
+ onFontSizeChanged: Function,
37457
38305
  };
37458
38306
  static defaultProps = {
37459
38307
  title: "",
@@ -37468,6 +38316,9 @@ stores.inject(MyMetaStore, storeInstance);
37468
38316
  updateTitle(ev) {
37469
38317
  this.props.updateTitle(ev.target.value);
37470
38318
  }
38319
+ updateFontSize(fontSize) {
38320
+ this.props.onFontSizeChanged(fontSize);
38321
+ }
37471
38322
  toggleDropdownTool(tool, ev) {
37472
38323
  const isOpen = this.state.activeTool === tool;
37473
38324
  this.closeMenus();
@@ -37510,6 +38361,7 @@ stores.inject(MyMetaStore, storeInstance);
37510
38361
  return {
37511
38362
  color: "",
37512
38363
  align: "center",
38364
+ fontSize: CHART_AXIS_TITLE_FONT_SIZE,
37513
38365
  ...axisDesign.title,
37514
38366
  };
37515
38367
  }
@@ -37527,6 +38379,17 @@ stores.inject(MyMetaStore, storeInstance);
37527
38379
  };
37528
38380
  this.props.updateChart(this.props.figureId, { axesDesign });
37529
38381
  }
38382
+ updateAxisTitleFontSize(fontSize) {
38383
+ const axesDesign = this.props.definition.axesDesign ?? {};
38384
+ axesDesign[this.state.currentAxis] = {
38385
+ ...axesDesign[this.state.currentAxis],
38386
+ title: {
38387
+ ...(axesDesign[this.state.currentAxis]?.title ?? {}),
38388
+ fontSize,
38389
+ },
38390
+ };
38391
+ this.props.updateChart(this.props.figureId, { axesDesign });
38392
+ }
37530
38393
  toggleBoldAxisTitle() {
37531
38394
  const axesDesign = this.props.definition.axesDesign ?? {};
37532
38395
  const title = axesDesign[this.state.currentAxis]?.title ?? {};
@@ -37646,8 +38509,12 @@ stores.inject(MyMetaStore, storeInstance);
37646
38509
  figureId: String,
37647
38510
  definition: Object,
37648
38511
  updateChart: Function,
38512
+ defaultChartTitleFontSize: { type: Number, optional: true },
37649
38513
  slots: { type: Object, optional: true },
37650
38514
  };
38515
+ static defaultProps = {
38516
+ defaultChartTitleFontSize: CHART_TITLE_FONT_SIZE,
38517
+ };
37651
38518
  state;
37652
38519
  setup() {
37653
38520
  this.state = owl.useState({
@@ -37673,6 +38540,7 @@ stores.inject(MyMetaStore, storeInstance);
37673
38540
  get titleStyle() {
37674
38541
  return {
37675
38542
  align: "left",
38543
+ fontSize: this.props.defaultChartTitleFontSize,
37676
38544
  ...this.title,
37677
38545
  };
37678
38546
  }
@@ -37681,6 +38549,10 @@ stores.inject(MyMetaStore, storeInstance);
37681
38549
  this.props.updateChart(this.props.figureId, { title });
37682
38550
  this.state.activeTool = "";
37683
38551
  }
38552
+ updateChartTitleFontSize(fontSize) {
38553
+ const title = { ...this.title, fontSize };
38554
+ this.props.updateChart(this.props.figureId, { title });
38555
+ }
37684
38556
  toggleBoldChartTitle() {
37685
38557
  let title = this.title;
37686
38558
  title = { ...title, bold: !title.bold };
@@ -37888,7 +38760,7 @@ stores.inject(MyMetaStore, storeInstance);
37888
38760
  case "polynomial":
37889
38761
  config = {
37890
38762
  type: "polynomial",
37891
- order: type === "linear" ? 1 : 2,
38763
+ order: type === "linear" ? 1 : this.getMaxPolynomialDegree(index),
37892
38764
  };
37893
38765
  break;
37894
38766
  case "exponential":
@@ -38159,6 +39031,104 @@ stores.inject(MyMetaStore, storeInstance);
38159
39031
  }
38160
39032
  }
38161
39033
 
39034
+ class GeoChartRegionSelectSection extends owl.Component {
39035
+ static template = "o-spreadsheet-GeoChartRegionSelectSection";
39036
+ static components = { Section };
39037
+ static props = {
39038
+ figureId: String,
39039
+ definition: Object,
39040
+ updateChart: Function,
39041
+ };
39042
+ updateSelectedRegion(ev) {
39043
+ const value = ev.target.value;
39044
+ this.props.updateChart(this.props.figureId, { region: value });
39045
+ }
39046
+ get availableRegions() {
39047
+ return this.env.model.getters.getGeoChartAvailableRegions();
39048
+ }
39049
+ get selectedRegion() {
39050
+ return this.props.definition.region || this.availableRegions[0]?.id;
39051
+ }
39052
+ }
39053
+
39054
+ class GeoChartConfigPanel extends GenericChartConfigPanel {
39055
+ static template = "o-spreadsheet-GeoChartConfigPanel";
39056
+ static components = { ...GenericChartConfigPanel.components, GeoChartRegionSelectSection };
39057
+ get dataRanges() {
39058
+ return this.getDataSeriesRanges().slice(0, 1);
39059
+ }
39060
+ onDataSeriesConfirmed() {
39061
+ this.dataSeriesRanges = spreadRange(this.env.model.getters, this.dataSeriesRanges).slice(0, 1);
39062
+ this.state.datasetDispatchResult = this.props.updateChart(this.props.figureId, {
39063
+ dataSets: this.dataSeriesRanges,
39064
+ });
39065
+ }
39066
+ getLabelRangeOptions() {
39067
+ return [
39068
+ {
39069
+ name: "dataSetsHaveTitle",
39070
+ label: this.dataSetsHaveTitleLabel,
39071
+ value: this.props.definition.dataSetsHaveTitle,
39072
+ onChange: this.onUpdateDataSetsHaveTitle.bind(this),
39073
+ },
39074
+ ];
39075
+ }
39076
+ }
39077
+
39078
+ const DEFAULT_CUSTOM_COLOR_SCALE = {
39079
+ minColor: "#FFF5EB",
39080
+ midColor: "#FD8D3C",
39081
+ maxColor: "#7F2704",
39082
+ };
39083
+ class GeoChartDesignPanel extends ChartWithAxisDesignPanel {
39084
+ static template = "o-spreadsheet-GeoChartDesignPanel";
39085
+ static components = { ...ChartWithAxisDesignPanel.components, RoundColorPicker };
39086
+ colorScalesChoices = ChartTerms.GeoChart.ColorScales;
39087
+ updateColorScaleType(ev) {
39088
+ const value = ev.target.value;
39089
+ value === "custom"
39090
+ ? this.updateColorScale(DEFAULT_CUSTOM_COLOR_SCALE)
39091
+ : this.updateColorScale(value);
39092
+ }
39093
+ updateColorScale(colorScale) {
39094
+ this.props.updateChart(this.props.figureId, { colorScale });
39095
+ }
39096
+ updateMissingValueColor(color) {
39097
+ this.props.updateChart(this.props.figureId, { missingValueColor: color });
39098
+ }
39099
+ updateLegendPosition(ev) {
39100
+ const value = ev.target.value;
39101
+ this.props.updateChart(this.props.figureId, { legendPosition: value });
39102
+ }
39103
+ get selectedColorScale() {
39104
+ return typeof this.props.definition.colorScale === "object"
39105
+ ? "custom"
39106
+ : this.props.definition.colorScale || "oranges";
39107
+ }
39108
+ get selectedMissingValueColor() {
39109
+ return this.props.definition.missingValueColor || "#ffffff";
39110
+ }
39111
+ get customColorScale() {
39112
+ if (typeof this.props.definition.colorScale === "object") {
39113
+ return this.props.definition.colorScale;
39114
+ }
39115
+ return undefined;
39116
+ }
39117
+ getCustomColorScaleColor(color) {
39118
+ return this.customColorScale?.[color] ?? "";
39119
+ }
39120
+ setCustomColorScaleColor(colorType, color) {
39121
+ if (!color && colorType !== "midColor") {
39122
+ color = "#fff";
39123
+ }
39124
+ const customColorScale = this.customColorScale;
39125
+ if (!customColorScale) {
39126
+ return;
39127
+ }
39128
+ this.updateColorScale({ ...customColorScale, [colorType]: color });
39129
+ }
39130
+ }
39131
+
38162
39132
  class LineConfigPanel extends GenericChartConfigPanel {
38163
39133
  static template = "o-spreadsheet-LineConfigPanel";
38164
39134
  get canTreatLabelsAsText() {
@@ -38348,6 +39318,9 @@ stores.inject(MyMetaStore, storeInstance);
38348
39318
  get humanizeNumbersLabel() {
38349
39319
  return _t("Humanize numbers");
38350
39320
  }
39321
+ get defaultScorecardTitleFontSize() {
39322
+ return SCORECARD_CHART_TITLE_FONT_SIZE;
39323
+ }
38351
39324
  updateHumanizeNumbers(humanize) {
38352
39325
  this.props.updateChart(this.props.figureId, { humanize });
38353
39326
  }
@@ -38469,6 +39442,10 @@ stores.inject(MyMetaStore, storeInstance);
38469
39442
  .add("radar", {
38470
39443
  configuration: GenericChartConfigPanel,
38471
39444
  design: RadarChartDesignPanel,
39445
+ })
39446
+ .add("geo", {
39447
+ configuration: GeoChartConfigPanel,
39448
+ design: GeoChartDesignPanel,
38472
39449
  });
38473
39450
 
38474
39451
  css /* scss */ `
@@ -39811,6 +40788,15 @@ stores.inject(MyMetaStore, storeInstance);
39811
40788
  confirmEdition(content) {
39812
40789
  this.args().onConfirm(content);
39813
40790
  }
40791
+ getTokenColor(token) {
40792
+ if (token.type === "SYMBOL") {
40793
+ const matchedColor = this.args().getContextualColoredSymbolToken?.(token);
40794
+ if (matchedColor) {
40795
+ return matchedColor;
40796
+ }
40797
+ }
40798
+ return super.getTokenColor(token);
40799
+ }
39814
40800
  }
39815
40801
 
39816
40802
  css /* scss */ `
@@ -39823,7 +40809,7 @@ stores.inject(MyMetaStore, storeInstance);
39823
40809
  border-color: ${GRAY_300};
39824
40810
 
39825
40811
  &.active {
39826
- border-color: ${SELECTION_BORDER_COLOR};
40812
+ border-color: ${ACTION_COLOR};
39827
40813
  }
39828
40814
 
39829
40815
  &.o-invalid {
@@ -39848,6 +40834,7 @@ stores.inject(MyMetaStore, storeInstance);
39848
40834
  placeholder: { type: String, optional: true },
39849
40835
  class: { type: String, optional: true },
39850
40836
  invalid: { type: Boolean, optional: true },
40837
+ getContextualColoredSymbolToken: { type: Function, optional: true },
39851
40838
  };
39852
40839
  static components = { Composer };
39853
40840
  static defaultProps = {
@@ -39864,6 +40851,7 @@ stores.inject(MyMetaStore, storeInstance);
39864
40851
  content: this.props.composerContent,
39865
40852
  contextualAutocomplete: this.props.contextualAutocomplete,
39866
40853
  defaultRangeSheetId: this.props.defaultRangeSheetId,
40854
+ getContextualColoredSymbolToken: this.props.getContextualColoredSymbolToken,
39867
40855
  }));
39868
40856
  this.standaloneComposerStore = standaloneComposerStore;
39869
40857
  this.composerInterface = {
@@ -40779,7 +41767,7 @@ stores.inject(MyMetaStore, storeInstance);
40779
41767
  }
40780
41768
  const point = this.state.rules.colorScale[target];
40781
41769
  if (point) {
40782
- point.color = Number.parseInt(color.slice(1), 16);
41770
+ point.color = colorToNumber(color);
40783
41771
  }
40784
41772
  this.updateConditionalFormat({ rule: this.state.rules.colorScale });
40785
41773
  this.closeMenus();
@@ -40924,6 +41912,9 @@ stores.inject(MyMetaStore, storeInstance);
40924
41912
  return [this.state.rules.dataBar.rangeValues || ""];
40925
41913
  }
40926
41914
  updateDataBarColor(color) {
41915
+ if (!isColorValid(color)) {
41916
+ return;
41917
+ }
40927
41918
  this.state.rules.dataBar.color = Number.parseInt(color.slice(1), 16);
40928
41919
  this.updateConditionalFormat({ rule: this.state.rules.dataBar });
40929
41920
  }
@@ -43424,7 +44415,7 @@ stores.inject(MyMetaStore, storeInstance);
43424
44415
  return {
43425
44416
  text: text,
43426
44417
  description: measure.displayName,
43427
- htmlContent: [{ value: text, color: tokenColors.FUNCTION }],
44418
+ htmlContent: [{ value: text, color: PIVOT_TOKEN_COLOR }],
43428
44419
  fuzzySearchKey: measure.displayName + text + measure.fieldName,
43429
44420
  };
43430
44421
  });
@@ -43433,7 +44424,7 @@ stores.inject(MyMetaStore, storeInstance);
43433
44424
  return {
43434
44425
  text: text,
43435
44426
  description: dimension.displayName,
43436
- htmlContent: [{ value: text, color: tokenColors.FUNCTION }],
44427
+ htmlContent: [{ value: text, color: PIVOT_TOKEN_COLOR }],
43437
44428
  fuzzySearchKey: dimension.displayName + text + dimension.fieldName,
43438
44429
  };
43439
44430
  });
@@ -43513,6 +44504,76 @@ stores.inject(MyMetaStore, storeInstance);
43513
44504
  measure: this.props.measure,
43514
44505
  });
43515
44506
  }
44507
+ getColoredSymbolToken(token) {
44508
+ if (token.type !== "SYMBOL") {
44509
+ return undefined;
44510
+ }
44511
+ const tokenValue = unquote(token.value, "'");
44512
+ if (this.props.definition.columns.some((col) => col.nameWithGranularity === tokenValue) ||
44513
+ this.props.definition.rows.some((row) => row.nameWithGranularity === tokenValue) ||
44514
+ this.props.definition.measures.some((measure) => measure.id === tokenValue && measure.id !== this.props.measure.id)) {
44515
+ return PIVOT_TOKEN_COLOR;
44516
+ }
44517
+ return undefined;
44518
+ }
44519
+ }
44520
+
44521
+ css /* scss */ `
44522
+ .o-pivot-sort {
44523
+ .o-sort-card {
44524
+ width: fit-content;
44525
+ background-color: ${GRAY_100};
44526
+ border: 1px solid ${GRAY_300};
44527
+
44528
+ .o-sort-value {
44529
+ color: ${PRIMARY_BUTTON_BG};
44530
+ }
44531
+ }
44532
+ }
44533
+ `;
44534
+ class PivotSortSection extends owl.Component {
44535
+ static template = "o-spreadsheet-PivotSortSection";
44536
+ static components = {
44537
+ Section,
44538
+ };
44539
+ static props = {
44540
+ definition: Object,
44541
+ pivotId: String,
44542
+ };
44543
+ get hasValidSort() {
44544
+ const pivot = this.env.model.getters.getPivot(this.props.pivotId);
44545
+ return (!!this.props.definition.sortedColumn &&
44546
+ isSortedColumnValid(this.props.definition.sortedColumn, pivot));
44547
+ }
44548
+ get sortDescription() {
44549
+ const sortOrder = this.props.definition.sortedColumn?.order === "asc" ? _t("ascending") : _t("descending");
44550
+ return _t("Sorted on column (%(ascOrDesc)s):", {
44551
+ ascOrDesc: sortOrder,
44552
+ });
44553
+ }
44554
+ get sortValuesAndFields() {
44555
+ const sortedColumn = this.props.definition.sortedColumn;
44556
+ if (!sortedColumn) {
44557
+ return [];
44558
+ }
44559
+ const pivot = this.env.model.getters.getPivot(this.props.pivotId);
44560
+ const locale = this.env.model.getters.getLocale();
44561
+ const currentDomain = [];
44562
+ const sortValues = [];
44563
+ for (const domainItem of sortedColumn.domain) {
44564
+ currentDomain.push(domainItem);
44565
+ const { value, format } = pivot.getPivotHeaderValueAndFormat(currentDomain);
44566
+ const label = formatValue(value, { format, locale });
44567
+ const field = pivot.definition.getDimension(domainItem.field);
44568
+ sortValues.push({ field: getFieldDisplayName(field), value: label });
44569
+ }
44570
+ if (sortedColumn.domain.length === 0) {
44571
+ sortValues.push({ value: _t("Total") });
44572
+ }
44573
+ const measureLabel = pivot.getMeasure(sortedColumn.measure).displayName;
44574
+ sortValues.push({ value: measureLabel, field: _t("Measure") });
44575
+ return sortValues;
44576
+ }
43516
44577
  }
43517
44578
 
43518
44579
  css /* scss */ `
@@ -43528,6 +44589,7 @@ stores.inject(MyMetaStore, storeInstance);
43528
44589
  PivotDimensionOrder,
43529
44590
  PivotDimensionGranularity,
43530
44591
  PivotMeasureEditor,
44592
+ PivotSortSection,
43531
44593
  };
43532
44594
  static props = {
43533
44595
  definition: Object,
@@ -43684,9 +44746,13 @@ stores.inject(MyMetaStore, storeInstance);
43684
44746
  }
43685
44747
  updateMeasure(measure, newMeasure) {
43686
44748
  const { measures } = this.props.definition;
43687
- this.props.onDimensionsUpdated({
44749
+ const update = {
43688
44750
  measures: measures.map((m) => (m.id === measure.id ? newMeasure : m)),
43689
- });
44751
+ };
44752
+ if (this.props.definition.sortedColumn?.measure === measure.id) {
44753
+ update.sortedColumn = { ...this.props.definition.sortedColumn, measure: newMeasure.id };
44754
+ }
44755
+ this.props.onDimensionsUpdated(update);
43690
44756
  }
43691
44757
  getMeasureId(fieldName, aggregator) {
43692
44758
  const baseId = fieldName + (aggregator ? `:${aggregator}` : "");
@@ -43842,10 +44908,12 @@ stores.inject(MyMetaStore, storeInstance);
43842
44908
  measures;
43843
44909
  columns;
43844
44910
  rows;
44911
+ sortedColumn;
43845
44912
  constructor(definition, fields) {
43846
44913
  this.measures = definition.measures.map((measure) => createMeasure(fields, measure));
43847
44914
  this.columns = definition.columns.map((dimension) => createPivotDimension(fields, dimension));
43848
44915
  this.rows = definition.rows.map((dimension) => createPivotDimension(fields, dimension));
44916
+ this.sortedColumn = definition.sortedColumn;
43849
44917
  }
43850
44918
  getDimension(nameWithGranularity) {
43851
44919
  const dimension = this.columns.find((d) => d.nameWithGranularity === nameWithGranularity) ||
@@ -43999,6 +45067,7 @@ stores.inject(MyMetaStore, storeInstance);
43999
45067
  pivotCells = {};
44000
45068
  rowTree;
44001
45069
  colTree;
45070
+ isSorted = false;
44002
45071
  constructor(columns, rows, measures, fieldsType) {
44003
45072
  this.columns = columns.map((row) => {
44004
45073
  // offset in the pivot table
@@ -44172,6 +45241,7 @@ stores.inject(MyMetaStore, storeInstance);
44172
45241
  value,
44173
45242
  field: row.fields[rowDepth],
44174
45243
  children: [],
45244
+ type: this.fieldsType[fieldName] || "char",
44175
45245
  width: 0, // not used
44176
45246
  };
44177
45247
  treesAtDepth[depth].push(node);
@@ -44194,6 +45264,7 @@ stores.inject(MyMetaStore, storeInstance);
44194
45264
  field: leaf.fields[depth],
44195
45265
  children: [],
44196
45266
  width: leaf.width,
45267
+ type: this.fieldsType[fieldName] || "char",
44197
45268
  };
44198
45269
  if (treesAtDepth[depth]?.at(-1)?.value !== value) {
44199
45270
  treesAtDepth[depth + 1] = [];
@@ -44212,6 +45283,35 @@ stores.inject(MyMetaStore, storeInstance);
44212
45283
  fieldsType: this.fieldsType,
44213
45284
  };
44214
45285
  }
45286
+ sort(measure, sortedColumn, getValue) {
45287
+ if (this.isSorted) {
45288
+ return;
45289
+ }
45290
+ const getSortValue = (measure, domain) => {
45291
+ const rawValue = getValue(measure, domain).value;
45292
+ return typeof rawValue === "number" ? rawValue : -Infinity;
45293
+ };
45294
+ const sortColDomain = sortedColumn.domain;
45295
+ const sortFn = (rowDomain1, rowDomain2) => {
45296
+ const value1 = getSortValue(measure, [...rowDomain1, ...sortColDomain]);
45297
+ const value2 = getSortValue(measure, [...rowDomain2, ...sortColDomain]);
45298
+ return sortedColumn.order === "asc" ? value1 - value2 : value2 - value1;
45299
+ };
45300
+ const sortedRowTree = sortPivotTree(this.rowTree(), [], sortFn);
45301
+ this.rowTree = lazy(sortedRowTree);
45302
+ this.rows = [...this.rowTreeToRows(sortedRowTree), this.rows[this.rows.length - 1]];
45303
+ this.isSorted = true;
45304
+ }
45305
+ rowTreeToRows(tree, parentRow) {
45306
+ return tree.flatMap((node) => {
45307
+ const row = {
45308
+ indent: parentRow ? parentRow.indent + 1 : 0,
45309
+ fields: [...(parentRow?.fields || []), node.field],
45310
+ values: [...(parentRow?.values || []), node.value],
45311
+ };
45312
+ return [row, ...this.rowTreeToRows(node.children, row)];
45313
+ });
45314
+ }
44215
45315
  }
44216
45316
  const EMPTY_PIVOT_CELL = { type: "EMPTY" };
44217
45317
 
@@ -44289,6 +45389,7 @@ stores.inject(MyMetaStore, storeInstance);
44289
45389
  value: groups[key]?.[0]?.[column.nameWithGranularity]?.value ?? null,
44290
45390
  field: colName,
44291
45391
  children: dataEntriesToColumnsTree(groups[key] || [], columns, index + 1),
45392
+ type: column.type,
44292
45393
  width: 0,
44293
45394
  };
44294
45395
  });
@@ -45154,6 +46255,7 @@ stores.inject(MyMetaStore, storeInstance);
45154
46255
  format: measure.format,
45155
46256
  display: measure.display,
45156
46257
  })),
46258
+ sortedColumn: this.shouldKeepSortedColumn(definition) ? definition.sortedColumn : undefined,
45157
46259
  };
45158
46260
  if (!this.draft && deepEquals(coreDefinition, cleanedDefinition)) {
45159
46261
  return;
@@ -45212,6 +46314,19 @@ stores.inject(MyMetaStore, storeInstance);
45212
46314
  }
45213
46315
  return granularitiesPerFields;
45214
46316
  }
46317
+ /**
46318
+ * Check if we want to keep the sorted column when updating the pivot definition. We should remove it if either
46319
+ * the measure is not in the new definition or the columns have changed.
46320
+ */
46321
+ shouldKeepSortedColumn(newDefinition) {
46322
+ const { sortedColumn } = newDefinition;
46323
+ if (!sortedColumn) {
46324
+ return true;
46325
+ }
46326
+ const oldDefinition = this.getters.getPivotCoreDefinition(this.pivotId);
46327
+ return (newDefinition.measures.find((measure) => measure.id === sortedColumn.measure) &&
46328
+ deepEquals(oldDefinition.columns, newDefinition.columns));
46329
+ }
45215
46330
  }
45216
46331
 
45217
46332
  class PivotSpreadsheetSidePanel extends owl.Component {
@@ -46313,6 +47428,7 @@ stores.inject(MyMetaStore, storeInstance);
46313
47428
  }
46314
47429
  }
46315
47430
  `;
47431
+ const DEFAULT_TABLE_STYLE_COLOR = "#3C78D8";
46316
47432
  class TableStyleEditorPanel extends owl.Component {
46317
47433
  static template = "o-spreadsheet-TableStyleEditorPanel";
46318
47434
  static components = { Section, RoundColorPicker, TableStylePreview };
@@ -46331,7 +47447,7 @@ stores.inject(MyMetaStore, storeInstance);
46331
47447
  : null;
46332
47448
  return {
46333
47449
  pickerOpened: false,
46334
- primaryColor: editedStyle?.primaryColor || "#3C78D8",
47450
+ primaryColor: editedStyle?.primaryColor || DEFAULT_TABLE_STYLE_COLOR,
46335
47451
  selectedTemplateName: editedStyle?.templateName || "lightColoredText",
46336
47452
  styleName: editedStyle?.displayName || this.env.model.getters.getNewCustomTableStyleName(),
46337
47453
  };
@@ -46340,7 +47456,7 @@ stores.inject(MyMetaStore, storeInstance);
46340
47456
  this.state.pickerOpened = !this.state.pickerOpened;
46341
47457
  }
46342
47458
  onColorPicked(color) {
46343
- this.state.primaryColor = color;
47459
+ this.state.primaryColor = isColorValid(color) ? color : DEFAULT_TABLE_STYLE_COLOR;
46344
47460
  this.state.pickerOpened = false;
46345
47461
  }
46346
47462
  onTemplatePicked(templateName) {
@@ -47956,6 +49072,7 @@ stores.inject(MyMetaStore, storeInstance);
47956
49072
  draggedFigure: undefined,
47957
49073
  horizontalSnap: undefined,
47958
49074
  verticalSnap: undefined,
49075
+ cancelDnd: undefined,
47959
49076
  });
47960
49077
  setup() {
47961
49078
  owl.onMounted(() => {
@@ -47968,12 +49085,28 @@ stores.inject(MyMetaStore, storeInstance);
47968
49085
  // new rendering
47969
49086
  this.render();
47970
49087
  });
49088
+ owl.onWillUpdateProps(() => {
49089
+ const sheetId = this.env.model.getters.getActiveSheetId();
49090
+ const draggedFigureId = this.dnd.draggedFigure?.id;
49091
+ if (draggedFigureId && !this.env.model.getters.getFigure(sheetId, draggedFigureId)) {
49092
+ if (this.dnd.cancelDnd) {
49093
+ this.dnd.cancelDnd();
49094
+ }
49095
+ this.dnd.draggedFigure = undefined;
49096
+ this.dnd.horizontalSnap = undefined;
49097
+ this.dnd.verticalSnap = undefined;
49098
+ this.dnd.cancelDnd = undefined;
49099
+ }
49100
+ });
47971
49101
  }
47972
49102
  getVisibleFigures() {
47973
49103
  const visibleFigures = this.env.model.getters.getVisibleFigures();
47974
49104
  if (this.dnd.draggedFigure &&
47975
49105
  !visibleFigures.some((figure) => figure.id === this.dnd.draggedFigure?.id)) {
47976
- visibleFigures.push(this.env.model.getters.getFigure(this.env.model.getters.getActiveSheetId(), this.dnd.draggedFigure?.id));
49106
+ const draggedFigure = this.env.model.getters.getFigure(this.env.model.getters.getActiveSheetId(), this.dnd.draggedFigure?.id);
49107
+ if (draggedFigure) {
49108
+ visibleFigures.push(draggedFigure);
49109
+ }
47977
49110
  }
47978
49111
  return visibleFigures;
47979
49112
  }
@@ -48092,7 +49225,7 @@ stores.inject(MyMetaStore, storeInstance);
48092
49225
  this.dnd.verticalSnap = undefined;
48093
49226
  this.env.model.dispatch("UPDATE_FIGURE", { sheetId, id: figure.id, x, y });
48094
49227
  };
48095
- startDnd(onMouseMove, onMouseUp);
49228
+ this.dnd.cancelDnd = startDnd(onMouseMove, onMouseUp);
48096
49229
  }
48097
49230
  /**
48098
49231
  * Initialize the resize of a figure with mouse movements
@@ -48140,7 +49273,7 @@ stores.inject(MyMetaStore, storeInstance);
48140
49273
  this.dnd.horizontalSnap = undefined;
48141
49274
  this.dnd.verticalSnap = undefined;
48142
49275
  };
48143
- startDnd(onMouseMove, onMouseUp);
49276
+ this.dnd.cancelDnd = startDnd(onMouseMove, onMouseUp);
48144
49277
  }
48145
49278
  getOtherFigures(figId) {
48146
49279
  return this.getVisibleFigures().filter((f) => f.id !== figId);
@@ -49247,9 +50380,11 @@ stores.inject(MyMetaStore, storeInstance);
49247
50380
  class GridRenderer {
49248
50381
  getters;
49249
50382
  renderer;
50383
+ fingerprints;
49250
50384
  constructor(get) {
49251
50385
  this.getters = get(ModelStore).getters;
49252
50386
  this.renderer = get(RendererStore);
50387
+ this.fingerprints = get(FormulaFingerprintStore);
49253
50388
  this.renderer.register(this);
49254
50389
  }
49255
50390
  get renderingLayers() {
@@ -49727,14 +50862,22 @@ stores.inject(MyMetaStore, storeInstance);
49727
50862
  const showFormula = this.getters.shouldShowFormulas();
49728
50863
  const { x, y, width, height } = this.getters.getVisibleRect(zone);
49729
50864
  const { verticalAlign } = this.getters.getCellStyle(position);
50865
+ let style = this.getters.getCellComputedStyle(position);
50866
+ if (this.fingerprints.isEnabled) {
50867
+ const fingerprintColor = this.fingerprints.colors.get(position);
50868
+ style = { ...style, fillColor: fingerprintColor };
50869
+ }
50870
+ const dataBarFill = this.fingerprints.isEnabled
50871
+ ? undefined
50872
+ : this.getters.getConditionalDataBar(position);
49730
50873
  const box = {
49731
50874
  x,
49732
50875
  y,
49733
50876
  width,
49734
50877
  height,
49735
50878
  border: this.getters.getCellComputedBorder(position) || undefined,
49736
- style: this.getters.getCellComputedStyle(position),
49737
- dataBarFill: this.getters.getConditionalDataBar(position),
50879
+ style,
50880
+ dataBarFill,
49738
50881
  verticalAlign,
49739
50882
  isError: (cell.type === CellValueType.error && !!cell.message) ||
49740
50883
  this.getters.isDataValidationInvalid(position),
@@ -49759,7 +50902,6 @@ stores.inject(MyMetaStore, storeInstance);
49759
50902
  box.hasIcon = this.getters.doesCellHaveGridIcon(position);
49760
50903
  const headerIconWidth = box.hasIcon ? GRID_ICON_EDGE_LENGTH + GRID_ICON_MARGIN : 0;
49761
50904
  /** Content */
49762
- const style = this.getters.getCellComputedStyle(position);
49763
50905
  const wrapping = style.wrapping || "overflow";
49764
50906
  const wrapText = wrapping === "wrap" && !showFormula;
49765
50907
  const maxWidth = width - 2 * MIN_CELL_TEXT_MARGIN;
@@ -51737,62 +52879,6 @@ stores.inject(MyMetaStore, storeInstance);
51737
52879
  }
51738
52880
  }
51739
52881
 
51740
- class PositionMap {
51741
- map = {};
51742
- constructor(entries = []) {
51743
- for (const [position, value] of entries) {
51744
- this.set(position, value);
51745
- }
51746
- }
51747
- set({ sheetId, col, row }, value) {
51748
- const map = this.map;
51749
- if (!map[sheetId]) {
51750
- map[sheetId] = {};
51751
- }
51752
- if (!map[sheetId][col]) {
51753
- map[sheetId][col] = {};
51754
- }
51755
- map[sheetId][col][row] = value;
51756
- }
51757
- get({ sheetId, col, row }) {
51758
- return this.map[sheetId]?.[col]?.[row];
51759
- }
51760
- getSheet(sheetId) {
51761
- return this.map[sheetId];
51762
- }
51763
- has({ sheetId, col, row }) {
51764
- return this.map[sheetId]?.[col]?.[row] !== undefined;
51765
- }
51766
- delete({ sheetId, col, row }) {
51767
- delete this.map[sheetId]?.[col]?.[row];
51768
- }
51769
- keys() {
51770
- const map = this.map;
51771
- const keys = [];
51772
- for (const sheetId in map) {
51773
- for (const col in map[sheetId]) {
51774
- for (const row in map[sheetId][col]) {
51775
- keys.push({ sheetId, col: parseInt(col), row: parseInt(row) });
51776
- }
51777
- }
51778
- }
51779
- return keys;
51780
- }
51781
- keysForSheet(sheetId) {
51782
- const map = this.map[sheetId];
51783
- if (!map) {
51784
- return [];
51785
- }
51786
- const keys = [];
51787
- for (const col in map) {
51788
- for (const row in map[col]) {
51789
- keys.push({ sheetId, col: parseInt(col), row: parseInt(row) });
51790
- }
51791
- }
51792
- return keys;
51793
- }
51794
- }
51795
-
51796
52882
  /**
51797
52883
  * Core Plugin
51798
52884
  *
@@ -51887,7 +52973,7 @@ stores.inject(MyMetaStore, storeInstance);
51887
52973
  for (let col = zone.left; col <= zone.right; col++) {
51888
52974
  for (let row = zone.top; row <= zone.bottom; row++) {
51889
52975
  const cell = this.getters.getCell({ sheetId, col, row });
51890
- if (cell) {
52976
+ if (cell?.isFormula || cell?.content) {
51891
52977
  this.dispatch("UPDATE_CELL", {
51892
52978
  sheetId: sheetId,
51893
52979
  content: "",
@@ -51923,7 +53009,6 @@ stores.inject(MyMetaStore, storeInstance);
51923
53009
  for (let zone of recomputeZones(zones)) {
51924
53010
  for (let col = zone.left; col <= zone.right; col++) {
51925
53011
  for (let row = zone.top; row <= zone.bottom; row++) {
51926
- // commandHelpers.updateCell(sheetId, col, row, { style: undefined});
51927
53012
  this.dispatch("UPDATE_CELL", {
51928
53013
  sheetId,
51929
53014
  col,
@@ -52232,7 +53317,7 @@ stores.inject(MyMetaStore, storeInstance);
52232
53317
  const before = this.getters.getCell({ sheetId, col, row });
52233
53318
  const hasContent = "content" in after || "formula" in after;
52234
53319
  // Compute the new cell properties
52235
- const afterContent = hasContent ? replaceSpecialSpaces(after?.content) : before?.content || "";
53320
+ const afterContent = hasContent ? replaceNewLines(after?.content) : before?.content || "";
52236
53321
  let style;
52237
53322
  if (after.style !== undefined) {
52238
53323
  style = after.style || undefined;
@@ -55177,6 +56262,9 @@ stores.inject(MyMetaStore, storeInstance);
55177
56262
  };
55178
56263
  }
55179
56264
  getUnboundedZone(sheetId, zone) {
56265
+ if (zone.bottom === undefined || zone.right === undefined) {
56266
+ return zone;
56267
+ }
55180
56268
  const isFullRow = zone.left === 0 && zone.right === this.getNumberCols(sheetId) - 1;
55181
56269
  const isFullCol = zone.top === 0 && zone.bottom === this.getNumberRows(sheetId) - 1;
55182
56270
  return {
@@ -55342,7 +56430,7 @@ stores.inject(MyMetaStore, storeInstance);
55342
56430
  if (orderedSheetIds.find((id) => sheets[id]?.name.toLowerCase() === name && id !== cmd.sheetId)) {
55343
56431
  return "DuplicatedSheetName" /* CommandResult.DuplicatedSheetName */;
55344
56432
  }
55345
- if (FORBIDDEN_IN_EXCEL_REGEX.test(name)) {
56433
+ if (FORBIDDEN_SHEETNAME_CHARS_IN_EXCEL_REGEX.test(name)) {
55346
56434
  return "ForbiddenCharactersInSheetName" /* CommandResult.ForbiddenCharactersInSheetName */;
55347
56435
  }
55348
56436
  return "Success" /* CommandResult.Success */;
@@ -59460,66 +60548,25 @@ stores.inject(MyMetaStore, storeInstance);
59460
60548
  return;
59461
60549
  }
59462
60550
  const zone = this.getters.getRangeFromSheetXC(sheetId, range).zone;
59463
- const colorCellArgs = [];
60551
+ const colorThresholds = [{ value: minValue, color: rule.minimum.color }];
59464
60552
  if (rule.midpoint && midValue) {
59465
- colorCellArgs.push({
59466
- minValue,
59467
- minColor: rule.minimum.color,
59468
- colorDiffUnit: this.computeColorDiffUnits(minValue, midValue, rule.minimum.color, rule.midpoint.color),
59469
- });
59470
- colorCellArgs.push({
59471
- minValue: midValue,
59472
- minColor: rule.midpoint.color,
59473
- colorDiffUnit: this.computeColorDiffUnits(midValue, maxValue, rule.midpoint.color, rule.maximum.color),
59474
- });
59475
- }
59476
- else {
59477
- colorCellArgs.push({
59478
- minValue,
59479
- minColor: rule.minimum.color,
59480
- colorDiffUnit: this.computeColorDiffUnits(minValue, maxValue, rule.minimum.color, rule.maximum.color),
59481
- });
60553
+ colorThresholds.push({ value: midValue, color: rule.midpoint.color });
59482
60554
  }
60555
+ colorThresholds.push({ value: maxValue, color: rule.maximum.color });
60556
+ const colorScale = getColorScale(colorThresholds);
59483
60557
  for (let row = zone.top; row <= zone.bottom; row++) {
59484
60558
  for (let col = zone.left; col <= zone.right; col++) {
59485
60559
  const cell = this.getters.getEvaluatedCell({ sheetId, col, row });
59486
60560
  if (cell.type === CellValueType.number) {
59487
60561
  const value = clip(cell.value, minValue, maxValue);
59488
- let color;
59489
- if (colorCellArgs.length === 2 && midValue) {
59490
- color =
59491
- value <= midValue
59492
- ? this.colorCell(value, colorCellArgs[0].minValue, colorCellArgs[0].minColor, colorCellArgs[0].colorDiffUnit)
59493
- : this.colorCell(value, colorCellArgs[1].minValue, colorCellArgs[1].minColor, colorCellArgs[1].colorDiffUnit);
59494
- }
59495
- else {
59496
- color = this.colorCell(value, colorCellArgs[0].minValue, colorCellArgs[0].minColor, colorCellArgs[0].colorDiffUnit);
59497
- }
59498
60562
  if (!computedStyle[col])
59499
60563
  computedStyle[col] = [];
59500
60564
  computedStyle[col][row] = computedStyle[col]?.[row] || {};
59501
- computedStyle[col][row].fillColor = colorNumberString(color);
60565
+ computedStyle[col][row].fillColor = colorScale(value);
59502
60566
  }
59503
60567
  }
59504
60568
  }
59505
60569
  }
59506
- computeColorDiffUnits(minValue, maxValue, minColor, maxColor) {
59507
- const deltaValue = maxValue - minValue;
59508
- const deltaColorR = ((minColor >> 16) % 256) - ((maxColor >> 16) % 256);
59509
- const deltaColorG = ((minColor >> 8) % 256) - ((maxColor >> 8) % 256);
59510
- const deltaColorB = (minColor % 256) - (maxColor % 256);
59511
- const colorDiffUnitR = deltaColorR / deltaValue;
59512
- const colorDiffUnitG = deltaColorG / deltaValue;
59513
- const colorDiffUnitB = deltaColorB / deltaValue;
59514
- return [colorDiffUnitR, colorDiffUnitG, colorDiffUnitB];
59515
- }
59516
- colorCell(value, minValue, minColor, colorDiffUnit) {
59517
- const [colorDiffUnitR, colorDiffUnitG, colorDiffUnitB] = colorDiffUnit;
59518
- const r = Math.round(((minColor >> 16) % 256) - colorDiffUnitR * (value - minValue));
59519
- const g = Math.round(((minColor >> 8) % 256) - colorDiffUnitG * (value - minValue));
59520
- const b = Math.round((minColor % 256) - colorDiffUnitB * (value - minValue));
59521
- return (r << 16) | (g << 8) | b;
59522
- }
59523
60570
  /**
59524
60571
  * Execute the predicate to know if a conditional formatting rule should be applied to a cell
59525
60572
  */
@@ -60145,7 +61192,7 @@ stores.inject(MyMetaStore, storeInstance);
60145
61192
  }
60146
61193
  getValuesToAggregate(measure, domain) {
60147
61194
  const { rowDomain, colDomain } = domainToColRowDomain(this, domain);
60148
- const table = this.getTableStructure();
61195
+ const table = super.getTableStructure();
60149
61196
  const values = [];
60150
61197
  if (colDomain.length === 0 &&
60151
61198
  rowDomain.length < this.definition.rows.length &&
@@ -60579,6 +61626,21 @@ stores.inject(MyMetaStore, storeInstance);
60579
61626
  }
60580
61627
  throw new Error(`Value ${result.value} is not a number`);
60581
61628
  }
61629
+ getTableStructure() {
61630
+ const table = super.getTableStructure();
61631
+ this.sortTableStructure(table);
61632
+ return table;
61633
+ }
61634
+ sortTableStructure(table) {
61635
+ if (!this.definition.sortedColumn || table.isSorted) {
61636
+ return;
61637
+ }
61638
+ const measure = this.definition.sortedColumn.measure;
61639
+ const isSortValid = isSortedColumnValid(this.definition.sortedColumn, this);
61640
+ if (isSortValid) {
61641
+ table.sort(measure, this.definition.sortedColumn, (measure, domain) => this._getPivotCellValueAndFormat(measure, domain));
61642
+ }
61643
+ }
60582
61644
  }
60583
61645
  return PivotPresentationLayer;
60584
61646
  }
@@ -60750,11 +61812,13 @@ stores.inject(MyMetaStore, storeInstance);
60750
61812
  return EMPTY_PIVOT_CELL;
60751
61813
  }
60752
61814
  if (functionName === "PIVOT") {
60753
- const includeTotal = args[2] === false ? false : undefined;
60754
- const includeColumnHeaders = args[3] === false ? false : undefined;
61815
+ const includeTotal = toScalar(args[2]);
61816
+ const shouldIncludeTotal = includeTotal === undefined ? true : toBoolean(includeTotal);
61817
+ const includeColumnHeaders = toScalar(args[3]);
61818
+ const shouldIncludeColumnHeaders = includeColumnHeaders === undefined ? true : toBoolean(includeColumnHeaders);
60755
61819
  const pivotCells = pivot
60756
61820
  .getTableStructure()
60757
- .getPivotCells(includeTotal, includeColumnHeaders);
61821
+ .getPivotCells(shouldIncludeTotal, shouldIncludeColumnHeaders);
60758
61822
  const pivotCol = position.col - mainPosition.col;
60759
61823
  const pivotRow = position.row - mainPosition.row;
60760
61824
  return pivotCells[pivotCol][pivotRow];
@@ -62352,23 +63416,6 @@ stores.inject(MyMetaStore, storeInstance);
62352
63416
  }
62353
63417
  }
62354
63418
 
62355
- function randomChoice(arr) {
62356
- return arr[Math.floor(Math.random() * arr.length)];
62357
- }
62358
- const colors = [
62359
- "#ff851b",
62360
- "#0074d9",
62361
- "#7fdbff",
62362
- "#b10dc9",
62363
- "#39cccc",
62364
- "#f012be",
62365
- "#3d9970",
62366
- "#111111",
62367
- "#ff4136",
62368
- "#aaaaaa",
62369
- "#85144b",
62370
- "#001f3f",
62371
- ];
62372
63419
  class CollaborativePlugin extends UIPlugin {
62373
63420
  static getters = [
62374
63421
  "getClientsToDisplay",
@@ -62377,7 +63424,7 @@ stores.inject(MyMetaStore, storeInstance);
62377
63424
  "isFullySynchronized",
62378
63425
  ];
62379
63426
  static layers = ["Selection"];
62380
- availableColors = new Set(colors);
63427
+ availableColors = new AlternatingColorGenerator(12);
62381
63428
  colors = {};
62382
63429
  session;
62383
63430
  constructor(config) {
@@ -62388,14 +63435,6 @@ stores.inject(MyMetaStore, storeInstance);
62388
63435
  return (position.row < this.getters.getNumberRows(position.sheetId) &&
62389
63436
  position.col < this.getters.getNumberCols(position.sheetId));
62390
63437
  }
62391
- chooseNewColor() {
62392
- if (this.availableColors.size === 0) {
62393
- this.availableColors = new Set(colors);
62394
- }
62395
- const color = randomChoice([...this.availableColors.values()]);
62396
- this.availableColors.delete(color);
62397
- return color;
62398
- }
62399
63438
  getClient() {
62400
63439
  return this.session.getClient();
62401
63440
  }
@@ -62430,7 +63469,7 @@ stores.inject(MyMetaStore, storeInstance);
62430
63469
  this.isPositionValid(client.position)) {
62431
63470
  const position = client.position;
62432
63471
  if (!this.colors[client.id]) {
62433
- this.colors[client.id] = this.chooseNewColor();
63472
+ this.colors[client.id] = this.availableColors.next();
62434
63473
  }
62435
63474
  const color = this.colors[client.id];
62436
63475
  clients.push({ ...client, position, color });
@@ -62667,7 +63706,7 @@ stores.inject(MyMetaStore, storeInstance);
62667
63706
  for (let row = zone.top; row <= zone.bottom; row++) {
62668
63707
  const position = { sheetId, col, row };
62669
63708
  const pivotCell = this.getters.getPivotCellFromPosition(position);
62670
- if (pivotCell.type === "VALUE") {
63709
+ if (this.isSpilledPivotValueFormula(position, pivotCell)) {
62671
63710
  measurePositions.push(position);
62672
63711
  const pivotId = this.getters.getPivotIdFromPosition(position) || "";
62673
63712
  measuresByPivotId[pivotId] ??= new Set();
@@ -62704,6 +63743,10 @@ stores.inject(MyMetaStore, storeInstance);
62704
63743
  format,
62705
63744
  });
62706
63745
  }
63746
+ isSpilledPivotValueFormula(position, pivotCell) {
63747
+ const cell = this.getters.getCell(position);
63748
+ return pivotCell.type === "VALUE" && !cell?.isFormula;
63749
+ }
62707
63750
  /**
62708
63751
  * This function allows to adjust the quantity of decimal places after a decimal
62709
63752
  * point on cells containing number value. It does this by changing the cells
@@ -62754,6 +63797,69 @@ stores.inject(MyMetaStore, storeInstance);
62754
63797
  }
62755
63798
  }
62756
63799
 
63800
+ class GeoFeaturePlugin extends UIPlugin {
63801
+ static getters = [
63802
+ "getGeoJsonFeatures",
63803
+ "geoFeatureNameToId",
63804
+ "getGeoChartAvailableRegions",
63805
+ ];
63806
+ geoJsonService;
63807
+ geoJsonCache = {};
63808
+ constructor(config) {
63809
+ super(config);
63810
+ this.geoJsonService = config.external.geoJsonService;
63811
+ }
63812
+ getGeoChartAvailableRegions() {
63813
+ if (!this.geoJsonService) {
63814
+ console.error("No geoJsonService provided to the model");
63815
+ return [];
63816
+ }
63817
+ return this.geoJsonService.getAvailableRegions() || [];
63818
+ }
63819
+ getGeoJsonFeatures(region) {
63820
+ if (!this.geoJsonService) {
63821
+ console.error("No geoJsonService provided to the model");
63822
+ return;
63823
+ }
63824
+ const cachedGeoJson = this.geoJsonCache[region];
63825
+ if (cachedGeoJson instanceof Promise) {
63826
+ return undefined;
63827
+ }
63828
+ if (cachedGeoJson !== undefined) {
63829
+ return cachedGeoJson ?? undefined;
63830
+ }
63831
+ this.geoJsonCache[region] = new Promise(async (resolve) => {
63832
+ const json = await this.geoJsonService?.getTopoJson(region);
63833
+ this.geoJsonCache[region] = this.convertToGeoJson(json);
63834
+ this.dispatch("EVALUATE_CHARTS");
63835
+ resolve();
63836
+ });
63837
+ return undefined;
63838
+ }
63839
+ geoFeatureNameToId(region, featureName) {
63840
+ if (!this.geoJsonService) {
63841
+ console.error("No geoJsonService provided to the model");
63842
+ return;
63843
+ }
63844
+ return this.geoJsonService.geoFeatureNameToId(region, featureName);
63845
+ }
63846
+ convertToGeoJson(json) {
63847
+ if (!json) {
63848
+ return null;
63849
+ }
63850
+ // TopoJSON
63851
+ if (json.type === "Topology") {
63852
+ const features = window.ChartGeo.topojson.feature(json, Object.values(json.objects)[0]);
63853
+ return features.type === "FeatureCollection" ? features.features : [features];
63854
+ }
63855
+ // GeoJSON
63856
+ else if (json.type === "FeatureCollection") {
63857
+ return json.features;
63858
+ }
63859
+ throw new Error("Invalid TopoJSON");
63860
+ }
63861
+ }
63862
+
62757
63863
  class HeaderVisibilityUIPlugin extends UIPlugin {
62758
63864
  static getters = [
62759
63865
  "getNextVisibleCellPosition",
@@ -62908,7 +64014,7 @@ stores.inject(MyMetaStore, storeInstance);
62908
64014
  getPivotDuplicateSheetName(pivotName) {
62909
64015
  let i = 1;
62910
64016
  const names = this.getters.getSheetIds().map((id) => this.getters.getSheetName(id));
62911
- const sanitizedName = pivotName.replace(new RegExp(FORBIDDEN_IN_EXCEL_REGEX, "g"), " ");
64017
+ const sanitizedName = sanitizeSheetName(pivotName);
62912
64018
  let name = sanitizedName;
62913
64019
  while (names.includes(name)) {
62914
64020
  name = `${sanitizedName} (${i})`;
@@ -64291,10 +65397,13 @@ stores.inject(MyMetaStore, storeInstance);
64291
65397
  case "RESIZE_TABLE": {
64292
65398
  const table = this.getters.getCoreTableMatchingTopLeft(cmd.sheetId, cmd.zone);
64293
65399
  this.dispatch("UPDATE_TABLE", { ...cmd });
64294
- if (!table || !table.config.automaticAutofill)
65400
+ if (!table)
64295
65401
  return;
64296
- const oldTableZone = table.range.zone;
64297
65402
  const newTableZone = this.getters.getRangeFromRangeData(cmd.newTableRange).zone;
65403
+ this.selection.selectCell(newTableZone.right, newTableZone.bottom);
65404
+ if (!table.config.automaticAutofill)
65405
+ return;
65406
+ const oldTableZone = table.range.zone;
64298
65407
  if (newTableZone.bottom >= oldTableZone.bottom) {
64299
65408
  for (let col = newTableZone.left; col <= newTableZone.right; col++) {
64300
65409
  const autofillSource = { col, row: oldTableZone.bottom, sheetId: cmd.sheetId };
@@ -65500,15 +66609,23 @@ stores.inject(MyMetaStore, storeInstance);
65500
66609
  handler.paste({ zones: pasteTarget, sheetId }, data, { isCutOperation: true });
65501
66610
  const toRemove = isBasedBefore ? cmd.elements.map((el) => el + thickness) : cmd.elements;
65502
66611
  let currentIndex = cmd.base;
66612
+ const resizingGroups = {};
65503
66613
  for (const element of toRemove) {
65504
66614
  const size = this.getters.getHeaderSize(cmd.sheetId, cmd.dimension, element);
66615
+ const currentSize = this.getters.getHeaderSize(cmd.sheetId, cmd.dimension, currentIndex);
66616
+ if (size != currentSize) {
66617
+ resizingGroups[size] ??= [];
66618
+ resizingGroups[size].push(currentIndex);
66619
+ currentIndex += 1;
66620
+ }
66621
+ }
66622
+ for (const size in resizingGroups) {
65505
66623
  this.dispatch("RESIZE_COLUMNS_ROWS", {
65506
66624
  dimension: cmd.dimension,
65507
66625
  sheetId: cmd.sheetId,
65508
- size,
65509
- elements: [currentIndex],
66626
+ size: parseInt(size, 10),
66627
+ elements: resizingGroups[size],
65510
66628
  });
65511
- currentIndex += 1;
65512
66629
  }
65513
66630
  this.dispatch("REMOVE_COLUMNS_ROWS", {
65514
66631
  dimension: cmd.dimension,
@@ -66090,6 +67207,7 @@ stores.inject(MyMetaStore, storeInstance);
66090
67207
  break;
66091
67208
  case "DELETE_SHEET":
66092
67209
  this.cleanViewports();
67210
+ this.sheetsWithDirtyViewports.delete(cmd.sheetId);
66093
67211
  break;
66094
67212
  case "ACTIVATE_SHEET":
66095
67213
  this.sheetsWithDirtyViewports.add(cmd.sheetIdTo);
@@ -66740,7 +67858,8 @@ stores.inject(MyMetaStore, storeInstance);
66740
67858
  .add("data_cleanup", DataCleanupPlugin)
66741
67859
  .add("table_autofill", TableAutofillPlugin)
66742
67860
  .add("table_ui_resize", TableResizeUI)
66743
- .add("datavalidation_insert", DataValidationInsertionPlugin);
67861
+ .add("datavalidation_insert", DataValidationInsertionPlugin)
67862
+ .add("geo_features", GeoFeaturePlugin);
66744
67863
  // Plugins which have a state, but which should not be shared in collaborative
66745
67864
  const statefulUIPluginRegistry = new Registry()
66746
67865
  .add("selection", GridSelectionPlugin)
@@ -66984,7 +68103,7 @@ stores.inject(MyMetaStore, storeInstance);
66984
68103
  env.raiseError(_t("A sheet with the name %s already exists. Please select another name.", name), errorCallback);
66985
68104
  }
66986
68105
  else if (result.reasons.includes("ForbiddenCharactersInSheetName" /* CommandResult.ForbiddenCharactersInSheetName */)) {
66987
- env.raiseError(_t("Some used characters are not allowed in a sheet name (Forbidden characters are %s).", FORBIDDEN_SHEET_CHARS.join(" ")), errorCallback);
68106
+ env.raiseError(_t("Some used characters are not allowed in a sheet name (Forbidden characters are %s).", FORBIDDEN_SHEETNAME_CHARS.join(" ")), errorCallback);
66988
68107
  }
66989
68108
  }
66990
68109
 
@@ -68214,7 +69333,7 @@ stores.inject(MyMetaStore, storeInstance);
68214
69333
  setup() {
68215
69334
  owl.onWillUpdateProps((nextProps) => {
68216
69335
  if (nextProps.action !== this.props.action) {
68217
- this.actionButton = createAction(this.props.action);
69336
+ this.actionButton = createAction(nextProps.action);
68218
69337
  }
68219
69338
  });
68220
69339
  }
@@ -68484,8 +69603,8 @@ stores.inject(MyMetaStore, storeInstance);
68484
69603
  .o-topbar-composer {
68485
69604
  height: fit-content;
68486
69605
  margin-top: -1px;
69606
+ margin-bottom: -1px;
68487
69607
  border: 1px solid;
68488
- z-index: ${ComponentsImportance.TopBarComposer};
68489
69608
  font-family: ${DEFAULT_FONT};
68490
69609
 
68491
69610
  .o-composer:empty:not(:focus):not(.active)::before {
@@ -68543,6 +69662,7 @@ stores.inject(MyMetaStore, storeInstance);
68543
69662
  }
68544
69663
  return cssPropertiesToCss({
68545
69664
  "border-color": SELECTION_BORDER_COLOR,
69665
+ "z-index": String(ComponentsImportance.TopBarComposer),
68546
69666
  });
68547
69667
  }
68548
69668
  onFocus(selection) {
@@ -68550,88 +69670,6 @@ stores.inject(MyMetaStore, storeInstance);
68550
69670
  }
68551
69671
  }
68552
69672
 
68553
- css /* scss */ `
68554
- .o-font-size-editor {
68555
- height: calc(100% - 4px);
68556
- input.o-font-size {
68557
- outline-color: ${SELECTION_BORDER_COLOR};
68558
- height: 20px;
68559
- width: 23px;
68560
- }
68561
- }
68562
- .o-text-options > div {
68563
- cursor: pointer;
68564
- line-height: 26px;
68565
- padding: 3px 12px;
68566
- &:hover {
68567
- background-color: rgba(0, 0, 0, 0.08);
68568
- }
68569
- }
68570
- `;
68571
- class FontSizeEditor extends owl.Component {
68572
- static template = "o-spreadsheet-FontSizeEditor";
68573
- static props = {
68574
- onToggle: Function,
68575
- dropdownStyle: String,
68576
- class: String,
68577
- };
68578
- static components = {};
68579
- fontSizes = FONT_SIZES;
68580
- dropdown = owl.useState({ isOpen: false });
68581
- inputRef = owl.useRef("inputFontSize");
68582
- rootEditorRef = owl.useRef("FontSizeEditor");
68583
- setup() {
68584
- owl.useExternalListener(window, "click", this.onExternalClick, { capture: true });
68585
- }
68586
- onExternalClick(ev) {
68587
- if (!isChildEvent(this.rootEditorRef.el, ev)) {
68588
- this.closeFontList();
68589
- }
68590
- }
68591
- get currentFontSize() {
68592
- return this.env.model.getters.getCurrentStyle().fontSize || DEFAULT_FONT_SIZE;
68593
- }
68594
- toggleFontList() {
68595
- const isOpen = this.dropdown.isOpen;
68596
- if (!isOpen) {
68597
- this.props.onToggle();
68598
- this.inputRef.el.focus();
68599
- }
68600
- else {
68601
- this.closeFontList();
68602
- }
68603
- }
68604
- closeFontList() {
68605
- this.dropdown.isOpen = false;
68606
- }
68607
- setSize(fontSizeStr) {
68608
- const fontSize = clip(Math.floor(parseFloat(fontSizeStr)), 1, 400);
68609
- setStyle(this.env, { fontSize });
68610
- this.closeFontList();
68611
- }
68612
- setSizeFromInput(ev) {
68613
- this.setSize(ev.target.value);
68614
- }
68615
- setSizeFromList(fontSizeStr) {
68616
- this.setSize(fontSizeStr);
68617
- }
68618
- onInputFocused(ev) {
68619
- this.dropdown.isOpen = true;
68620
- ev.target.select();
68621
- }
68622
- onInputKeydown(ev) {
68623
- if (ev.key === "Enter" || ev.key === "Escape") {
68624
- this.closeFontList();
68625
- const target = ev.target;
68626
- // In the case of a ESCAPE key, we get the previous font size back
68627
- if (ev.key === "Escape") {
68628
- target.value = `${this.currentFontSize}`;
68629
- }
68630
- this.props.onToggle();
68631
- }
68632
- }
68633
- }
68634
-
68635
69673
  class TableDropdownButton extends owl.Component {
68636
69674
  static template = "o-spreadsheet-TableDropdownButton";
68637
69675
  static components = { TableStylesPopover, ActionButton };
@@ -68733,6 +69771,15 @@ stores.inject(MyMetaStore, storeInstance);
68733
69771
  }
68734
69772
  }
68735
69773
 
69774
+ .irregularity-map {
69775
+ border-top: 1px solid ${SEPARATOR_COLOR};
69776
+ height: ${TOPBAR_TOOLBAR_HEIGHT}px;
69777
+
69778
+ .alert-info {
69779
+ border-left: 3px solid ${ALERT_INFO_BORDER};
69780
+ }
69781
+ }
69782
+
68736
69783
  .o-topbar-composer {
68737
69784
  flex-grow: 1;
68738
69785
  }
@@ -68800,9 +69847,6 @@ stores.inject(MyMetaStore, storeInstance);
68800
69847
  onClick: Function,
68801
69848
  dropdownMaxHeight: Number,
68802
69849
  };
68803
- get dropdownStyle() {
68804
- return `max-height:${this.props.dropdownMaxHeight}px`;
68805
- }
68806
69850
  static components = {
68807
69851
  ColorPickerWidget,
68808
69852
  ColorPicker,
@@ -68829,8 +69873,10 @@ stores.inject(MyMetaStore, storeInstance);
68829
69873
  formatNumberMenuItemSpec = formatNumberMenuItemSpec;
68830
69874
  isntToolbarMenu = false;
68831
69875
  composerFocusStore;
69876
+ fingerprints;
68832
69877
  setup() {
68833
69878
  this.composerFocusStore = useStore(ComposerFocusStore);
69879
+ this.fingerprints = useStore(FormulaFingerprintStore);
68834
69880
  owl.useExternalListener(window, "click", this.onExternalClick);
68835
69881
  owl.onWillStart(() => this.updateCellState());
68836
69882
  owl.onWillUpdateProps(() => this.updateCellState());
@@ -68840,6 +69886,9 @@ stores.inject(MyMetaStore, storeInstance);
68840
69886
  .getAllOrdered()
68841
69887
  .filter((item) => !item.isVisible || item.isVisible(this.env));
68842
69888
  }
69889
+ get currentFontSize() {
69890
+ return this.env.model.getters.getCurrentStyle().fontSize || DEFAULT_FONT_SIZE;
69891
+ }
68843
69892
  onExternalClick(ev) {
68844
69893
  // TODO : manage click events better. We need this piece of code
68845
69894
  // otherwise the event opening the menu would close it on the same frame.
@@ -68918,6 +69967,9 @@ stores.inject(MyMetaStore, storeInstance);
68918
69967
  setStyle(this.env, { [target]: color });
68919
69968
  this.onClick();
68920
69969
  }
69970
+ setFontSize(fontSize) {
69971
+ setStyle(this.env, { fontSize });
69972
+ }
68921
69973
  }
68922
69974
 
68923
69975
  function instantiateClipboard() {
@@ -69295,7 +70347,7 @@ stores.inject(MyMetaStore, storeInstance);
69295
70347
  properties["grid-template-rows"] = `auto`;
69296
70348
  }
69297
70349
  else {
69298
- properties["grid-template-rows"] = `${TOPBAR_HEIGHT}px auto ${BOTTOMBAR_HEIGHT + 1}px`;
70350
+ properties["grid-template-rows"] = `max-content auto ${BOTTOMBAR_HEIGHT + 1}px`;
69299
70351
  }
69300
70352
  properties["grid-template-columns"] = `auto ${this.sidePanel.panelSize}px`;
69301
70353
  return cssPropertiesToCss(properties);
@@ -70373,9 +71425,6 @@ stores.inject(MyMetaStore, storeInstance);
70373
71425
  getBackToDefault() {
70374
71426
  this.stream.getBackToDefault();
70375
71427
  }
70376
- getAnchor() {
70377
- return this.anchor;
70378
- }
70379
71428
  modifyAnchor(anchor, mode, options) {
70380
71429
  const sheetId = this.getters.getActiveSheetId();
70381
71430
  anchor = {
@@ -70918,12 +71967,11 @@ stores.inject(MyMetaStore, storeInstance);
70918
71967
  // <manualLayout/> to manually position the chart in the figure container
70919
71968
  let title = escapeXml ``;
70920
71969
  if (chart.data.title?.text) {
70921
- const color = chart.data.title.color
70922
- ? toXlsxHexColor(chart.data.title.color)
70923
- : chart.data.fontColor;
71970
+ const titleColor = toXlsxHexColor(chartMutedFontColor(chart.data.backgroundColor));
71971
+ const fontSize = chart.data.title.fontSize ?? CHART_TITLE_FONT_SIZE;
70924
71972
  title = escapeXml /*xml*/ `
70925
71973
  <c:title>
70926
- ${insertText(chart.data.title.text, color, DEFAULT_CHART_FONT_SIZE, chart.data.title)}
71974
+ ${insertText(chart.data.title.text, titleColor, fontSize, chart.data.title)}
70927
71975
  <c:overlay val="0" />
70928
71976
  </c:title>
70929
71977
  `;
@@ -71013,7 +72061,7 @@ stores.inject(MyMetaStore, storeInstance);
71013
72061
  </a:ln>
71014
72062
  `;
71015
72063
  }
71016
- function insertText(text, fontColor = "000000", fontsize = DEFAULT_CHART_FONT_SIZE, style = {}) {
72064
+ function insertText(text, fontColor = "000000", fontsize = CHART_TITLE_FONT_SIZE, style = {}) {
71017
72065
  return escapeXml /*xml*/ `
71018
72066
  <c:tx>
71019
72067
  <c:rich>
@@ -71514,6 +72562,7 @@ stores.inject(MyMetaStore, storeInstance);
71514
72562
  // Each Axis present inside a graph needs to be identified by an unsigned integer in order to be referenced by its crossAxis.
71515
72563
  // I.e. x-axis, will reference y-axis and vice-versa.
71516
72564
  const color = title?.color ? toXlsxHexColor(title.color) : defaultFontColor;
72565
+ const fontSize = title?.fontSize ?? CHART_AXIS_TITLE_FONT_SIZE;
71517
72566
  return escapeXml /*xml*/ `
71518
72567
  <${axisName}>
71519
72568
  <c:axId val="${axId}"/>
@@ -71529,7 +72578,7 @@ stores.inject(MyMetaStore, storeInstance);
71529
72578
  <c:minorTickMark val="none" />
71530
72579
  <c:numFmt formatCode="General" sourceLinked="1" />
71531
72580
  <c:title>
71532
- ${insertText(title?.text ?? "", color, 10, title)}
72581
+ ${insertText(title?.text ?? "", color, fontSize, title)}
71533
72582
  </c:title>
71534
72583
  ${insertTextProperties(10, defaultFontColor)}
71535
72584
  </${axisName}>
@@ -73307,6 +74356,7 @@ stores.inject(MyMetaStore, storeInstance);
73307
74356
  session: this.session,
73308
74357
  defaultCurrency: this.config.defaultCurrency,
73309
74358
  customColors: this.config.customColors || [],
74359
+ external: this.config.external,
73310
74360
  };
73311
74361
  }
73312
74362
  // ---------------------------------------------------------------------------
@@ -73538,7 +74588,6 @@ stores.inject(MyMetaStore, storeInstance);
73538
74588
  MIN_COL_WIDTH,
73539
74589
  HEADER_HEIGHT,
73540
74590
  HEADER_WIDTH,
73541
- TOPBAR_HEIGHT,
73542
74591
  BOTTOMBAR_HEIGHT,
73543
74592
  DEFAULT_CELL_WIDTH,
73544
74593
  DEFAULT_CELL_HEIGHT,
@@ -73640,6 +74689,7 @@ stores.inject(MyMetaStore, storeInstance);
73640
74689
  createPivotFormula,
73641
74690
  areDomainArgsFieldsValid,
73642
74691
  splitReference,
74692
+ sanitizeSheetName,
73643
74693
  };
73644
74694
  const links = {
73645
74695
  isMarkdownLink,
@@ -73778,9 +74828,9 @@ stores.inject(MyMetaStore, storeInstance);
73778
74828
  exports.tokenize = tokenize;
73779
74829
 
73780
74830
 
73781
- __info__.version = "18.1.0-alpha.6";
73782
- __info__.date = "2024-11-28T09:06:59.527Z";
73783
- __info__.hash = "875c901";
74831
+ __info__.version = "18.1.0-alpha.8";
74832
+ __info__.date = "2024-12-19T07:49:54.421Z";
74833
+ __info__.hash = "87a1567";
73784
74834
 
73785
74835
 
73786
74836
  })(this.o_spreadsheet = this.o_spreadsheet || {}, owl);