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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,9 +2,9 @@
2
2
  /**
3
3
  * This file is generated by o-spreadsheet build tools. Do not edit it.
4
4
  * @see https://github.com/odoo/o-spreadsheet
5
- * @version 18.1.0-alpha.7
6
- * @date 2024-12-05T10:40:26.512Z
7
- * @hash 7b1c39b
5
+ * @version 18.1.0
6
+ * @date 2024-12-26T06:37:07.879Z
7
+ * @hash c520e89
8
8
  */
9
9
 
10
10
  '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
  }
@@ -41172,6 +42046,10 @@ css /* scss */ `
41172
42046
  border: 1px solid #d8dadd;
41173
42047
  color: #374151;
41174
42048
  }
42049
+
42050
+ table {
42051
+ table-layout: fixed;
42052
+ }
41175
42053
  }
41176
42054
  `;
41177
42055
  class CustomCurrencyPanel extends owl.Component {
@@ -43645,6 +44523,64 @@ class PivotMeasureEditor extends owl.Component {
43645
44523
  }
43646
44524
  }
43647
44525
 
44526
+ css /* scss */ `
44527
+ .o-pivot-sort {
44528
+ .o-sort-card {
44529
+ width: fit-content;
44530
+ background-color: ${GRAY_100};
44531
+ border: 1px solid ${GRAY_300};
44532
+
44533
+ .o-sort-value {
44534
+ color: ${PRIMARY_BUTTON_BG};
44535
+ }
44536
+ }
44537
+ }
44538
+ `;
44539
+ class PivotSortSection extends owl.Component {
44540
+ static template = "o-spreadsheet-PivotSortSection";
44541
+ static components = {
44542
+ Section,
44543
+ };
44544
+ static props = {
44545
+ definition: Object,
44546
+ pivotId: String,
44547
+ };
44548
+ get hasValidSort() {
44549
+ const pivot = this.env.model.getters.getPivot(this.props.pivotId);
44550
+ return (!!this.props.definition.sortedColumn &&
44551
+ isSortedColumnValid(this.props.definition.sortedColumn, pivot));
44552
+ }
44553
+ get sortDescription() {
44554
+ const sortOrder = this.props.definition.sortedColumn?.order === "asc" ? _t("ascending") : _t("descending");
44555
+ return _t("Sorted on column (%(ascOrDesc)s):", {
44556
+ ascOrDesc: sortOrder,
44557
+ });
44558
+ }
44559
+ get sortValuesAndFields() {
44560
+ const sortedColumn = this.props.definition.sortedColumn;
44561
+ if (!sortedColumn) {
44562
+ return [];
44563
+ }
44564
+ const pivot = this.env.model.getters.getPivot(this.props.pivotId);
44565
+ const locale = this.env.model.getters.getLocale();
44566
+ const currentDomain = [];
44567
+ const sortValues = [];
44568
+ for (const domainItem of sortedColumn.domain) {
44569
+ currentDomain.push(domainItem);
44570
+ const { value, format } = pivot.getPivotHeaderValueAndFormat(currentDomain);
44571
+ const label = formatValue(value, { format, locale });
44572
+ const field = pivot.definition.getDimension(domainItem.field);
44573
+ sortValues.push({ field: getFieldDisplayName(field), value: label });
44574
+ }
44575
+ if (sortedColumn.domain.length === 0) {
44576
+ sortValues.push({ value: _t("Total") });
44577
+ }
44578
+ const measureLabel = pivot.getMeasure(sortedColumn.measure).displayName;
44579
+ sortValues.push({ value: measureLabel, field: _t("Measure") });
44580
+ return sortValues;
44581
+ }
44582
+ }
44583
+
43648
44584
  css /* scss */ `
43649
44585
  .add-calculated-measure {
43650
44586
  cursor: pointer;
@@ -43658,6 +44594,7 @@ class PivotLayoutConfigurator extends owl.Component {
43658
44594
  PivotDimensionOrder,
43659
44595
  PivotDimensionGranularity,
43660
44596
  PivotMeasureEditor,
44597
+ PivotSortSection,
43661
44598
  };
43662
44599
  static props = {
43663
44600
  definition: Object,
@@ -43814,9 +44751,13 @@ class PivotLayoutConfigurator extends owl.Component {
43814
44751
  }
43815
44752
  updateMeasure(measure, newMeasure) {
43816
44753
  const { measures } = this.props.definition;
43817
- this.props.onDimensionsUpdated({
44754
+ const update = {
43818
44755
  measures: measures.map((m) => (m.id === measure.id ? newMeasure : m)),
43819
- });
44756
+ };
44757
+ if (this.props.definition.sortedColumn?.measure === measure.id) {
44758
+ update.sortedColumn = { ...this.props.definition.sortedColumn, measure: newMeasure.id };
44759
+ }
44760
+ this.props.onDimensionsUpdated(update);
43820
44761
  }
43821
44762
  getMeasureId(fieldName, aggregator) {
43822
44763
  const baseId = fieldName + (aggregator ? `:${aggregator}` : "");
@@ -43972,10 +44913,12 @@ class PivotRuntimeDefinition {
43972
44913
  measures;
43973
44914
  columns;
43974
44915
  rows;
44916
+ sortedColumn;
43975
44917
  constructor(definition, fields) {
43976
44918
  this.measures = definition.measures.map((measure) => createMeasure(fields, measure));
43977
44919
  this.columns = definition.columns.map((dimension) => createPivotDimension(fields, dimension));
43978
44920
  this.rows = definition.rows.map((dimension) => createPivotDimension(fields, dimension));
44921
+ this.sortedColumn = definition.sortedColumn;
43979
44922
  }
43980
44923
  getDimension(nameWithGranularity) {
43981
44924
  const dimension = this.columns.find((d) => d.nameWithGranularity === nameWithGranularity) ||
@@ -44129,6 +45072,7 @@ class SpreadsheetPivotTable {
44129
45072
  pivotCells = {};
44130
45073
  rowTree;
44131
45074
  colTree;
45075
+ isSorted = false;
44132
45076
  constructor(columns, rows, measures, fieldsType) {
44133
45077
  this.columns = columns.map((row) => {
44134
45078
  // offset in the pivot table
@@ -44302,6 +45246,7 @@ class SpreadsheetPivotTable {
44302
45246
  value,
44303
45247
  field: row.fields[rowDepth],
44304
45248
  children: [],
45249
+ type: this.fieldsType[fieldName] || "char",
44305
45250
  width: 0, // not used
44306
45251
  };
44307
45252
  treesAtDepth[depth].push(node);
@@ -44324,6 +45269,7 @@ class SpreadsheetPivotTable {
44324
45269
  field: leaf.fields[depth],
44325
45270
  children: [],
44326
45271
  width: leaf.width,
45272
+ type: this.fieldsType[fieldName] || "char",
44327
45273
  };
44328
45274
  if (treesAtDepth[depth]?.at(-1)?.value !== value) {
44329
45275
  treesAtDepth[depth + 1] = [];
@@ -44342,6 +45288,35 @@ class SpreadsheetPivotTable {
44342
45288
  fieldsType: this.fieldsType,
44343
45289
  };
44344
45290
  }
45291
+ sort(measure, sortedColumn, getValue) {
45292
+ if (this.isSorted) {
45293
+ return;
45294
+ }
45295
+ const getSortValue = (measure, domain) => {
45296
+ const rawValue = getValue(measure, domain).value;
45297
+ return typeof rawValue === "number" ? rawValue : -Infinity;
45298
+ };
45299
+ const sortColDomain = sortedColumn.domain;
45300
+ const sortFn = (rowDomain1, rowDomain2) => {
45301
+ const value1 = getSortValue(measure, [...rowDomain1, ...sortColDomain]);
45302
+ const value2 = getSortValue(measure, [...rowDomain2, ...sortColDomain]);
45303
+ return sortedColumn.order === "asc" ? value1 - value2 : value2 - value1;
45304
+ };
45305
+ const sortedRowTree = sortPivotTree(this.rowTree(), [], sortFn);
45306
+ this.rowTree = lazy(sortedRowTree);
45307
+ this.rows = [...this.rowTreeToRows(sortedRowTree), this.rows[this.rows.length - 1]];
45308
+ this.isSorted = true;
45309
+ }
45310
+ rowTreeToRows(tree, parentRow) {
45311
+ return tree.flatMap((node) => {
45312
+ const row = {
45313
+ indent: parentRow ? parentRow.indent + 1 : 0,
45314
+ fields: [...(parentRow?.fields || []), node.field],
45315
+ values: [...(parentRow?.values || []), node.value],
45316
+ };
45317
+ return [row, ...this.rowTreeToRows(node.children, row)];
45318
+ });
45319
+ }
44345
45320
  }
44346
45321
  const EMPTY_PIVOT_CELL = { type: "EMPTY" };
44347
45322
 
@@ -44419,6 +45394,7 @@ function dataEntriesToColumnsTree(dataEntries, columns, index) {
44419
45394
  value: groups[key]?.[0]?.[column.nameWithGranularity]?.value ?? null,
44420
45395
  field: colName,
44421
45396
  children: dataEntriesToColumnsTree(groups[key] || [], columns, index + 1),
45397
+ type: column.type,
44422
45398
  width: 0,
44423
45399
  };
44424
45400
  });
@@ -45284,6 +46260,7 @@ class PivotSidePanelStore extends SpreadsheetStore {
45284
46260
  format: measure.format,
45285
46261
  display: measure.display,
45286
46262
  })),
46263
+ sortedColumn: this.shouldKeepSortedColumn(definition) ? definition.sortedColumn : undefined,
45287
46264
  };
45288
46265
  if (!this.draft && deepEquals(coreDefinition, cleanedDefinition)) {
45289
46266
  return;
@@ -45342,6 +46319,19 @@ class PivotSidePanelStore extends SpreadsheetStore {
45342
46319
  }
45343
46320
  return granularitiesPerFields;
45344
46321
  }
46322
+ /**
46323
+ * Check if we want to keep the sorted column when updating the pivot definition. We should remove it if either
46324
+ * the measure is not in the new definition or the columns have changed.
46325
+ */
46326
+ shouldKeepSortedColumn(newDefinition) {
46327
+ const { sortedColumn } = newDefinition;
46328
+ if (!sortedColumn) {
46329
+ return true;
46330
+ }
46331
+ const oldDefinition = this.getters.getPivotCoreDefinition(this.pivotId);
46332
+ return (newDefinition.measures.find((measure) => measure.id === sortedColumn.measure) &&
46333
+ deepEquals(oldDefinition.columns, newDefinition.columns));
46334
+ }
45345
46335
  }
45346
46336
 
45347
46337
  class PivotSpreadsheetSidePanel extends owl.Component {
@@ -46443,6 +47433,7 @@ css /* scss */ `
46443
47433
  }
46444
47434
  }
46445
47435
  `;
47436
+ const DEFAULT_TABLE_STYLE_COLOR = "#3C78D8";
46446
47437
  class TableStyleEditorPanel extends owl.Component {
46447
47438
  static template = "o-spreadsheet-TableStyleEditorPanel";
46448
47439
  static components = { Section, RoundColorPicker, TableStylePreview };
@@ -46461,7 +47452,7 @@ class TableStyleEditorPanel extends owl.Component {
46461
47452
  : null;
46462
47453
  return {
46463
47454
  pickerOpened: false,
46464
- primaryColor: editedStyle?.primaryColor || "#3C78D8",
47455
+ primaryColor: editedStyle?.primaryColor || DEFAULT_TABLE_STYLE_COLOR,
46465
47456
  selectedTemplateName: editedStyle?.templateName || "lightColoredText",
46466
47457
  styleName: editedStyle?.displayName || this.env.model.getters.getNewCustomTableStyleName(),
46467
47458
  };
@@ -46470,7 +47461,7 @@ class TableStyleEditorPanel extends owl.Component {
46470
47461
  this.state.pickerOpened = !this.state.pickerOpened;
46471
47462
  }
46472
47463
  onColorPicked(color) {
46473
- this.state.primaryColor = color;
47464
+ this.state.primaryColor = isColorValid(color) ? color : DEFAULT_TABLE_STYLE_COLOR;
46474
47465
  this.state.pickerOpened = false;
46475
47466
  }
46476
47467
  onTemplatePicked(templateName) {
@@ -48086,6 +49077,7 @@ class FiguresContainer extends owl.Component {
48086
49077
  draggedFigure: undefined,
48087
49078
  horizontalSnap: undefined,
48088
49079
  verticalSnap: undefined,
49080
+ cancelDnd: undefined,
48089
49081
  });
48090
49082
  setup() {
48091
49083
  owl.onMounted(() => {
@@ -48098,12 +49090,28 @@ class FiguresContainer extends owl.Component {
48098
49090
  // new rendering
48099
49091
  this.render();
48100
49092
  });
49093
+ owl.onWillUpdateProps(() => {
49094
+ const sheetId = this.env.model.getters.getActiveSheetId();
49095
+ const draggedFigureId = this.dnd.draggedFigure?.id;
49096
+ if (draggedFigureId && !this.env.model.getters.getFigure(sheetId, draggedFigureId)) {
49097
+ if (this.dnd.cancelDnd) {
49098
+ this.dnd.cancelDnd();
49099
+ }
49100
+ this.dnd.draggedFigure = undefined;
49101
+ this.dnd.horizontalSnap = undefined;
49102
+ this.dnd.verticalSnap = undefined;
49103
+ this.dnd.cancelDnd = undefined;
49104
+ }
49105
+ });
48101
49106
  }
48102
49107
  getVisibleFigures() {
48103
49108
  const visibleFigures = this.env.model.getters.getVisibleFigures();
48104
49109
  if (this.dnd.draggedFigure &&
48105
49110
  !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));
49111
+ const draggedFigure = this.env.model.getters.getFigure(this.env.model.getters.getActiveSheetId(), this.dnd.draggedFigure?.id);
49112
+ if (draggedFigure) {
49113
+ visibleFigures.push(draggedFigure);
49114
+ }
48107
49115
  }
48108
49116
  return visibleFigures;
48109
49117
  }
@@ -48222,7 +49230,7 @@ class FiguresContainer extends owl.Component {
48222
49230
  this.dnd.verticalSnap = undefined;
48223
49231
  this.env.model.dispatch("UPDATE_FIGURE", { sheetId, id: figure.id, x, y });
48224
49232
  };
48225
- startDnd(onMouseMove, onMouseUp);
49233
+ this.dnd.cancelDnd = startDnd(onMouseMove, onMouseUp);
48226
49234
  }
48227
49235
  /**
48228
49236
  * Initialize the resize of a figure with mouse movements
@@ -48270,7 +49278,7 @@ class FiguresContainer extends owl.Component {
48270
49278
  this.dnd.horizontalSnap = undefined;
48271
49279
  this.dnd.verticalSnap = undefined;
48272
49280
  };
48273
- startDnd(onMouseMove, onMouseUp);
49281
+ this.dnd.cancelDnd = startDnd(onMouseMove, onMouseUp);
48274
49282
  }
48275
49283
  getOtherFigures(figId) {
48276
49284
  return this.getVisibleFigures().filter((f) => f.id !== figId);
@@ -49377,9 +50385,11 @@ class HeadersOverlay extends owl.Component {
49377
50385
  class GridRenderer {
49378
50386
  getters;
49379
50387
  renderer;
50388
+ fingerprints;
49380
50389
  constructor(get) {
49381
50390
  this.getters = get(ModelStore).getters;
49382
50391
  this.renderer = get(RendererStore);
50392
+ this.fingerprints = get(FormulaFingerprintStore);
49383
50393
  this.renderer.register(this);
49384
50394
  }
49385
50395
  get renderingLayers() {
@@ -49857,14 +50867,22 @@ class GridRenderer {
49857
50867
  const showFormula = this.getters.shouldShowFormulas();
49858
50868
  const { x, y, width, height } = this.getters.getVisibleRect(zone);
49859
50869
  const { verticalAlign } = this.getters.getCellStyle(position);
50870
+ let style = this.getters.getCellComputedStyle(position);
50871
+ if (this.fingerprints.isEnabled) {
50872
+ const fingerprintColor = this.fingerprints.colors.get(position);
50873
+ style = { ...style, fillColor: fingerprintColor };
50874
+ }
50875
+ const dataBarFill = this.fingerprints.isEnabled
50876
+ ? undefined
50877
+ : this.getters.getConditionalDataBar(position);
49860
50878
  const box = {
49861
50879
  x,
49862
50880
  y,
49863
50881
  width,
49864
50882
  height,
49865
50883
  border: this.getters.getCellComputedBorder(position) || undefined,
49866
- style: this.getters.getCellComputedStyle(position),
49867
- dataBarFill: this.getters.getConditionalDataBar(position),
50884
+ style,
50885
+ dataBarFill,
49868
50886
  verticalAlign,
49869
50887
  isError: (cell.type === CellValueType.error && !!cell.message) ||
49870
50888
  this.getters.isDataValidationInvalid(position),
@@ -49889,7 +50907,6 @@ class GridRenderer {
49889
50907
  box.hasIcon = this.getters.doesCellHaveGridIcon(position);
49890
50908
  const headerIconWidth = box.hasIcon ? GRID_ICON_EDGE_LENGTH + GRID_ICON_MARGIN : 0;
49891
50909
  /** Content */
49892
- const style = this.getters.getCellComputedStyle(position);
49893
50910
  const wrapping = style.wrapping || "overflow";
49894
50911
  const wrapText = wrapping === "wrap" && !showFormula;
49895
50912
  const maxWidth = width - 2 * MIN_CELL_TEXT_MARGIN;
@@ -51867,62 +52884,6 @@ class BordersPlugin extends CorePlugin {
51867
52884
  }
51868
52885
  }
51869
52886
 
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
52887
  /**
51927
52888
  * Core Plugin
51928
52889
  *
@@ -52361,7 +53322,7 @@ class CellPlugin extends CorePlugin {
52361
53322
  const before = this.getters.getCell({ sheetId, col, row });
52362
53323
  const hasContent = "content" in after || "formula" in after;
52363
53324
  // Compute the new cell properties
52364
- const afterContent = hasContent ? replaceSpecialSpaces(after?.content) : before?.content || "";
53325
+ const afterContent = hasContent ? replaceNewLines(after?.content) : before?.content || "";
52365
53326
  let style;
52366
53327
  if (after.style !== undefined) {
52367
53328
  style = after.style || undefined;
@@ -55306,6 +56267,9 @@ class SheetPlugin extends CorePlugin {
55306
56267
  };
55307
56268
  }
55308
56269
  getUnboundedZone(sheetId, zone) {
56270
+ if (zone.bottom === undefined || zone.right === undefined) {
56271
+ return zone;
56272
+ }
55309
56273
  const isFullRow = zone.left === 0 && zone.right === this.getNumberCols(sheetId) - 1;
55310
56274
  const isFullCol = zone.top === 0 && zone.bottom === this.getNumberRows(sheetId) - 1;
55311
56275
  return {
@@ -59589,66 +60553,25 @@ class EvaluationConditionalFormatPlugin extends UIPlugin {
59589
60553
  return;
59590
60554
  }
59591
60555
  const zone = this.getters.getRangeFromSheetXC(sheetId, range).zone;
59592
- const colorCellArgs = [];
60556
+ const colorThresholds = [{ value: minValue, color: rule.minimum.color }];
59593
60557
  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
- });
60558
+ colorThresholds.push({ value: midValue, color: rule.midpoint.color });
59611
60559
  }
60560
+ colorThresholds.push({ value: maxValue, color: rule.maximum.color });
60561
+ const colorScale = getColorScale(colorThresholds);
59612
60562
  for (let row = zone.top; row <= zone.bottom; row++) {
59613
60563
  for (let col = zone.left; col <= zone.right; col++) {
59614
60564
  const cell = this.getters.getEvaluatedCell({ sheetId, col, row });
59615
60565
  if (cell.type === CellValueType.number) {
59616
60566
  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
60567
  if (!computedStyle[col])
59628
60568
  computedStyle[col] = [];
59629
60569
  computedStyle[col][row] = computedStyle[col]?.[row] || {};
59630
- computedStyle[col][row].fillColor = colorNumberString(color);
60570
+ computedStyle[col][row].fillColor = colorScale(value);
59631
60571
  }
59632
60572
  }
59633
60573
  }
59634
60574
  }
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
60575
  /**
59653
60576
  * Execute the predicate to know if a conditional formatting rule should be applied to a cell
59654
60577
  */
@@ -60274,7 +61197,7 @@ function withPivotPresentationLayer (PivotClass) {
60274
61197
  }
60275
61198
  getValuesToAggregate(measure, domain) {
60276
61199
  const { rowDomain, colDomain } = domainToColRowDomain(this, domain);
60277
- const table = this.getTableStructure();
61200
+ const table = super.getTableStructure();
60278
61201
  const values = [];
60279
61202
  if (colDomain.length === 0 &&
60280
61203
  rowDomain.length < this.definition.rows.length &&
@@ -60708,6 +61631,21 @@ function withPivotPresentationLayer (PivotClass) {
60708
61631
  }
60709
61632
  throw new Error(`Value ${result.value} is not a number`);
60710
61633
  }
61634
+ getTableStructure() {
61635
+ const table = super.getTableStructure();
61636
+ this.sortTableStructure(table);
61637
+ return table;
61638
+ }
61639
+ sortTableStructure(table) {
61640
+ if (!this.definition.sortedColumn || table.isSorted) {
61641
+ return;
61642
+ }
61643
+ const measure = this.definition.sortedColumn.measure;
61644
+ const isSortValid = isSortedColumnValid(this.definition.sortedColumn, this);
61645
+ if (isSortValid) {
61646
+ table.sort(measure, this.definition.sortedColumn, (measure, domain) => this._getPivotCellValueAndFormat(measure, domain));
61647
+ }
61648
+ }
60711
61649
  }
60712
61650
  return PivotPresentationLayer;
60713
61651
  }
@@ -62483,23 +63421,6 @@ class Session extends EventBus {
62483
63421
  }
62484
63422
  }
62485
63423
 
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
63424
  class CollaborativePlugin extends UIPlugin {
62504
63425
  static getters = [
62505
63426
  "getClientsToDisplay",
@@ -62508,7 +63429,7 @@ class CollaborativePlugin extends UIPlugin {
62508
63429
  "isFullySynchronized",
62509
63430
  ];
62510
63431
  static layers = ["Selection"];
62511
- availableColors = new Set(colors);
63432
+ availableColors = new AlternatingColorGenerator(12);
62512
63433
  colors = {};
62513
63434
  session;
62514
63435
  constructor(config) {
@@ -62519,14 +63440,6 @@ class CollaborativePlugin extends UIPlugin {
62519
63440
  return (position.row < this.getters.getNumberRows(position.sheetId) &&
62520
63441
  position.col < this.getters.getNumberCols(position.sheetId));
62521
63442
  }
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
63443
  getClient() {
62531
63444
  return this.session.getClient();
62532
63445
  }
@@ -62561,7 +63474,7 @@ class CollaborativePlugin extends UIPlugin {
62561
63474
  this.isPositionValid(client.position)) {
62562
63475
  const position = client.position;
62563
63476
  if (!this.colors[client.id]) {
62564
- this.colors[client.id] = this.chooseNewColor();
63477
+ this.colors[client.id] = this.availableColors.next();
62565
63478
  }
62566
63479
  const color = this.colors[client.id];
62567
63480
  clients.push({ ...client, position, color });
@@ -62798,7 +63711,7 @@ class FormatPlugin extends UIPlugin {
62798
63711
  for (let row = zone.top; row <= zone.bottom; row++) {
62799
63712
  const position = { sheetId, col, row };
62800
63713
  const pivotCell = this.getters.getPivotCellFromPosition(position);
62801
- if (pivotCell.type === "VALUE") {
63714
+ if (this.isSpilledPivotValueFormula(position, pivotCell)) {
62802
63715
  measurePositions.push(position);
62803
63716
  const pivotId = this.getters.getPivotIdFromPosition(position) || "";
62804
63717
  measuresByPivotId[pivotId] ??= new Set();
@@ -62835,6 +63748,10 @@ class FormatPlugin extends UIPlugin {
62835
63748
  format,
62836
63749
  });
62837
63750
  }
63751
+ isSpilledPivotValueFormula(position, pivotCell) {
63752
+ const cell = this.getters.getCell(position);
63753
+ return pivotCell.type === "VALUE" && !cell?.isFormula;
63754
+ }
62838
63755
  /**
62839
63756
  * This function allows to adjust the quantity of decimal places after a decimal
62840
63757
  * point on cells containing number value. It does this by changing the cells
@@ -62885,6 +63802,69 @@ class FormatPlugin extends UIPlugin {
62885
63802
  }
62886
63803
  }
62887
63804
 
63805
+ class GeoFeaturePlugin extends UIPlugin {
63806
+ static getters = [
63807
+ "getGeoJsonFeatures",
63808
+ "geoFeatureNameToId",
63809
+ "getGeoChartAvailableRegions",
63810
+ ];
63811
+ geoJsonService;
63812
+ geoJsonCache = {};
63813
+ constructor(config) {
63814
+ super(config);
63815
+ this.geoJsonService = config.external.geoJsonService;
63816
+ }
63817
+ getGeoChartAvailableRegions() {
63818
+ if (!this.geoJsonService) {
63819
+ console.error("No geoJsonService provided to the model");
63820
+ return [];
63821
+ }
63822
+ return this.geoJsonService.getAvailableRegions() || [];
63823
+ }
63824
+ getGeoJsonFeatures(region) {
63825
+ if (!this.geoJsonService) {
63826
+ console.error("No geoJsonService provided to the model");
63827
+ return;
63828
+ }
63829
+ const cachedGeoJson = this.geoJsonCache[region];
63830
+ if (cachedGeoJson instanceof Promise) {
63831
+ return undefined;
63832
+ }
63833
+ if (cachedGeoJson !== undefined) {
63834
+ return cachedGeoJson ?? undefined;
63835
+ }
63836
+ this.geoJsonCache[region] = new Promise(async (resolve) => {
63837
+ const json = await this.geoJsonService?.getTopoJson(region);
63838
+ this.geoJsonCache[region] = this.convertToGeoJson(json);
63839
+ this.dispatch("EVALUATE_CHARTS");
63840
+ resolve();
63841
+ });
63842
+ return undefined;
63843
+ }
63844
+ geoFeatureNameToId(region, featureName) {
63845
+ if (!this.geoJsonService) {
63846
+ console.error("No geoJsonService provided to the model");
63847
+ return;
63848
+ }
63849
+ return this.geoJsonService.geoFeatureNameToId(region, featureName);
63850
+ }
63851
+ convertToGeoJson(json) {
63852
+ if (!json) {
63853
+ return null;
63854
+ }
63855
+ // TopoJSON
63856
+ if (json.type === "Topology") {
63857
+ const features = window.ChartGeo.topojson.feature(json, Object.values(json.objects)[0]);
63858
+ return features.type === "FeatureCollection" ? features.features : [features];
63859
+ }
63860
+ // GeoJSON
63861
+ else if (json.type === "FeatureCollection") {
63862
+ return json.features;
63863
+ }
63864
+ throw new Error("Invalid TopoJSON");
63865
+ }
63866
+ }
63867
+
62888
63868
  class HeaderVisibilityUIPlugin extends UIPlugin {
62889
63869
  static getters = [
62890
63870
  "getNextVisibleCellPosition",
@@ -64422,10 +65402,13 @@ class TableResizeUI extends UIPlugin {
64422
65402
  case "RESIZE_TABLE": {
64423
65403
  const table = this.getters.getCoreTableMatchingTopLeft(cmd.sheetId, cmd.zone);
64424
65404
  this.dispatch("UPDATE_TABLE", { ...cmd });
64425
- if (!table || !table.config.automaticAutofill)
65405
+ if (!table)
64426
65406
  return;
64427
- const oldTableZone = table.range.zone;
64428
65407
  const newTableZone = this.getters.getRangeFromRangeData(cmd.newTableRange).zone;
65408
+ this.selection.selectCell(newTableZone.right, newTableZone.bottom);
65409
+ if (!table.config.automaticAutofill)
65410
+ return;
65411
+ const oldTableZone = table.range.zone;
64429
65412
  if (newTableZone.bottom >= oldTableZone.bottom) {
64430
65413
  for (let col = newTableZone.left; col <= newTableZone.right; col++) {
64431
65414
  const autofillSource = { col, row: oldTableZone.bottom, sheetId: cmd.sheetId };
@@ -65631,15 +66614,23 @@ class GridSelectionPlugin extends UIPlugin {
65631
66614
  handler.paste({ zones: pasteTarget, sheetId }, data, { isCutOperation: true });
65632
66615
  const toRemove = isBasedBefore ? cmd.elements.map((el) => el + thickness) : cmd.elements;
65633
66616
  let currentIndex = cmd.base;
66617
+ const resizingGroups = {};
65634
66618
  for (const element of toRemove) {
65635
66619
  const size = this.getters.getHeaderSize(cmd.sheetId, cmd.dimension, element);
66620
+ const currentSize = this.getters.getHeaderSize(cmd.sheetId, cmd.dimension, currentIndex);
66621
+ if (size != currentSize) {
66622
+ resizingGroups[size] ??= [];
66623
+ resizingGroups[size].push(currentIndex);
66624
+ currentIndex += 1;
66625
+ }
66626
+ }
66627
+ for (const size in resizingGroups) {
65636
66628
  this.dispatch("RESIZE_COLUMNS_ROWS", {
65637
66629
  dimension: cmd.dimension,
65638
66630
  sheetId: cmd.sheetId,
65639
- size,
65640
- elements: [currentIndex],
66631
+ size: parseInt(size, 10),
66632
+ elements: resizingGroups[size],
65641
66633
  });
65642
- currentIndex += 1;
65643
66634
  }
65644
66635
  this.dispatch("REMOVE_COLUMNS_ROWS", {
65645
66636
  dimension: cmd.dimension,
@@ -66221,6 +67212,7 @@ class SheetViewPlugin extends UIPlugin {
66221
67212
  break;
66222
67213
  case "DELETE_SHEET":
66223
67214
  this.cleanViewports();
67215
+ this.sheetsWithDirtyViewports.delete(cmd.sheetId);
66224
67216
  break;
66225
67217
  case "ACTIVATE_SHEET":
66226
67218
  this.sheetsWithDirtyViewports.add(cmd.sheetIdTo);
@@ -66871,7 +67863,8 @@ const featurePluginRegistry = new Registry()
66871
67863
  .add("data_cleanup", DataCleanupPlugin)
66872
67864
  .add("table_autofill", TableAutofillPlugin)
66873
67865
  .add("table_ui_resize", TableResizeUI)
66874
- .add("datavalidation_insert", DataValidationInsertionPlugin);
67866
+ .add("datavalidation_insert", DataValidationInsertionPlugin)
67867
+ .add("geo_features", GeoFeaturePlugin);
66875
67868
  // Plugins which have a state, but which should not be shared in collaborative
66876
67869
  const statefulUIPluginRegistry = new Registry()
66877
67870
  .add("selection", GridSelectionPlugin)
@@ -68615,8 +69608,8 @@ css /* scss */ `
68615
69608
  .o-topbar-composer {
68616
69609
  height: fit-content;
68617
69610
  margin-top: -1px;
69611
+ margin-bottom: -1px;
68618
69612
  border: 1px solid;
68619
- z-index: ${ComponentsImportance.TopBarComposer};
68620
69613
  font-family: ${DEFAULT_FONT};
68621
69614
 
68622
69615
  .o-composer:empty:not(:focus):not(.active)::before {
@@ -68674,6 +69667,7 @@ class TopBarComposer extends owl.Component {
68674
69667
  }
68675
69668
  return cssPropertiesToCss({
68676
69669
  "border-color": SELECTION_BORDER_COLOR,
69670
+ "z-index": String(ComponentsImportance.TopBarComposer),
68677
69671
  });
68678
69672
  }
68679
69673
  onFocus(selection) {
@@ -68782,6 +69776,15 @@ css /* scss */ `
68782
69776
  }
68783
69777
  }
68784
69778
 
69779
+ .irregularity-map {
69780
+ border-top: 1px solid ${SEPARATOR_COLOR};
69781
+ height: ${TOPBAR_TOOLBAR_HEIGHT}px;
69782
+
69783
+ .alert-info {
69784
+ border-left: 3px solid ${ALERT_INFO_BORDER};
69785
+ }
69786
+ }
69787
+
68785
69788
  .o-topbar-composer {
68786
69789
  flex-grow: 1;
68787
69790
  }
@@ -68875,8 +69878,10 @@ class TopBar extends owl.Component {
68875
69878
  formatNumberMenuItemSpec = formatNumberMenuItemSpec;
68876
69879
  isntToolbarMenu = false;
68877
69880
  composerFocusStore;
69881
+ fingerprints;
68878
69882
  setup() {
68879
69883
  this.composerFocusStore = useStore(ComposerFocusStore);
69884
+ this.fingerprints = useStore(FormulaFingerprintStore);
68880
69885
  owl.useExternalListener(window, "click", this.onExternalClick);
68881
69886
  owl.onWillStart(() => this.updateCellState());
68882
69887
  owl.onWillUpdateProps(() => this.updateCellState());
@@ -69347,7 +70352,7 @@ class Spreadsheet extends owl.Component {
69347
70352
  properties["grid-template-rows"] = `auto`;
69348
70353
  }
69349
70354
  else {
69350
- properties["grid-template-rows"] = `${TOPBAR_HEIGHT}px auto ${BOTTOMBAR_HEIGHT + 1}px`;
70355
+ properties["grid-template-rows"] = `max-content auto ${BOTTOMBAR_HEIGHT + 1}px`;
69351
70356
  }
69352
70357
  properties["grid-template-columns"] = `auto ${this.sidePanel.panelSize}px`;
69353
70358
  return cssPropertiesToCss(properties);
@@ -70425,9 +71430,6 @@ class SelectionStreamProcessorImpl {
70425
71430
  getBackToDefault() {
70426
71431
  this.stream.getBackToDefault();
70427
71432
  }
70428
- getAnchor() {
70429
- return this.anchor;
70430
- }
70431
71433
  modifyAnchor(anchor, mode, options) {
70432
71434
  const sheetId = this.getters.getActiveSheetId();
70433
71435
  anchor = {
@@ -73359,6 +74361,7 @@ class Model extends EventBus {
73359
74361
  session: this.session,
73360
74362
  defaultCurrency: this.config.defaultCurrency,
73361
74363
  customColors: this.config.customColors || [],
74364
+ external: this.config.external,
73362
74365
  };
73363
74366
  }
73364
74367
  // ---------------------------------------------------------------------------
@@ -73590,7 +74593,6 @@ const SPREADSHEET_DIMENSIONS = {
73590
74593
  MIN_COL_WIDTH,
73591
74594
  HEADER_HEIGHT,
73592
74595
  HEADER_WIDTH,
73593
- TOPBAR_HEIGHT,
73594
74596
  BOTTOMBAR_HEIGHT,
73595
74597
  DEFAULT_CELL_WIDTH,
73596
74598
  DEFAULT_CELL_HEIGHT,
@@ -73831,6 +74833,6 @@ exports.tokenColors = tokenColors;
73831
74833
  exports.tokenize = tokenize;
73832
74834
 
73833
74835
 
73834
- __info__.version = "18.1.0-alpha.7";
73835
- __info__.date = "2024-12-05T10:40:26.512Z";
73836
- __info__.hash = "7b1c39b";
74836
+ __info__.version = "18.1.0";
74837
+ __info__.date = "2024-12-26T06:37:07.879Z";
74838
+ __info__.hash = "c520e89";