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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,9 +2,9 @@
2
2
  /**
3
3
  * This file is generated by o-spreadsheet build tools. Do not edit it.
4
4
  * @see https://github.com/odoo/o-spreadsheet
5
- * @version 18.1.0-alpha.7
6
- * @date 2024-12-05T10:40:26.512Z
7
- * @hash 7b1c39b
5
+ * @version 18.1.0-alpha.8
6
+ * @date 2024-12-19T07:49:54.421Z
7
+ * @hash 87a1567
8
8
  */
9
9
 
10
10
  'use strict';
@@ -267,7 +267,6 @@ const MIN_ROW_HEIGHT = 10;
267
267
  const MIN_COL_WIDTH = 5;
268
268
  const HEADER_HEIGHT = 26;
269
269
  const HEADER_WIDTH = 48;
270
- const TOPBAR_HEIGHT = 63;
271
270
  const TOPBAR_TOOLBAR_HEIGHT = 34;
272
271
  const BOTTOMBAR_HEIGHT = 36;
273
272
  const DEFAULT_CELL_WIDTH = 96;
@@ -791,6 +790,7 @@ function removeFalsyAttributes(obj) {
791
790
  * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Character_Classes
792
791
  */
793
792
  const whiteSpaceSpecialCharacters = [
793
+ " ",
794
794
  "\t",
795
795
  "\f",
796
796
  "\v",
@@ -805,17 +805,15 @@ const whiteSpaceSpecialCharacters = [
805
805
  String.fromCharCode(parseInt("3000", 16)),
806
806
  String.fromCharCode(parseInt("feff", 16)),
807
807
  ];
808
- const whiteSpaceRegexp = new RegExp(whiteSpaceSpecialCharacters.join("|") + "|(\r\n|\r|\n)", "g");
808
+ const whiteSpaceRegexp = new RegExp(whiteSpaceSpecialCharacters.join("|"), "g");
809
+ const newLineRegexp = /(\r\n|\r)/g;
809
810
  /**
810
- * Replace all the special spaces in a string (non-breaking, tabs, ...) by normal spaces, and all the
811
- * different newlines types by \n.
811
+ * Replace all different newlines characters by \n
812
812
  */
813
- function replaceSpecialSpaces(text) {
813
+ function replaceNewLines(text) {
814
814
  if (!text)
815
815
  return "";
816
- if (!whiteSpaceRegexp.test(text))
817
- return text;
818
- return text.replace(whiteSpaceRegexp, (match, newLine) => (newLine ? NEWLINE : " "));
816
+ return text.replace(newLineRegexp, NEWLINE);
819
817
  }
820
818
  /**
821
819
  * Determine if the numbers are consecutive.
@@ -981,7 +979,7 @@ function transpose2dPOJO(pojo) {
981
979
 
982
980
  const RBA_REGEX = /rgba?\(|\s+|\)/gi;
983
981
  const HEX_MATCH = /^#([A-F\d]{2}){3,4}$/;
984
- const colors$1 = [
982
+ const colors = [
985
983
  "#eb6d00",
986
984
  "#0074d9",
987
985
  "#ad8e00",
@@ -1004,6 +1002,12 @@ const colors$1 = [
1004
1002
  function colorNumberString(color) {
1005
1003
  return toHex(color.toString(16).padStart(6, "0"));
1006
1004
  }
1005
+ function colorToNumber(color) {
1006
+ if (typeof color === "number") {
1007
+ return color;
1008
+ }
1009
+ return Number.parseInt(toHex(color).slice(1), 16);
1010
+ }
1007
1011
  /**
1008
1012
  * Converts any CSS color value to a standardized hex6 value.
1009
1013
  * Accepts: hex3, hex6, hex8, rgb[1] and rgba[1].
@@ -1354,9 +1358,9 @@ const COLORS_XL = [
1354
1358
  "#056BD9", // Blue #3
1355
1359
  "#155193", // Blue #4
1356
1360
  "#A76DBC", // Violet #1
1357
- "#7F4295", // Violet #1
1358
- "#6D2387", // Violet #1
1359
- "#4F1565", // Violet #1
1361
+ "#7F4295", // Violet #2
1362
+ "#6D2387", // Violet #3
1363
+ "#4F1565", // Violet #4
1360
1364
  "#EA6175", // Red #1
1361
1365
  "#CE4257", // Red #2
1362
1366
  "#982738", // Red #3
@@ -1382,6 +1386,81 @@ const COLORS_XL = [
1382
1386
  "#C08A16", // Yellow #3
1383
1387
  "#936A12", // Yellow #4
1384
1388
  ];
1389
+ // Same as above but with alternating colors
1390
+ const ALTERNATING_COLORS_MD = [
1391
+ "#4EA7F2", // Blue #1
1392
+ "#43C5B1", // Teal #1
1393
+ "#EA6175", // Red #1
1394
+ "#F4A261", // Orange #1
1395
+ "#8481DD", // Purple #1
1396
+ "#FFD86D", // Yellow #1
1397
+ "#3188E6", // Blue #2
1398
+ "#00A78D", // Teal #2
1399
+ "#CE4257", // Red #2
1400
+ "#F48935", // Orange #2
1401
+ "#5752D1", // Purple #2
1402
+ "#FFBC2C", // Yellow #2
1403
+ ];
1404
+ const ALTERNATING_COLORS_LG = [
1405
+ "#4EA7F2", // Blue #1
1406
+ "#A76DBC", // Violet #1
1407
+ "#EA6175", // Red #1
1408
+ "#43C5B1", // Teal #1
1409
+ "#F4A261", // Orange #1
1410
+ "#8481DD", // Purple #1
1411
+ "#A4A8B6", // Gray #1
1412
+ "#FFD86D", // Yellow #1
1413
+ "#3188E6", // Blue #2
1414
+ "#7F4295", // Violet #2
1415
+ "#CE4257", // Red #2
1416
+ "#00A78D", // Teal #2
1417
+ "#F48935", // Orange #2
1418
+ "#5752D1", // Purple #2
1419
+ "#7E8290", // Gray #2
1420
+ "#FFBC2C", // Yellow #2
1421
+ "#056BD9", // Blue #3
1422
+ "#6D2387", // Violet #3
1423
+ "#982738", // Red #3
1424
+ "#0E8270", // Teal #3
1425
+ "#BE5D10", // Orange #3
1426
+ "#3A3580", // Purple #3
1427
+ "#545B70", // Gray #3
1428
+ "#C08A16", // Yellow #3
1429
+ ];
1430
+ const ALTERNATING_COLORS_XL = [
1431
+ "#4EA7F2", // Blue #1
1432
+ "#A76DBC", // Violet #1
1433
+ "#EA6175", // Red #1
1434
+ "#43C5B1", // Teal #1
1435
+ "#F4A261", // Orange #1
1436
+ "#8481DD", // Purple #1
1437
+ "#A4A8B6", // Grey #1
1438
+ "#FFD86D", // Yellow #1
1439
+ "#3188E6", // Blue #2
1440
+ "#7F4295", // Violet #2
1441
+ "#CE4257", // Red #2
1442
+ "#00A78D", // Teal #2
1443
+ "#F48935", // Orange #2
1444
+ "#5752D1", // Purple #2
1445
+ "#7E8290", // Grey #2
1446
+ "#FFBC2C", // Yellow #2
1447
+ "#056BD9", // Blue #3
1448
+ "#6D2387", // Violet #3
1449
+ "#982738", // Red #3
1450
+ "#0E8270", // Teal #3
1451
+ "#BE5D10", // Orange #3
1452
+ "#3A3580", // Purple #3
1453
+ "#545B70", // Grey #3
1454
+ "#C08A16", // Yellow #3
1455
+ "#155193", // Blue #4
1456
+ "#4F1565", // Violet #4
1457
+ "#791B29", // Red #4
1458
+ "#105F53", // Teal #4
1459
+ "#7D380D", // Orange #4
1460
+ "#26235F", // Purple #4
1461
+ "#3F4250", // Grey #4
1462
+ "#936A12", // Yellow #4
1463
+ ];
1385
1464
  function getNthColor(index, palette) {
1386
1465
  return palette[index % palette.length];
1387
1466
  }
@@ -1399,6 +1478,20 @@ function getColorsPalette(quantity) {
1399
1478
  return COLORS_XL;
1400
1479
  }
1401
1480
  }
1481
+ function getAlternatingColorsPalette(quantity) {
1482
+ if (quantity <= 6) {
1483
+ return COLORS_SM;
1484
+ }
1485
+ else if (quantity <= 12) {
1486
+ return ALTERNATING_COLORS_MD;
1487
+ }
1488
+ else if (quantity <= 24) {
1489
+ return ALTERNATING_COLORS_LG;
1490
+ }
1491
+ else {
1492
+ return ALTERNATING_COLORS_XL;
1493
+ }
1494
+ }
1402
1495
  class ColorGenerator {
1403
1496
  preferredColors;
1404
1497
  currentColorIndex = 0;
@@ -1413,6 +1506,62 @@ class ColorGenerator {
1413
1506
  : getNthColor(this.currentColorIndex++, this.palette);
1414
1507
  }
1415
1508
  }
1509
+ class AlternatingColorGenerator extends ColorGenerator {
1510
+ constructor(paletteSize, preferredColors = []) {
1511
+ super(paletteSize, preferredColors);
1512
+ this.palette = getAlternatingColorsPalette(paletteSize).filter((c) => !preferredColors.includes(c));
1513
+ }
1514
+ }
1515
+ /**
1516
+ * Returns a function that maps a value to a color using a color scale defined by the given
1517
+ * color/threshold values pairs.
1518
+ */
1519
+ function getColorScale(colorScalePoints) {
1520
+ if (colorScalePoints.length < 2) {
1521
+ throw new Error("Color scale must have at least 2 points");
1522
+ }
1523
+ const sortedColorScalePoints = [...colorScalePoints.sort((a, b) => a.value - b.value)];
1524
+ const thresholds = [];
1525
+ for (let i = 1; i < sortedColorScalePoints.length; i++) {
1526
+ const minColor = colorToNumber(sortedColorScalePoints[i - 1].color);
1527
+ const maxColor = colorToNumber(sortedColorScalePoints[i].color);
1528
+ thresholds.push({
1529
+ min: sortedColorScalePoints[i - 1].value,
1530
+ max: sortedColorScalePoints[i].value,
1531
+ minColor,
1532
+ maxColor,
1533
+ colorDiff: computeColorDiffUnits(sortedColorScalePoints[i - 1].value, sortedColorScalePoints[i].value, minColor, maxColor),
1534
+ });
1535
+ }
1536
+ return (value) => {
1537
+ if (value < thresholds[0].min) {
1538
+ return colorNumberString(thresholds[0].minColor);
1539
+ }
1540
+ for (const threshold of thresholds) {
1541
+ if (value >= threshold.min && value <= threshold.max) {
1542
+ return colorNumberString(colorCell(value, threshold.min, threshold.minColor, threshold.colorDiff));
1543
+ }
1544
+ }
1545
+ return colorNumberString(thresholds[thresholds.length - 1].maxColor);
1546
+ };
1547
+ }
1548
+ function computeColorDiffUnits(minValue, maxValue, minColor, maxColor) {
1549
+ const deltaValue = maxValue - minValue;
1550
+ const deltaColorR = ((minColor >> 16) % 256) - ((maxColor >> 16) % 256);
1551
+ const deltaColorG = ((minColor >> 8) % 256) - ((maxColor >> 8) % 256);
1552
+ const deltaColorB = (minColor % 256) - (maxColor % 256);
1553
+ const colorDiffUnitR = deltaColorR / deltaValue;
1554
+ const colorDiffUnitG = deltaColorG / deltaValue;
1555
+ const colorDiffUnitB = deltaColorB / deltaValue;
1556
+ return [colorDiffUnitR, colorDiffUnitG, colorDiffUnitB];
1557
+ }
1558
+ function colorCell(value, minValue, minColor, colorDiffUnit) {
1559
+ const [colorDiffUnitR, colorDiffUnitG, colorDiffUnitB] = colorDiffUnit;
1560
+ const r = Math.round(((minColor >> 16) % 256) - colorDiffUnitR * (value - minValue));
1561
+ const g = Math.round(((minColor >> 8) % 256) - colorDiffUnitG * (value - minValue));
1562
+ const b = Math.round((minColor % 256) - colorDiffUnitB * (value - minValue));
1563
+ return (r << 16) | (g << 8) | b;
1564
+ }
1416
1565
 
1417
1566
  //------------------------------------------------------------------------------
1418
1567
  // Coordinate
@@ -3284,6 +3433,7 @@ const invalidateEvaluationCommands = new Set([
3284
3433
  ]);
3285
3434
  const invalidateChartEvaluationCommands = new Set([
3286
3435
  "EVALUATE_CELLS",
3436
+ "EVALUATE_CHARTS",
3287
3437
  "UPDATE_CELL",
3288
3438
  "UNHIDE_COLUMNS_ROWS",
3289
3439
  "HIDE_COLUMNS_ROWS",
@@ -3320,6 +3470,7 @@ const readonlyAllowedCommands = new Set([
3320
3470
  "RESIZE_SHEETVIEW",
3321
3471
  "SET_VIEWPORT_OFFSET",
3322
3472
  "EVALUATE_CELLS",
3473
+ "EVALUATE_CHARTS",
3323
3474
  "SET_FORMULA_VISIBILITY",
3324
3475
  "UPDATE_FILTER",
3325
3476
  ]);
@@ -6524,7 +6675,7 @@ class BorderClipboardHandler extends AbstractCellClipboardHandler {
6524
6675
  const POSTFIX_UNARY_OPERATORS = ["%"];
6525
6676
  const OPERATORS = "+,-,*,/,:,=,<>,>=,>,<=,<,^,&".split(",").concat(POSTFIX_UNARY_OPERATORS);
6526
6677
  function tokenize(str, locale = DEFAULT_LOCALE) {
6527
- str = replaceSpecialSpaces(str);
6678
+ str = replaceNewLines(str);
6528
6679
  const chars = new TokenizingChars(str);
6529
6680
  const result = [];
6530
6681
  while (!chars.isOver()) {
@@ -6671,12 +6822,12 @@ function tokenizeSpace(chars) {
6671
6822
  if (length) {
6672
6823
  return { type: "SPACE", value: NEWLINE.repeat(length) };
6673
6824
  }
6674
- while (chars.current === " ") {
6675
- length++;
6676
- chars.shift();
6825
+ let spaces = "";
6826
+ while (chars.current && chars.current.match(whiteSpaceRegexp)) {
6827
+ spaces += chars.shift();
6677
6828
  }
6678
- if (length) {
6679
- return { type: "SPACE", value: " ".repeat(length) };
6829
+ if (spaces) {
6830
+ return { type: "SPACE", value: spaces };
6680
6831
  }
6681
6832
  return null;
6682
6833
  }
@@ -7535,6 +7686,16 @@ function getRunningTotalDomainKey(domain, fieldNameWithGranularity) {
7535
7686
  }
7536
7687
  return domainToString([...domain.slice(0, index), ...domain.slice(index + 1)]);
7537
7688
  }
7689
+ function sortPivotTree(tree, baseDomain, sortFn) {
7690
+ const sortedTree = [...tree];
7691
+ const domain = [...baseDomain];
7692
+ sortedTree.sort((node1, node2) => sortFn([...domain, node1], [...domain, node2]));
7693
+ for (const node of tree) {
7694
+ const children = sortPivotTree(node.children, [...domain, node], sortFn);
7695
+ node.children = children;
7696
+ }
7697
+ return sortedTree;
7698
+ }
7538
7699
 
7539
7700
  const pivotTimeAdapterRegistry = new Registry();
7540
7701
  function pivotTimeAdapter(granularity) {
@@ -8045,6 +8206,29 @@ function addIndentAndAlignToPivotHeader(pivot, domain, functionResult) {
8045
8206
  format: `${" ".repeat(indent)}${format}* `,
8046
8207
  };
8047
8208
  }
8209
+ function isSortedColumnValid(sortedColumn, pivot) {
8210
+ try {
8211
+ if (!pivot.getMeasure(sortedColumn.measure)) {
8212
+ return false;
8213
+ }
8214
+ const columns = pivot.definition.columns;
8215
+ for (let i = 0; i < sortedColumn.domain.length; i++) {
8216
+ if (columns[i].nameWithGranularity !== sortedColumn.domain[i].field) {
8217
+ return false;
8218
+ }
8219
+ const possibleValues = pivot
8220
+ .getPossibleFieldValues(columns[i])
8221
+ .map((v) => v.value);
8222
+ if (!possibleValues.includes(sortedColumn.domain[i].value)) {
8223
+ return false;
8224
+ }
8225
+ }
8226
+ return true;
8227
+ }
8228
+ catch (e) {
8229
+ return false;
8230
+ }
8231
+ }
8048
8232
 
8049
8233
  class CellClipboardHandler extends AbstractCellClipboardHandler {
8050
8234
  isCutAllowed(data) {
@@ -14653,7 +14837,7 @@ const SORT_TYPES = [
14653
14837
  CellValueType.boolean,
14654
14838
  ];
14655
14839
  function cellsSortingCriterion(sortingOrder) {
14656
- const inverse = sortingOrder === "ascending" ? 1 : -1;
14840
+ const inverse = sortingOrder === "asc" ? 1 : -1;
14657
14841
  return (left, right) => {
14658
14842
  if (left.type === CellValueType.empty) {
14659
14843
  return right.type === CellValueType.empty ? 0 : 1;
@@ -14745,7 +14929,7 @@ function sortMatrix(matrix, locale, ...criteria) {
14745
14929
  const sortColumns = [];
14746
14930
  const nRows = matrix.length;
14747
14931
  for (let i = 0; i < criteria.length; i += 2) {
14748
- sortingOrders.push(toBoolean(toScalar(criteria[i + 1])?.value) ? "ascending" : "descending");
14932
+ sortingOrders.push(toBoolean(toScalar(criteria[i + 1])?.value) ? "asc" : "desc");
14749
14933
  const sortColumn = criteria[i];
14750
14934
  if (isMatrix(sortColumn) && (sortColumn.length > 1 || sortColumn[0].length > 1)) {
14751
14935
  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));
@@ -14762,12 +14946,12 @@ function sortMatrix(matrix, locale, ...criteria) {
14762
14946
  if (sortColumns.length === 0) {
14763
14947
  for (let i = 0; i < matrix[0].length; i++) {
14764
14948
  sortColumns.push(matrix.map((row) => row[i].value));
14765
- sortingOrders.push("ascending");
14949
+ sortingOrders.push("asc");
14766
14950
  }
14767
14951
  }
14768
14952
  const sortingCriteria = {
14769
- descending: cellsSortingCriterion("descending"),
14770
- ascending: cellsSortingCriterion("ascending"),
14953
+ desc: cellsSortingCriterion("desc"),
14954
+ asc: cellsSortingCriterion("asc"),
14771
14955
  };
14772
14956
  const indexes = range(0, matrix.length);
14773
14957
  indexes.sort((a, b) => {
@@ -20491,20 +20675,8 @@ class AbstractComposerStore extends SpreadsheetStore {
20491
20675
  replaceSelectedRange(zone) {
20492
20676
  const ref = this.getZoneReference(zone);
20493
20677
  const currentToken = this.tokenAtCursor;
20494
- let replaceStart = this.selectionStart;
20495
- if (currentToken?.type === "REFERENCE") {
20496
- replaceStart = currentToken.start;
20497
- }
20498
- else if (currentToken?.type === "RIGHT_PAREN") {
20499
- // match left parenthesis
20500
- const leftParenthesisIndex = this.currentTokens.findIndex((token) => token.type === "LEFT_PAREN" && token.parenthesesCode === currentToken.parenthesesCode);
20501
- const functionToken = this.currentTokens[leftParenthesisIndex - 1];
20502
- if (functionToken === undefined) {
20503
- return;
20504
- }
20505
- replaceStart = functionToken.start;
20506
- }
20507
- this.replaceText(ref, replaceStart, this.selectionEnd);
20678
+ const start = currentToken?.type === "REFERENCE" ? currentToken.start : this.selectionStart;
20679
+ this.replaceText(ref, start, this.selectionEnd);
20508
20680
  }
20509
20681
  /**
20510
20682
  * Replace the reference of the old zone by the new one.
@@ -20537,17 +20709,6 @@ class AbstractComposerStore extends SpreadsheetStore {
20537
20709
  getZoneReference(zone) {
20538
20710
  const inputSheetId = this.sheetId;
20539
20711
  const sheetId = this.getters.getActiveSheetId();
20540
- if (zone.top === zone.bottom && zone.left === zone.right) {
20541
- const position = { sheetId, col: zone.left, row: zone.top };
20542
- const pivotId = this.getters.getPivotIdFromPosition(position);
20543
- const pivotCell = this.getters.getPivotCellFromPosition(position);
20544
- const cell = this.getters.getCell(position);
20545
- if (pivotId && pivotCell.type !== "EMPTY" && !cell?.isFormula) {
20546
- const formulaPivotId = this.getters.getPivotFormulaId(pivotId);
20547
- const formula = createPivotFormula(formulaPivotId, pivotCell);
20548
- return localizeFormula(formula, this.getters.getLocale()).slice(1); // strip leading =
20549
- }
20550
- }
20551
20712
  const range = this.getters.getRangeFromZone(sheetId, zone);
20552
20713
  return this.getters.getSelectionRangeString(range, inputSheetId);
20553
20714
  }
@@ -20718,37 +20879,21 @@ class AbstractComposerStore extends SpreadsheetStore {
20718
20879
  const editionSheetId = this.sheetId;
20719
20880
  const rangeColor = (rangeString) => {
20720
20881
  const colorIndex = this.colorIndexByRange[rangeString];
20721
- return colors$1[colorIndex % colors$1.length];
20882
+ return colors[colorIndex % colors.length];
20722
20883
  };
20723
- const highlights = [];
20724
- for (const range of this.getReferencedRanges()) {
20884
+ return this.getReferencedRanges().map((range) => {
20725
20885
  const rangeString = this.getters.getRangeString(range, editionSheetId);
20726
20886
  const { numberOfRows, numberOfCols } = zoneToDimension(range.zone);
20727
20887
  const zone = numberOfRows * numberOfCols === 1
20728
20888
  ? this.getters.expandZone(range.sheetId, range.zone)
20729
20889
  : range.zone;
20730
- highlights.push({
20890
+ return {
20731
20891
  zone,
20732
20892
  color: rangeColor(rangeString),
20733
20893
  sheetId: range.sheetId,
20734
20894
  interactive: true,
20735
- });
20736
- }
20737
- const activeSheetId = this.getters.getActiveSheetId();
20738
- const selectionZone = this.model.selection.getAnchor().zone;
20739
- const isSelectionHightlighted = highlights.find((highlight) => highlight.sheetId === activeSheetId && isEqual(highlight.zone, selectionZone));
20740
- if (this.editionMode === "selecting" && !isSelectionHightlighted) {
20741
- highlights.push({
20742
- zone: selectionZone,
20743
- color: "#445566",
20744
- sheetId: activeSheetId,
20745
- dashed: true,
20746
- interactive: false,
20747
- noFill: true,
20748
- thinLine: true,
20749
- });
20750
- }
20751
- return highlights;
20895
+ };
20896
+ });
20752
20897
  }
20753
20898
  /**
20754
20899
  * Return ranges currently referenced in the composer
@@ -20976,6 +21121,7 @@ function compileTokens(tokens) {
20976
21121
  return error;
20977
21122
  },
20978
21123
  isBadExpression: true,
21124
+ normalizedFormula: tokens.map((t) => t.value).join(""),
20979
21125
  };
20980
21126
  }
20981
21127
  }
@@ -21103,6 +21249,7 @@ function compileTokensOrThrow(tokens) {
21103
21249
  symbols,
21104
21250
  tokens,
21105
21251
  isBadExpression: false,
21252
+ normalizedFormula: cacheKey,
21106
21253
  };
21107
21254
  return compiledFormula;
21108
21255
  }
@@ -21367,6 +21514,7 @@ autoCompleteProviders.add("pivot_ids", {
21367
21514
  description: definition.name,
21368
21515
  htmlContent: [{ value: str, color: tokenColors.NUMBER }],
21369
21516
  fuzzySearchKey: str + definition.name,
21517
+ alwaysExpanded: true,
21370
21518
  };
21371
21519
  })
21372
21520
  .filter(isDefined);
@@ -27760,6 +27908,19 @@ const ChartTerms = {
27760
27908
  ["GaugeLowerInflectionPointNaN" /* CommandResult.GaugeLowerInflectionPointNaN */]: _t("The lower inflection point value must be a number"),
27761
27909
  ["GaugeUpperInflectionPointNaN" /* CommandResult.GaugeUpperInflectionPointNaN */]: _t("The upper inflection point value must be a number"),
27762
27910
  },
27911
+ GeoChart: {
27912
+ ColorScales: {
27913
+ blues: _t("Blues"),
27914
+ cividis: _t("Cividis"),
27915
+ greens: _t("Greens"),
27916
+ greys: _t("Greys"),
27917
+ oranges: _t("Oranges"),
27918
+ purples: _t("Purples"),
27919
+ rainbow: _t("Rainbow"),
27920
+ reds: _t("Reds"),
27921
+ viridis: _t("Viridis"),
27922
+ },
27923
+ },
27763
27924
  };
27764
27925
  const CustomCurrencyTerms = {
27765
27926
  Custom: _t("Custom"),
@@ -28163,6 +28324,26 @@ function getRadarChartData(definition, dataSets, labelRange, getters) {
28163
28324
  locale: getters.getLocale(),
28164
28325
  };
28165
28326
  }
28327
+ function getGeoChartData(definition, dataSets, labelRange, getters) {
28328
+ const labelValues = getChartLabelValues(getters, dataSets, labelRange);
28329
+ let labels = labelValues.formattedValues;
28330
+ if (definition.dataSetsHaveTitle) {
28331
+ labels.shift();
28332
+ }
28333
+ let dataSetsValues = getChartDatasetValues(getters, dataSets);
28334
+ ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));
28335
+ const format = getChartDatasetFormat(getters, dataSets, "left") ||
28336
+ getChartDatasetFormat(getters, dataSets, "right");
28337
+ return {
28338
+ dataSetsValues,
28339
+ axisFormats: { y: format },
28340
+ labels,
28341
+ locale: getters.getLocale(),
28342
+ availableRegions: getters.getGeoChartAvailableRegions(),
28343
+ geoFeatureNameToId: getters.geoFeatureNameToId,
28344
+ getGeoJsonFeatures: getters.getGeoJsonFeatures,
28345
+ };
28346
+ }
28166
28347
  function getTrendDatasetForBarChart(config, data) {
28167
28348
  const filteredValues = [];
28168
28349
  const filteredLabels = [];
@@ -28715,6 +28896,43 @@ function getRadarChartDatasets(definition, args) {
28715
28896
  }
28716
28897
  return datasets;
28717
28898
  }
28899
+ function getGeoChartDatasets(definition, args) {
28900
+ const { availableRegions, dataSetsValues, labels } = args;
28901
+ const regionName = definition.region || availableRegions[0]?.id;
28902
+ const features = regionName ? args.getGeoJsonFeatures(regionName) : undefined;
28903
+ const dataset = {
28904
+ outline: features,
28905
+ showOutline: !!features,
28906
+ data: [],
28907
+ };
28908
+ if (features && regionName) {
28909
+ const labelsAndValues = {};
28910
+ if (dataSetsValues[0]) {
28911
+ for (let i = 0; i < dataSetsValues[0].data.length; i++) {
28912
+ if (!labels[i] || dataSetsValues[0].data[i] === undefined) {
28913
+ continue;
28914
+ }
28915
+ const featureId = args.geoFeatureNameToId(regionName, labels[i]);
28916
+ if (featureId) {
28917
+ labelsAndValues[featureId] = { value: dataSetsValues[0].data[i], label: labels[i] };
28918
+ }
28919
+ }
28920
+ }
28921
+ for (const feature of features) {
28922
+ if (!feature.id) {
28923
+ continue;
28924
+ }
28925
+ dataset.data.push({
28926
+ feature: {
28927
+ ...feature,
28928
+ properties: { name: labelsAndValues[feature.id]?.label },
28929
+ },
28930
+ value: labelsAndValues[feature.id]?.value,
28931
+ });
28932
+ }
28933
+ }
28934
+ return [dataset];
28935
+ }
28718
28936
  function getTrendingLineDataSet(dataset, config, data) {
28719
28937
  const defaultBorderColor = colorToRGBA(dataset.backgroundColor);
28720
28938
  defaultBorderColor.a = 1;
@@ -29092,6 +29310,44 @@ function getRadarChartScales(definition, args) {
29092
29310
  },
29093
29311
  };
29094
29312
  }
29313
+ function getGeoChartScales(definition, args) {
29314
+ const { locale, axisFormats, availableRegions } = args;
29315
+ const geoLegendPosition = legendPositionToGeoLegendPosition(definition.legendPosition);
29316
+ const region = definition.region
29317
+ ? availableRegions.find((r) => r.id === definition.region)
29318
+ : availableRegions[0];
29319
+ const format = axisFormats?.y || axisFormats?.y1;
29320
+ return {
29321
+ projection: {
29322
+ // projection: region?.defaultProjection,
29323
+ projection: getGeoChartProjection(region?.defaultProjection || "mercator"),
29324
+ axis: "x",
29325
+ },
29326
+ color: {
29327
+ axis: "x",
29328
+ display: definition.legendPosition !== "none",
29329
+ border: { color: GRAY_300 },
29330
+ grid: { color: GRAY_300 },
29331
+ ticks: {
29332
+ color: chartFontColor(definition.background),
29333
+ callback: formatTickValue({ locale, format }),
29334
+ },
29335
+ legend: {
29336
+ position: geoLegendPosition,
29337
+ align: geoLegendPosition.includes("right") ? "left" : "right",
29338
+ margin: getLegendMargin(definition),
29339
+ },
29340
+ interpolate: getRuntimeColorScale(definition),
29341
+ missing: definition.missingValueColor || "#ffffff",
29342
+ },
29343
+ };
29344
+ }
29345
+ function getGeoChartProjection(projection) {
29346
+ if (projection === "conicConformal") {
29347
+ return window.ChartGeo.geoConicConformal().rotate([100, 0]); // Centered on the US
29348
+ }
29349
+ return projection;
29350
+ }
29095
29351
  function getChartAxisTitleRuntime(design) {
29096
29352
  if (design?.title?.text) {
29097
29353
  const { text, color, align, italic, bold } = design.title;
@@ -29155,6 +29411,44 @@ function getChartAxis(definition, position, type, options) {
29155
29411
  };
29156
29412
  }
29157
29413
  }
29414
+ function getRuntimeColorScale(definition) {
29415
+ if (!definition.colorScale || typeof definition.colorScale === "string") {
29416
+ return definition.colorScale || "oranges";
29417
+ }
29418
+ const scaleColors = [{ value: 0, color: definition.colorScale.minColor }];
29419
+ if (definition.colorScale.midColor) {
29420
+ scaleColors.push({ value: 0.5, color: definition.colorScale.midColor });
29421
+ }
29422
+ scaleColors.push({ value: 1, color: definition.colorScale.maxColor });
29423
+ return getColorScale(scaleColors);
29424
+ }
29425
+ function getLegendMargin(definition) {
29426
+ switch (definition.legendPosition) {
29427
+ case "top":
29428
+ case "right":
29429
+ const hasTitle = !!definition.title.text;
29430
+ const topMargin = hasTitle ? CHART_PADDING_TOP + 30 : CHART_PADDING_TOP;
29431
+ return { top: topMargin, left: CHART_PADDING$1, right: CHART_PADDING$1 };
29432
+ case "bottom":
29433
+ case "left":
29434
+ case "none":
29435
+ return { left: CHART_PADDING$1, right: CHART_PADDING$1, bottom: CHART_PADDING_BOTTOM };
29436
+ }
29437
+ }
29438
+ function legendPositionToGeoLegendPosition(position) {
29439
+ switch (position) {
29440
+ case "top":
29441
+ return "top-left";
29442
+ case "right":
29443
+ return "top-right";
29444
+ case "bottom":
29445
+ return "bottom-right";
29446
+ case "left":
29447
+ return "bottom-left";
29448
+ case "none":
29449
+ return "bottom-left";
29450
+ }
29451
+ }
29158
29452
 
29159
29453
  function getChartShowValues(definition, args) {
29160
29454
  const { axisFormats, locale } = args;
@@ -29314,6 +29608,25 @@ function getRadarChartTooltip(definition, args) {
29314
29608
  },
29315
29609
  };
29316
29610
  }
29611
+ function getGeoChartTooltip(definition, args) {
29612
+ const { locale, axisFormats } = args;
29613
+ const format = axisFormats?.y || axisFormats?.y1;
29614
+ return {
29615
+ filter: function (tooltipItem) {
29616
+ return tooltipItem.raw.value !== undefined;
29617
+ },
29618
+ callbacks: {
29619
+ label: function (tooltipItem) {
29620
+ const rawItem = tooltipItem.raw;
29621
+ const xLabel = rawItem.feature.properties.name;
29622
+ const yLabel = rawItem.value;
29623
+ const toolTipFormat = !format && Math.abs(yLabel) >= 1000 ? "#,##" : format;
29624
+ const yLabelStr = formatValue(yLabel, { format: toolTipFormat, locale });
29625
+ return xLabel ? `${xLabel}: ${yLabelStr}` : yLabelStr;
29626
+ },
29627
+ },
29628
+ };
29629
+ }
29317
29630
  function calculatePercentage(dataset, dataIndex) {
29318
29631
  const numericData = dataset.filter((value) => typeof value === "number");
29319
29632
  const total = numericData.reduce((sum, value) => sum + value, 0);
@@ -29340,6 +29653,10 @@ var CHART_RUNTIME_HELPERS = /*#__PURE__*/Object.freeze({
29340
29653
  getComboChartDatasets: getComboChartDatasets,
29341
29654
  getComboChartLegend: getComboChartLegend,
29342
29655
  getData: getData,
29656
+ getGeoChartData: getGeoChartData,
29657
+ getGeoChartDatasets: getGeoChartDatasets,
29658
+ getGeoChartScales: getGeoChartScales,
29659
+ getGeoChartTooltip: getGeoChartTooltip,
29343
29660
  getLineChartData: getLineChartData,
29344
29661
  getLineChartDatasets: getLineChartDatasets,
29345
29662
  getLineChartLegend: getLineChartLegend,
@@ -29913,6 +30230,133 @@ function getSectionThresholdValue(threshold, minValue, maxValue) {
29913
30230
  return clip(value, minValue, maxValue);
29914
30231
  }
29915
30232
 
30233
+ class GeoChart extends AbstractChart {
30234
+ dataSets;
30235
+ labelRange;
30236
+ background;
30237
+ legendPosition;
30238
+ type = "geo";
30239
+ dataSetsHaveTitle;
30240
+ dataSetDesign;
30241
+ colorScale;
30242
+ missingValueColor;
30243
+ region;
30244
+ constructor(definition, sheetId, getters) {
30245
+ super(definition, sheetId, getters);
30246
+ this.dataSets = createDataSets(getters, definition.dataSets, sheetId, definition.dataSetsHaveTitle);
30247
+ this.labelRange = createValidRange(getters, sheetId, definition.labelRange);
30248
+ this.background = definition.background;
30249
+ this.legendPosition = definition.legendPosition;
30250
+ this.dataSetsHaveTitle = definition.dataSetsHaveTitle;
30251
+ this.dataSetDesign = definition.dataSets;
30252
+ this.colorScale = definition.colorScale;
30253
+ this.missingValueColor = definition.missingValueColor;
30254
+ this.region = definition.region;
30255
+ }
30256
+ static transformDefinition(definition, executed) {
30257
+ return transformChartDefinitionWithDataSetsWithZone(definition, executed);
30258
+ }
30259
+ static validateChartDefinition(validator, definition) {
30260
+ return validator.checkValidations(definition, checkDataset, checkLabelRange);
30261
+ }
30262
+ static getDefinitionFromContextCreation(context) {
30263
+ return {
30264
+ background: context.background,
30265
+ dataSets: context.range ?? [],
30266
+ dataSetsHaveTitle: context.dataSetsHaveTitle ?? false,
30267
+ legendPosition: context.legendPosition ?? "top",
30268
+ title: context.title || { text: "" },
30269
+ type: "geo",
30270
+ labelRange: context.auxiliaryRange || undefined,
30271
+ aggregated: context.aggregated,
30272
+ };
30273
+ }
30274
+ getContextCreation() {
30275
+ const range = [];
30276
+ for (const [i, dataSet] of this.dataSets.entries()) {
30277
+ range.push({
30278
+ ...this.dataSetDesign?.[i],
30279
+ dataRange: this.getters.getRangeString(dataSet.dataRange, this.sheetId),
30280
+ });
30281
+ }
30282
+ return {
30283
+ ...this,
30284
+ range,
30285
+ auxiliaryRange: this.labelRange
30286
+ ? this.getters.getRangeString(this.labelRange, this.sheetId)
30287
+ : undefined,
30288
+ };
30289
+ }
30290
+ copyForSheetId(sheetId) {
30291
+ const dataSets = copyDataSetsWithNewSheetId(this.sheetId, sheetId, this.dataSets);
30292
+ const labelRange = copyLabelRangeWithNewSheetId(this.sheetId, sheetId, this.labelRange);
30293
+ const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange, sheetId);
30294
+ return new GeoChart(definition, sheetId, this.getters);
30295
+ }
30296
+ copyInSheetId(sheetId) {
30297
+ const definition = this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange, sheetId);
30298
+ return new GeoChart(definition, sheetId, this.getters);
30299
+ }
30300
+ getDefinition() {
30301
+ return this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange);
30302
+ }
30303
+ getDefinitionWithSpecificDataSets(dataSets, labelRange, targetSheetId) {
30304
+ const ranges = [];
30305
+ for (const [i, dataSet] of dataSets.entries()) {
30306
+ ranges.push({
30307
+ ...this.dataSetDesign?.[i],
30308
+ dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId),
30309
+ });
30310
+ }
30311
+ return {
30312
+ type: "geo",
30313
+ dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false,
30314
+ background: this.background,
30315
+ dataSets: ranges,
30316
+ legendPosition: this.legendPosition,
30317
+ labelRange: labelRange
30318
+ ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId)
30319
+ : undefined,
30320
+ title: this.title,
30321
+ colorScale: this.colorScale,
30322
+ missingValueColor: this.missingValueColor,
30323
+ region: this.region,
30324
+ };
30325
+ }
30326
+ getDefinitionForExcel() {
30327
+ return undefined;
30328
+ }
30329
+ updateRanges(applyChange) {
30330
+ const { dataSets, labelRange, isStale } = updateChartRangesWithDataSets(this.getters, applyChange, this.dataSets, this.labelRange);
30331
+ if (!isStale) {
30332
+ return this;
30333
+ }
30334
+ const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange);
30335
+ return new GeoChart(definition, this.sheetId, this.getters);
30336
+ }
30337
+ }
30338
+ function createGeoChartRuntime(chart, getters) {
30339
+ const definition = chart.getDefinition();
30340
+ const chartData = getGeoChartData(definition, chart.dataSets, chart.labelRange, getters);
30341
+ const config = {
30342
+ type: "choropleth",
30343
+ data: {
30344
+ datasets: getGeoChartDatasets(definition, chartData),
30345
+ },
30346
+ options: {
30347
+ ...CHART_COMMON_OPTIONS,
30348
+ layout: getChartLayout(),
30349
+ scales: getGeoChartScales(definition, chartData),
30350
+ plugins: {
30351
+ title: getChartTitle(definition),
30352
+ tooltip: getGeoChartTooltip(definition, chartData),
30353
+ legend: { display: false },
30354
+ },
30355
+ },
30356
+ };
30357
+ return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
30358
+ }
30359
+
29916
30360
  class LineChart extends AbstractChart {
29917
30361
  dataSets;
29918
30362
  labelRange;
@@ -30906,6 +31350,15 @@ chartRegistry.add("radar", {
30906
31350
  getChartDefinitionFromContextCreation: RadarChart.getDefinitionFromContextCreation,
30907
31351
  sequence: 80,
30908
31352
  });
31353
+ chartRegistry.add("geo", {
31354
+ match: (type) => type === "geo",
31355
+ createChart: (definition, sheetId, getters) => new GeoChart(definition, sheetId, getters),
31356
+ getChartRuntime: createGeoChartRuntime,
31357
+ validateChartDefinition: GeoChart.validateChartDefinition,
31358
+ transformDefinition: GeoChart.transformDefinition,
31359
+ getChartDefinitionFromContextCreation: GeoChart.getDefinitionFromContextCreation,
31360
+ sequence: 90,
31361
+ });
30909
31362
  const chartComponentRegistry = new Registry();
30910
31363
  chartComponentRegistry.add("line", ChartJsComponent);
30911
31364
  chartComponentRegistry.add("bar", ChartJsComponent);
@@ -30917,6 +31370,7 @@ chartComponentRegistry.add("scorecard", ScorecardChart);
30917
31370
  chartComponentRegistry.add("waterfall", ChartJsComponent);
30918
31371
  chartComponentRegistry.add("pyramid", ChartJsComponent);
30919
31372
  chartComponentRegistry.add("radar", ChartJsComponent);
31373
+ chartComponentRegistry.add("geo", ChartJsComponent);
30920
31374
  const chartCategories = {
30921
31375
  line: _t("Line"),
30922
31376
  column: _t("Column"),
@@ -31076,6 +31530,13 @@ chartSubtypeRegistry
31076
31530
  subtypeDefinition: { fillArea: true },
31077
31531
  category: "misc",
31078
31532
  preview: "o-spreadsheet-ChartPreview.FILLED_RADAR_CHART",
31533
+ })
31534
+ .add("geo", {
31535
+ displayName: _t("Geo Chart"),
31536
+ chartSubtype: "geo",
31537
+ chartType: "geo",
31538
+ category: "misc",
31539
+ preview: "o-spreadsheet-ChartPreview.GEO_CHART",
31079
31540
  });
31080
31541
 
31081
31542
  /**
@@ -31926,18 +32387,66 @@ const ErrorToolTipPopoverBuilder = {
31926
32387
  },
31927
32388
  };
31928
32389
 
31929
- css /*SCSS*/ `
31930
- .o-filter-menu-value {
31931
- padding: 4px;
31932
- line-height: 20px;
31933
- height: 28px;
31934
- .o-filter-menu-value-checked {
31935
- width: 20px;
32390
+ const CHECK_SVG = /*xml*/ `
32391
+ <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'>
32392
+ <path fill='none' stroke='#FFF' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/>
32393
+ </svg>
32394
+ `;
32395
+ const CHECKBOX_WIDTH = 14;
32396
+ css /* scss */ `
32397
+ label.o-checkbox {
32398
+ input {
32399
+ appearance: none;
32400
+ -webkit-appearance: none;
32401
+ -moz-appearance: none;
32402
+ border-radius: 0;
32403
+ width: ${CHECKBOX_WIDTH}px;
32404
+ height: ${CHECKBOX_WIDTH}px;
32405
+ vertical-align: top;
32406
+ box-sizing: border-box;
32407
+ outline: none;
32408
+ border: 1px solid ${GRAY_300};
32409
+ cursor: pointer;
32410
+
32411
+ &:hover {
32412
+ border-color: ${ACTION_COLOR};
32413
+ }
32414
+
32415
+ &:checked {
32416
+ background: url("data:image/svg+xml,${encodeURIComponent(CHECK_SVG)}");
32417
+ background-color: ${ACTION_COLOR};
32418
+ border-color: ${ACTION_COLOR};
32419
+ }
32420
+
32421
+ &:focus {
32422
+ outline: none;
32423
+ box-shadow: 0 0 0 0.25rem rgba(113, 75, 103, 0.25);
32424
+ border-color: ${ACTION_COLOR};
32425
+ }
31936
32426
  }
31937
32427
  }
31938
32428
  `;
32429
+ class Checkbox extends owl.Component {
32430
+ static template = "o-spreadsheet.Checkbox";
32431
+ static props = {
32432
+ label: { type: String, optional: true },
32433
+ value: { type: Boolean, optional: true },
32434
+ className: { type: String, optional: true },
32435
+ name: { type: String, optional: true },
32436
+ title: { type: String, optional: true },
32437
+ disabled: { type: Boolean, optional: true },
32438
+ onChange: Function,
32439
+ };
32440
+ static defaultProps = { value: false };
32441
+ onChange(ev) {
32442
+ const value = ev.target.checked;
32443
+ this.props.onChange(value);
32444
+ }
32445
+ }
32446
+
31939
32447
  class FilterMenuValueItem extends owl.Component {
31940
32448
  static template = "o-spreadsheet-FilterMenuValueItem";
32449
+ static components = { Checkbox };
31941
32450
  static props = {
31942
32451
  value: String,
31943
32452
  isChecked: Boolean,
@@ -31975,8 +32484,6 @@ const CSS = css /* scss */ `
31975
32484
  .o-filter-menu-item {
31976
32485
  display: flex;
31977
32486
  box-sizing: border-box;
31978
- height: ${MENU_ITEM_HEIGHT}px;
31979
- padding: 4px 4px 4px 0px;
31980
32487
  cursor: pointer;
31981
32488
  user-select: none;
31982
32489
 
@@ -31985,14 +32492,6 @@ const CSS = css /* scss */ `
31985
32492
  }
31986
32493
  }
31987
32494
 
31988
- input {
31989
- box-sizing: border-box;
31990
- margin-bottom: 5px;
31991
- border: 1px solid #949494;
31992
- height: 24px;
31993
- padding-right: 28px;
31994
- }
31995
-
31996
32495
  .o-search-icon {
31997
32496
  right: 5px;
31998
32497
  top: 3px;
@@ -32009,19 +32508,12 @@ const CSS = css /* scss */ `
32009
32508
  display: flex;
32010
32509
  flex-direction: row;
32011
32510
  margin-bottom: 4px;
32012
-
32013
- .o-filter-menu-action-text {
32014
- cursor: pointer;
32015
- margin-right: 10px;
32016
- color: blue;
32017
- text-decoration: underline;
32018
- }
32019
32511
  }
32020
32512
 
32021
32513
  .o-filter-menu-list {
32022
32514
  flex: auto;
32023
32515
  overflow-y: auto;
32024
- border: 1px solid #949494;
32516
+ border: 1px solid ${GRAY_300};
32025
32517
 
32026
32518
  .o-filter-menu-no-values {
32027
32519
  color: #949494;
@@ -34213,6 +34705,21 @@ const pivotProperties = {
34213
34705
  isReadonlyAllowed: true,
34214
34706
  icon: "o-spreadsheet-Icon.PIVOT",
34215
34707
  };
34708
+ const pivotSortingAsc = {
34709
+ name: _t("Ascending"),
34710
+ execute: (env) => sortPivot(env, "asc"),
34711
+ isActive: (env) => isPivotSortMenuItemActive(env, "asc"),
34712
+ };
34713
+ const pivotSortingDesc = {
34714
+ name: _t("Descending"),
34715
+ execute: (env) => sortPivot(env, "desc"),
34716
+ isActive: (env) => isPivotSortMenuItemActive(env, "desc"),
34717
+ };
34718
+ const noPivotSorting = {
34719
+ name: _t("No sorting"),
34720
+ execute: (env) => sortPivot(env, "none"),
34721
+ isActive: (env) => isPivotSortMenuItemActive(env, "none"),
34722
+ };
34216
34723
  const FIX_FORMULAS = {
34217
34724
  name: _t("Convert to individual formulas"),
34218
34725
  execute(env) {
@@ -34249,6 +34756,66 @@ const FIX_FORMULAS = {
34249
34756
  },
34250
34757
  icon: "o-spreadsheet-Icon.PIVOT",
34251
34758
  };
34759
+ function canSortPivot(env) {
34760
+ const position = env.model.getters.getActivePosition();
34761
+ const pivotId = env.model.getters.getPivotIdFromPosition(position);
34762
+ if (!pivotId ||
34763
+ !env.model.getters.isExistingPivot(pivotId) ||
34764
+ !env.model.getters.isSpillPivotFormula(position)) {
34765
+ return false;
34766
+ }
34767
+ const pivot = env.model.getters.getPivot(pivotId);
34768
+ if (!pivot.isValid()) {
34769
+ return false;
34770
+ }
34771
+ const pivotCell = env.model.getters.getPivotCellFromPosition(position);
34772
+ return pivotCell.type === "VALUE" || pivotCell.type === "MEASURE_HEADER";
34773
+ }
34774
+ function sortPivot(env, order) {
34775
+ const position = env.model.getters.getActivePosition();
34776
+ const pivotId = env.model.getters.getPivotIdFromPosition(position);
34777
+ const pivotCell = env.model.getters.getPivotCellFromPosition(position);
34778
+ if (pivotCell.type === "EMPTY" || pivotCell.type === "HEADER" || !pivotId) {
34779
+ return;
34780
+ }
34781
+ if (order === "none") {
34782
+ env.model.dispatch("UPDATE_PIVOT", {
34783
+ pivotId: pivotId,
34784
+ pivot: {
34785
+ ...env.model.getters.getPivotCoreDefinition(pivotId),
34786
+ sortedColumn: undefined,
34787
+ },
34788
+ });
34789
+ return;
34790
+ }
34791
+ const pivot = env.model.getters.getPivot(pivotId);
34792
+ const colDomain = domainToColRowDomain(pivot, pivotCell.domain).colDomain;
34793
+ env.model.dispatch("UPDATE_PIVOT", {
34794
+ pivotId: pivotId,
34795
+ pivot: {
34796
+ ...env.model.getters.getPivotCoreDefinition(pivotId),
34797
+ sortedColumn: { domain: colDomain, order, measure: pivotCell.measure },
34798
+ },
34799
+ });
34800
+ }
34801
+ function isPivotSortMenuItemActive(env, order) {
34802
+ const position = env.model.getters.getActivePosition();
34803
+ const pivotId = env.model.getters.getPivotIdFromPosition(position);
34804
+ const pivotCell = env.model.getters.getPivotCellFromPosition(position);
34805
+ if (pivotCell.type === "EMPTY" || pivotCell.type === "HEADER" || !pivotId) {
34806
+ return false;
34807
+ }
34808
+ const pivot = env.model.getters.getPivot(pivotId);
34809
+ const colDomain = domainToColRowDomain(pivot, pivotCell.domain).colDomain;
34810
+ const sortedColumn = pivot.definition.sortedColumn;
34811
+ if (order === "none") {
34812
+ return !sortedColumn;
34813
+ }
34814
+ if (!sortedColumn || sortedColumn.order !== order) {
34815
+ return false;
34816
+ }
34817
+ return sortedColumn.measure === pivotCell.measure && deepEquals(sortedColumn.domain, colDomain);
34818
+ }
34252
34819
 
34253
34820
  //------------------------------------------------------------------------------
34254
34821
  // Context Menu Registry
@@ -34347,14 +34914,33 @@ cellMenuRegistry
34347
34914
  name: INSERT_LINK_NAME,
34348
34915
  sequence: 150,
34349
34916
  separator: true,
34917
+ })
34918
+ .add("pivot_sorting", {
34919
+ name: _t("Sort pivot"),
34920
+ sequence: 155,
34921
+ icon: "o-spreadsheet-Icon.SORT_RANGE",
34922
+ isVisible: canSortPivot,
34350
34923
  })
34351
34924
  .add("pivot_fix_formulas", {
34352
34925
  ...FIX_FORMULAS,
34353
- sequence: 155,
34926
+ sequence: 160,
34354
34927
  })
34355
34928
  .add("pivot_properties", {
34356
34929
  ...pivotProperties,
34357
34930
  sequence: 170,
34931
+ separator: true,
34932
+ })
34933
+ .addChild("pivot_sorting_asc", ["pivot_sorting"], {
34934
+ ...pivotSortingAsc,
34935
+ sequence: 10,
34936
+ })
34937
+ .addChild("pivot_sorting_desc", ["pivot_sorting"], {
34938
+ ...pivotSortingDesc,
34939
+ sequence: 20,
34940
+ })
34941
+ .addChild("pivot_sorting_none", ["pivot_sorting"], {
34942
+ ...noPivotSorting,
34943
+ sequence: 30,
34358
34944
  });
34359
34945
 
34360
34946
  const sortRange = {
@@ -34367,7 +34953,7 @@ const sortAscending = {
34367
34953
  execute: (env) => {
34368
34954
  const { anchor, zones } = env.model.getters.getSelection();
34369
34955
  const sheetId = env.model.getters.getActiveSheetId();
34370
- interactiveSortSelection(env, sheetId, anchor.cell, zones[0], "ascending");
34956
+ interactiveSortSelection(env, sheetId, anchor.cell, zones[0], "asc");
34371
34957
  },
34372
34958
  icon: "o-spreadsheet-Icon.SORT_ASCENDING",
34373
34959
  };
@@ -34395,7 +34981,7 @@ const sortDescending = {
34395
34981
  execute: (env) => {
34396
34982
  const { anchor, zones } = env.model.getters.getSelection();
34397
34983
  const sheetId = env.model.getters.getActiveSheetId();
34398
- interactiveSortSelection(env, sheetId, anchor.cell, zones[0], "descending");
34984
+ interactiveSortSelection(env, sheetId, anchor.cell, zones[0], "desc");
34399
34985
  },
34400
34986
  icon: "o-spreadsheet-Icon.SORT_DESCENDING",
34401
34987
  };
@@ -34878,6 +35464,227 @@ function interactiveFreezeColumnsRows(env, dimension, base) {
34878
35464
  }
34879
35465
  }
34880
35466
 
35467
+ class PositionMap {
35468
+ map = {};
35469
+ constructor(entries = []) {
35470
+ for (const [position, value] of entries) {
35471
+ this.set(position, value);
35472
+ }
35473
+ }
35474
+ set({ sheetId, col, row }, value) {
35475
+ const map = this.map;
35476
+ if (!map[sheetId]) {
35477
+ map[sheetId] = {};
35478
+ }
35479
+ if (!map[sheetId][col]) {
35480
+ map[sheetId][col] = {};
35481
+ }
35482
+ map[sheetId][col][row] = value;
35483
+ }
35484
+ get({ sheetId, col, row }) {
35485
+ return this.map[sheetId]?.[col]?.[row];
35486
+ }
35487
+ getSheet(sheetId) {
35488
+ return this.map[sheetId];
35489
+ }
35490
+ has({ sheetId, col, row }) {
35491
+ return this.map[sheetId]?.[col]?.[row] !== undefined;
35492
+ }
35493
+ delete({ sheetId, col, row }) {
35494
+ delete this.map[sheetId]?.[col]?.[row];
35495
+ }
35496
+ keys() {
35497
+ const map = this.map;
35498
+ const keys = [];
35499
+ for (const sheetId in map) {
35500
+ for (const col in map[sheetId]) {
35501
+ for (const row in map[sheetId][col]) {
35502
+ keys.push({ sheetId, col: parseInt(col), row: parseInt(row) });
35503
+ }
35504
+ }
35505
+ }
35506
+ return keys;
35507
+ }
35508
+ keysForSheet(sheetId) {
35509
+ const map = this.map[sheetId];
35510
+ if (!map) {
35511
+ return [];
35512
+ }
35513
+ const keys = [];
35514
+ for (const col in map) {
35515
+ for (const row in map[col]) {
35516
+ keys.push({ sheetId, col: parseInt(col), row: parseInt(row) });
35517
+ }
35518
+ }
35519
+ return keys;
35520
+ }
35521
+ *entries() {
35522
+ const map = this.map;
35523
+ for (const position of this.keys()) {
35524
+ const { sheetId, col, row } = position;
35525
+ yield [position, map[sheetId][col][row]];
35526
+ }
35527
+ }
35528
+ }
35529
+
35530
+ class FormulaFingerprintStore extends SpreadsheetStore {
35531
+ mutators = ["enable", "disable"];
35532
+ isInvalidated = false;
35533
+ fingerprintColors = {
35534
+ [DATA_FINGERPRINT]: "#D9D9D9",
35535
+ };
35536
+ isEnabled = false;
35537
+ colors = new PositionMap();
35538
+ handle(cmd) {
35539
+ if (isCoreCommand(cmd) && this.isEnabled) {
35540
+ this.isInvalidated = true;
35541
+ }
35542
+ switch (cmd.type) {
35543
+ case "UNDO":
35544
+ case "REDO":
35545
+ case "ACTIVATE_SHEET":
35546
+ if (this.isEnabled) {
35547
+ this.isInvalidated = true;
35548
+ }
35549
+ break;
35550
+ }
35551
+ }
35552
+ finalize() {
35553
+ if (this.isInvalidated) {
35554
+ this.isInvalidated = false;
35555
+ this.computeFingerprints();
35556
+ }
35557
+ }
35558
+ enable() {
35559
+ this.isEnabled = true;
35560
+ this.computeFingerprints();
35561
+ }
35562
+ disable() {
35563
+ this.isEnabled = false;
35564
+ this.colors = new PositionMap();
35565
+ }
35566
+ computeFingerprints() {
35567
+ this.colors = new PositionMap();
35568
+ const fingerprints = new PositionMap();
35569
+ const allFingerprints = new Set();
35570
+ const activeSheetId = this.getters.getActiveSheetId();
35571
+ const cells = this.getters.getCells(activeSheetId);
35572
+ for (const cellId in cells) {
35573
+ const fingerprint = this.computeFingerprint(cells[cellId]);
35574
+ if (!fingerprint) {
35575
+ continue;
35576
+ }
35577
+ allFingerprints.add(fingerprint);
35578
+ const position = this.getters.getCellPosition(cellId);
35579
+ fingerprints.set(position, fingerprint);
35580
+ }
35581
+ this.assignColors(allFingerprints);
35582
+ for (const [position, fingerprint] of fingerprints.entries()) {
35583
+ const color = this.fingerprintColors[fingerprint];
35584
+ this.colors.set(position, color);
35585
+ this.colorSpreadZone(position, color);
35586
+ }
35587
+ }
35588
+ colorSpreadZone(position, fingerprintColor) {
35589
+ const spreadZone = this.getters.getSpreadZone(position);
35590
+ if (!spreadZone) {
35591
+ return;
35592
+ }
35593
+ const sheetId = position.sheetId;
35594
+ for (let row = spreadZone.top; row <= spreadZone.bottom; row++) {
35595
+ for (let col = spreadZone.left; col <= spreadZone.right; col++) {
35596
+ const spreadPosition = { sheetId, col, row };
35597
+ this.colors.set(spreadPosition, fingerprintColor);
35598
+ }
35599
+ }
35600
+ }
35601
+ assignColors(fingerprints) {
35602
+ const colors = new AlternatingColorGenerator(fingerprints.size);
35603
+ Object.keys(this.fingerprintColors).forEach(() => colors.next());
35604
+ for (const fingerprint of fingerprints) {
35605
+ if (!this.fingerprintColors[fingerprint]) {
35606
+ this.fingerprintColors[fingerprint] = setColorAlpha(colors.next(), 0.8);
35607
+ }
35608
+ }
35609
+ }
35610
+ computeFingerprint(cell) {
35611
+ const position = this.getters.getCellPosition(cell.id);
35612
+ if (cell.isFormula) {
35613
+ return this.computeFormulaFingerprint(position, cell);
35614
+ }
35615
+ else {
35616
+ return this.getLiteralFingerprint(position);
35617
+ }
35618
+ }
35619
+ computeFormulaFingerprint(position, cell) {
35620
+ const dependencies = cell.compiledFormula.dependencies;
35621
+ const colCellOffset = position.col;
35622
+ const rowCellOffset = position.row;
35623
+ const positionSheetIndex = this.getters.getSheetIds().indexOf(position.sheetId);
35624
+ // As an optimization, we do not build each reference vector individually
35625
+ // to sum them up, but instead we directly add each component to the resulting
35626
+ // vector. This is equivalent to summing up all reference vectors.
35627
+ const fingerprintVector = {
35628
+ dx: 0,
35629
+ dy: 0,
35630
+ dSheet: 0,
35631
+ };
35632
+ for (const range of dependencies) {
35633
+ const zone = range.zone;
35634
+ const [left, right] = range.parts;
35635
+ const rangeSheetIndex = this.getters.getSheetIds().indexOf(range.sheetId);
35636
+ fingerprintVector.dSheet = rangeSheetIndex - positionSheetIndex;
35637
+ // in relative mode, we offset the col and row by the cell's position
35638
+ // in absolute mode, we offset the col and row relative to the sheet
35639
+ const isLeftUnbounded = range.isFullRow && !range.unboundedZone.hasHeader;
35640
+ const isTopUnbounded = range.isFullCol && !range.unboundedZone.hasHeader;
35641
+ const leftOffset = isLeftUnbounded || left?.colFixed ? 0 : colCellOffset;
35642
+ const topOffset = isTopUnbounded || left?.rowFixed ? 0 : rowCellOffset;
35643
+ const isRightFixed = (!right && left?.colFixed) || right?.colFixed;
35644
+ const isBottomFixed = (!right && left.rowFixed) || right?.rowFixed;
35645
+ const isRightUnbounded = range.unboundedZone.right === undefined;
35646
+ const isBottomUnbounded = range.unboundedZone.bottom === undefined;
35647
+ const rightOffset = isRightUnbounded || isRightFixed ? 0 : colCellOffset;
35648
+ const bottomOffset = isBottomUnbounded || isBottomFixed ? 0 : rowCellOffset;
35649
+ const referenceZone = reorderZone({
35650
+ top: zone.top - topOffset,
35651
+ left: zone.left - leftOffset,
35652
+ right: zone.right - rightOffset,
35653
+ bottom: zone.bottom - bottomOffset,
35654
+ });
35655
+ for (let dy = referenceZone.top; dy <= referenceZone.bottom; dy++) {
35656
+ for (let dx = referenceZone.left; dx <= referenceZone.right; dx++) {
35657
+ fingerprintVector.dx += dx;
35658
+ fingerprintVector.dy += dy;
35659
+ }
35660
+ }
35661
+ }
35662
+ // removes the index placeholders from the normalized formula
35663
+ // =|N0|+|N1|+|N0| -> =|N|+|N|+|N|
35664
+ const normalizedFormula = cell.compiledFormula.normalizedFormula.replace(/(|\w)(\d)(|)/g, "$1$3");
35665
+ return hash(fingerprintVector) + normalizedFormula;
35666
+ }
35667
+ getLiteralFingerprint(position) {
35668
+ const evaluatedCell = this.getters.getEvaluatedCell(position);
35669
+ switch (evaluatedCell.type) {
35670
+ case CellValueType.number:
35671
+ case CellValueType.boolean:
35672
+ return DATA_FINGERPRINT;
35673
+ case CellValueType.text:
35674
+ case CellValueType.empty:
35675
+ case CellValueType.error:
35676
+ return undefined;
35677
+ }
35678
+ }
35679
+ }
35680
+ function hash(vector) {
35681
+ return Object.entries(vector)
35682
+ .sort(([a], [b]) => a.localeCompare(b))
35683
+ .map(([_, value]) => value)
35684
+ .join(",");
35685
+ }
35686
+ const DATA_FINGERPRINT = "DATA_FINGERPRINT";
35687
+
34881
35688
  const hideCols = {
34882
35689
  name: HIDE_COLUMNS_NAME,
34883
35690
  execute: (env) => {
@@ -35049,6 +35856,19 @@ const viewGridlines = {
35049
35856
  return env.model.getters.getGridLinesVisibility(sheetId);
35050
35857
  },
35051
35858
  };
35859
+ const irregularityMap = {
35860
+ name: _t("Irregularity map"),
35861
+ execute: (env) => {
35862
+ const fingerprintStore = env.getStore(FormulaFingerprintStore);
35863
+ if (fingerprintStore.isEnabled) {
35864
+ fingerprintStore.disable();
35865
+ }
35866
+ else {
35867
+ fingerprintStore.enable();
35868
+ }
35869
+ },
35870
+ icon: "o-spreadsheet-Icon.IRREGULARITY_MAP",
35871
+ };
35052
35872
  const viewFormulas = {
35053
35873
  name: _t("Formulas"),
35054
35874
  isActive: (env) => env.model.getters.shouldShowFormulas(),
@@ -35666,6 +36486,11 @@ topbarMenuRegistry
35666
36486
  .addChild("view_formulas", ["view", "show"], {
35667
36487
  ...viewFormulas,
35668
36488
  sequence: 10,
36489
+ })
36490
+ .addChild("view_irregularity_map", ["view"], {
36491
+ ...irregularityMap,
36492
+ sequence: 40,
36493
+ separator: true,
35669
36494
  })
35670
36495
  // ---------------------------------------------------------------------
35671
36496
  // INSERT MENU ITEMS
@@ -35968,62 +36793,6 @@ class OTRegistry extends Registry {
35968
36793
  }
35969
36794
  const otRegistry = new OTRegistry();
35970
36795
 
35971
- const CHECK_SVG = /*xml*/ `
35972
- <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'>
35973
- <path fill='none' stroke='#FFF' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/>
35974
- </svg>
35975
- `;
35976
- const CHECKBOX_WIDTH = 14;
35977
- css /* scss */ `
35978
- label.o-checkbox {
35979
- input {
35980
- appearance: none;
35981
- -webkit-appearance: none;
35982
- -moz-appearance: none;
35983
- border-radius: 0;
35984
- width: ${CHECKBOX_WIDTH}px;
35985
- height: ${CHECKBOX_WIDTH}px;
35986
- vertical-align: top;
35987
- box-sizing: border-box;
35988
- outline: none;
35989
- border: 1px solid ${GRAY_300};
35990
-
35991
- &:hover {
35992
- border-color: ${ACTION_COLOR};
35993
- }
35994
-
35995
- &:checked {
35996
- background: url("data:image/svg+xml,${encodeURIComponent(CHECK_SVG)}");
35997
- background-color: ${ACTION_COLOR};
35998
- border-color: ${ACTION_COLOR};
35999
- }
36000
-
36001
- &:focus {
36002
- outline: none;
36003
- box-shadow: 0 0 0 0.25rem rgba(113, 75, 103, 0.25);
36004
- border-color: ${ACTION_COLOR};
36005
- }
36006
- }
36007
- }
36008
- `;
36009
- class Checkbox extends owl.Component {
36010
- static template = "o-spreadsheet.Checkbox";
36011
- static props = {
36012
- label: { type: String, optional: true },
36013
- value: { type: Boolean, optional: true },
36014
- className: { type: String, optional: true },
36015
- name: { type: String, optional: true },
36016
- title: { type: String, optional: true },
36017
- disabled: { type: Boolean, optional: true },
36018
- onChange: Function,
36019
- };
36020
- static defaultProps = { value: false };
36021
- onChange(ev) {
36022
- const value = ev.target.checked;
36023
- this.props.onChange(value);
36024
- }
36025
- }
36026
-
36027
36796
  class Section extends owl.Component {
36028
36797
  static template = "o_spreadsheet.Section";
36029
36798
  static props = {
@@ -38263,6 +39032,104 @@ class GaugeChartDesignPanel extends owl.Component {
38263
39032
  }
38264
39033
  }
38265
39034
 
39035
+ class GeoChartRegionSelectSection extends owl.Component {
39036
+ static template = "o-spreadsheet-GeoChartRegionSelectSection";
39037
+ static components = { Section };
39038
+ static props = {
39039
+ figureId: String,
39040
+ definition: Object,
39041
+ updateChart: Function,
39042
+ };
39043
+ updateSelectedRegion(ev) {
39044
+ const value = ev.target.value;
39045
+ this.props.updateChart(this.props.figureId, { region: value });
39046
+ }
39047
+ get availableRegions() {
39048
+ return this.env.model.getters.getGeoChartAvailableRegions();
39049
+ }
39050
+ get selectedRegion() {
39051
+ return this.props.definition.region || this.availableRegions[0]?.id;
39052
+ }
39053
+ }
39054
+
39055
+ class GeoChartConfigPanel extends GenericChartConfigPanel {
39056
+ static template = "o-spreadsheet-GeoChartConfigPanel";
39057
+ static components = { ...GenericChartConfigPanel.components, GeoChartRegionSelectSection };
39058
+ get dataRanges() {
39059
+ return this.getDataSeriesRanges().slice(0, 1);
39060
+ }
39061
+ onDataSeriesConfirmed() {
39062
+ this.dataSeriesRanges = spreadRange(this.env.model.getters, this.dataSeriesRanges).slice(0, 1);
39063
+ this.state.datasetDispatchResult = this.props.updateChart(this.props.figureId, {
39064
+ dataSets: this.dataSeriesRanges,
39065
+ });
39066
+ }
39067
+ getLabelRangeOptions() {
39068
+ return [
39069
+ {
39070
+ name: "dataSetsHaveTitle",
39071
+ label: this.dataSetsHaveTitleLabel,
39072
+ value: this.props.definition.dataSetsHaveTitle,
39073
+ onChange: this.onUpdateDataSetsHaveTitle.bind(this),
39074
+ },
39075
+ ];
39076
+ }
39077
+ }
39078
+
39079
+ const DEFAULT_CUSTOM_COLOR_SCALE = {
39080
+ minColor: "#FFF5EB",
39081
+ midColor: "#FD8D3C",
39082
+ maxColor: "#7F2704",
39083
+ };
39084
+ class GeoChartDesignPanel extends ChartWithAxisDesignPanel {
39085
+ static template = "o-spreadsheet-GeoChartDesignPanel";
39086
+ static components = { ...ChartWithAxisDesignPanel.components, RoundColorPicker };
39087
+ colorScalesChoices = ChartTerms.GeoChart.ColorScales;
39088
+ updateColorScaleType(ev) {
39089
+ const value = ev.target.value;
39090
+ value === "custom"
39091
+ ? this.updateColorScale(DEFAULT_CUSTOM_COLOR_SCALE)
39092
+ : this.updateColorScale(value);
39093
+ }
39094
+ updateColorScale(colorScale) {
39095
+ this.props.updateChart(this.props.figureId, { colorScale });
39096
+ }
39097
+ updateMissingValueColor(color) {
39098
+ this.props.updateChart(this.props.figureId, { missingValueColor: color });
39099
+ }
39100
+ updateLegendPosition(ev) {
39101
+ const value = ev.target.value;
39102
+ this.props.updateChart(this.props.figureId, { legendPosition: value });
39103
+ }
39104
+ get selectedColorScale() {
39105
+ return typeof this.props.definition.colorScale === "object"
39106
+ ? "custom"
39107
+ : this.props.definition.colorScale || "oranges";
39108
+ }
39109
+ get selectedMissingValueColor() {
39110
+ return this.props.definition.missingValueColor || "#ffffff";
39111
+ }
39112
+ get customColorScale() {
39113
+ if (typeof this.props.definition.colorScale === "object") {
39114
+ return this.props.definition.colorScale;
39115
+ }
39116
+ return undefined;
39117
+ }
39118
+ getCustomColorScaleColor(color) {
39119
+ return this.customColorScale?.[color] ?? "";
39120
+ }
39121
+ setCustomColorScaleColor(colorType, color) {
39122
+ if (!color && colorType !== "midColor") {
39123
+ color = "#fff";
39124
+ }
39125
+ const customColorScale = this.customColorScale;
39126
+ if (!customColorScale) {
39127
+ return;
39128
+ }
39129
+ this.updateColorScale({ ...customColorScale, [colorType]: color });
39130
+ }
39131
+ }
39132
+
38266
39133
  class LineConfigPanel extends GenericChartConfigPanel {
38267
39134
  static template = "o-spreadsheet-LineConfigPanel";
38268
39135
  get canTreatLabelsAsText() {
@@ -38576,6 +39443,10 @@ chartSidePanelComponentRegistry
38576
39443
  .add("radar", {
38577
39444
  configuration: GenericChartConfigPanel,
38578
39445
  design: RadarChartDesignPanel,
39446
+ })
39447
+ .add("geo", {
39448
+ configuration: GeoChartConfigPanel,
39449
+ design: GeoChartDesignPanel,
38579
39450
  });
38580
39451
 
38581
39452
  css /* scss */ `
@@ -39939,7 +40810,7 @@ css /* scss */ `
39939
40810
  border-color: ${GRAY_300};
39940
40811
 
39941
40812
  &.active {
39942
- border-color: ${SELECTION_BORDER_COLOR};
40813
+ border-color: ${ACTION_COLOR};
39943
40814
  }
39944
40815
 
39945
40816
  &.o-invalid {
@@ -40897,7 +41768,7 @@ class ConditionalFormattingEditor extends owl.Component {
40897
41768
  }
40898
41769
  const point = this.state.rules.colorScale[target];
40899
41770
  if (point) {
40900
- point.color = Number.parseInt(color.slice(1), 16);
41771
+ point.color = colorToNumber(color);
40901
41772
  }
40902
41773
  this.updateConditionalFormat({ rule: this.state.rules.colorScale });
40903
41774
  this.closeMenus();
@@ -41042,6 +41913,9 @@ class ConditionalFormattingEditor extends owl.Component {
41042
41913
  return [this.state.rules.dataBar.rangeValues || ""];
41043
41914
  }
41044
41915
  updateDataBarColor(color) {
41916
+ if (!isColorValid(color)) {
41917
+ return;
41918
+ }
41045
41919
  this.state.rules.dataBar.color = Number.parseInt(color.slice(1), 16);
41046
41920
  this.updateConditionalFormat({ rule: this.state.rules.dataBar });
41047
41921
  }
@@ -43645,6 +44519,64 @@ class PivotMeasureEditor extends owl.Component {
43645
44519
  }
43646
44520
  }
43647
44521
 
44522
+ css /* scss */ `
44523
+ .o-pivot-sort {
44524
+ .o-sort-card {
44525
+ width: fit-content;
44526
+ background-color: ${GRAY_100};
44527
+ border: 1px solid ${GRAY_300};
44528
+
44529
+ .o-sort-value {
44530
+ color: ${PRIMARY_BUTTON_BG};
44531
+ }
44532
+ }
44533
+ }
44534
+ `;
44535
+ class PivotSortSection extends owl.Component {
44536
+ static template = "o-spreadsheet-PivotSortSection";
44537
+ static components = {
44538
+ Section,
44539
+ };
44540
+ static props = {
44541
+ definition: Object,
44542
+ pivotId: String,
44543
+ };
44544
+ get hasValidSort() {
44545
+ const pivot = this.env.model.getters.getPivot(this.props.pivotId);
44546
+ return (!!this.props.definition.sortedColumn &&
44547
+ isSortedColumnValid(this.props.definition.sortedColumn, pivot));
44548
+ }
44549
+ get sortDescription() {
44550
+ const sortOrder = this.props.definition.sortedColumn?.order === "asc" ? _t("ascending") : _t("descending");
44551
+ return _t("Sorted on column (%(ascOrDesc)s):", {
44552
+ ascOrDesc: sortOrder,
44553
+ });
44554
+ }
44555
+ get sortValuesAndFields() {
44556
+ const sortedColumn = this.props.definition.sortedColumn;
44557
+ if (!sortedColumn) {
44558
+ return [];
44559
+ }
44560
+ const pivot = this.env.model.getters.getPivot(this.props.pivotId);
44561
+ const locale = this.env.model.getters.getLocale();
44562
+ const currentDomain = [];
44563
+ const sortValues = [];
44564
+ for (const domainItem of sortedColumn.domain) {
44565
+ currentDomain.push(domainItem);
44566
+ const { value, format } = pivot.getPivotHeaderValueAndFormat(currentDomain);
44567
+ const label = formatValue(value, { format, locale });
44568
+ const field = pivot.definition.getDimension(domainItem.field);
44569
+ sortValues.push({ field: getFieldDisplayName(field), value: label });
44570
+ }
44571
+ if (sortedColumn.domain.length === 0) {
44572
+ sortValues.push({ value: _t("Total") });
44573
+ }
44574
+ const measureLabel = pivot.getMeasure(sortedColumn.measure).displayName;
44575
+ sortValues.push({ value: measureLabel, field: _t("Measure") });
44576
+ return sortValues;
44577
+ }
44578
+ }
44579
+
43648
44580
  css /* scss */ `
43649
44581
  .add-calculated-measure {
43650
44582
  cursor: pointer;
@@ -43658,6 +44590,7 @@ class PivotLayoutConfigurator extends owl.Component {
43658
44590
  PivotDimensionOrder,
43659
44591
  PivotDimensionGranularity,
43660
44592
  PivotMeasureEditor,
44593
+ PivotSortSection,
43661
44594
  };
43662
44595
  static props = {
43663
44596
  definition: Object,
@@ -43814,9 +44747,13 @@ class PivotLayoutConfigurator extends owl.Component {
43814
44747
  }
43815
44748
  updateMeasure(measure, newMeasure) {
43816
44749
  const { measures } = this.props.definition;
43817
- this.props.onDimensionsUpdated({
44750
+ const update = {
43818
44751
  measures: measures.map((m) => (m.id === measure.id ? newMeasure : m)),
43819
- });
44752
+ };
44753
+ if (this.props.definition.sortedColumn?.measure === measure.id) {
44754
+ update.sortedColumn = { ...this.props.definition.sortedColumn, measure: newMeasure.id };
44755
+ }
44756
+ this.props.onDimensionsUpdated(update);
43820
44757
  }
43821
44758
  getMeasureId(fieldName, aggregator) {
43822
44759
  const baseId = fieldName + (aggregator ? `:${aggregator}` : "");
@@ -43972,10 +44909,12 @@ class PivotRuntimeDefinition {
43972
44909
  measures;
43973
44910
  columns;
43974
44911
  rows;
44912
+ sortedColumn;
43975
44913
  constructor(definition, fields) {
43976
44914
  this.measures = definition.measures.map((measure) => createMeasure(fields, measure));
43977
44915
  this.columns = definition.columns.map((dimension) => createPivotDimension(fields, dimension));
43978
44916
  this.rows = definition.rows.map((dimension) => createPivotDimension(fields, dimension));
44917
+ this.sortedColumn = definition.sortedColumn;
43979
44918
  }
43980
44919
  getDimension(nameWithGranularity) {
43981
44920
  const dimension = this.columns.find((d) => d.nameWithGranularity === nameWithGranularity) ||
@@ -44129,6 +45068,7 @@ class SpreadsheetPivotTable {
44129
45068
  pivotCells = {};
44130
45069
  rowTree;
44131
45070
  colTree;
45071
+ isSorted = false;
44132
45072
  constructor(columns, rows, measures, fieldsType) {
44133
45073
  this.columns = columns.map((row) => {
44134
45074
  // offset in the pivot table
@@ -44302,6 +45242,7 @@ class SpreadsheetPivotTable {
44302
45242
  value,
44303
45243
  field: row.fields[rowDepth],
44304
45244
  children: [],
45245
+ type: this.fieldsType[fieldName] || "char",
44305
45246
  width: 0, // not used
44306
45247
  };
44307
45248
  treesAtDepth[depth].push(node);
@@ -44324,6 +45265,7 @@ class SpreadsheetPivotTable {
44324
45265
  field: leaf.fields[depth],
44325
45266
  children: [],
44326
45267
  width: leaf.width,
45268
+ type: this.fieldsType[fieldName] || "char",
44327
45269
  };
44328
45270
  if (treesAtDepth[depth]?.at(-1)?.value !== value) {
44329
45271
  treesAtDepth[depth + 1] = [];
@@ -44342,6 +45284,35 @@ class SpreadsheetPivotTable {
44342
45284
  fieldsType: this.fieldsType,
44343
45285
  };
44344
45286
  }
45287
+ sort(measure, sortedColumn, getValue) {
45288
+ if (this.isSorted) {
45289
+ return;
45290
+ }
45291
+ const getSortValue = (measure, domain) => {
45292
+ const rawValue = getValue(measure, domain).value;
45293
+ return typeof rawValue === "number" ? rawValue : -Infinity;
45294
+ };
45295
+ const sortColDomain = sortedColumn.domain;
45296
+ const sortFn = (rowDomain1, rowDomain2) => {
45297
+ const value1 = getSortValue(measure, [...rowDomain1, ...sortColDomain]);
45298
+ const value2 = getSortValue(measure, [...rowDomain2, ...sortColDomain]);
45299
+ return sortedColumn.order === "asc" ? value1 - value2 : value2 - value1;
45300
+ };
45301
+ const sortedRowTree = sortPivotTree(this.rowTree(), [], sortFn);
45302
+ this.rowTree = lazy(sortedRowTree);
45303
+ this.rows = [...this.rowTreeToRows(sortedRowTree), this.rows[this.rows.length - 1]];
45304
+ this.isSorted = true;
45305
+ }
45306
+ rowTreeToRows(tree, parentRow) {
45307
+ return tree.flatMap((node) => {
45308
+ const row = {
45309
+ indent: parentRow ? parentRow.indent + 1 : 0,
45310
+ fields: [...(parentRow?.fields || []), node.field],
45311
+ values: [...(parentRow?.values || []), node.value],
45312
+ };
45313
+ return [row, ...this.rowTreeToRows(node.children, row)];
45314
+ });
45315
+ }
44345
45316
  }
44346
45317
  const EMPTY_PIVOT_CELL = { type: "EMPTY" };
44347
45318
 
@@ -44419,6 +45390,7 @@ function dataEntriesToColumnsTree(dataEntries, columns, index) {
44419
45390
  value: groups[key]?.[0]?.[column.nameWithGranularity]?.value ?? null,
44420
45391
  field: colName,
44421
45392
  children: dataEntriesToColumnsTree(groups[key] || [], columns, index + 1),
45393
+ type: column.type,
44422
45394
  width: 0,
44423
45395
  };
44424
45396
  });
@@ -45284,6 +46256,7 @@ class PivotSidePanelStore extends SpreadsheetStore {
45284
46256
  format: measure.format,
45285
46257
  display: measure.display,
45286
46258
  })),
46259
+ sortedColumn: this.shouldKeepSortedColumn(definition) ? definition.sortedColumn : undefined,
45287
46260
  };
45288
46261
  if (!this.draft && deepEquals(coreDefinition, cleanedDefinition)) {
45289
46262
  return;
@@ -45342,6 +46315,19 @@ class PivotSidePanelStore extends SpreadsheetStore {
45342
46315
  }
45343
46316
  return granularitiesPerFields;
45344
46317
  }
46318
+ /**
46319
+ * Check if we want to keep the sorted column when updating the pivot definition. We should remove it if either
46320
+ * the measure is not in the new definition or the columns have changed.
46321
+ */
46322
+ shouldKeepSortedColumn(newDefinition) {
46323
+ const { sortedColumn } = newDefinition;
46324
+ if (!sortedColumn) {
46325
+ return true;
46326
+ }
46327
+ const oldDefinition = this.getters.getPivotCoreDefinition(this.pivotId);
46328
+ return (newDefinition.measures.find((measure) => measure.id === sortedColumn.measure) &&
46329
+ deepEquals(oldDefinition.columns, newDefinition.columns));
46330
+ }
45345
46331
  }
45346
46332
 
45347
46333
  class PivotSpreadsheetSidePanel extends owl.Component {
@@ -46443,6 +47429,7 @@ css /* scss */ `
46443
47429
  }
46444
47430
  }
46445
47431
  `;
47432
+ const DEFAULT_TABLE_STYLE_COLOR = "#3C78D8";
46446
47433
  class TableStyleEditorPanel extends owl.Component {
46447
47434
  static template = "o-spreadsheet-TableStyleEditorPanel";
46448
47435
  static components = { Section, RoundColorPicker, TableStylePreview };
@@ -46461,7 +47448,7 @@ class TableStyleEditorPanel extends owl.Component {
46461
47448
  : null;
46462
47449
  return {
46463
47450
  pickerOpened: false,
46464
- primaryColor: editedStyle?.primaryColor || "#3C78D8",
47451
+ primaryColor: editedStyle?.primaryColor || DEFAULT_TABLE_STYLE_COLOR,
46465
47452
  selectedTemplateName: editedStyle?.templateName || "lightColoredText",
46466
47453
  styleName: editedStyle?.displayName || this.env.model.getters.getNewCustomTableStyleName(),
46467
47454
  };
@@ -46470,7 +47457,7 @@ class TableStyleEditorPanel extends owl.Component {
46470
47457
  this.state.pickerOpened = !this.state.pickerOpened;
46471
47458
  }
46472
47459
  onColorPicked(color) {
46473
- this.state.primaryColor = color;
47460
+ this.state.primaryColor = isColorValid(color) ? color : DEFAULT_TABLE_STYLE_COLOR;
46474
47461
  this.state.pickerOpened = false;
46475
47462
  }
46476
47463
  onTemplatePicked(templateName) {
@@ -48086,6 +49073,7 @@ class FiguresContainer extends owl.Component {
48086
49073
  draggedFigure: undefined,
48087
49074
  horizontalSnap: undefined,
48088
49075
  verticalSnap: undefined,
49076
+ cancelDnd: undefined,
48089
49077
  });
48090
49078
  setup() {
48091
49079
  owl.onMounted(() => {
@@ -48098,12 +49086,28 @@ class FiguresContainer extends owl.Component {
48098
49086
  // new rendering
48099
49087
  this.render();
48100
49088
  });
49089
+ owl.onWillUpdateProps(() => {
49090
+ const sheetId = this.env.model.getters.getActiveSheetId();
49091
+ const draggedFigureId = this.dnd.draggedFigure?.id;
49092
+ if (draggedFigureId && !this.env.model.getters.getFigure(sheetId, draggedFigureId)) {
49093
+ if (this.dnd.cancelDnd) {
49094
+ this.dnd.cancelDnd();
49095
+ }
49096
+ this.dnd.draggedFigure = undefined;
49097
+ this.dnd.horizontalSnap = undefined;
49098
+ this.dnd.verticalSnap = undefined;
49099
+ this.dnd.cancelDnd = undefined;
49100
+ }
49101
+ });
48101
49102
  }
48102
49103
  getVisibleFigures() {
48103
49104
  const visibleFigures = this.env.model.getters.getVisibleFigures();
48104
49105
  if (this.dnd.draggedFigure &&
48105
49106
  !visibleFigures.some((figure) => figure.id === this.dnd.draggedFigure?.id)) {
48106
- visibleFigures.push(this.env.model.getters.getFigure(this.env.model.getters.getActiveSheetId(), this.dnd.draggedFigure?.id));
49107
+ const draggedFigure = this.env.model.getters.getFigure(this.env.model.getters.getActiveSheetId(), this.dnd.draggedFigure?.id);
49108
+ if (draggedFigure) {
49109
+ visibleFigures.push(draggedFigure);
49110
+ }
48107
49111
  }
48108
49112
  return visibleFigures;
48109
49113
  }
@@ -48222,7 +49226,7 @@ class FiguresContainer extends owl.Component {
48222
49226
  this.dnd.verticalSnap = undefined;
48223
49227
  this.env.model.dispatch("UPDATE_FIGURE", { sheetId, id: figure.id, x, y });
48224
49228
  };
48225
- startDnd(onMouseMove, onMouseUp);
49229
+ this.dnd.cancelDnd = startDnd(onMouseMove, onMouseUp);
48226
49230
  }
48227
49231
  /**
48228
49232
  * Initialize the resize of a figure with mouse movements
@@ -48270,7 +49274,7 @@ class FiguresContainer extends owl.Component {
48270
49274
  this.dnd.horizontalSnap = undefined;
48271
49275
  this.dnd.verticalSnap = undefined;
48272
49276
  };
48273
- startDnd(onMouseMove, onMouseUp);
49277
+ this.dnd.cancelDnd = startDnd(onMouseMove, onMouseUp);
48274
49278
  }
48275
49279
  getOtherFigures(figId) {
48276
49280
  return this.getVisibleFigures().filter((f) => f.id !== figId);
@@ -49377,9 +50381,11 @@ class HeadersOverlay extends owl.Component {
49377
50381
  class GridRenderer {
49378
50382
  getters;
49379
50383
  renderer;
50384
+ fingerprints;
49380
50385
  constructor(get) {
49381
50386
  this.getters = get(ModelStore).getters;
49382
50387
  this.renderer = get(RendererStore);
50388
+ this.fingerprints = get(FormulaFingerprintStore);
49383
50389
  this.renderer.register(this);
49384
50390
  }
49385
50391
  get renderingLayers() {
@@ -49857,14 +50863,22 @@ class GridRenderer {
49857
50863
  const showFormula = this.getters.shouldShowFormulas();
49858
50864
  const { x, y, width, height } = this.getters.getVisibleRect(zone);
49859
50865
  const { verticalAlign } = this.getters.getCellStyle(position);
50866
+ let style = this.getters.getCellComputedStyle(position);
50867
+ if (this.fingerprints.isEnabled) {
50868
+ const fingerprintColor = this.fingerprints.colors.get(position);
50869
+ style = { ...style, fillColor: fingerprintColor };
50870
+ }
50871
+ const dataBarFill = this.fingerprints.isEnabled
50872
+ ? undefined
50873
+ : this.getters.getConditionalDataBar(position);
49860
50874
  const box = {
49861
50875
  x,
49862
50876
  y,
49863
50877
  width,
49864
50878
  height,
49865
50879
  border: this.getters.getCellComputedBorder(position) || undefined,
49866
- style: this.getters.getCellComputedStyle(position),
49867
- dataBarFill: this.getters.getConditionalDataBar(position),
50880
+ style,
50881
+ dataBarFill,
49868
50882
  verticalAlign,
49869
50883
  isError: (cell.type === CellValueType.error && !!cell.message) ||
49870
50884
  this.getters.isDataValidationInvalid(position),
@@ -49889,7 +50903,6 @@ class GridRenderer {
49889
50903
  box.hasIcon = this.getters.doesCellHaveGridIcon(position);
49890
50904
  const headerIconWidth = box.hasIcon ? GRID_ICON_EDGE_LENGTH + GRID_ICON_MARGIN : 0;
49891
50905
  /** Content */
49892
- const style = this.getters.getCellComputedStyle(position);
49893
50906
  const wrapping = style.wrapping || "overflow";
49894
50907
  const wrapText = wrapping === "wrap" && !showFormula;
49895
50908
  const maxWidth = width - 2 * MIN_CELL_TEXT_MARGIN;
@@ -51867,62 +52880,6 @@ class BordersPlugin extends CorePlugin {
51867
52880
  }
51868
52881
  }
51869
52882
 
51870
- class PositionMap {
51871
- map = {};
51872
- constructor(entries = []) {
51873
- for (const [position, value] of entries) {
51874
- this.set(position, value);
51875
- }
51876
- }
51877
- set({ sheetId, col, row }, value) {
51878
- const map = this.map;
51879
- if (!map[sheetId]) {
51880
- map[sheetId] = {};
51881
- }
51882
- if (!map[sheetId][col]) {
51883
- map[sheetId][col] = {};
51884
- }
51885
- map[sheetId][col][row] = value;
51886
- }
51887
- get({ sheetId, col, row }) {
51888
- return this.map[sheetId]?.[col]?.[row];
51889
- }
51890
- getSheet(sheetId) {
51891
- return this.map[sheetId];
51892
- }
51893
- has({ sheetId, col, row }) {
51894
- return this.map[sheetId]?.[col]?.[row] !== undefined;
51895
- }
51896
- delete({ sheetId, col, row }) {
51897
- delete this.map[sheetId]?.[col]?.[row];
51898
- }
51899
- keys() {
51900
- const map = this.map;
51901
- const keys = [];
51902
- for (const sheetId in map) {
51903
- for (const col in map[sheetId]) {
51904
- for (const row in map[sheetId][col]) {
51905
- keys.push({ sheetId, col: parseInt(col), row: parseInt(row) });
51906
- }
51907
- }
51908
- }
51909
- return keys;
51910
- }
51911
- keysForSheet(sheetId) {
51912
- const map = this.map[sheetId];
51913
- if (!map) {
51914
- return [];
51915
- }
51916
- const keys = [];
51917
- for (const col in map) {
51918
- for (const row in map[col]) {
51919
- keys.push({ sheetId, col: parseInt(col), row: parseInt(row) });
51920
- }
51921
- }
51922
- return keys;
51923
- }
51924
- }
51925
-
51926
52883
  /**
51927
52884
  * Core Plugin
51928
52885
  *
@@ -52361,7 +53318,7 @@ class CellPlugin extends CorePlugin {
52361
53318
  const before = this.getters.getCell({ sheetId, col, row });
52362
53319
  const hasContent = "content" in after || "formula" in after;
52363
53320
  // Compute the new cell properties
52364
- const afterContent = hasContent ? replaceSpecialSpaces(after?.content) : before?.content || "";
53321
+ const afterContent = hasContent ? replaceNewLines(after?.content) : before?.content || "";
52365
53322
  let style;
52366
53323
  if (after.style !== undefined) {
52367
53324
  style = after.style || undefined;
@@ -55306,6 +56263,9 @@ class SheetPlugin extends CorePlugin {
55306
56263
  };
55307
56264
  }
55308
56265
  getUnboundedZone(sheetId, zone) {
56266
+ if (zone.bottom === undefined || zone.right === undefined) {
56267
+ return zone;
56268
+ }
55309
56269
  const isFullRow = zone.left === 0 && zone.right === this.getNumberCols(sheetId) - 1;
55310
56270
  const isFullCol = zone.top === 0 && zone.bottom === this.getNumberRows(sheetId) - 1;
55311
56271
  return {
@@ -59589,66 +60549,25 @@ class EvaluationConditionalFormatPlugin extends UIPlugin {
59589
60549
  return;
59590
60550
  }
59591
60551
  const zone = this.getters.getRangeFromSheetXC(sheetId, range).zone;
59592
- const colorCellArgs = [];
60552
+ const colorThresholds = [{ value: minValue, color: rule.minimum.color }];
59593
60553
  if (rule.midpoint && midValue) {
59594
- colorCellArgs.push({
59595
- minValue,
59596
- minColor: rule.minimum.color,
59597
- colorDiffUnit: this.computeColorDiffUnits(minValue, midValue, rule.minimum.color, rule.midpoint.color),
59598
- });
59599
- colorCellArgs.push({
59600
- minValue: midValue,
59601
- minColor: rule.midpoint.color,
59602
- colorDiffUnit: this.computeColorDiffUnits(midValue, maxValue, rule.midpoint.color, rule.maximum.color),
59603
- });
59604
- }
59605
- else {
59606
- colorCellArgs.push({
59607
- minValue,
59608
- minColor: rule.minimum.color,
59609
- colorDiffUnit: this.computeColorDiffUnits(minValue, maxValue, rule.minimum.color, rule.maximum.color),
59610
- });
60554
+ colorThresholds.push({ value: midValue, color: rule.midpoint.color });
59611
60555
  }
60556
+ colorThresholds.push({ value: maxValue, color: rule.maximum.color });
60557
+ const colorScale = getColorScale(colorThresholds);
59612
60558
  for (let row = zone.top; row <= zone.bottom; row++) {
59613
60559
  for (let col = zone.left; col <= zone.right; col++) {
59614
60560
  const cell = this.getters.getEvaluatedCell({ sheetId, col, row });
59615
60561
  if (cell.type === CellValueType.number) {
59616
60562
  const value = clip(cell.value, minValue, maxValue);
59617
- let color;
59618
- if (colorCellArgs.length === 2 && midValue) {
59619
- color =
59620
- value <= midValue
59621
- ? this.colorCell(value, colorCellArgs[0].minValue, colorCellArgs[0].minColor, colorCellArgs[0].colorDiffUnit)
59622
- : this.colorCell(value, colorCellArgs[1].minValue, colorCellArgs[1].minColor, colorCellArgs[1].colorDiffUnit);
59623
- }
59624
- else {
59625
- color = this.colorCell(value, colorCellArgs[0].minValue, colorCellArgs[0].minColor, colorCellArgs[0].colorDiffUnit);
59626
- }
59627
60563
  if (!computedStyle[col])
59628
60564
  computedStyle[col] = [];
59629
60565
  computedStyle[col][row] = computedStyle[col]?.[row] || {};
59630
- computedStyle[col][row].fillColor = colorNumberString(color);
60566
+ computedStyle[col][row].fillColor = colorScale(value);
59631
60567
  }
59632
60568
  }
59633
60569
  }
59634
60570
  }
59635
- computeColorDiffUnits(minValue, maxValue, minColor, maxColor) {
59636
- const deltaValue = maxValue - minValue;
59637
- const deltaColorR = ((minColor >> 16) % 256) - ((maxColor >> 16) % 256);
59638
- const deltaColorG = ((minColor >> 8) % 256) - ((maxColor >> 8) % 256);
59639
- const deltaColorB = (minColor % 256) - (maxColor % 256);
59640
- const colorDiffUnitR = deltaColorR / deltaValue;
59641
- const colorDiffUnitG = deltaColorG / deltaValue;
59642
- const colorDiffUnitB = deltaColorB / deltaValue;
59643
- return [colorDiffUnitR, colorDiffUnitG, colorDiffUnitB];
59644
- }
59645
- colorCell(value, minValue, minColor, colorDiffUnit) {
59646
- const [colorDiffUnitR, colorDiffUnitG, colorDiffUnitB] = colorDiffUnit;
59647
- const r = Math.round(((minColor >> 16) % 256) - colorDiffUnitR * (value - minValue));
59648
- const g = Math.round(((minColor >> 8) % 256) - colorDiffUnitG * (value - minValue));
59649
- const b = Math.round((minColor % 256) - colorDiffUnitB * (value - minValue));
59650
- return (r << 16) | (g << 8) | b;
59651
- }
59652
60571
  /**
59653
60572
  * Execute the predicate to know if a conditional formatting rule should be applied to a cell
59654
60573
  */
@@ -60274,7 +61193,7 @@ function withPivotPresentationLayer (PivotClass) {
60274
61193
  }
60275
61194
  getValuesToAggregate(measure, domain) {
60276
61195
  const { rowDomain, colDomain } = domainToColRowDomain(this, domain);
60277
- const table = this.getTableStructure();
61196
+ const table = super.getTableStructure();
60278
61197
  const values = [];
60279
61198
  if (colDomain.length === 0 &&
60280
61199
  rowDomain.length < this.definition.rows.length &&
@@ -60708,6 +61627,21 @@ function withPivotPresentationLayer (PivotClass) {
60708
61627
  }
60709
61628
  throw new Error(`Value ${result.value} is not a number`);
60710
61629
  }
61630
+ getTableStructure() {
61631
+ const table = super.getTableStructure();
61632
+ this.sortTableStructure(table);
61633
+ return table;
61634
+ }
61635
+ sortTableStructure(table) {
61636
+ if (!this.definition.sortedColumn || table.isSorted) {
61637
+ return;
61638
+ }
61639
+ const measure = this.definition.sortedColumn.measure;
61640
+ const isSortValid = isSortedColumnValid(this.definition.sortedColumn, this);
61641
+ if (isSortValid) {
61642
+ table.sort(measure, this.definition.sortedColumn, (measure, domain) => this._getPivotCellValueAndFormat(measure, domain));
61643
+ }
61644
+ }
60711
61645
  }
60712
61646
  return PivotPresentationLayer;
60713
61647
  }
@@ -62483,23 +63417,6 @@ class Session extends EventBus {
62483
63417
  }
62484
63418
  }
62485
63419
 
62486
- function randomChoice(arr) {
62487
- return arr[Math.floor(Math.random() * arr.length)];
62488
- }
62489
- const colors = [
62490
- "#ff851b",
62491
- "#0074d9",
62492
- "#7fdbff",
62493
- "#b10dc9",
62494
- "#39cccc",
62495
- "#f012be",
62496
- "#3d9970",
62497
- "#111111",
62498
- "#ff4136",
62499
- "#aaaaaa",
62500
- "#85144b",
62501
- "#001f3f",
62502
- ];
62503
63420
  class CollaborativePlugin extends UIPlugin {
62504
63421
  static getters = [
62505
63422
  "getClientsToDisplay",
@@ -62508,7 +63425,7 @@ class CollaborativePlugin extends UIPlugin {
62508
63425
  "isFullySynchronized",
62509
63426
  ];
62510
63427
  static layers = ["Selection"];
62511
- availableColors = new Set(colors);
63428
+ availableColors = new AlternatingColorGenerator(12);
62512
63429
  colors = {};
62513
63430
  session;
62514
63431
  constructor(config) {
@@ -62519,14 +63436,6 @@ class CollaborativePlugin extends UIPlugin {
62519
63436
  return (position.row < this.getters.getNumberRows(position.sheetId) &&
62520
63437
  position.col < this.getters.getNumberCols(position.sheetId));
62521
63438
  }
62522
- chooseNewColor() {
62523
- if (this.availableColors.size === 0) {
62524
- this.availableColors = new Set(colors);
62525
- }
62526
- const color = randomChoice([...this.availableColors.values()]);
62527
- this.availableColors.delete(color);
62528
- return color;
62529
- }
62530
63439
  getClient() {
62531
63440
  return this.session.getClient();
62532
63441
  }
@@ -62561,7 +63470,7 @@ class CollaborativePlugin extends UIPlugin {
62561
63470
  this.isPositionValid(client.position)) {
62562
63471
  const position = client.position;
62563
63472
  if (!this.colors[client.id]) {
62564
- this.colors[client.id] = this.chooseNewColor();
63473
+ this.colors[client.id] = this.availableColors.next();
62565
63474
  }
62566
63475
  const color = this.colors[client.id];
62567
63476
  clients.push({ ...client, position, color });
@@ -62798,7 +63707,7 @@ class FormatPlugin extends UIPlugin {
62798
63707
  for (let row = zone.top; row <= zone.bottom; row++) {
62799
63708
  const position = { sheetId, col, row };
62800
63709
  const pivotCell = this.getters.getPivotCellFromPosition(position);
62801
- if (pivotCell.type === "VALUE") {
63710
+ if (this.isSpilledPivotValueFormula(position, pivotCell)) {
62802
63711
  measurePositions.push(position);
62803
63712
  const pivotId = this.getters.getPivotIdFromPosition(position) || "";
62804
63713
  measuresByPivotId[pivotId] ??= new Set();
@@ -62835,6 +63744,10 @@ class FormatPlugin extends UIPlugin {
62835
63744
  format,
62836
63745
  });
62837
63746
  }
63747
+ isSpilledPivotValueFormula(position, pivotCell) {
63748
+ const cell = this.getters.getCell(position);
63749
+ return pivotCell.type === "VALUE" && !cell?.isFormula;
63750
+ }
62838
63751
  /**
62839
63752
  * This function allows to adjust the quantity of decimal places after a decimal
62840
63753
  * point on cells containing number value. It does this by changing the cells
@@ -62885,6 +63798,69 @@ class FormatPlugin extends UIPlugin {
62885
63798
  }
62886
63799
  }
62887
63800
 
63801
+ class GeoFeaturePlugin extends UIPlugin {
63802
+ static getters = [
63803
+ "getGeoJsonFeatures",
63804
+ "geoFeatureNameToId",
63805
+ "getGeoChartAvailableRegions",
63806
+ ];
63807
+ geoJsonService;
63808
+ geoJsonCache = {};
63809
+ constructor(config) {
63810
+ super(config);
63811
+ this.geoJsonService = config.external.geoJsonService;
63812
+ }
63813
+ getGeoChartAvailableRegions() {
63814
+ if (!this.geoJsonService) {
63815
+ console.error("No geoJsonService provided to the model");
63816
+ return [];
63817
+ }
63818
+ return this.geoJsonService.getAvailableRegions() || [];
63819
+ }
63820
+ getGeoJsonFeatures(region) {
63821
+ if (!this.geoJsonService) {
63822
+ console.error("No geoJsonService provided to the model");
63823
+ return;
63824
+ }
63825
+ const cachedGeoJson = this.geoJsonCache[region];
63826
+ if (cachedGeoJson instanceof Promise) {
63827
+ return undefined;
63828
+ }
63829
+ if (cachedGeoJson !== undefined) {
63830
+ return cachedGeoJson ?? undefined;
63831
+ }
63832
+ this.geoJsonCache[region] = new Promise(async (resolve) => {
63833
+ const json = await this.geoJsonService?.getTopoJson(region);
63834
+ this.geoJsonCache[region] = this.convertToGeoJson(json);
63835
+ this.dispatch("EVALUATE_CHARTS");
63836
+ resolve();
63837
+ });
63838
+ return undefined;
63839
+ }
63840
+ geoFeatureNameToId(region, featureName) {
63841
+ if (!this.geoJsonService) {
63842
+ console.error("No geoJsonService provided to the model");
63843
+ return;
63844
+ }
63845
+ return this.geoJsonService.geoFeatureNameToId(region, featureName);
63846
+ }
63847
+ convertToGeoJson(json) {
63848
+ if (!json) {
63849
+ return null;
63850
+ }
63851
+ // TopoJSON
63852
+ if (json.type === "Topology") {
63853
+ const features = window.ChartGeo.topojson.feature(json, Object.values(json.objects)[0]);
63854
+ return features.type === "FeatureCollection" ? features.features : [features];
63855
+ }
63856
+ // GeoJSON
63857
+ else if (json.type === "FeatureCollection") {
63858
+ return json.features;
63859
+ }
63860
+ throw new Error("Invalid TopoJSON");
63861
+ }
63862
+ }
63863
+
62888
63864
  class HeaderVisibilityUIPlugin extends UIPlugin {
62889
63865
  static getters = [
62890
63866
  "getNextVisibleCellPosition",
@@ -64422,10 +65398,13 @@ class TableResizeUI extends UIPlugin {
64422
65398
  case "RESIZE_TABLE": {
64423
65399
  const table = this.getters.getCoreTableMatchingTopLeft(cmd.sheetId, cmd.zone);
64424
65400
  this.dispatch("UPDATE_TABLE", { ...cmd });
64425
- if (!table || !table.config.automaticAutofill)
65401
+ if (!table)
64426
65402
  return;
64427
- const oldTableZone = table.range.zone;
64428
65403
  const newTableZone = this.getters.getRangeFromRangeData(cmd.newTableRange).zone;
65404
+ this.selection.selectCell(newTableZone.right, newTableZone.bottom);
65405
+ if (!table.config.automaticAutofill)
65406
+ return;
65407
+ const oldTableZone = table.range.zone;
64429
65408
  if (newTableZone.bottom >= oldTableZone.bottom) {
64430
65409
  for (let col = newTableZone.left; col <= newTableZone.right; col++) {
64431
65410
  const autofillSource = { col, row: oldTableZone.bottom, sheetId: cmd.sheetId };
@@ -65631,15 +66610,23 @@ class GridSelectionPlugin extends UIPlugin {
65631
66610
  handler.paste({ zones: pasteTarget, sheetId }, data, { isCutOperation: true });
65632
66611
  const toRemove = isBasedBefore ? cmd.elements.map((el) => el + thickness) : cmd.elements;
65633
66612
  let currentIndex = cmd.base;
66613
+ const resizingGroups = {};
65634
66614
  for (const element of toRemove) {
65635
66615
  const size = this.getters.getHeaderSize(cmd.sheetId, cmd.dimension, element);
66616
+ const currentSize = this.getters.getHeaderSize(cmd.sheetId, cmd.dimension, currentIndex);
66617
+ if (size != currentSize) {
66618
+ resizingGroups[size] ??= [];
66619
+ resizingGroups[size].push(currentIndex);
66620
+ currentIndex += 1;
66621
+ }
66622
+ }
66623
+ for (const size in resizingGroups) {
65636
66624
  this.dispatch("RESIZE_COLUMNS_ROWS", {
65637
66625
  dimension: cmd.dimension,
65638
66626
  sheetId: cmd.sheetId,
65639
- size,
65640
- elements: [currentIndex],
66627
+ size: parseInt(size, 10),
66628
+ elements: resizingGroups[size],
65641
66629
  });
65642
- currentIndex += 1;
65643
66630
  }
65644
66631
  this.dispatch("REMOVE_COLUMNS_ROWS", {
65645
66632
  dimension: cmd.dimension,
@@ -66221,6 +67208,7 @@ class SheetViewPlugin extends UIPlugin {
66221
67208
  break;
66222
67209
  case "DELETE_SHEET":
66223
67210
  this.cleanViewports();
67211
+ this.sheetsWithDirtyViewports.delete(cmd.sheetId);
66224
67212
  break;
66225
67213
  case "ACTIVATE_SHEET":
66226
67214
  this.sheetsWithDirtyViewports.add(cmd.sheetIdTo);
@@ -66871,7 +67859,8 @@ const featurePluginRegistry = new Registry()
66871
67859
  .add("data_cleanup", DataCleanupPlugin)
66872
67860
  .add("table_autofill", TableAutofillPlugin)
66873
67861
  .add("table_ui_resize", TableResizeUI)
66874
- .add("datavalidation_insert", DataValidationInsertionPlugin);
67862
+ .add("datavalidation_insert", DataValidationInsertionPlugin)
67863
+ .add("geo_features", GeoFeaturePlugin);
66875
67864
  // Plugins which have a state, but which should not be shared in collaborative
66876
67865
  const statefulUIPluginRegistry = new Registry()
66877
67866
  .add("selection", GridSelectionPlugin)
@@ -68615,8 +69604,8 @@ css /* scss */ `
68615
69604
  .o-topbar-composer {
68616
69605
  height: fit-content;
68617
69606
  margin-top: -1px;
69607
+ margin-bottom: -1px;
68618
69608
  border: 1px solid;
68619
- z-index: ${ComponentsImportance.TopBarComposer};
68620
69609
  font-family: ${DEFAULT_FONT};
68621
69610
 
68622
69611
  .o-composer:empty:not(:focus):not(.active)::before {
@@ -68674,6 +69663,7 @@ class TopBarComposer extends owl.Component {
68674
69663
  }
68675
69664
  return cssPropertiesToCss({
68676
69665
  "border-color": SELECTION_BORDER_COLOR,
69666
+ "z-index": String(ComponentsImportance.TopBarComposer),
68677
69667
  });
68678
69668
  }
68679
69669
  onFocus(selection) {
@@ -68782,6 +69772,15 @@ css /* scss */ `
68782
69772
  }
68783
69773
  }
68784
69774
 
69775
+ .irregularity-map {
69776
+ border-top: 1px solid ${SEPARATOR_COLOR};
69777
+ height: ${TOPBAR_TOOLBAR_HEIGHT}px;
69778
+
69779
+ .alert-info {
69780
+ border-left: 3px solid ${ALERT_INFO_BORDER};
69781
+ }
69782
+ }
69783
+
68785
69784
  .o-topbar-composer {
68786
69785
  flex-grow: 1;
68787
69786
  }
@@ -68875,8 +69874,10 @@ class TopBar extends owl.Component {
68875
69874
  formatNumberMenuItemSpec = formatNumberMenuItemSpec;
68876
69875
  isntToolbarMenu = false;
68877
69876
  composerFocusStore;
69877
+ fingerprints;
68878
69878
  setup() {
68879
69879
  this.composerFocusStore = useStore(ComposerFocusStore);
69880
+ this.fingerprints = useStore(FormulaFingerprintStore);
68880
69881
  owl.useExternalListener(window, "click", this.onExternalClick);
68881
69882
  owl.onWillStart(() => this.updateCellState());
68882
69883
  owl.onWillUpdateProps(() => this.updateCellState());
@@ -69347,7 +70348,7 @@ class Spreadsheet extends owl.Component {
69347
70348
  properties["grid-template-rows"] = `auto`;
69348
70349
  }
69349
70350
  else {
69350
- properties["grid-template-rows"] = `${TOPBAR_HEIGHT}px auto ${BOTTOMBAR_HEIGHT + 1}px`;
70351
+ properties["grid-template-rows"] = `max-content auto ${BOTTOMBAR_HEIGHT + 1}px`;
69351
70352
  }
69352
70353
  properties["grid-template-columns"] = `auto ${this.sidePanel.panelSize}px`;
69353
70354
  return cssPropertiesToCss(properties);
@@ -70425,9 +71426,6 @@ class SelectionStreamProcessorImpl {
70425
71426
  getBackToDefault() {
70426
71427
  this.stream.getBackToDefault();
70427
71428
  }
70428
- getAnchor() {
70429
- return this.anchor;
70430
- }
70431
71429
  modifyAnchor(anchor, mode, options) {
70432
71430
  const sheetId = this.getters.getActiveSheetId();
70433
71431
  anchor = {
@@ -73359,6 +74357,7 @@ class Model extends EventBus {
73359
74357
  session: this.session,
73360
74358
  defaultCurrency: this.config.defaultCurrency,
73361
74359
  customColors: this.config.customColors || [],
74360
+ external: this.config.external,
73362
74361
  };
73363
74362
  }
73364
74363
  // ---------------------------------------------------------------------------
@@ -73590,7 +74589,6 @@ const SPREADSHEET_DIMENSIONS = {
73590
74589
  MIN_COL_WIDTH,
73591
74590
  HEADER_HEIGHT,
73592
74591
  HEADER_WIDTH,
73593
- TOPBAR_HEIGHT,
73594
74592
  BOTTOMBAR_HEIGHT,
73595
74593
  DEFAULT_CELL_WIDTH,
73596
74594
  DEFAULT_CELL_HEIGHT,
@@ -73831,6 +74829,6 @@ exports.tokenColors = tokenColors;
73831
74829
  exports.tokenize = tokenize;
73832
74830
 
73833
74831
 
73834
- __info__.version = "18.1.0-alpha.7";
73835
- __info__.date = "2024-12-05T10:40:26.512Z";
73836
- __info__.hash = "7b1c39b";
74832
+ __info__.version = "18.1.0-alpha.8";
74833
+ __info__.date = "2024-12-19T07:49:54.421Z";
74834
+ __info__.hash = "87a1567";