@odoo/o-spreadsheet 18.1.0-alpha.7 → 18.1.0

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.7
6
- * @date 2024-12-05T10:40:26.512Z
7
- * @hash 7b1c39b
5
+ * @version 18.1.0
6
+ * @date 2024-12-26T06:37:07.879Z
7
+ * @hash c520e89
8
8
  */
9
9
 
10
10
  (function (exports, owl) {
@@ -266,7 +266,6 @@
266
266
  const MIN_COL_WIDTH = 5;
267
267
  const HEADER_HEIGHT = 26;
268
268
  const HEADER_WIDTH = 48;
269
- const TOPBAR_HEIGHT = 63;
270
269
  const TOPBAR_TOOLBAR_HEIGHT = 34;
271
270
  const BOTTOMBAR_HEIGHT = 36;
272
271
  const DEFAULT_CELL_WIDTH = 96;
@@ -790,6 +789,7 @@
790
789
  * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Character_Classes
791
790
  */
792
791
  const whiteSpaceSpecialCharacters = [
792
+ " ",
793
793
  "\t",
794
794
  "\f",
795
795
  "\v",
@@ -804,17 +804,15 @@
804
804
  String.fromCharCode(parseInt("3000", 16)),
805
805
  String.fromCharCode(parseInt("feff", 16)),
806
806
  ];
807
- 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;
808
809
  /**
809
- * Replace all the special spaces in a string (non-breaking, tabs, ...) by normal spaces, and all the
810
- * different newlines types by \n.
810
+ * Replace all different newlines characters by \n
811
811
  */
812
- function replaceSpecialSpaces(text) {
812
+ function replaceNewLines(text) {
813
813
  if (!text)
814
814
  return "";
815
- if (!whiteSpaceRegexp.test(text))
816
- return text;
817
- return text.replace(whiteSpaceRegexp, (match, newLine) => (newLine ? NEWLINE : " "));
815
+ return text.replace(newLineRegexp, NEWLINE);
818
816
  }
819
817
  /**
820
818
  * Determine if the numbers are consecutive.
@@ -980,7 +978,7 @@
980
978
 
981
979
  const RBA_REGEX = /rgba?\(|\s+|\)/gi;
982
980
  const HEX_MATCH = /^#([A-F\d]{2}){3,4}$/;
983
- const colors$1 = [
981
+ const colors = [
984
982
  "#eb6d00",
985
983
  "#0074d9",
986
984
  "#ad8e00",
@@ -1003,6 +1001,12 @@
1003
1001
  function colorNumberString(color) {
1004
1002
  return toHex(color.toString(16).padStart(6, "0"));
1005
1003
  }
1004
+ function colorToNumber(color) {
1005
+ if (typeof color === "number") {
1006
+ return color;
1007
+ }
1008
+ return Number.parseInt(toHex(color).slice(1), 16);
1009
+ }
1006
1010
  /**
1007
1011
  * Converts any CSS color value to a standardized hex6 value.
1008
1012
  * Accepts: hex3, hex6, hex8, rgb[1] and rgba[1].
@@ -1353,9 +1357,9 @@
1353
1357
  "#056BD9", // Blue #3
1354
1358
  "#155193", // Blue #4
1355
1359
  "#A76DBC", // Violet #1
1356
- "#7F4295", // Violet #1
1357
- "#6D2387", // Violet #1
1358
- "#4F1565", // Violet #1
1360
+ "#7F4295", // Violet #2
1361
+ "#6D2387", // Violet #3
1362
+ "#4F1565", // Violet #4
1359
1363
  "#EA6175", // Red #1
1360
1364
  "#CE4257", // Red #2
1361
1365
  "#982738", // Red #3
@@ -1381,6 +1385,81 @@
1381
1385
  "#C08A16", // Yellow #3
1382
1386
  "#936A12", // Yellow #4
1383
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
+ ];
1384
1463
  function getNthColor(index, palette) {
1385
1464
  return palette[index % palette.length];
1386
1465
  }
@@ -1398,6 +1477,20 @@
1398
1477
  return COLORS_XL;
1399
1478
  }
1400
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
+ }
1401
1494
  class ColorGenerator {
1402
1495
  preferredColors;
1403
1496
  currentColorIndex = 0;
@@ -1412,6 +1505,62 @@
1412
1505
  : getNthColor(this.currentColorIndex++, this.palette);
1413
1506
  }
1414
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
+ }
1415
1564
 
1416
1565
  //------------------------------------------------------------------------------
1417
1566
  // Coordinate
@@ -3283,6 +3432,7 @@
3283
3432
  ]);
3284
3433
  const invalidateChartEvaluationCommands = new Set([
3285
3434
  "EVALUATE_CELLS",
3435
+ "EVALUATE_CHARTS",
3286
3436
  "UPDATE_CELL",
3287
3437
  "UNHIDE_COLUMNS_ROWS",
3288
3438
  "HIDE_COLUMNS_ROWS",
@@ -3319,6 +3469,7 @@
3319
3469
  "RESIZE_SHEETVIEW",
3320
3470
  "SET_VIEWPORT_OFFSET",
3321
3471
  "EVALUATE_CELLS",
3472
+ "EVALUATE_CHARTS",
3322
3473
  "SET_FORMULA_VISIBILITY",
3323
3474
  "UPDATE_FILTER",
3324
3475
  ]);
@@ -6523,7 +6674,7 @@
6523
6674
  const POSTFIX_UNARY_OPERATORS = ["%"];
6524
6675
  const OPERATORS = "+,-,*,/,:,=,<>,>=,>,<=,<,^,&".split(",").concat(POSTFIX_UNARY_OPERATORS);
6525
6676
  function tokenize(str, locale = DEFAULT_LOCALE) {
6526
- str = replaceSpecialSpaces(str);
6677
+ str = replaceNewLines(str);
6527
6678
  const chars = new TokenizingChars(str);
6528
6679
  const result = [];
6529
6680
  while (!chars.isOver()) {
@@ -6670,12 +6821,12 @@
6670
6821
  if (length) {
6671
6822
  return { type: "SPACE", value: NEWLINE.repeat(length) };
6672
6823
  }
6673
- while (chars.current === " ") {
6674
- length++;
6675
- chars.shift();
6824
+ let spaces = "";
6825
+ while (chars.current && chars.current.match(whiteSpaceRegexp)) {
6826
+ spaces += chars.shift();
6676
6827
  }
6677
- if (length) {
6678
- return { type: "SPACE", value: " ".repeat(length) };
6828
+ if (spaces) {
6829
+ return { type: "SPACE", value: spaces };
6679
6830
  }
6680
6831
  return null;
6681
6832
  }
@@ -7534,6 +7685,16 @@
7534
7685
  }
7535
7686
  return domainToString([...domain.slice(0, index), ...domain.slice(index + 1)]);
7536
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
+ }
7537
7698
 
7538
7699
  const pivotTimeAdapterRegistry = new Registry();
7539
7700
  function pivotTimeAdapter(granularity) {
@@ -8044,6 +8205,29 @@
8044
8205
  format: `${" ".repeat(indent)}${format}* `,
8045
8206
  };
8046
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
+ }
8047
8231
 
8048
8232
  class CellClipboardHandler extends AbstractCellClipboardHandler {
8049
8233
  isCutAllowed(data) {
@@ -14652,7 +14836,7 @@ stores.inject(MyMetaStore, storeInstance);
14652
14836
  CellValueType.boolean,
14653
14837
  ];
14654
14838
  function cellsSortingCriterion(sortingOrder) {
14655
- const inverse = sortingOrder === "ascending" ? 1 : -1;
14839
+ const inverse = sortingOrder === "asc" ? 1 : -1;
14656
14840
  return (left, right) => {
14657
14841
  if (left.type === CellValueType.empty) {
14658
14842
  return right.type === CellValueType.empty ? 0 : 1;
@@ -14744,7 +14928,7 @@ stores.inject(MyMetaStore, storeInstance);
14744
14928
  const sortColumns = [];
14745
14929
  const nRows = matrix.length;
14746
14930
  for (let i = 0; i < criteria.length; i += 2) {
14747
- sortingOrders.push(toBoolean(toScalar(criteria[i + 1])?.value) ? "ascending" : "descending");
14931
+ sortingOrders.push(toBoolean(toScalar(criteria[i + 1])?.value) ? "asc" : "desc");
14748
14932
  const sortColumn = criteria[i];
14749
14933
  if (isMatrix(sortColumn) && (sortColumn.length > 1 || sortColumn[0].length > 1)) {
14750
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));
@@ -14761,12 +14945,12 @@ stores.inject(MyMetaStore, storeInstance);
14761
14945
  if (sortColumns.length === 0) {
14762
14946
  for (let i = 0; i < matrix[0].length; i++) {
14763
14947
  sortColumns.push(matrix.map((row) => row[i].value));
14764
- sortingOrders.push("ascending");
14948
+ sortingOrders.push("asc");
14765
14949
  }
14766
14950
  }
14767
14951
  const sortingCriteria = {
14768
- descending: cellsSortingCriterion("descending"),
14769
- ascending: cellsSortingCriterion("ascending"),
14952
+ desc: cellsSortingCriterion("desc"),
14953
+ asc: cellsSortingCriterion("asc"),
14770
14954
  };
14771
14955
  const indexes = range(0, matrix.length);
14772
14956
  indexes.sort((a, b) => {
@@ -20490,20 +20674,8 @@ stores.inject(MyMetaStore, storeInstance);
20490
20674
  replaceSelectedRange(zone) {
20491
20675
  const ref = this.getZoneReference(zone);
20492
20676
  const currentToken = this.tokenAtCursor;
20493
- let replaceStart = this.selectionStart;
20494
- if (currentToken?.type === "REFERENCE") {
20495
- replaceStart = currentToken.start;
20496
- }
20497
- else if (currentToken?.type === "RIGHT_PAREN") {
20498
- // match left parenthesis
20499
- const leftParenthesisIndex = this.currentTokens.findIndex((token) => token.type === "LEFT_PAREN" && token.parenthesesCode === currentToken.parenthesesCode);
20500
- const functionToken = this.currentTokens[leftParenthesisIndex - 1];
20501
- if (functionToken === undefined) {
20502
- return;
20503
- }
20504
- replaceStart = functionToken.start;
20505
- }
20506
- this.replaceText(ref, replaceStart, this.selectionEnd);
20677
+ const start = currentToken?.type === "REFERENCE" ? currentToken.start : this.selectionStart;
20678
+ this.replaceText(ref, start, this.selectionEnd);
20507
20679
  }
20508
20680
  /**
20509
20681
  * Replace the reference of the old zone by the new one.
@@ -20536,17 +20708,6 @@ stores.inject(MyMetaStore, storeInstance);
20536
20708
  getZoneReference(zone) {
20537
20709
  const inputSheetId = this.sheetId;
20538
20710
  const sheetId = this.getters.getActiveSheetId();
20539
- if (zone.top === zone.bottom && zone.left === zone.right) {
20540
- const position = { sheetId, col: zone.left, row: zone.top };
20541
- const pivotId = this.getters.getPivotIdFromPosition(position);
20542
- const pivotCell = this.getters.getPivotCellFromPosition(position);
20543
- const cell = this.getters.getCell(position);
20544
- if (pivotId && pivotCell.type !== "EMPTY" && !cell?.isFormula) {
20545
- const formulaPivotId = this.getters.getPivotFormulaId(pivotId);
20546
- const formula = createPivotFormula(formulaPivotId, pivotCell);
20547
- return localizeFormula(formula, this.getters.getLocale()).slice(1); // strip leading =
20548
- }
20549
- }
20550
20711
  const range = this.getters.getRangeFromZone(sheetId, zone);
20551
20712
  return this.getters.getSelectionRangeString(range, inputSheetId);
20552
20713
  }
@@ -20717,37 +20878,21 @@ stores.inject(MyMetaStore, storeInstance);
20717
20878
  const editionSheetId = this.sheetId;
20718
20879
  const rangeColor = (rangeString) => {
20719
20880
  const colorIndex = this.colorIndexByRange[rangeString];
20720
- return colors$1[colorIndex % colors$1.length];
20881
+ return colors[colorIndex % colors.length];
20721
20882
  };
20722
- const highlights = [];
20723
- for (const range of this.getReferencedRanges()) {
20883
+ return this.getReferencedRanges().map((range) => {
20724
20884
  const rangeString = this.getters.getRangeString(range, editionSheetId);
20725
20885
  const { numberOfRows, numberOfCols } = zoneToDimension(range.zone);
20726
20886
  const zone = numberOfRows * numberOfCols === 1
20727
20887
  ? this.getters.expandZone(range.sheetId, range.zone)
20728
20888
  : range.zone;
20729
- highlights.push({
20889
+ return {
20730
20890
  zone,
20731
20891
  color: rangeColor(rangeString),
20732
20892
  sheetId: range.sheetId,
20733
20893
  interactive: true,
20734
- });
20735
- }
20736
- const activeSheetId = this.getters.getActiveSheetId();
20737
- const selectionZone = this.model.selection.getAnchor().zone;
20738
- const isSelectionHightlighted = highlights.find((highlight) => highlight.sheetId === activeSheetId && isEqual(highlight.zone, selectionZone));
20739
- if (this.editionMode === "selecting" && !isSelectionHightlighted) {
20740
- highlights.push({
20741
- zone: selectionZone,
20742
- color: "#445566",
20743
- sheetId: activeSheetId,
20744
- dashed: true,
20745
- interactive: false,
20746
- noFill: true,
20747
- thinLine: true,
20748
- });
20749
- }
20750
- return highlights;
20894
+ };
20895
+ });
20751
20896
  }
20752
20897
  /**
20753
20898
  * Return ranges currently referenced in the composer
@@ -20975,6 +21120,7 @@ stores.inject(MyMetaStore, storeInstance);
20975
21120
  return error;
20976
21121
  },
20977
21122
  isBadExpression: true,
21123
+ normalizedFormula: tokens.map((t) => t.value).join(""),
20978
21124
  };
20979
21125
  }
20980
21126
  }
@@ -21102,6 +21248,7 @@ stores.inject(MyMetaStore, storeInstance);
21102
21248
  symbols,
21103
21249
  tokens,
21104
21250
  isBadExpression: false,
21251
+ normalizedFormula: cacheKey,
21105
21252
  };
21106
21253
  return compiledFormula;
21107
21254
  }
@@ -21366,6 +21513,7 @@ stores.inject(MyMetaStore, storeInstance);
21366
21513
  description: definition.name,
21367
21514
  htmlContent: [{ value: str, color: tokenColors.NUMBER }],
21368
21515
  fuzzySearchKey: str + definition.name,
21516
+ alwaysExpanded: true,
21369
21517
  };
21370
21518
  })
21371
21519
  .filter(isDefined);
@@ -27759,6 +27907,19 @@ stores.inject(MyMetaStore, storeInstance);
27759
27907
  ["GaugeLowerInflectionPointNaN" /* CommandResult.GaugeLowerInflectionPointNaN */]: _t("The lower inflection point value must be a number"),
27760
27908
  ["GaugeUpperInflectionPointNaN" /* CommandResult.GaugeUpperInflectionPointNaN */]: _t("The upper inflection point value must be a number"),
27761
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
+ },
27762
27923
  };
27763
27924
  const CustomCurrencyTerms = {
27764
27925
  Custom: _t("Custom"),
@@ -28162,6 +28323,26 @@ stores.inject(MyMetaStore, storeInstance);
28162
28323
  locale: getters.getLocale(),
28163
28324
  };
28164
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
+ }
28165
28346
  function getTrendDatasetForBarChart(config, data) {
28166
28347
  const filteredValues = [];
28167
28348
  const filteredLabels = [];
@@ -28714,6 +28895,43 @@ stores.inject(MyMetaStore, storeInstance);
28714
28895
  }
28715
28896
  return datasets;
28716
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
+ }
28717
28935
  function getTrendingLineDataSet(dataset, config, data) {
28718
28936
  const defaultBorderColor = colorToRGBA(dataset.backgroundColor);
28719
28937
  defaultBorderColor.a = 1;
@@ -29091,6 +29309,44 @@ stores.inject(MyMetaStore, storeInstance);
29091
29309
  },
29092
29310
  };
29093
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
+ }
29094
29350
  function getChartAxisTitleRuntime(design) {
29095
29351
  if (design?.title?.text) {
29096
29352
  const { text, color, align, italic, bold } = design.title;
@@ -29154,6 +29410,44 @@ stores.inject(MyMetaStore, storeInstance);
29154
29410
  };
29155
29411
  }
29156
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
+ }
29157
29451
 
29158
29452
  function getChartShowValues(definition, args) {
29159
29453
  const { axisFormats, locale } = args;
@@ -29313,6 +29607,25 @@ stores.inject(MyMetaStore, storeInstance);
29313
29607
  },
29314
29608
  };
29315
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
+ }
29316
29629
  function calculatePercentage(dataset, dataIndex) {
29317
29630
  const numericData = dataset.filter((value) => typeof value === "number");
29318
29631
  const total = numericData.reduce((sum, value) => sum + value, 0);
@@ -29339,6 +29652,10 @@ stores.inject(MyMetaStore, storeInstance);
29339
29652
  getComboChartDatasets: getComboChartDatasets,
29340
29653
  getComboChartLegend: getComboChartLegend,
29341
29654
  getData: getData,
29655
+ getGeoChartData: getGeoChartData,
29656
+ getGeoChartDatasets: getGeoChartDatasets,
29657
+ getGeoChartScales: getGeoChartScales,
29658
+ getGeoChartTooltip: getGeoChartTooltip,
29342
29659
  getLineChartData: getLineChartData,
29343
29660
  getLineChartDatasets: getLineChartDatasets,
29344
29661
  getLineChartLegend: getLineChartLegend,
@@ -29912,6 +30229,133 @@ stores.inject(MyMetaStore, storeInstance);
29912
30229
  return clip(value, minValue, maxValue);
29913
30230
  }
29914
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
+
29915
30359
  class LineChart extends AbstractChart {
29916
30360
  dataSets;
29917
30361
  labelRange;
@@ -30905,6 +31349,15 @@ stores.inject(MyMetaStore, storeInstance);
30905
31349
  getChartDefinitionFromContextCreation: RadarChart.getDefinitionFromContextCreation,
30906
31350
  sequence: 80,
30907
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
+ });
30908
31361
  const chartComponentRegistry = new Registry();
30909
31362
  chartComponentRegistry.add("line", ChartJsComponent);
30910
31363
  chartComponentRegistry.add("bar", ChartJsComponent);
@@ -30916,6 +31369,7 @@ stores.inject(MyMetaStore, storeInstance);
30916
31369
  chartComponentRegistry.add("waterfall", ChartJsComponent);
30917
31370
  chartComponentRegistry.add("pyramid", ChartJsComponent);
30918
31371
  chartComponentRegistry.add("radar", ChartJsComponent);
31372
+ chartComponentRegistry.add("geo", ChartJsComponent);
30919
31373
  const chartCategories = {
30920
31374
  line: _t("Line"),
30921
31375
  column: _t("Column"),
@@ -31075,6 +31529,13 @@ stores.inject(MyMetaStore, storeInstance);
31075
31529
  subtypeDefinition: { fillArea: true },
31076
31530
  category: "misc",
31077
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",
31078
31539
  });
31079
31540
 
31080
31541
  /**
@@ -31925,18 +32386,66 @@ stores.inject(MyMetaStore, storeInstance);
31925
32386
  },
31926
32387
  };
31927
32388
 
31928
- css /*SCSS*/ `
31929
- .o-filter-menu-value {
31930
- padding: 4px;
31931
- line-height: 20px;
31932
- height: 28px;
31933
- .o-filter-menu-value-checked {
31934
- 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
+ }
31935
32425
  }
31936
32426
  }
31937
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
+
31938
32446
  class FilterMenuValueItem extends owl.Component {
31939
32447
  static template = "o-spreadsheet-FilterMenuValueItem";
32448
+ static components = { Checkbox };
31940
32449
  static props = {
31941
32450
  value: String,
31942
32451
  isChecked: Boolean,
@@ -31974,8 +32483,6 @@ stores.inject(MyMetaStore, storeInstance);
31974
32483
  .o-filter-menu-item {
31975
32484
  display: flex;
31976
32485
  box-sizing: border-box;
31977
- height: ${MENU_ITEM_HEIGHT}px;
31978
- padding: 4px 4px 4px 0px;
31979
32486
  cursor: pointer;
31980
32487
  user-select: none;
31981
32488
 
@@ -31984,14 +32491,6 @@ stores.inject(MyMetaStore, storeInstance);
31984
32491
  }
31985
32492
  }
31986
32493
 
31987
- input {
31988
- box-sizing: border-box;
31989
- margin-bottom: 5px;
31990
- border: 1px solid #949494;
31991
- height: 24px;
31992
- padding-right: 28px;
31993
- }
31994
-
31995
32494
  .o-search-icon {
31996
32495
  right: 5px;
31997
32496
  top: 3px;
@@ -32008,19 +32507,12 @@ stores.inject(MyMetaStore, storeInstance);
32008
32507
  display: flex;
32009
32508
  flex-direction: row;
32010
32509
  margin-bottom: 4px;
32011
-
32012
- .o-filter-menu-action-text {
32013
- cursor: pointer;
32014
- margin-right: 10px;
32015
- color: blue;
32016
- text-decoration: underline;
32017
- }
32018
32510
  }
32019
32511
 
32020
32512
  .o-filter-menu-list {
32021
32513
  flex: auto;
32022
32514
  overflow-y: auto;
32023
- border: 1px solid #949494;
32515
+ border: 1px solid ${GRAY_300};
32024
32516
 
32025
32517
  .o-filter-menu-no-values {
32026
32518
  color: #949494;
@@ -34212,6 +34704,21 @@ stores.inject(MyMetaStore, storeInstance);
34212
34704
  isReadonlyAllowed: true,
34213
34705
  icon: "o-spreadsheet-Icon.PIVOT",
34214
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
+ };
34215
34722
  const FIX_FORMULAS = {
34216
34723
  name: _t("Convert to individual formulas"),
34217
34724
  execute(env) {
@@ -34248,6 +34755,66 @@ stores.inject(MyMetaStore, storeInstance);
34248
34755
  },
34249
34756
  icon: "o-spreadsheet-Icon.PIVOT",
34250
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
+ }
34251
34818
 
34252
34819
  //------------------------------------------------------------------------------
34253
34820
  // Context Menu Registry
@@ -34346,14 +34913,33 @@ stores.inject(MyMetaStore, storeInstance);
34346
34913
  name: INSERT_LINK_NAME,
34347
34914
  sequence: 150,
34348
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,
34349
34922
  })
34350
34923
  .add("pivot_fix_formulas", {
34351
34924
  ...FIX_FORMULAS,
34352
- sequence: 155,
34925
+ sequence: 160,
34353
34926
  })
34354
34927
  .add("pivot_properties", {
34355
34928
  ...pivotProperties,
34356
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,
34357
34943
  });
34358
34944
 
34359
34945
  const sortRange = {
@@ -34366,7 +34952,7 @@ stores.inject(MyMetaStore, storeInstance);
34366
34952
  execute: (env) => {
34367
34953
  const { anchor, zones } = env.model.getters.getSelection();
34368
34954
  const sheetId = env.model.getters.getActiveSheetId();
34369
- interactiveSortSelection(env, sheetId, anchor.cell, zones[0], "ascending");
34955
+ interactiveSortSelection(env, sheetId, anchor.cell, zones[0], "asc");
34370
34956
  },
34371
34957
  icon: "o-spreadsheet-Icon.SORT_ASCENDING",
34372
34958
  };
@@ -34394,7 +34980,7 @@ stores.inject(MyMetaStore, storeInstance);
34394
34980
  execute: (env) => {
34395
34981
  const { anchor, zones } = env.model.getters.getSelection();
34396
34982
  const sheetId = env.model.getters.getActiveSheetId();
34397
- interactiveSortSelection(env, sheetId, anchor.cell, zones[0], "descending");
34983
+ interactiveSortSelection(env, sheetId, anchor.cell, zones[0], "desc");
34398
34984
  },
34399
34985
  icon: "o-spreadsheet-Icon.SORT_DESCENDING",
34400
34986
  };
@@ -34877,6 +35463,227 @@ stores.inject(MyMetaStore, storeInstance);
34877
35463
  }
34878
35464
  }
34879
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
+
34880
35687
  const hideCols = {
34881
35688
  name: HIDE_COLUMNS_NAME,
34882
35689
  execute: (env) => {
@@ -35048,6 +35855,19 @@ stores.inject(MyMetaStore, storeInstance);
35048
35855
  return env.model.getters.getGridLinesVisibility(sheetId);
35049
35856
  },
35050
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
+ };
35051
35871
  const viewFormulas = {
35052
35872
  name: _t("Formulas"),
35053
35873
  isActive: (env) => env.model.getters.shouldShowFormulas(),
@@ -35665,6 +36485,11 @@ stores.inject(MyMetaStore, storeInstance);
35665
36485
  .addChild("view_formulas", ["view", "show"], {
35666
36486
  ...viewFormulas,
35667
36487
  sequence: 10,
36488
+ })
36489
+ .addChild("view_irregularity_map", ["view"], {
36490
+ ...irregularityMap,
36491
+ sequence: 40,
36492
+ separator: true,
35668
36493
  })
35669
36494
  // ---------------------------------------------------------------------
35670
36495
  // INSERT MENU ITEMS
@@ -35967,62 +36792,6 @@ stores.inject(MyMetaStore, storeInstance);
35967
36792
  }
35968
36793
  const otRegistry = new OTRegistry();
35969
36794
 
35970
- const CHECK_SVG = /*xml*/ `
35971
- <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'>
35972
- <path fill='none' stroke='#FFF' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/>
35973
- </svg>
35974
- `;
35975
- const CHECKBOX_WIDTH = 14;
35976
- css /* scss */ `
35977
- label.o-checkbox {
35978
- input {
35979
- appearance: none;
35980
- -webkit-appearance: none;
35981
- -moz-appearance: none;
35982
- border-radius: 0;
35983
- width: ${CHECKBOX_WIDTH}px;
35984
- height: ${CHECKBOX_WIDTH}px;
35985
- vertical-align: top;
35986
- box-sizing: border-box;
35987
- outline: none;
35988
- border: 1px solid ${GRAY_300};
35989
-
35990
- &:hover {
35991
- border-color: ${ACTION_COLOR};
35992
- }
35993
-
35994
- &:checked {
35995
- background: url("data:image/svg+xml,${encodeURIComponent(CHECK_SVG)}");
35996
- background-color: ${ACTION_COLOR};
35997
- border-color: ${ACTION_COLOR};
35998
- }
35999
-
36000
- &:focus {
36001
- outline: none;
36002
- box-shadow: 0 0 0 0.25rem rgba(113, 75, 103, 0.25);
36003
- border-color: ${ACTION_COLOR};
36004
- }
36005
- }
36006
- }
36007
- `;
36008
- class Checkbox extends owl.Component {
36009
- static template = "o-spreadsheet.Checkbox";
36010
- static props = {
36011
- label: { type: String, optional: true },
36012
- value: { type: Boolean, optional: true },
36013
- className: { type: String, optional: true },
36014
- name: { type: String, optional: true },
36015
- title: { type: String, optional: true },
36016
- disabled: { type: Boolean, optional: true },
36017
- onChange: Function,
36018
- };
36019
- static defaultProps = { value: false };
36020
- onChange(ev) {
36021
- const value = ev.target.checked;
36022
- this.props.onChange(value);
36023
- }
36024
- }
36025
-
36026
36795
  class Section extends owl.Component {
36027
36796
  static template = "o_spreadsheet.Section";
36028
36797
  static props = {
@@ -38262,6 +39031,104 @@ stores.inject(MyMetaStore, storeInstance);
38262
39031
  }
38263
39032
  }
38264
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
+
38265
39132
  class LineConfigPanel extends GenericChartConfigPanel {
38266
39133
  static template = "o-spreadsheet-LineConfigPanel";
38267
39134
  get canTreatLabelsAsText() {
@@ -38575,6 +39442,10 @@ stores.inject(MyMetaStore, storeInstance);
38575
39442
  .add("radar", {
38576
39443
  configuration: GenericChartConfigPanel,
38577
39444
  design: RadarChartDesignPanel,
39445
+ })
39446
+ .add("geo", {
39447
+ configuration: GeoChartConfigPanel,
39448
+ design: GeoChartDesignPanel,
38578
39449
  });
38579
39450
 
38580
39451
  css /* scss */ `
@@ -39938,7 +40809,7 @@ stores.inject(MyMetaStore, storeInstance);
39938
40809
  border-color: ${GRAY_300};
39939
40810
 
39940
40811
  &.active {
39941
- border-color: ${SELECTION_BORDER_COLOR};
40812
+ border-color: ${ACTION_COLOR};
39942
40813
  }
39943
40814
 
39944
40815
  &.o-invalid {
@@ -40896,7 +41767,7 @@ stores.inject(MyMetaStore, storeInstance);
40896
41767
  }
40897
41768
  const point = this.state.rules.colorScale[target];
40898
41769
  if (point) {
40899
- point.color = Number.parseInt(color.slice(1), 16);
41770
+ point.color = colorToNumber(color);
40900
41771
  }
40901
41772
  this.updateConditionalFormat({ rule: this.state.rules.colorScale });
40902
41773
  this.closeMenus();
@@ -41041,6 +41912,9 @@ stores.inject(MyMetaStore, storeInstance);
41041
41912
  return [this.state.rules.dataBar.rangeValues || ""];
41042
41913
  }
41043
41914
  updateDataBarColor(color) {
41915
+ if (!isColorValid(color)) {
41916
+ return;
41917
+ }
41044
41918
  this.state.rules.dataBar.color = Number.parseInt(color.slice(1), 16);
41045
41919
  this.updateConditionalFormat({ rule: this.state.rules.dataBar });
41046
41920
  }
@@ -41171,6 +42045,10 @@ stores.inject(MyMetaStore, storeInstance);
41171
42045
  border: 1px solid #d8dadd;
41172
42046
  color: #374151;
41173
42047
  }
42048
+
42049
+ table {
42050
+ table-layout: fixed;
42051
+ }
41174
42052
  }
41175
42053
  `;
41176
42054
  class CustomCurrencyPanel extends owl.Component {
@@ -43644,6 +44522,64 @@ stores.inject(MyMetaStore, storeInstance);
43644
44522
  }
43645
44523
  }
43646
44524
 
44525
+ css /* scss */ `
44526
+ .o-pivot-sort {
44527
+ .o-sort-card {
44528
+ width: fit-content;
44529
+ background-color: ${GRAY_100};
44530
+ border: 1px solid ${GRAY_300};
44531
+
44532
+ .o-sort-value {
44533
+ color: ${PRIMARY_BUTTON_BG};
44534
+ }
44535
+ }
44536
+ }
44537
+ `;
44538
+ class PivotSortSection extends owl.Component {
44539
+ static template = "o-spreadsheet-PivotSortSection";
44540
+ static components = {
44541
+ Section,
44542
+ };
44543
+ static props = {
44544
+ definition: Object,
44545
+ pivotId: String,
44546
+ };
44547
+ get hasValidSort() {
44548
+ const pivot = this.env.model.getters.getPivot(this.props.pivotId);
44549
+ return (!!this.props.definition.sortedColumn &&
44550
+ isSortedColumnValid(this.props.definition.sortedColumn, pivot));
44551
+ }
44552
+ get sortDescription() {
44553
+ const sortOrder = this.props.definition.sortedColumn?.order === "asc" ? _t("ascending") : _t("descending");
44554
+ return _t("Sorted on column (%(ascOrDesc)s):", {
44555
+ ascOrDesc: sortOrder,
44556
+ });
44557
+ }
44558
+ get sortValuesAndFields() {
44559
+ const sortedColumn = this.props.definition.sortedColumn;
44560
+ if (!sortedColumn) {
44561
+ return [];
44562
+ }
44563
+ const pivot = this.env.model.getters.getPivot(this.props.pivotId);
44564
+ const locale = this.env.model.getters.getLocale();
44565
+ const currentDomain = [];
44566
+ const sortValues = [];
44567
+ for (const domainItem of sortedColumn.domain) {
44568
+ currentDomain.push(domainItem);
44569
+ const { value, format } = pivot.getPivotHeaderValueAndFormat(currentDomain);
44570
+ const label = formatValue(value, { format, locale });
44571
+ const field = pivot.definition.getDimension(domainItem.field);
44572
+ sortValues.push({ field: getFieldDisplayName(field), value: label });
44573
+ }
44574
+ if (sortedColumn.domain.length === 0) {
44575
+ sortValues.push({ value: _t("Total") });
44576
+ }
44577
+ const measureLabel = pivot.getMeasure(sortedColumn.measure).displayName;
44578
+ sortValues.push({ value: measureLabel, field: _t("Measure") });
44579
+ return sortValues;
44580
+ }
44581
+ }
44582
+
43647
44583
  css /* scss */ `
43648
44584
  .add-calculated-measure {
43649
44585
  cursor: pointer;
@@ -43657,6 +44593,7 @@ stores.inject(MyMetaStore, storeInstance);
43657
44593
  PivotDimensionOrder,
43658
44594
  PivotDimensionGranularity,
43659
44595
  PivotMeasureEditor,
44596
+ PivotSortSection,
43660
44597
  };
43661
44598
  static props = {
43662
44599
  definition: Object,
@@ -43813,9 +44750,13 @@ stores.inject(MyMetaStore, storeInstance);
43813
44750
  }
43814
44751
  updateMeasure(measure, newMeasure) {
43815
44752
  const { measures } = this.props.definition;
43816
- this.props.onDimensionsUpdated({
44753
+ const update = {
43817
44754
  measures: measures.map((m) => (m.id === measure.id ? newMeasure : m)),
43818
- });
44755
+ };
44756
+ if (this.props.definition.sortedColumn?.measure === measure.id) {
44757
+ update.sortedColumn = { ...this.props.definition.sortedColumn, measure: newMeasure.id };
44758
+ }
44759
+ this.props.onDimensionsUpdated(update);
43819
44760
  }
43820
44761
  getMeasureId(fieldName, aggregator) {
43821
44762
  const baseId = fieldName + (aggregator ? `:${aggregator}` : "");
@@ -43971,10 +44912,12 @@ stores.inject(MyMetaStore, storeInstance);
43971
44912
  measures;
43972
44913
  columns;
43973
44914
  rows;
44915
+ sortedColumn;
43974
44916
  constructor(definition, fields) {
43975
44917
  this.measures = definition.measures.map((measure) => createMeasure(fields, measure));
43976
44918
  this.columns = definition.columns.map((dimension) => createPivotDimension(fields, dimension));
43977
44919
  this.rows = definition.rows.map((dimension) => createPivotDimension(fields, dimension));
44920
+ this.sortedColumn = definition.sortedColumn;
43978
44921
  }
43979
44922
  getDimension(nameWithGranularity) {
43980
44923
  const dimension = this.columns.find((d) => d.nameWithGranularity === nameWithGranularity) ||
@@ -44128,6 +45071,7 @@ stores.inject(MyMetaStore, storeInstance);
44128
45071
  pivotCells = {};
44129
45072
  rowTree;
44130
45073
  colTree;
45074
+ isSorted = false;
44131
45075
  constructor(columns, rows, measures, fieldsType) {
44132
45076
  this.columns = columns.map((row) => {
44133
45077
  // offset in the pivot table
@@ -44301,6 +45245,7 @@ stores.inject(MyMetaStore, storeInstance);
44301
45245
  value,
44302
45246
  field: row.fields[rowDepth],
44303
45247
  children: [],
45248
+ type: this.fieldsType[fieldName] || "char",
44304
45249
  width: 0, // not used
44305
45250
  };
44306
45251
  treesAtDepth[depth].push(node);
@@ -44323,6 +45268,7 @@ stores.inject(MyMetaStore, storeInstance);
44323
45268
  field: leaf.fields[depth],
44324
45269
  children: [],
44325
45270
  width: leaf.width,
45271
+ type: this.fieldsType[fieldName] || "char",
44326
45272
  };
44327
45273
  if (treesAtDepth[depth]?.at(-1)?.value !== value) {
44328
45274
  treesAtDepth[depth + 1] = [];
@@ -44341,6 +45287,35 @@ stores.inject(MyMetaStore, storeInstance);
44341
45287
  fieldsType: this.fieldsType,
44342
45288
  };
44343
45289
  }
45290
+ sort(measure, sortedColumn, getValue) {
45291
+ if (this.isSorted) {
45292
+ return;
45293
+ }
45294
+ const getSortValue = (measure, domain) => {
45295
+ const rawValue = getValue(measure, domain).value;
45296
+ return typeof rawValue === "number" ? rawValue : -Infinity;
45297
+ };
45298
+ const sortColDomain = sortedColumn.domain;
45299
+ const sortFn = (rowDomain1, rowDomain2) => {
45300
+ const value1 = getSortValue(measure, [...rowDomain1, ...sortColDomain]);
45301
+ const value2 = getSortValue(measure, [...rowDomain2, ...sortColDomain]);
45302
+ return sortedColumn.order === "asc" ? value1 - value2 : value2 - value1;
45303
+ };
45304
+ const sortedRowTree = sortPivotTree(this.rowTree(), [], sortFn);
45305
+ this.rowTree = lazy(sortedRowTree);
45306
+ this.rows = [...this.rowTreeToRows(sortedRowTree), this.rows[this.rows.length - 1]];
45307
+ this.isSorted = true;
45308
+ }
45309
+ rowTreeToRows(tree, parentRow) {
45310
+ return tree.flatMap((node) => {
45311
+ const row = {
45312
+ indent: parentRow ? parentRow.indent + 1 : 0,
45313
+ fields: [...(parentRow?.fields || []), node.field],
45314
+ values: [...(parentRow?.values || []), node.value],
45315
+ };
45316
+ return [row, ...this.rowTreeToRows(node.children, row)];
45317
+ });
45318
+ }
44344
45319
  }
44345
45320
  const EMPTY_PIVOT_CELL = { type: "EMPTY" };
44346
45321
 
@@ -44418,6 +45393,7 @@ stores.inject(MyMetaStore, storeInstance);
44418
45393
  value: groups[key]?.[0]?.[column.nameWithGranularity]?.value ?? null,
44419
45394
  field: colName,
44420
45395
  children: dataEntriesToColumnsTree(groups[key] || [], columns, index + 1),
45396
+ type: column.type,
44421
45397
  width: 0,
44422
45398
  };
44423
45399
  });
@@ -45283,6 +46259,7 @@ stores.inject(MyMetaStore, storeInstance);
45283
46259
  format: measure.format,
45284
46260
  display: measure.display,
45285
46261
  })),
46262
+ sortedColumn: this.shouldKeepSortedColumn(definition) ? definition.sortedColumn : undefined,
45286
46263
  };
45287
46264
  if (!this.draft && deepEquals(coreDefinition, cleanedDefinition)) {
45288
46265
  return;
@@ -45341,6 +46318,19 @@ stores.inject(MyMetaStore, storeInstance);
45341
46318
  }
45342
46319
  return granularitiesPerFields;
45343
46320
  }
46321
+ /**
46322
+ * Check if we want to keep the sorted column when updating the pivot definition. We should remove it if either
46323
+ * the measure is not in the new definition or the columns have changed.
46324
+ */
46325
+ shouldKeepSortedColumn(newDefinition) {
46326
+ const { sortedColumn } = newDefinition;
46327
+ if (!sortedColumn) {
46328
+ return true;
46329
+ }
46330
+ const oldDefinition = this.getters.getPivotCoreDefinition(this.pivotId);
46331
+ return (newDefinition.measures.find((measure) => measure.id === sortedColumn.measure) &&
46332
+ deepEquals(oldDefinition.columns, newDefinition.columns));
46333
+ }
45344
46334
  }
45345
46335
 
45346
46336
  class PivotSpreadsheetSidePanel extends owl.Component {
@@ -46442,6 +47432,7 @@ stores.inject(MyMetaStore, storeInstance);
46442
47432
  }
46443
47433
  }
46444
47434
  `;
47435
+ const DEFAULT_TABLE_STYLE_COLOR = "#3C78D8";
46445
47436
  class TableStyleEditorPanel extends owl.Component {
46446
47437
  static template = "o-spreadsheet-TableStyleEditorPanel";
46447
47438
  static components = { Section, RoundColorPicker, TableStylePreview };
@@ -46460,7 +47451,7 @@ stores.inject(MyMetaStore, storeInstance);
46460
47451
  : null;
46461
47452
  return {
46462
47453
  pickerOpened: false,
46463
- primaryColor: editedStyle?.primaryColor || "#3C78D8",
47454
+ primaryColor: editedStyle?.primaryColor || DEFAULT_TABLE_STYLE_COLOR,
46464
47455
  selectedTemplateName: editedStyle?.templateName || "lightColoredText",
46465
47456
  styleName: editedStyle?.displayName || this.env.model.getters.getNewCustomTableStyleName(),
46466
47457
  };
@@ -46469,7 +47460,7 @@ stores.inject(MyMetaStore, storeInstance);
46469
47460
  this.state.pickerOpened = !this.state.pickerOpened;
46470
47461
  }
46471
47462
  onColorPicked(color) {
46472
- this.state.primaryColor = color;
47463
+ this.state.primaryColor = isColorValid(color) ? color : DEFAULT_TABLE_STYLE_COLOR;
46473
47464
  this.state.pickerOpened = false;
46474
47465
  }
46475
47466
  onTemplatePicked(templateName) {
@@ -48085,6 +49076,7 @@ stores.inject(MyMetaStore, storeInstance);
48085
49076
  draggedFigure: undefined,
48086
49077
  horizontalSnap: undefined,
48087
49078
  verticalSnap: undefined,
49079
+ cancelDnd: undefined,
48088
49080
  });
48089
49081
  setup() {
48090
49082
  owl.onMounted(() => {
@@ -48097,12 +49089,28 @@ stores.inject(MyMetaStore, storeInstance);
48097
49089
  // new rendering
48098
49090
  this.render();
48099
49091
  });
49092
+ owl.onWillUpdateProps(() => {
49093
+ const sheetId = this.env.model.getters.getActiveSheetId();
49094
+ const draggedFigureId = this.dnd.draggedFigure?.id;
49095
+ if (draggedFigureId && !this.env.model.getters.getFigure(sheetId, draggedFigureId)) {
49096
+ if (this.dnd.cancelDnd) {
49097
+ this.dnd.cancelDnd();
49098
+ }
49099
+ this.dnd.draggedFigure = undefined;
49100
+ this.dnd.horizontalSnap = undefined;
49101
+ this.dnd.verticalSnap = undefined;
49102
+ this.dnd.cancelDnd = undefined;
49103
+ }
49104
+ });
48100
49105
  }
48101
49106
  getVisibleFigures() {
48102
49107
  const visibleFigures = this.env.model.getters.getVisibleFigures();
48103
49108
  if (this.dnd.draggedFigure &&
48104
49109
  !visibleFigures.some((figure) => figure.id === this.dnd.draggedFigure?.id)) {
48105
- visibleFigures.push(this.env.model.getters.getFigure(this.env.model.getters.getActiveSheetId(), this.dnd.draggedFigure?.id));
49110
+ const draggedFigure = this.env.model.getters.getFigure(this.env.model.getters.getActiveSheetId(), this.dnd.draggedFigure?.id);
49111
+ if (draggedFigure) {
49112
+ visibleFigures.push(draggedFigure);
49113
+ }
48106
49114
  }
48107
49115
  return visibleFigures;
48108
49116
  }
@@ -48221,7 +49229,7 @@ stores.inject(MyMetaStore, storeInstance);
48221
49229
  this.dnd.verticalSnap = undefined;
48222
49230
  this.env.model.dispatch("UPDATE_FIGURE", { sheetId, id: figure.id, x, y });
48223
49231
  };
48224
- startDnd(onMouseMove, onMouseUp);
49232
+ this.dnd.cancelDnd = startDnd(onMouseMove, onMouseUp);
48225
49233
  }
48226
49234
  /**
48227
49235
  * Initialize the resize of a figure with mouse movements
@@ -48269,7 +49277,7 @@ stores.inject(MyMetaStore, storeInstance);
48269
49277
  this.dnd.horizontalSnap = undefined;
48270
49278
  this.dnd.verticalSnap = undefined;
48271
49279
  };
48272
- startDnd(onMouseMove, onMouseUp);
49280
+ this.dnd.cancelDnd = startDnd(onMouseMove, onMouseUp);
48273
49281
  }
48274
49282
  getOtherFigures(figId) {
48275
49283
  return this.getVisibleFigures().filter((f) => f.id !== figId);
@@ -49376,9 +50384,11 @@ stores.inject(MyMetaStore, storeInstance);
49376
50384
  class GridRenderer {
49377
50385
  getters;
49378
50386
  renderer;
50387
+ fingerprints;
49379
50388
  constructor(get) {
49380
50389
  this.getters = get(ModelStore).getters;
49381
50390
  this.renderer = get(RendererStore);
50391
+ this.fingerprints = get(FormulaFingerprintStore);
49382
50392
  this.renderer.register(this);
49383
50393
  }
49384
50394
  get renderingLayers() {
@@ -49856,14 +50866,22 @@ stores.inject(MyMetaStore, storeInstance);
49856
50866
  const showFormula = this.getters.shouldShowFormulas();
49857
50867
  const { x, y, width, height } = this.getters.getVisibleRect(zone);
49858
50868
  const { verticalAlign } = this.getters.getCellStyle(position);
50869
+ let style = this.getters.getCellComputedStyle(position);
50870
+ if (this.fingerprints.isEnabled) {
50871
+ const fingerprintColor = this.fingerprints.colors.get(position);
50872
+ style = { ...style, fillColor: fingerprintColor };
50873
+ }
50874
+ const dataBarFill = this.fingerprints.isEnabled
50875
+ ? undefined
50876
+ : this.getters.getConditionalDataBar(position);
49859
50877
  const box = {
49860
50878
  x,
49861
50879
  y,
49862
50880
  width,
49863
50881
  height,
49864
50882
  border: this.getters.getCellComputedBorder(position) || undefined,
49865
- style: this.getters.getCellComputedStyle(position),
49866
- dataBarFill: this.getters.getConditionalDataBar(position),
50883
+ style,
50884
+ dataBarFill,
49867
50885
  verticalAlign,
49868
50886
  isError: (cell.type === CellValueType.error && !!cell.message) ||
49869
50887
  this.getters.isDataValidationInvalid(position),
@@ -49888,7 +50906,6 @@ stores.inject(MyMetaStore, storeInstance);
49888
50906
  box.hasIcon = this.getters.doesCellHaveGridIcon(position);
49889
50907
  const headerIconWidth = box.hasIcon ? GRID_ICON_EDGE_LENGTH + GRID_ICON_MARGIN : 0;
49890
50908
  /** Content */
49891
- const style = this.getters.getCellComputedStyle(position);
49892
50909
  const wrapping = style.wrapping || "overflow";
49893
50910
  const wrapText = wrapping === "wrap" && !showFormula;
49894
50911
  const maxWidth = width - 2 * MIN_CELL_TEXT_MARGIN;
@@ -51866,62 +52883,6 @@ stores.inject(MyMetaStore, storeInstance);
51866
52883
  }
51867
52884
  }
51868
52885
 
51869
- class PositionMap {
51870
- map = {};
51871
- constructor(entries = []) {
51872
- for (const [position, value] of entries) {
51873
- this.set(position, value);
51874
- }
51875
- }
51876
- set({ sheetId, col, row }, value) {
51877
- const map = this.map;
51878
- if (!map[sheetId]) {
51879
- map[sheetId] = {};
51880
- }
51881
- if (!map[sheetId][col]) {
51882
- map[sheetId][col] = {};
51883
- }
51884
- map[sheetId][col][row] = value;
51885
- }
51886
- get({ sheetId, col, row }) {
51887
- return this.map[sheetId]?.[col]?.[row];
51888
- }
51889
- getSheet(sheetId) {
51890
- return this.map[sheetId];
51891
- }
51892
- has({ sheetId, col, row }) {
51893
- return this.map[sheetId]?.[col]?.[row] !== undefined;
51894
- }
51895
- delete({ sheetId, col, row }) {
51896
- delete this.map[sheetId]?.[col]?.[row];
51897
- }
51898
- keys() {
51899
- const map = this.map;
51900
- const keys = [];
51901
- for (const sheetId in map) {
51902
- for (const col in map[sheetId]) {
51903
- for (const row in map[sheetId][col]) {
51904
- keys.push({ sheetId, col: parseInt(col), row: parseInt(row) });
51905
- }
51906
- }
51907
- }
51908
- return keys;
51909
- }
51910
- keysForSheet(sheetId) {
51911
- const map = this.map[sheetId];
51912
- if (!map) {
51913
- return [];
51914
- }
51915
- const keys = [];
51916
- for (const col in map) {
51917
- for (const row in map[col]) {
51918
- keys.push({ sheetId, col: parseInt(col), row: parseInt(row) });
51919
- }
51920
- }
51921
- return keys;
51922
- }
51923
- }
51924
-
51925
52886
  /**
51926
52887
  * Core Plugin
51927
52888
  *
@@ -52360,7 +53321,7 @@ stores.inject(MyMetaStore, storeInstance);
52360
53321
  const before = this.getters.getCell({ sheetId, col, row });
52361
53322
  const hasContent = "content" in after || "formula" in after;
52362
53323
  // Compute the new cell properties
52363
- const afterContent = hasContent ? replaceSpecialSpaces(after?.content) : before?.content || "";
53324
+ const afterContent = hasContent ? replaceNewLines(after?.content) : before?.content || "";
52364
53325
  let style;
52365
53326
  if (after.style !== undefined) {
52366
53327
  style = after.style || undefined;
@@ -55305,6 +56266,9 @@ stores.inject(MyMetaStore, storeInstance);
55305
56266
  };
55306
56267
  }
55307
56268
  getUnboundedZone(sheetId, zone) {
56269
+ if (zone.bottom === undefined || zone.right === undefined) {
56270
+ return zone;
56271
+ }
55308
56272
  const isFullRow = zone.left === 0 && zone.right === this.getNumberCols(sheetId) - 1;
55309
56273
  const isFullCol = zone.top === 0 && zone.bottom === this.getNumberRows(sheetId) - 1;
55310
56274
  return {
@@ -59588,66 +60552,25 @@ stores.inject(MyMetaStore, storeInstance);
59588
60552
  return;
59589
60553
  }
59590
60554
  const zone = this.getters.getRangeFromSheetXC(sheetId, range).zone;
59591
- const colorCellArgs = [];
60555
+ const colorThresholds = [{ value: minValue, color: rule.minimum.color }];
59592
60556
  if (rule.midpoint && midValue) {
59593
- colorCellArgs.push({
59594
- minValue,
59595
- minColor: rule.minimum.color,
59596
- colorDiffUnit: this.computeColorDiffUnits(minValue, midValue, rule.minimum.color, rule.midpoint.color),
59597
- });
59598
- colorCellArgs.push({
59599
- minValue: midValue,
59600
- minColor: rule.midpoint.color,
59601
- colorDiffUnit: this.computeColorDiffUnits(midValue, maxValue, rule.midpoint.color, rule.maximum.color),
59602
- });
59603
- }
59604
- else {
59605
- colorCellArgs.push({
59606
- minValue,
59607
- minColor: rule.minimum.color,
59608
- colorDiffUnit: this.computeColorDiffUnits(minValue, maxValue, rule.minimum.color, rule.maximum.color),
59609
- });
60557
+ colorThresholds.push({ value: midValue, color: rule.midpoint.color });
59610
60558
  }
60559
+ colorThresholds.push({ value: maxValue, color: rule.maximum.color });
60560
+ const colorScale = getColorScale(colorThresholds);
59611
60561
  for (let row = zone.top; row <= zone.bottom; row++) {
59612
60562
  for (let col = zone.left; col <= zone.right; col++) {
59613
60563
  const cell = this.getters.getEvaluatedCell({ sheetId, col, row });
59614
60564
  if (cell.type === CellValueType.number) {
59615
60565
  const value = clip(cell.value, minValue, maxValue);
59616
- let color;
59617
- if (colorCellArgs.length === 2 && midValue) {
59618
- color =
59619
- value <= midValue
59620
- ? this.colorCell(value, colorCellArgs[0].minValue, colorCellArgs[0].minColor, colorCellArgs[0].colorDiffUnit)
59621
- : this.colorCell(value, colorCellArgs[1].minValue, colorCellArgs[1].minColor, colorCellArgs[1].colorDiffUnit);
59622
- }
59623
- else {
59624
- color = this.colorCell(value, colorCellArgs[0].minValue, colorCellArgs[0].minColor, colorCellArgs[0].colorDiffUnit);
59625
- }
59626
60566
  if (!computedStyle[col])
59627
60567
  computedStyle[col] = [];
59628
60568
  computedStyle[col][row] = computedStyle[col]?.[row] || {};
59629
- computedStyle[col][row].fillColor = colorNumberString(color);
60569
+ computedStyle[col][row].fillColor = colorScale(value);
59630
60570
  }
59631
60571
  }
59632
60572
  }
59633
60573
  }
59634
- computeColorDiffUnits(minValue, maxValue, minColor, maxColor) {
59635
- const deltaValue = maxValue - minValue;
59636
- const deltaColorR = ((minColor >> 16) % 256) - ((maxColor >> 16) % 256);
59637
- const deltaColorG = ((minColor >> 8) % 256) - ((maxColor >> 8) % 256);
59638
- const deltaColorB = (minColor % 256) - (maxColor % 256);
59639
- const colorDiffUnitR = deltaColorR / deltaValue;
59640
- const colorDiffUnitG = deltaColorG / deltaValue;
59641
- const colorDiffUnitB = deltaColorB / deltaValue;
59642
- return [colorDiffUnitR, colorDiffUnitG, colorDiffUnitB];
59643
- }
59644
- colorCell(value, minValue, minColor, colorDiffUnit) {
59645
- const [colorDiffUnitR, colorDiffUnitG, colorDiffUnitB] = colorDiffUnit;
59646
- const r = Math.round(((minColor >> 16) % 256) - colorDiffUnitR * (value - minValue));
59647
- const g = Math.round(((minColor >> 8) % 256) - colorDiffUnitG * (value - minValue));
59648
- const b = Math.round((minColor % 256) - colorDiffUnitB * (value - minValue));
59649
- return (r << 16) | (g << 8) | b;
59650
- }
59651
60574
  /**
59652
60575
  * Execute the predicate to know if a conditional formatting rule should be applied to a cell
59653
60576
  */
@@ -60273,7 +61196,7 @@ stores.inject(MyMetaStore, storeInstance);
60273
61196
  }
60274
61197
  getValuesToAggregate(measure, domain) {
60275
61198
  const { rowDomain, colDomain } = domainToColRowDomain(this, domain);
60276
- const table = this.getTableStructure();
61199
+ const table = super.getTableStructure();
60277
61200
  const values = [];
60278
61201
  if (colDomain.length === 0 &&
60279
61202
  rowDomain.length < this.definition.rows.length &&
@@ -60707,6 +61630,21 @@ stores.inject(MyMetaStore, storeInstance);
60707
61630
  }
60708
61631
  throw new Error(`Value ${result.value} is not a number`);
60709
61632
  }
61633
+ getTableStructure() {
61634
+ const table = super.getTableStructure();
61635
+ this.sortTableStructure(table);
61636
+ return table;
61637
+ }
61638
+ sortTableStructure(table) {
61639
+ if (!this.definition.sortedColumn || table.isSorted) {
61640
+ return;
61641
+ }
61642
+ const measure = this.definition.sortedColumn.measure;
61643
+ const isSortValid = isSortedColumnValid(this.definition.sortedColumn, this);
61644
+ if (isSortValid) {
61645
+ table.sort(measure, this.definition.sortedColumn, (measure, domain) => this._getPivotCellValueAndFormat(measure, domain));
61646
+ }
61647
+ }
60710
61648
  }
60711
61649
  return PivotPresentationLayer;
60712
61650
  }
@@ -62482,23 +63420,6 @@ stores.inject(MyMetaStore, storeInstance);
62482
63420
  }
62483
63421
  }
62484
63422
 
62485
- function randomChoice(arr) {
62486
- return arr[Math.floor(Math.random() * arr.length)];
62487
- }
62488
- const colors = [
62489
- "#ff851b",
62490
- "#0074d9",
62491
- "#7fdbff",
62492
- "#b10dc9",
62493
- "#39cccc",
62494
- "#f012be",
62495
- "#3d9970",
62496
- "#111111",
62497
- "#ff4136",
62498
- "#aaaaaa",
62499
- "#85144b",
62500
- "#001f3f",
62501
- ];
62502
63423
  class CollaborativePlugin extends UIPlugin {
62503
63424
  static getters = [
62504
63425
  "getClientsToDisplay",
@@ -62507,7 +63428,7 @@ stores.inject(MyMetaStore, storeInstance);
62507
63428
  "isFullySynchronized",
62508
63429
  ];
62509
63430
  static layers = ["Selection"];
62510
- availableColors = new Set(colors);
63431
+ availableColors = new AlternatingColorGenerator(12);
62511
63432
  colors = {};
62512
63433
  session;
62513
63434
  constructor(config) {
@@ -62518,14 +63439,6 @@ stores.inject(MyMetaStore, storeInstance);
62518
63439
  return (position.row < this.getters.getNumberRows(position.sheetId) &&
62519
63440
  position.col < this.getters.getNumberCols(position.sheetId));
62520
63441
  }
62521
- chooseNewColor() {
62522
- if (this.availableColors.size === 0) {
62523
- this.availableColors = new Set(colors);
62524
- }
62525
- const color = randomChoice([...this.availableColors.values()]);
62526
- this.availableColors.delete(color);
62527
- return color;
62528
- }
62529
63442
  getClient() {
62530
63443
  return this.session.getClient();
62531
63444
  }
@@ -62560,7 +63473,7 @@ stores.inject(MyMetaStore, storeInstance);
62560
63473
  this.isPositionValid(client.position)) {
62561
63474
  const position = client.position;
62562
63475
  if (!this.colors[client.id]) {
62563
- this.colors[client.id] = this.chooseNewColor();
63476
+ this.colors[client.id] = this.availableColors.next();
62564
63477
  }
62565
63478
  const color = this.colors[client.id];
62566
63479
  clients.push({ ...client, position, color });
@@ -62797,7 +63710,7 @@ stores.inject(MyMetaStore, storeInstance);
62797
63710
  for (let row = zone.top; row <= zone.bottom; row++) {
62798
63711
  const position = { sheetId, col, row };
62799
63712
  const pivotCell = this.getters.getPivotCellFromPosition(position);
62800
- if (pivotCell.type === "VALUE") {
63713
+ if (this.isSpilledPivotValueFormula(position, pivotCell)) {
62801
63714
  measurePositions.push(position);
62802
63715
  const pivotId = this.getters.getPivotIdFromPosition(position) || "";
62803
63716
  measuresByPivotId[pivotId] ??= new Set();
@@ -62834,6 +63747,10 @@ stores.inject(MyMetaStore, storeInstance);
62834
63747
  format,
62835
63748
  });
62836
63749
  }
63750
+ isSpilledPivotValueFormula(position, pivotCell) {
63751
+ const cell = this.getters.getCell(position);
63752
+ return pivotCell.type === "VALUE" && !cell?.isFormula;
63753
+ }
62837
63754
  /**
62838
63755
  * This function allows to adjust the quantity of decimal places after a decimal
62839
63756
  * point on cells containing number value. It does this by changing the cells
@@ -62884,6 +63801,69 @@ stores.inject(MyMetaStore, storeInstance);
62884
63801
  }
62885
63802
  }
62886
63803
 
63804
+ class GeoFeaturePlugin extends UIPlugin {
63805
+ static getters = [
63806
+ "getGeoJsonFeatures",
63807
+ "geoFeatureNameToId",
63808
+ "getGeoChartAvailableRegions",
63809
+ ];
63810
+ geoJsonService;
63811
+ geoJsonCache = {};
63812
+ constructor(config) {
63813
+ super(config);
63814
+ this.geoJsonService = config.external.geoJsonService;
63815
+ }
63816
+ getGeoChartAvailableRegions() {
63817
+ if (!this.geoJsonService) {
63818
+ console.error("No geoJsonService provided to the model");
63819
+ return [];
63820
+ }
63821
+ return this.geoJsonService.getAvailableRegions() || [];
63822
+ }
63823
+ getGeoJsonFeatures(region) {
63824
+ if (!this.geoJsonService) {
63825
+ console.error("No geoJsonService provided to the model");
63826
+ return;
63827
+ }
63828
+ const cachedGeoJson = this.geoJsonCache[region];
63829
+ if (cachedGeoJson instanceof Promise) {
63830
+ return undefined;
63831
+ }
63832
+ if (cachedGeoJson !== undefined) {
63833
+ return cachedGeoJson ?? undefined;
63834
+ }
63835
+ this.geoJsonCache[region] = new Promise(async (resolve) => {
63836
+ const json = await this.geoJsonService?.getTopoJson(region);
63837
+ this.geoJsonCache[region] = this.convertToGeoJson(json);
63838
+ this.dispatch("EVALUATE_CHARTS");
63839
+ resolve();
63840
+ });
63841
+ return undefined;
63842
+ }
63843
+ geoFeatureNameToId(region, featureName) {
63844
+ if (!this.geoJsonService) {
63845
+ console.error("No geoJsonService provided to the model");
63846
+ return;
63847
+ }
63848
+ return this.geoJsonService.geoFeatureNameToId(region, featureName);
63849
+ }
63850
+ convertToGeoJson(json) {
63851
+ if (!json) {
63852
+ return null;
63853
+ }
63854
+ // TopoJSON
63855
+ if (json.type === "Topology") {
63856
+ const features = window.ChartGeo.topojson.feature(json, Object.values(json.objects)[0]);
63857
+ return features.type === "FeatureCollection" ? features.features : [features];
63858
+ }
63859
+ // GeoJSON
63860
+ else if (json.type === "FeatureCollection") {
63861
+ return json.features;
63862
+ }
63863
+ throw new Error("Invalid TopoJSON");
63864
+ }
63865
+ }
63866
+
62887
63867
  class HeaderVisibilityUIPlugin extends UIPlugin {
62888
63868
  static getters = [
62889
63869
  "getNextVisibleCellPosition",
@@ -64421,10 +65401,13 @@ stores.inject(MyMetaStore, storeInstance);
64421
65401
  case "RESIZE_TABLE": {
64422
65402
  const table = this.getters.getCoreTableMatchingTopLeft(cmd.sheetId, cmd.zone);
64423
65403
  this.dispatch("UPDATE_TABLE", { ...cmd });
64424
- if (!table || !table.config.automaticAutofill)
65404
+ if (!table)
64425
65405
  return;
64426
- const oldTableZone = table.range.zone;
64427
65406
  const newTableZone = this.getters.getRangeFromRangeData(cmd.newTableRange).zone;
65407
+ this.selection.selectCell(newTableZone.right, newTableZone.bottom);
65408
+ if (!table.config.automaticAutofill)
65409
+ return;
65410
+ const oldTableZone = table.range.zone;
64428
65411
  if (newTableZone.bottom >= oldTableZone.bottom) {
64429
65412
  for (let col = newTableZone.left; col <= newTableZone.right; col++) {
64430
65413
  const autofillSource = { col, row: oldTableZone.bottom, sheetId: cmd.sheetId };
@@ -65630,15 +66613,23 @@ stores.inject(MyMetaStore, storeInstance);
65630
66613
  handler.paste({ zones: pasteTarget, sheetId }, data, { isCutOperation: true });
65631
66614
  const toRemove = isBasedBefore ? cmd.elements.map((el) => el + thickness) : cmd.elements;
65632
66615
  let currentIndex = cmd.base;
66616
+ const resizingGroups = {};
65633
66617
  for (const element of toRemove) {
65634
66618
  const size = this.getters.getHeaderSize(cmd.sheetId, cmd.dimension, element);
66619
+ const currentSize = this.getters.getHeaderSize(cmd.sheetId, cmd.dimension, currentIndex);
66620
+ if (size != currentSize) {
66621
+ resizingGroups[size] ??= [];
66622
+ resizingGroups[size].push(currentIndex);
66623
+ currentIndex += 1;
66624
+ }
66625
+ }
66626
+ for (const size in resizingGroups) {
65635
66627
  this.dispatch("RESIZE_COLUMNS_ROWS", {
65636
66628
  dimension: cmd.dimension,
65637
66629
  sheetId: cmd.sheetId,
65638
- size,
65639
- elements: [currentIndex],
66630
+ size: parseInt(size, 10),
66631
+ elements: resizingGroups[size],
65640
66632
  });
65641
- currentIndex += 1;
65642
66633
  }
65643
66634
  this.dispatch("REMOVE_COLUMNS_ROWS", {
65644
66635
  dimension: cmd.dimension,
@@ -66220,6 +67211,7 @@ stores.inject(MyMetaStore, storeInstance);
66220
67211
  break;
66221
67212
  case "DELETE_SHEET":
66222
67213
  this.cleanViewports();
67214
+ this.sheetsWithDirtyViewports.delete(cmd.sheetId);
66223
67215
  break;
66224
67216
  case "ACTIVATE_SHEET":
66225
67217
  this.sheetsWithDirtyViewports.add(cmd.sheetIdTo);
@@ -66870,7 +67862,8 @@ stores.inject(MyMetaStore, storeInstance);
66870
67862
  .add("data_cleanup", DataCleanupPlugin)
66871
67863
  .add("table_autofill", TableAutofillPlugin)
66872
67864
  .add("table_ui_resize", TableResizeUI)
66873
- .add("datavalidation_insert", DataValidationInsertionPlugin);
67865
+ .add("datavalidation_insert", DataValidationInsertionPlugin)
67866
+ .add("geo_features", GeoFeaturePlugin);
66874
67867
  // Plugins which have a state, but which should not be shared in collaborative
66875
67868
  const statefulUIPluginRegistry = new Registry()
66876
67869
  .add("selection", GridSelectionPlugin)
@@ -68614,8 +69607,8 @@ stores.inject(MyMetaStore, storeInstance);
68614
69607
  .o-topbar-composer {
68615
69608
  height: fit-content;
68616
69609
  margin-top: -1px;
69610
+ margin-bottom: -1px;
68617
69611
  border: 1px solid;
68618
- z-index: ${ComponentsImportance.TopBarComposer};
68619
69612
  font-family: ${DEFAULT_FONT};
68620
69613
 
68621
69614
  .o-composer:empty:not(:focus):not(.active)::before {
@@ -68673,6 +69666,7 @@ stores.inject(MyMetaStore, storeInstance);
68673
69666
  }
68674
69667
  return cssPropertiesToCss({
68675
69668
  "border-color": SELECTION_BORDER_COLOR,
69669
+ "z-index": String(ComponentsImportance.TopBarComposer),
68676
69670
  });
68677
69671
  }
68678
69672
  onFocus(selection) {
@@ -68781,6 +69775,15 @@ stores.inject(MyMetaStore, storeInstance);
68781
69775
  }
68782
69776
  }
68783
69777
 
69778
+ .irregularity-map {
69779
+ border-top: 1px solid ${SEPARATOR_COLOR};
69780
+ height: ${TOPBAR_TOOLBAR_HEIGHT}px;
69781
+
69782
+ .alert-info {
69783
+ border-left: 3px solid ${ALERT_INFO_BORDER};
69784
+ }
69785
+ }
69786
+
68784
69787
  .o-topbar-composer {
68785
69788
  flex-grow: 1;
68786
69789
  }
@@ -68874,8 +69877,10 @@ stores.inject(MyMetaStore, storeInstance);
68874
69877
  formatNumberMenuItemSpec = formatNumberMenuItemSpec;
68875
69878
  isntToolbarMenu = false;
68876
69879
  composerFocusStore;
69880
+ fingerprints;
68877
69881
  setup() {
68878
69882
  this.composerFocusStore = useStore(ComposerFocusStore);
69883
+ this.fingerprints = useStore(FormulaFingerprintStore);
68879
69884
  owl.useExternalListener(window, "click", this.onExternalClick);
68880
69885
  owl.onWillStart(() => this.updateCellState());
68881
69886
  owl.onWillUpdateProps(() => this.updateCellState());
@@ -69346,7 +70351,7 @@ stores.inject(MyMetaStore, storeInstance);
69346
70351
  properties["grid-template-rows"] = `auto`;
69347
70352
  }
69348
70353
  else {
69349
- properties["grid-template-rows"] = `${TOPBAR_HEIGHT}px auto ${BOTTOMBAR_HEIGHT + 1}px`;
70354
+ properties["grid-template-rows"] = `max-content auto ${BOTTOMBAR_HEIGHT + 1}px`;
69350
70355
  }
69351
70356
  properties["grid-template-columns"] = `auto ${this.sidePanel.panelSize}px`;
69352
70357
  return cssPropertiesToCss(properties);
@@ -70424,9 +71429,6 @@ stores.inject(MyMetaStore, storeInstance);
70424
71429
  getBackToDefault() {
70425
71430
  this.stream.getBackToDefault();
70426
71431
  }
70427
- getAnchor() {
70428
- return this.anchor;
70429
- }
70430
71432
  modifyAnchor(anchor, mode, options) {
70431
71433
  const sheetId = this.getters.getActiveSheetId();
70432
71434
  anchor = {
@@ -73358,6 +74360,7 @@ stores.inject(MyMetaStore, storeInstance);
73358
74360
  session: this.session,
73359
74361
  defaultCurrency: this.config.defaultCurrency,
73360
74362
  customColors: this.config.customColors || [],
74363
+ external: this.config.external,
73361
74364
  };
73362
74365
  }
73363
74366
  // ---------------------------------------------------------------------------
@@ -73589,7 +74592,6 @@ stores.inject(MyMetaStore, storeInstance);
73589
74592
  MIN_COL_WIDTH,
73590
74593
  HEADER_HEIGHT,
73591
74594
  HEADER_WIDTH,
73592
- TOPBAR_HEIGHT,
73593
74595
  BOTTOMBAR_HEIGHT,
73594
74596
  DEFAULT_CELL_WIDTH,
73595
74597
  DEFAULT_CELL_HEIGHT,
@@ -73830,9 +74832,9 @@ stores.inject(MyMetaStore, storeInstance);
73830
74832
  exports.tokenize = tokenize;
73831
74833
 
73832
74834
 
73833
- __info__.version = "18.1.0-alpha.7";
73834
- __info__.date = "2024-12-05T10:40:26.512Z";
73835
- __info__.hash = "7b1c39b";
74835
+ __info__.version = "18.1.0";
74836
+ __info__.date = "2024-12-26T06:37:07.879Z";
74837
+ __info__.hash = "c520e89";
73836
74838
 
73837
74839
 
73838
74840
  })(this.o_spreadsheet = this.o_spreadsheet || {}, owl);