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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,9 +2,9 @@
2
2
  /**
3
3
  * This file is generated by o-spreadsheet build tools. Do not edit it.
4
4
  * @see https://github.com/odoo/o-spreadsheet
5
- * @version 18.1.0-alpha.6
6
- * @date 2024-11-28T09:06:59.527Z
7
- * @hash 875c901
5
+ * @version 18.1.0-alpha.8
6
+ * @date 2024-12-19T07:49:54.421Z
7
+ * @hash 87a1567
8
8
  */
9
9
 
10
10
  'use strict';
@@ -172,10 +172,13 @@ const ALERT_INFO_BG = "#CDEDF1";
172
172
  const ALERT_INFO_BORDER = "#98DBE2";
173
173
  const ALERT_INFO_TEXT_COLOR = "#09414A";
174
174
  const BADGE_SELECTED_COLOR = "#E6F2F3";
175
- const DEFAULT_CHART_PADDING = 20;
176
- const DEFAULT_CHART_FONT_SIZE = 22;
177
- const SCORECARD_GAUGE_CHART_PADDING = 10;
178
- const SCORECARD_GAUGE_CHART_FONT_SIZE = 14;
175
+ const CHART_PADDING$1 = 20;
176
+ const CHART_PADDING_BOTTOM = 10;
177
+ const CHART_PADDING_TOP = 15;
178
+ const CHART_TITLE_FONT_SIZE = 16;
179
+ const CHART_AXIS_TITLE_FONT_SIZE = 12;
180
+ const SCORECARD_CHART_TITLE_FONT_SIZE = 14;
181
+ const PIVOT_TOKEN_COLOR = "#F28C28";
179
182
  // Color picker defaults as upper case HEX to match `toHex`helper
180
183
  const COLOR_PICKER_DEFAULTS = [
181
184
  "#000000",
@@ -264,7 +267,6 @@ const MIN_ROW_HEIGHT = 10;
264
267
  const MIN_COL_WIDTH = 5;
265
268
  const HEADER_HEIGHT = 26;
266
269
  const HEADER_WIDTH = 48;
267
- const TOPBAR_HEIGHT = 63;
268
270
  const TOPBAR_TOOLBAR_HEIGHT = 34;
269
271
  const BOTTOMBAR_HEIGHT = 36;
270
272
  const DEFAULT_CELL_WIDTH = 96;
@@ -335,8 +337,8 @@ const DEFAULT_WINDOW_SIZE = 2;
335
337
  const DEBOUNCE_TIME = 200;
336
338
  const MESSAGE_VERSION = 1;
337
339
  // Sheets
338
- const FORBIDDEN_SHEET_CHARS = ["'", "*", "?", "/", "\\", "[", "]"];
339
- const FORBIDDEN_IN_EXCEL_REGEX = /'|\*|\?|\/|\\|\[|\]/;
340
+ const FORBIDDEN_SHEETNAME_CHARS = ["'", "*", "?", "/", "\\", "[", "]"];
341
+ const FORBIDDEN_SHEETNAME_CHARS_IN_EXCEL_REGEX = /'|\*|\?|\/|\\|\[|\]/;
340
342
  // Cells
341
343
  const FORMULA_REF_IDENTIFIER = "|";
342
344
  // Components
@@ -389,6 +391,7 @@ const DEFAULT_CURRENCY = {
389
391
  //------------------------------------------------------------------------------
390
392
  // Miscellaneous
391
393
  //------------------------------------------------------------------------------
394
+ const sanitizeSheetNameRegex = new RegExp(FORBIDDEN_SHEETNAME_CHARS_IN_EXCEL_REGEX, "g");
392
395
  /**
393
396
  * Remove quotes from a quoted string
394
397
  * ```js
@@ -484,6 +487,10 @@ function getCanonicalSymbolName(symbolName) {
484
487
  }
485
488
  return symbolName;
486
489
  }
490
+ /** Replace the excel-excluded characters of a sheetName */
491
+ function sanitizeSheetName(sheetName, replacementChar = " ") {
492
+ return sheetName.replace(sanitizeSheetNameRegex, replacementChar);
493
+ }
487
494
  function clip(val, min, max) {
488
495
  return val < min ? min : val > max ? max : val;
489
496
  }
@@ -783,6 +790,7 @@ function removeFalsyAttributes(obj) {
783
790
  * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Character_Classes
784
791
  */
785
792
  const whiteSpaceSpecialCharacters = [
793
+ " ",
786
794
  "\t",
787
795
  "\f",
788
796
  "\v",
@@ -797,17 +805,15 @@ const whiteSpaceSpecialCharacters = [
797
805
  String.fromCharCode(parseInt("3000", 16)),
798
806
  String.fromCharCode(parseInt("feff", 16)),
799
807
  ];
800
- 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;
801
810
  /**
802
- * Replace all the special spaces in a string (non-breaking, tabs, ...) by normal spaces, and all the
803
- * different newlines types by \n.
811
+ * Replace all different newlines characters by \n
804
812
  */
805
- function replaceSpecialSpaces(text) {
813
+ function replaceNewLines(text) {
806
814
  if (!text)
807
815
  return "";
808
- if (!whiteSpaceRegexp.test(text))
809
- return text;
810
- return text.replace(whiteSpaceRegexp, (match, newLine) => (newLine ? NEWLINE : " "));
816
+ return text.replace(newLineRegexp, NEWLINE);
811
817
  }
812
818
  /**
813
819
  * Determine if the numbers are consecutive.
@@ -973,7 +979,7 @@ function transpose2dPOJO(pojo) {
973
979
 
974
980
  const RBA_REGEX = /rgba?\(|\s+|\)/gi;
975
981
  const HEX_MATCH = /^#([A-F\d]{2}){3,4}$/;
976
- const colors$1 = [
982
+ const colors = [
977
983
  "#eb6d00",
978
984
  "#0074d9",
979
985
  "#ad8e00",
@@ -996,6 +1002,12 @@ const colors$1 = [
996
1002
  function colorNumberString(color) {
997
1003
  return toHex(color.toString(16).padStart(6, "0"));
998
1004
  }
1005
+ function colorToNumber(color) {
1006
+ if (typeof color === "number") {
1007
+ return color;
1008
+ }
1009
+ return Number.parseInt(toHex(color).slice(1), 16);
1010
+ }
999
1011
  /**
1000
1012
  * Converts any CSS color value to a standardized hex6 value.
1001
1013
  * Accepts: hex3, hex6, hex8, rgb[1] and rgba[1].
@@ -1346,9 +1358,9 @@ const COLORS_XL = [
1346
1358
  "#056BD9", // Blue #3
1347
1359
  "#155193", // Blue #4
1348
1360
  "#A76DBC", // Violet #1
1349
- "#7F4295", // Violet #1
1350
- "#6D2387", // Violet #1
1351
- "#4F1565", // Violet #1
1361
+ "#7F4295", // Violet #2
1362
+ "#6D2387", // Violet #3
1363
+ "#4F1565", // Violet #4
1352
1364
  "#EA6175", // Red #1
1353
1365
  "#CE4257", // Red #2
1354
1366
  "#982738", // Red #3
@@ -1374,6 +1386,81 @@ const COLORS_XL = [
1374
1386
  "#C08A16", // Yellow #3
1375
1387
  "#936A12", // Yellow #4
1376
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
+ ];
1377
1464
  function getNthColor(index, palette) {
1378
1465
  return palette[index % palette.length];
1379
1466
  }
@@ -1391,6 +1478,20 @@ function getColorsPalette(quantity) {
1391
1478
  return COLORS_XL;
1392
1479
  }
1393
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
+ }
1394
1495
  class ColorGenerator {
1395
1496
  preferredColors;
1396
1497
  currentColorIndex = 0;
@@ -1405,6 +1506,62 @@ class ColorGenerator {
1405
1506
  : getNthColor(this.currentColorIndex++, this.palette);
1406
1507
  }
1407
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
+ }
1408
1565
 
1409
1566
  //------------------------------------------------------------------------------
1410
1567
  // Coordinate
@@ -3276,6 +3433,7 @@ const invalidateEvaluationCommands = new Set([
3276
3433
  ]);
3277
3434
  const invalidateChartEvaluationCommands = new Set([
3278
3435
  "EVALUATE_CELLS",
3436
+ "EVALUATE_CHARTS",
3279
3437
  "UPDATE_CELL",
3280
3438
  "UNHIDE_COLUMNS_ROWS",
3281
3439
  "HIDE_COLUMNS_ROWS",
@@ -3312,6 +3470,7 @@ const readonlyAllowedCommands = new Set([
3312
3470
  "RESIZE_SHEETVIEW",
3313
3471
  "SET_VIEWPORT_OFFSET",
3314
3472
  "EVALUATE_CELLS",
3473
+ "EVALUATE_CHARTS",
3315
3474
  "SET_FORMULA_VISIBILITY",
3316
3475
  "UPDATE_FILTER",
3317
3476
  ]);
@@ -6516,7 +6675,7 @@ class BorderClipboardHandler extends AbstractCellClipboardHandler {
6516
6675
  const POSTFIX_UNARY_OPERATORS = ["%"];
6517
6676
  const OPERATORS = "+,-,*,/,:,=,<>,>=,>,<=,<,^,&".split(",").concat(POSTFIX_UNARY_OPERATORS);
6518
6677
  function tokenize(str, locale = DEFAULT_LOCALE) {
6519
- str = replaceSpecialSpaces(str);
6678
+ str = replaceNewLines(str);
6520
6679
  const chars = new TokenizingChars(str);
6521
6680
  const result = [];
6522
6681
  while (!chars.isOver()) {
@@ -6663,12 +6822,12 @@ function tokenizeSpace(chars) {
6663
6822
  if (length) {
6664
6823
  return { type: "SPACE", value: NEWLINE.repeat(length) };
6665
6824
  }
6666
- while (chars.current === " ") {
6667
- length++;
6668
- chars.shift();
6825
+ let spaces = "";
6826
+ while (chars.current && chars.current.match(whiteSpaceRegexp)) {
6827
+ spaces += chars.shift();
6669
6828
  }
6670
- if (length) {
6671
- return { type: "SPACE", value: " ".repeat(length) };
6829
+ if (spaces) {
6830
+ return { type: "SPACE", value: spaces };
6672
6831
  }
6673
6832
  return null;
6674
6833
  }
@@ -7527,6 +7686,16 @@ function getRunningTotalDomainKey(domain, fieldNameWithGranularity) {
7527
7686
  }
7528
7687
  return domainToString([...domain.slice(0, index), ...domain.slice(index + 1)]);
7529
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
+ }
7530
7699
 
7531
7700
  const pivotTimeAdapterRegistry = new Registry();
7532
7701
  function pivotTimeAdapter(granularity) {
@@ -8037,6 +8206,29 @@ function addIndentAndAlignToPivotHeader(pivot, domain, functionResult) {
8037
8206
  format: `${" ".repeat(indent)}${format}* `,
8038
8207
  };
8039
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
+ }
8040
8232
 
8041
8233
  class CellClipboardHandler extends AbstractCellClipboardHandler {
8042
8234
  isCutAllowed(data) {
@@ -9516,6 +9708,12 @@ function chartFontColor(backgroundColor) {
9516
9708
  }
9517
9709
  return relativeLuminance(backgroundColor) < 0.3 ? "#FFFFFF" : "#000000";
9518
9710
  }
9711
+ function chartMutedFontColor(backgroundColor) {
9712
+ if (!backgroundColor) {
9713
+ return "#666666";
9714
+ }
9715
+ return relativeLuminance(backgroundColor) < 0.3 ? "#C8C8C8" : "#666666";
9716
+ }
9519
9717
  function checkDataset(definition) {
9520
9718
  if (definition.dataSets) {
9521
9719
  const invalidRanges = definition.dataSets.find((range) => !rangeReference.test(range.dataRange)) !== undefined;
@@ -9651,8 +9849,8 @@ function drawLineOrBarOrRadarChartValues(chart, options, ctx) {
9651
9849
  const yMin = chart.chartArea.top;
9652
9850
  const textsPositions = {};
9653
9851
  for (const dataset of chart._metasets) {
9654
- if (dataset.xAxisID === TREND_LINE_XAXIS_ID) {
9655
- return; // ignore trend lines
9852
+ if (dataset.xAxisID === TREND_LINE_XAXIS_ID || dataset.hidden) {
9853
+ continue;
9656
9854
  }
9657
9855
  for (let i = 0; i < dataset._parsed.length; i++) {
9658
9856
  const parsedValue = dataset._parsed[i];
@@ -10099,7 +10297,7 @@ let ScorecardChart$1 = class ScorecardChart extends AbstractChart {
10099
10297
  function drawScoreChart(structure, canvas) {
10100
10298
  const ctx = canvas.getContext("2d");
10101
10299
  canvas.width = structure.canvas.width;
10102
- const availableWidth = canvas.width - DEFAULT_CHART_PADDING;
10300
+ const availableWidth = canvas.width - CHART_PADDING$1 * 2;
10103
10301
  canvas.height = structure.canvas.height;
10104
10302
  ctx.fillStyle = structure.canvas.backgroundColor;
10105
10303
  ctx.fillRect(0, 0, structure.canvas.width, structure.canvas.height);
@@ -10234,10 +10432,9 @@ function createScorecardChartRuntime(chart, getters) {
10234
10432
  }
10235
10433
 
10236
10434
  /* Padding at the border of the chart */
10237
- const CHART_PADDING = SCORECARD_GAUGE_CHART_PADDING;
10435
+ const CHART_PADDING = 10;
10238
10436
  const BOTTOM_PADDING_RATIO = 0.05;
10239
10437
  /* Maximum font sizes of each element */
10240
- const CHART_TITLE_FONT_SIZE = SCORECARD_GAUGE_CHART_FONT_SIZE;
10241
10438
  const KEY_VALUE_FONT_SIZE = 32;
10242
10439
  const BASELINE_MAX_FONT_SIZE = 16;
10243
10440
  function formatBaselineDescr(baselineDescr, baseline) {
@@ -10309,7 +10506,7 @@ class ScorecardChartConfigBuilder {
10309
10506
  : this.height - (this.height - titleHeight - baselineHeight) / 2 - CHART_PADDING,
10310
10507
  },
10311
10508
  };
10312
- const minimalBaselinePosition = baselineArrowSize + DEFAULT_CHART_PADDING;
10509
+ const minimalBaselinePosition = baselineArrowSize + CHART_PADDING * 2;
10313
10510
  if (structure.baseline.position.x < minimalBaselinePosition) {
10314
10511
  structure.baseline.position.x = minimalBaselinePosition;
10315
10512
  }
@@ -10389,7 +10586,7 @@ class ScorecardChartConfigBuilder {
10389
10586
  return this.runtime.background;
10390
10587
  }
10391
10588
  get secondaryFontColor() {
10392
- return relativeLuminance(this.backgroundColor) > 0.3 ? "#525252" : "#C8C8C8";
10589
+ return chartMutedFontColor(this.backgroundColor);
10393
10590
  }
10394
10591
  getTextDimensions(text, font) {
10395
10592
  this.context.font = font;
@@ -10415,7 +10612,7 @@ class ScorecardChartConfigBuilder {
10415
10612
  }
10416
10613
  return {
10417
10614
  title: {
10418
- font: getDefaultContextFont(CHART_TITLE_FONT_SIZE, this.runtime.title.bold, this.runtime.title.italic),
10615
+ font: getDefaultContextFont(this.runtime.title.fontSize ?? SCORECARD_CHART_TITLE_FONT_SIZE, this.runtime.title.bold, this.runtime.title.italic),
10419
10616
  color: this.runtime.title.color ?? this.secondaryFontColor,
10420
10617
  },
10421
10618
  keyValue: {
@@ -14640,7 +14837,7 @@ const SORT_TYPES = [
14640
14837
  CellValueType.boolean,
14641
14838
  ];
14642
14839
  function cellsSortingCriterion(sortingOrder) {
14643
- const inverse = sortingOrder === "ascending" ? 1 : -1;
14840
+ const inverse = sortingOrder === "asc" ? 1 : -1;
14644
14841
  return (left, right) => {
14645
14842
  if (left.type === CellValueType.empty) {
14646
14843
  return right.type === CellValueType.empty ? 0 : 1;
@@ -14732,7 +14929,7 @@ function sortMatrix(matrix, locale, ...criteria) {
14732
14929
  const sortColumns = [];
14733
14930
  const nRows = matrix.length;
14734
14931
  for (let i = 0; i < criteria.length; i += 2) {
14735
- sortingOrders.push(toBoolean(toScalar(criteria[i + 1])?.value) ? "ascending" : "descending");
14932
+ sortingOrders.push(toBoolean(toScalar(criteria[i + 1])?.value) ? "asc" : "desc");
14736
14933
  const sortColumn = criteria[i];
14737
14934
  if (isMatrix(sortColumn) && (sortColumn.length > 1 || sortColumn[0].length > 1)) {
14738
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));
@@ -14749,12 +14946,12 @@ function sortMatrix(matrix, locale, ...criteria) {
14749
14946
  if (sortColumns.length === 0) {
14750
14947
  for (let i = 0; i < matrix[0].length; i++) {
14751
14948
  sortColumns.push(matrix.map((row) => row[i].value));
14752
- sortingOrders.push("ascending");
14949
+ sortingOrders.push("asc");
14753
14950
  }
14754
14951
  }
14755
14952
  const sortingCriteria = {
14756
- descending: cellsSortingCriterion("descending"),
14757
- ascending: cellsSortingCriterion("ascending"),
14953
+ desc: cellsSortingCriterion("desc"),
14954
+ asc: cellsSortingCriterion("asc"),
14758
14955
  };
14759
14956
  const indexes = range(0, matrix.length);
14760
14957
  indexes.sort((a, b) => {
@@ -20478,20 +20675,8 @@ class AbstractComposerStore extends SpreadsheetStore {
20478
20675
  replaceSelectedRange(zone) {
20479
20676
  const ref = this.getZoneReference(zone);
20480
20677
  const currentToken = this.tokenAtCursor;
20481
- let replaceStart = this.selectionStart;
20482
- if (currentToken?.type === "REFERENCE") {
20483
- replaceStart = currentToken.start;
20484
- }
20485
- else if (currentToken?.type === "RIGHT_PAREN") {
20486
- // match left parenthesis
20487
- const leftParenthesisIndex = this.currentTokens.findIndex((token) => token.type === "LEFT_PAREN" && token.parenthesesCode === currentToken.parenthesesCode);
20488
- const functionToken = this.currentTokens[leftParenthesisIndex - 1];
20489
- if (functionToken === undefined) {
20490
- return;
20491
- }
20492
- replaceStart = functionToken.start;
20493
- }
20494
- this.replaceText(ref, replaceStart, this.selectionEnd);
20678
+ const start = currentToken?.type === "REFERENCE" ? currentToken.start : this.selectionStart;
20679
+ this.replaceText(ref, start, this.selectionEnd);
20495
20680
  }
20496
20681
  /**
20497
20682
  * Replace the reference of the old zone by the new one.
@@ -20524,17 +20709,6 @@ class AbstractComposerStore extends SpreadsheetStore {
20524
20709
  getZoneReference(zone) {
20525
20710
  const inputSheetId = this.sheetId;
20526
20711
  const sheetId = this.getters.getActiveSheetId();
20527
- if (zone.top === zone.bottom && zone.left === zone.right) {
20528
- const position = { sheetId, col: zone.left, row: zone.top };
20529
- const pivotId = this.getters.getPivotIdFromPosition(position);
20530
- const pivotCell = this.getters.getPivotCellFromPosition(position);
20531
- const cell = this.getters.getCell(position);
20532
- if (pivotId && pivotCell.type !== "EMPTY" && !cell?.isFormula) {
20533
- const formulaPivotId = this.getters.getPivotFormulaId(pivotId);
20534
- const formula = createPivotFormula(formulaPivotId, pivotCell);
20535
- return localizeFormula(formula, this.getters.getLocale()).slice(1); // strip leading =
20536
- }
20537
- }
20538
20712
  const range = this.getters.getRangeFromZone(sheetId, zone);
20539
20713
  return this.getters.getSelectionRangeString(range, inputSheetId);
20540
20714
  }
@@ -20705,37 +20879,21 @@ class AbstractComposerStore extends SpreadsheetStore {
20705
20879
  const editionSheetId = this.sheetId;
20706
20880
  const rangeColor = (rangeString) => {
20707
20881
  const colorIndex = this.colorIndexByRange[rangeString];
20708
- return colors$1[colorIndex % colors$1.length];
20882
+ return colors[colorIndex % colors.length];
20709
20883
  };
20710
- const highlights = [];
20711
- for (const range of this.getReferencedRanges()) {
20884
+ return this.getReferencedRanges().map((range) => {
20712
20885
  const rangeString = this.getters.getRangeString(range, editionSheetId);
20713
20886
  const { numberOfRows, numberOfCols } = zoneToDimension(range.zone);
20714
20887
  const zone = numberOfRows * numberOfCols === 1
20715
20888
  ? this.getters.expandZone(range.sheetId, range.zone)
20716
20889
  : range.zone;
20717
- highlights.push({
20890
+ return {
20718
20891
  zone,
20719
20892
  color: rangeColor(rangeString),
20720
20893
  sheetId: range.sheetId,
20721
20894
  interactive: true,
20722
- });
20723
- }
20724
- const activeSheetId = this.getters.getActiveSheetId();
20725
- const selectionZone = this.model.selection.getAnchor().zone;
20726
- const isSelectionHightlighted = highlights.find((highlight) => highlight.sheetId === activeSheetId && isEqual(highlight.zone, selectionZone));
20727
- if (this.editionMode === "selecting" && !isSelectionHightlighted) {
20728
- highlights.push({
20729
- zone: selectionZone,
20730
- color: "#445566",
20731
- sheetId: activeSheetId,
20732
- dashed: true,
20733
- interactive: false,
20734
- noFill: true,
20735
- thinLine: true,
20736
- });
20737
- }
20738
- return highlights;
20895
+ };
20896
+ });
20739
20897
  }
20740
20898
  /**
20741
20899
  * Return ranges currently referenced in the composer
@@ -20963,6 +21121,7 @@ function compileTokens(tokens) {
20963
21121
  return error;
20964
21122
  },
20965
21123
  isBadExpression: true,
21124
+ normalizedFormula: tokens.map((t) => t.value).join(""),
20966
21125
  };
20967
21126
  }
20968
21127
  }
@@ -21090,6 +21249,7 @@ function compileTokensOrThrow(tokens) {
21090
21249
  symbols,
21091
21250
  tokens,
21092
21251
  isBadExpression: false,
21252
+ normalizedFormula: cacheKey,
21093
21253
  };
21094
21254
  return compiledFormula;
21095
21255
  }
@@ -21236,7 +21396,7 @@ function getFunctionsFromAST(ast, functionNames) {
21236
21396
 
21237
21397
  const PIVOT_FUNCTIONS = ["PIVOT.VALUE", "PIVOT.HEADER", "PIVOT"];
21238
21398
  /**
21239
- * Create a proposal entry for the compose autowcomplete
21399
+ * Create a proposal entry for the composer autocomplete
21240
21400
  * to insert a field name string in a formula.
21241
21401
  */
21242
21402
  function makeFieldProposal(field, granularity) {
@@ -21354,6 +21514,7 @@ autoCompleteProviders.add("pivot_ids", {
21354
21514
  description: definition.name,
21355
21515
  htmlContent: [{ value: str, color: tokenColors.NUMBER }],
21356
21516
  fuzzySearchKey: str + definition.name,
21517
+ alwaysExpanded: true,
21357
21518
  };
21358
21519
  })
21359
21520
  .filter(isDefined);
@@ -22019,14 +22180,8 @@ const GAUGE_PADDING_BOTTOM = 20;
22019
22180
  const GAUGE_LABELS_FONT_SIZE = 12;
22020
22181
  const GAUGE_DEFAULT_VALUE_FONT_SIZE = 80;
22021
22182
  const GAUGE_BACKGROUND_COLOR = "#F3F2F1";
22022
- const GAUGE_TEXT_COLOR = "#666666";
22023
- const GAUGE_TEXT_COLOR_HIGH_CONTRAST = "#C8C8C8";
22024
- const GAUGE_INFLECTION_MARKER_COLOR = "#666666aa";
22025
22183
  const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
22026
22184
  const GAUGE_TITLE_SECTION_HEIGHT = 25;
22027
- const GAUGE_TITLE_FONT_SIZE = SCORECARD_GAUGE_CHART_FONT_SIZE;
22028
- const GAUGE_TITLE_PADDING_LEFT = SCORECARD_GAUGE_CHART_PADDING;
22029
- const GAUGE_TITLE_PADDING_TOP = SCORECARD_GAUGE_CHART_PADDING;
22030
22185
  function drawGaugeChart(canvas, runtime) {
22031
22186
  const canvasBoundingRect = canvas.getBoundingClientRect();
22032
22187
  canvas.width = canvasBoundingRect.width;
@@ -22085,7 +22240,7 @@ function drawInflectionValues(ctx, config) {
22085
22240
  ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment
22086
22241
  ctx.rotate(Math.PI / 2 - inflectionValue.rotation);
22087
22242
  ctx.lineWidth = 2;
22088
- ctx.strokeStyle = GAUGE_INFLECTION_MARKER_COLOR;
22243
+ ctx.strokeStyle = chartMutedFontColor(config.backgroundColor) + "aa";
22089
22244
  ctx.beginPath();
22090
22245
  ctx.moveTo(0, -(height - config.gauge.arcWidth));
22091
22246
  ctx.lineTo(0, -height - 3);
@@ -22139,22 +22294,22 @@ function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
22139
22294
  x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
22140
22295
  y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
22141
22296
  };
22142
- const textColor = getContrastedTextColor(runtime.background);
22297
+ const textColor = chartMutedFontColor(runtime.background);
22143
22298
  const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);
22144
22299
  let x = 0, titleWidth = 0, titleHeight = 0;
22145
22300
  if (runtime.title.text) {
22146
- ({ width: titleWidth, height: titleHeight } = computeTextDimension(ctx, runtime.title.text, { ...runtime.title, fontSize: GAUGE_TITLE_FONT_SIZE }, "px"));
22301
+ ({ width: titleWidth, height: titleHeight } = computeTextDimension(ctx, runtime.title.text, { fontSize: CHART_TITLE_FONT_SIZE, ...runtime.title }, "px"));
22147
22302
  }
22148
22303
  switch (runtime.title.align) {
22149
22304
  case "right":
22150
- x = boundingRect.width - titleWidth - GAUGE_TITLE_PADDING_LEFT;
22305
+ x = boundingRect.width - titleWidth - CHART_PADDING$1;
22151
22306
  break;
22152
22307
  case "center":
22153
22308
  x = (boundingRect.width - titleWidth) / 2;
22154
22309
  break;
22155
22310
  case "left":
22156
22311
  default:
22157
- x = GAUGE_TITLE_PADDING_LEFT;
22312
+ x = CHART_PADDING$1;
22158
22313
  break;
22159
22314
  }
22160
22315
  return {
@@ -22162,10 +22317,10 @@ function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
22162
22317
  height: boundingRect.height,
22163
22318
  title: {
22164
22319
  label: runtime.title.text ?? "",
22165
- fontSize: GAUGE_TITLE_FONT_SIZE,
22320
+ fontSize: runtime.title.fontSize ?? CHART_TITLE_FONT_SIZE,
22166
22321
  textPosition: {
22167
22322
  x,
22168
- y: GAUGE_TITLE_PADDING_TOP + titleHeight / 2,
22323
+ y: CHART_PADDING_TOP + titleHeight / 2,
22169
22324
  },
22170
22325
  color: runtime.title.color ?? textColor,
22171
22326
  bold: runtime.title.bold,
@@ -22282,11 +22437,6 @@ function getGaugeColor(runtime) {
22282
22437
  }
22283
22438
  return runtime.colors.at(-1);
22284
22439
  }
22285
- function getContrastedTextColor(backgroundColor) {
22286
- return relativeLuminance(backgroundColor) > 0.3
22287
- ? GAUGE_TEXT_COLOR
22288
- : GAUGE_TEXT_COLOR_HIGH_CONTRAST;
22289
- }
22290
22440
  function getSegmentsOfRectangle(rectangle) {
22291
22441
  return [
22292
22442
  { start: rectangle.topLeft, end: rectangle.topRight },
@@ -27000,13 +27150,12 @@ migrationStepRegistry
27000
27150
  versionFrom: "7",
27001
27151
  migrate(data) {
27002
27152
  const namesTaken = [];
27003
- const globalForbiddenInExcel = new RegExp(FORBIDDEN_IN_EXCEL_REGEX, "g");
27004
27153
  for (let sheet of data.sheets || []) {
27005
27154
  if (!sheet.name) {
27006
27155
  continue;
27007
27156
  }
27008
27157
  const oldName = sheet.name;
27009
- const escapedName = oldName.replace(globalForbiddenInExcel, "_");
27158
+ const escapedName = sanitizeSheetName(oldName, "_");
27010
27159
  let i = 1;
27011
27160
  let newName = escapedName;
27012
27161
  while (namesTaken.includes(newName)) {
@@ -27759,6 +27908,19 @@ const ChartTerms = {
27759
27908
  ["GaugeLowerInflectionPointNaN" /* CommandResult.GaugeLowerInflectionPointNaN */]: _t("The lower inflection point value must be a number"),
27760
27909
  ["GaugeUpperInflectionPointNaN" /* CommandResult.GaugeUpperInflectionPointNaN */]: _t("The upper inflection point value must be a number"),
27761
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
+ },
27762
27924
  };
27763
27925
  const CustomCurrencyTerms = {
27764
27926
  Custom: _t("Custom"),
@@ -28162,6 +28324,26 @@ function getRadarChartData(definition, dataSets, labelRange, getters) {
28162
28324
  locale: getters.getLocale(),
28163
28325
  };
28164
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
+ }
28165
28347
  function getTrendDatasetForBarChart(config, data) {
28166
28348
  const filteredValues = [];
28167
28349
  const filteredLabels = [];
@@ -28242,37 +28424,45 @@ function interpolateData(config, values, labels, newLabels) {
28242
28424
  const labelRange = labelMax - labelMin;
28243
28425
  const normalizedLabels = labels.map((v) => (v - labelMin) / labelRange);
28244
28426
  const normalizedNewLabels = newLabels.map((v) => (v - labelMin) / labelRange);
28245
- switch (config.type) {
28246
- case "polynomial": {
28247
- const order = config.order ?? 2;
28248
- if (order === 1) {
28249
- return predictLinearValues([values], [normalizedLabels], [normalizedNewLabels], true)[0];
28250
- }
28251
- const coeffs = polynomialRegression(values, normalizedLabels, order, true).flat();
28252
- return normalizedNewLabels.map((v) => evaluatePolynomial(coeffs, v, order));
28253
- }
28254
- case "exponential": {
28255
- const positiveLogValues = [];
28256
- const filteredLabels = [];
28257
- for (let i = 0; i < values.length; i++) {
28258
- if (values[i] > 0) {
28259
- positiveLogValues.push(Math.log(values[i]));
28260
- filteredLabels.push(normalizedLabels[i]);
28427
+ try {
28428
+ switch (config.type) {
28429
+ case "polynomial": {
28430
+ const order = config.order;
28431
+ if (!order) {
28432
+ return Array.from({ length: newLabels.length }, () => NaN);
28433
+ }
28434
+ if (order === 1) {
28435
+ return predictLinearValues([values], [normalizedLabels], [normalizedNewLabels], true)[0];
28436
+ }
28437
+ const coeffs = polynomialRegression(values, normalizedLabels, order, true).flat();
28438
+ return normalizedNewLabels.map((v) => evaluatePolynomial(coeffs, v, order));
28439
+ }
28440
+ case "exponential": {
28441
+ const positiveLogValues = [];
28442
+ const filteredLabels = [];
28443
+ for (let i = 0; i < values.length; i++) {
28444
+ if (values[i] > 0) {
28445
+ positiveLogValues.push(Math.log(values[i]));
28446
+ filteredLabels.push(normalizedLabels[i]);
28447
+ }
28448
+ }
28449
+ if (!filteredLabels.length) {
28450
+ return Array.from({ length: newLabels.length }, () => NaN);
28261
28451
  }
28452
+ return expM(predictLinearValues([positiveLogValues], [filteredLabels], [normalizedNewLabels], true))[0];
28262
28453
  }
28263
- if (!filteredLabels.length) {
28264
- return [];
28454
+ case "logarithmic": {
28455
+ return predictLinearValues([values], logM([normalizedLabels]), logM([normalizedNewLabels]), true)[0];
28265
28456
  }
28266
- return expM(predictLinearValues([positiveLogValues], [filteredLabels], [normalizedNewLabels], true))[0];
28267
- }
28268
- case "logarithmic": {
28269
- return predictLinearValues([values], logM([normalizedLabels]), logM([normalizedNewLabels]), true)[0];
28270
- }
28271
- case "trailingMovingAverage": {
28272
- return getMovingAverageValues(values, config.window);
28457
+ case "trailingMovingAverage": {
28458
+ return getMovingAverageValues(values, config.window);
28459
+ }
28460
+ default:
28461
+ return Array.from({ length: newLabels.length }, () => NaN);
28273
28462
  }
28274
- default:
28275
- return [];
28463
+ }
28464
+ catch (e) {
28465
+ return Array.from({ length: newLabels.length }, () => NaN);
28276
28466
  }
28277
28467
  }
28278
28468
  function getChartAxisType(chart, labelRange, getters) {
@@ -28706,6 +28896,43 @@ function getRadarChartDatasets(definition, args) {
28706
28896
  }
28707
28897
  return datasets;
28708
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
+ }
28709
28936
  function getTrendingLineDataSet(dataset, config, data) {
28710
28937
  const defaultBorderColor = colorToRGBA(dataset.backgroundColor);
28711
28938
  defaultBorderColor.a = 1;
@@ -28743,54 +28970,16 @@ function getChartColorsGenerator(definition, dataSetsSize) {
28743
28970
  return new ColorGenerator(dataSetsSize, definition.dataSets?.map((ds) => ds.backgroundColor) || []);
28744
28971
  }
28745
28972
 
28746
- function getCommonChartLayout(definition) {
28747
- // TODO FIXME: this is unused ATM. All the charts should probably use this instead oh whatever padding they are using now
28748
- // also look into how DEFAULT_CHART_PADDING is used in scorecards, it look strange
28973
+ function getChartLayout(definition) {
28749
28974
  return {
28750
28975
  padding: {
28751
- left: DEFAULT_CHART_PADDING,
28752
- right: DEFAULT_CHART_PADDING,
28753
- top: definition.title?.text ? DEFAULT_CHART_PADDING / 2 : DEFAULT_CHART_PADDING + 5,
28754
- bottom: DEFAULT_CHART_PADDING,
28976
+ left: CHART_PADDING$1,
28977
+ right: CHART_PADDING$1,
28978
+ top: CHART_PADDING_TOP,
28979
+ bottom: CHART_PADDING_BOTTOM,
28755
28980
  },
28756
28981
  };
28757
28982
  }
28758
- function getBarChartLayout(definition) {
28759
- return {
28760
- padding: computeChartPadding({
28761
- displayTitle: !!definition.title?.text,
28762
- displayLegend: definition.legendPosition === "top",
28763
- }),
28764
- };
28765
- }
28766
- function getLineChartLayout(definition) {
28767
- return {
28768
- padding: computeChartPadding({
28769
- displayTitle: !!definition.title?.text,
28770
- displayLegend: definition.legendPosition === "top",
28771
- }),
28772
- };
28773
- }
28774
- function getPieChartLayout(definition) {
28775
- return {
28776
- padding: { left: 20, right: 20, top: definition.title ? 10 : 25, bottom: 10 },
28777
- };
28778
- }
28779
- function getWaterfallChartLayout(definition) {
28780
- return {
28781
- padding: { left: 20, right: 20, top: definition.title ? 10 : 25, bottom: 10 },
28782
- };
28783
- }
28784
- function computeChartPadding({ displayTitle, displayLegend, }) {
28785
- let top = 25;
28786
- if (displayTitle) {
28787
- top = 0;
28788
- }
28789
- else if (displayLegend) {
28790
- top = 10;
28791
- }
28792
- return { left: 20, right: 20, top, bottom: 10 };
28793
- }
28794
28983
 
28795
28984
  function getLegendDisplayOptions(definition, args) {
28796
28985
  return {
@@ -28848,11 +29037,12 @@ function getScatterChartLegend(definition, args) {
28848
29037
  return {
28849
29038
  ...INTERACTIVE_LEGEND_CONFIG,
28850
29039
  ...getLegendDisplayOptions(definition),
28851
- labels: {
28852
- color: chartFontColor(definition.background),
28853
- boxHeight: 6,
28854
- usePointStyle: true,
28855
- },
29040
+ ...getCustomLegendLabels(chartFontColor(definition.background), {
29041
+ pointStyle: "circle",
29042
+ // the stroke is the border around the circle, so increasing its size with the chart's color reduce the size of the circle
29043
+ strokeStyle: definition.background || "#ffffff",
29044
+ lineWidth: 8,
29045
+ }),
28856
29046
  };
28857
29047
  }
28858
29048
  function getComboChartLegend(definition, args) {
@@ -28941,10 +29131,10 @@ const INTERACTIVE_LEGEND_CONFIG = {
28941
29131
  target.style.cursor = "default";
28942
29132
  },
28943
29133
  onClick: (event, legendItem, legend) => {
28944
- if (!legend.legendItems) {
29134
+ const index = legendItem.datasetIndex;
29135
+ if (!legend.legendItems || index === undefined) {
28945
29136
  return;
28946
29137
  }
28947
- const index = legend.legendItems.indexOf(legendItem);
28948
29138
  if (legend.chart.isDatasetVisible(index)) {
28949
29139
  legend.chart.hide(index);
28950
29140
  }
@@ -28960,15 +29150,29 @@ function getCustomLegendLabels(fontColor, legendLabelConfig) {
28960
29150
  labels: {
28961
29151
  color: fontColor,
28962
29152
  usePointStyle: true,
28963
- generateLabels: (chart) => chart.data.datasets.map((dataset, index) => ({
28964
- text: dataset.label ?? "",
28965
- fontColor,
28966
- strokeStyle: dataset.borderColor,
28967
- fillStyle: dataset.backgroundColor,
28968
- hidden: !chart.isDatasetVisible(index),
28969
- pointStyle: dataset.type === "line" ? "line" : "rect",
28970
- ...legendLabelConfig,
28971
- })),
29153
+ generateLabels: (chart) => chart.data.datasets.map((dataset, index) => {
29154
+ if (dataset["xAxisID"] === TREND_LINE_XAXIS_ID) {
29155
+ return {
29156
+ text: dataset.label ?? "",
29157
+ fontColor,
29158
+ strokeStyle: dataset.borderColor,
29159
+ hidden: !chart.isDatasetVisible(index),
29160
+ pointStyle: "line",
29161
+ datasetIndex: index,
29162
+ lineWidth: 3,
29163
+ };
29164
+ }
29165
+ return {
29166
+ text: dataset.label ?? "",
29167
+ fontColor,
29168
+ strokeStyle: dataset.borderColor,
29169
+ fillStyle: dataset.backgroundColor,
29170
+ hidden: !chart.isDatasetVisible(index),
29171
+ pointStyle: dataset.type === "line" ? "line" : "rect",
29172
+ datasetIndex: index,
29173
+ ...legendLabelConfig,
29174
+ };
29175
+ }),
28972
29176
  },
28973
29177
  };
28974
29178
  }
@@ -29106,6 +29310,44 @@ function getRadarChartScales(definition, args) {
29106
29310
  },
29107
29311
  };
29108
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
+ }
29109
29351
  function getChartAxisTitleRuntime(design) {
29110
29352
  if (design?.title?.text) {
29111
29353
  const { text, color, align, italic, bold } = design.title;
@@ -29116,6 +29358,7 @@ function getChartAxisTitleRuntime(design) {
29116
29358
  font: {
29117
29359
  style: italic ? "italic" : "normal",
29118
29360
  weight: bold ? "bold" : "normal",
29361
+ size: design.title.fontSize ?? CHART_AXIS_TITLE_FONT_SIZE,
29119
29362
  },
29120
29363
  align: align === "left" ? "start" : align === "right" ? "end" : "center",
29121
29364
  };
@@ -29168,6 +29411,44 @@ function getChartAxis(definition, position, type, options) {
29168
29411
  };
29169
29412
  }
29170
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
+ }
29171
29452
 
29172
29453
  function getChartShowValues(definition, args) {
29173
29454
  const { axisFormats, locale } = args;
@@ -29181,17 +29462,22 @@ function getChartShowValues(definition, args) {
29181
29462
 
29182
29463
  function getChartTitle(definition) {
29183
29464
  const chartTitle = definition.title;
29184
- const fontColor = chartFontColor(definition.background);
29465
+ const fontColor = chartMutedFontColor(definition.background);
29185
29466
  return {
29186
29467
  display: !!chartTitle.text,
29187
29468
  text: _t(chartTitle.text),
29188
29469
  color: chartTitle?.color ?? fontColor,
29189
29470
  align: chartTitle.align === "center" ? "center" : chartTitle.align === "right" ? "end" : "start",
29190
29471
  font: {
29191
- size: DEFAULT_CHART_FONT_SIZE,
29472
+ size: definition.title.fontSize ?? CHART_TITLE_FONT_SIZE,
29192
29473
  weight: chartTitle.bold ? "bold" : "normal",
29193
29474
  style: chartTitle.italic ? "italic" : "normal",
29194
29475
  },
29476
+ padding: {
29477
+ // Disable title top/left/right padding to use the chart padding instead.
29478
+ // The legend already has a top padding, so bottom padding is useless for the title there.
29479
+ bottom: definition.legendPosition === "top" ? 0 : CHART_PADDING$1,
29480
+ },
29195
29481
  };
29196
29482
  }
29197
29483
 
@@ -29322,6 +29608,25 @@ function getRadarChartTooltip(definition, args) {
29322
29608
  },
29323
29609
  };
29324
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
+ }
29325
29630
  function calculatePercentage(dataset, dataIndex) {
29326
29631
  const numericData = dataset.filter((value) => typeof value === "number");
29327
29632
  const total = numericData.reduce((sum, value) => sum + value, 0);
@@ -29338,26 +29643,27 @@ var CHART_RUNTIME_HELPERS = /*#__PURE__*/Object.freeze({
29338
29643
  canChartParseLabels: canChartParseLabels,
29339
29644
  getBarChartData: getBarChartData,
29340
29645
  getBarChartDatasets: getBarChartDatasets,
29341
- getBarChartLayout: getBarChartLayout,
29342
29646
  getBarChartLegend: getBarChartLegend,
29343
29647
  getBarChartScales: getBarChartScales,
29344
29648
  getBarChartTooltip: getBarChartTooltip,
29345
29649
  getChartLabelFormat: getChartLabelFormat,
29650
+ getChartLayout: getChartLayout,
29346
29651
  getChartShowValues: getChartShowValues,
29347
29652
  getChartTitle: getChartTitle,
29348
29653
  getComboChartDatasets: getComboChartDatasets,
29349
29654
  getComboChartLegend: getComboChartLegend,
29350
- getCommonChartLayout: getCommonChartLayout,
29351
29655
  getData: getData,
29656
+ getGeoChartData: getGeoChartData,
29657
+ getGeoChartDatasets: getGeoChartDatasets,
29658
+ getGeoChartScales: getGeoChartScales,
29659
+ getGeoChartTooltip: getGeoChartTooltip,
29352
29660
  getLineChartData: getLineChartData,
29353
29661
  getLineChartDatasets: getLineChartDatasets,
29354
- getLineChartLayout: getLineChartLayout,
29355
29662
  getLineChartLegend: getLineChartLegend,
29356
29663
  getLineChartScales: getLineChartScales,
29357
29664
  getLineChartTooltip: getLineChartTooltip,
29358
29665
  getPieChartData: getPieChartData,
29359
29666
  getPieChartDatasets: getPieChartDatasets,
29360
- getPieChartLayout: getPieChartLayout,
29361
29667
  getPieChartLegend: getPieChartLegend,
29362
29668
  getPieChartTooltip: getPieChartTooltip,
29363
29669
  getPyramidChartData: getPyramidChartData,
@@ -29373,7 +29679,6 @@ var CHART_RUNTIME_HELPERS = /*#__PURE__*/Object.freeze({
29373
29679
  getScatterChartScales: getScatterChartScales,
29374
29680
  getTrendDatasetForBarChart: getTrendDatasetForBarChart,
29375
29681
  getTrendDatasetForLineChart: getTrendDatasetForLineChart,
29376
- getWaterfallChartLayout: getWaterfallChartLayout,
29377
29682
  getWaterfallChartLegend: getWaterfallChartLegend,
29378
29683
  getWaterfallChartScales: getWaterfallChartScales,
29379
29684
  getWaterfallChartTooltip: getWaterfallChartTooltip,
@@ -29521,7 +29826,7 @@ function createBarChartRuntime(chart, getters) {
29521
29826
  options: {
29522
29827
  ...CHART_COMMON_OPTIONS,
29523
29828
  indexAxis: chart.horizontal ? "y" : "x",
29524
- layout: getBarChartLayout(definition),
29829
+ layout: getChartLayout(),
29525
29830
  scales: getBarChartScales(definition, chartData),
29526
29831
  plugins: {
29527
29832
  title: getChartTitle(definition),
@@ -29673,7 +29978,7 @@ function createComboChartRuntime(chart, getters) {
29673
29978
  },
29674
29979
  options: {
29675
29980
  ...CHART_COMMON_OPTIONS,
29676
- layout: getBarChartLayout(definition),
29981
+ layout: getChartLayout(),
29677
29982
  scales: getBarChartScales(definition, chartData),
29678
29983
  plugins: {
29679
29984
  title: getChartTitle(definition),
@@ -29925,6 +30230,133 @@ function getSectionThresholdValue(threshold, minValue, maxValue) {
29925
30230
  return clip(value, minValue, maxValue);
29926
30231
  }
29927
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
+
29928
30360
  class LineChart extends AbstractChart {
29929
30361
  dataSets;
29930
30362
  labelRange;
@@ -30074,7 +30506,7 @@ function createLineChartRuntime(chart, getters) {
30074
30506
  },
30075
30507
  options: {
30076
30508
  ...CHART_COMMON_OPTIONS,
30077
- layout: getLineChartLayout(definition),
30509
+ layout: getChartLayout(),
30078
30510
  scales: getLineChartScales(definition, chartData),
30079
30511
  plugins: {
30080
30512
  title: getChartTitle(definition),
@@ -30209,7 +30641,7 @@ function createPieChartRuntime(chart, getters) {
30209
30641
  },
30210
30642
  options: {
30211
30643
  ...CHART_COMMON_OPTIONS,
30212
- layout: getPieChartLayout(definition),
30644
+ layout: getChartLayout(),
30213
30645
  plugins: {
30214
30646
  title: getChartTitle(definition),
30215
30647
  legend: getPieChartLegend(definition, chartData),
@@ -30346,7 +30778,7 @@ function createPyramidChartRuntime(chart, getters) {
30346
30778
  options: {
30347
30779
  ...CHART_COMMON_OPTIONS,
30348
30780
  indexAxis: "y",
30349
- layout: getBarChartLayout(definition),
30781
+ layout: getChartLayout(),
30350
30782
  scales: getPyramidChartScales(definition, chartData),
30351
30783
  plugins: {
30352
30784
  title: getChartTitle(definition),
@@ -30495,7 +30927,7 @@ function createRadarChartRuntime(chart, getters) {
30495
30927
  },
30496
30928
  options: {
30497
30929
  ...CHART_COMMON_OPTIONS,
30498
- layout: getBarChartLayout(definition),
30930
+ layout: getChartLayout(),
30499
30931
  scales: getRadarChartScales(definition, chartData),
30500
30932
  plugins: {
30501
30933
  title: getChartTitle(definition),
@@ -30648,7 +31080,7 @@ function createScatterChartRuntime(chart, getters) {
30648
31080
  },
30649
31081
  options: {
30650
31082
  ...CHART_COMMON_OPTIONS,
30651
- layout: getLineChartLayout(definition),
31083
+ layout: getChartLayout(),
30652
31084
  scales: getScatterChartScales(definition, chartData),
30653
31085
  plugins: {
30654
31086
  title: getChartTitle(definition),
@@ -30809,7 +31241,7 @@ function createWaterfallChartRuntime(chart, getters) {
30809
31241
  },
30810
31242
  options: {
30811
31243
  ...CHART_COMMON_OPTIONS,
30812
- layout: getWaterfallChartLayout(definition),
31244
+ layout: getChartLayout(),
30813
31245
  scales: getWaterfallChartScales(definition, chartData),
30814
31246
  plugins: {
30815
31247
  title: getChartTitle(definition),
@@ -30918,6 +31350,15 @@ chartRegistry.add("radar", {
30918
31350
  getChartDefinitionFromContextCreation: RadarChart.getDefinitionFromContextCreation,
30919
31351
  sequence: 80,
30920
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
+ });
30921
31362
  const chartComponentRegistry = new Registry();
30922
31363
  chartComponentRegistry.add("line", ChartJsComponent);
30923
31364
  chartComponentRegistry.add("bar", ChartJsComponent);
@@ -30929,6 +31370,7 @@ chartComponentRegistry.add("scorecard", ScorecardChart);
30929
31370
  chartComponentRegistry.add("waterfall", ChartJsComponent);
30930
31371
  chartComponentRegistry.add("pyramid", ChartJsComponent);
30931
31372
  chartComponentRegistry.add("radar", ChartJsComponent);
31373
+ chartComponentRegistry.add("geo", ChartJsComponent);
30932
31374
  const chartCategories = {
30933
31375
  line: _t("Line"),
30934
31376
  column: _t("Column"),
@@ -31088,6 +31530,13 @@ chartSubtypeRegistry
31088
31530
  subtypeDefinition: { fillArea: true },
31089
31531
  category: "misc",
31090
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",
31091
31540
  });
31092
31541
 
31093
31542
  /**
@@ -31938,18 +32387,66 @@ const ErrorToolTipPopoverBuilder = {
31938
32387
  },
31939
32388
  };
31940
32389
 
31941
- css /*SCSS*/ `
31942
- .o-filter-menu-value {
31943
- padding: 4px;
31944
- line-height: 20px;
31945
- height: 28px;
31946
- .o-filter-menu-value-checked {
31947
- 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
+ }
31948
32426
  }
31949
32427
  }
31950
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
+
31951
32447
  class FilterMenuValueItem extends owl.Component {
31952
32448
  static template = "o-spreadsheet-FilterMenuValueItem";
32449
+ static components = { Checkbox };
31953
32450
  static props = {
31954
32451
  value: String,
31955
32452
  isChecked: Boolean,
@@ -31987,8 +32484,6 @@ const CSS = css /* scss */ `
31987
32484
  .o-filter-menu-item {
31988
32485
  display: flex;
31989
32486
  box-sizing: border-box;
31990
- height: ${MENU_ITEM_HEIGHT}px;
31991
- padding: 4px 4px 4px 0px;
31992
32487
  cursor: pointer;
31993
32488
  user-select: none;
31994
32489
 
@@ -31997,14 +32492,6 @@ const CSS = css /* scss */ `
31997
32492
  }
31998
32493
  }
31999
32494
 
32000
- input {
32001
- box-sizing: border-box;
32002
- margin-bottom: 5px;
32003
- border: 1px solid #949494;
32004
- height: 24px;
32005
- padding-right: 28px;
32006
- }
32007
-
32008
32495
  .o-search-icon {
32009
32496
  right: 5px;
32010
32497
  top: 3px;
@@ -32021,19 +32508,12 @@ const CSS = css /* scss */ `
32021
32508
  display: flex;
32022
32509
  flex-direction: row;
32023
32510
  margin-bottom: 4px;
32024
-
32025
- .o-filter-menu-action-text {
32026
- cursor: pointer;
32027
- margin-right: 10px;
32028
- color: blue;
32029
- text-decoration: underline;
32030
- }
32031
32511
  }
32032
32512
 
32033
32513
  .o-filter-menu-list {
32034
32514
  flex: auto;
32035
32515
  overflow-y: auto;
32036
- border: 1px solid #949494;
32516
+ border: 1px solid ${GRAY_300};
32037
32517
 
32038
32518
  .o-filter-menu-no-values {
32039
32519
  color: #949494;
@@ -33119,6 +33599,7 @@ var CHART_HELPERS = /*#__PURE__*/Object.freeze({
33119
33599
  adaptChartRange: adaptChartRange,
33120
33600
  chartFactory: chartFactory,
33121
33601
  chartFontColor: chartFontColor,
33602
+ chartMutedFontColor: chartMutedFontColor,
33122
33603
  chartRuntimeFactory: chartRuntimeFactory,
33123
33604
  chartToImage: chartToImage,
33124
33605
  checkDataset: checkDataset,
@@ -34224,6 +34705,21 @@ const pivotProperties = {
34224
34705
  isReadonlyAllowed: true,
34225
34706
  icon: "o-spreadsheet-Icon.PIVOT",
34226
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
+ };
34227
34723
  const FIX_FORMULAS = {
34228
34724
  name: _t("Convert to individual formulas"),
34229
34725
  execute(env) {
@@ -34260,6 +34756,66 @@ const FIX_FORMULAS = {
34260
34756
  },
34261
34757
  icon: "o-spreadsheet-Icon.PIVOT",
34262
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
+ }
34263
34819
 
34264
34820
  //------------------------------------------------------------------------------
34265
34821
  // Context Menu Registry
@@ -34358,14 +34914,33 @@ cellMenuRegistry
34358
34914
  name: INSERT_LINK_NAME,
34359
34915
  sequence: 150,
34360
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,
34361
34923
  })
34362
34924
  .add("pivot_fix_formulas", {
34363
34925
  ...FIX_FORMULAS,
34364
- sequence: 155,
34926
+ sequence: 160,
34365
34927
  })
34366
34928
  .add("pivot_properties", {
34367
34929
  ...pivotProperties,
34368
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,
34369
34944
  });
34370
34945
 
34371
34946
  const sortRange = {
@@ -34378,7 +34953,7 @@ const sortAscending = {
34378
34953
  execute: (env) => {
34379
34954
  const { anchor, zones } = env.model.getters.getSelection();
34380
34955
  const sheetId = env.model.getters.getActiveSheetId();
34381
- interactiveSortSelection(env, sheetId, anchor.cell, zones[0], "ascending");
34956
+ interactiveSortSelection(env, sheetId, anchor.cell, zones[0], "asc");
34382
34957
  },
34383
34958
  icon: "o-spreadsheet-Icon.SORT_ASCENDING",
34384
34959
  };
@@ -34406,7 +34981,7 @@ const sortDescending = {
34406
34981
  execute: (env) => {
34407
34982
  const { anchor, zones } = env.model.getters.getSelection();
34408
34983
  const sheetId = env.model.getters.getActiveSheetId();
34409
- interactiveSortSelection(env, sheetId, anchor.cell, zones[0], "descending");
34984
+ interactiveSortSelection(env, sheetId, anchor.cell, zones[0], "desc");
34410
34985
  },
34411
34986
  icon: "o-spreadsheet-Icon.SORT_DESCENDING",
34412
34987
  };
@@ -34889,6 +35464,227 @@ function interactiveFreezeColumnsRows(env, dimension, base) {
34889
35464
  }
34890
35465
  }
34891
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
+
34892
35688
  const hideCols = {
34893
35689
  name: HIDE_COLUMNS_NAME,
34894
35690
  execute: (env) => {
@@ -35060,6 +35856,19 @@ const viewGridlines = {
35060
35856
  return env.model.getters.getGridLinesVisibility(sheetId);
35061
35857
  },
35062
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
+ };
35063
35872
  const viewFormulas = {
35064
35873
  name: _t("Formulas"),
35065
35874
  isActive: (env) => env.model.getters.shouldShowFormulas(),
@@ -35677,6 +36486,11 @@ topbarMenuRegistry
35677
36486
  .addChild("view_formulas", ["view", "show"], {
35678
36487
  ...viewFormulas,
35679
36488
  sequence: 10,
36489
+ })
36490
+ .addChild("view_irregularity_map", ["view"], {
36491
+ ...irregularityMap,
36492
+ sequence: 40,
36493
+ separator: true,
35680
36494
  })
35681
36495
  // ---------------------------------------------------------------------
35682
36496
  // INSERT MENU ITEMS
@@ -35979,62 +36793,6 @@ class OTRegistry extends Registry {
35979
36793
  }
35980
36794
  const otRegistry = new OTRegistry();
35981
36795
 
35982
- const CHECK_SVG = /*xml*/ `
35983
- <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'>
35984
- <path fill='none' stroke='#FFF' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/>
35985
- </svg>
35986
- `;
35987
- const CHECKBOX_WIDTH = 14;
35988
- css /* scss */ `
35989
- label.o-checkbox {
35990
- input {
35991
- appearance: none;
35992
- -webkit-appearance: none;
35993
- -moz-appearance: none;
35994
- border-radius: 0;
35995
- width: ${CHECKBOX_WIDTH}px;
35996
- height: ${CHECKBOX_WIDTH}px;
35997
- vertical-align: top;
35998
- box-sizing: border-box;
35999
- outline: none;
36000
- border: 1px solid ${GRAY_300};
36001
-
36002
- &:hover {
36003
- border-color: ${ACTION_COLOR};
36004
- }
36005
-
36006
- &:checked {
36007
- background: url("data:image/svg+xml,${encodeURIComponent(CHECK_SVG)}");
36008
- background-color: ${ACTION_COLOR};
36009
- border-color: ${ACTION_COLOR};
36010
- }
36011
-
36012
- &:focus {
36013
- outline: none;
36014
- box-shadow: 0 0 0 0.25rem rgba(113, 75, 103, 0.25);
36015
- border-color: ${ACTION_COLOR};
36016
- }
36017
- }
36018
- }
36019
- `;
36020
- class Checkbox extends owl.Component {
36021
- static template = "o-spreadsheet.Checkbox";
36022
- static props = {
36023
- label: { type: String, optional: true },
36024
- value: { type: Boolean, optional: true },
36025
- className: { type: String, optional: true },
36026
- name: { type: String, optional: true },
36027
- title: { type: String, optional: true },
36028
- disabled: { type: Boolean, optional: true },
36029
- onChange: Function,
36030
- };
36031
- static defaultProps = { value: false };
36032
- onChange(ev) {
36033
- const value = ev.target.checked;
36034
- this.props.onChange(value);
36035
- }
36036
- }
36037
-
36038
36796
  class Section extends owl.Component {
36039
36797
  static template = "o_spreadsheet.Section";
36040
36798
  static props = {
@@ -37411,6 +38169,95 @@ class ColorPickerWidget extends owl.Component {
37411
38169
  }
37412
38170
  }
37413
38171
 
38172
+ css /* scss */ `
38173
+ .o-font-size-editor {
38174
+ height: calc(100% - 4px);
38175
+ input.o-font-size {
38176
+ outline-color: ${SELECTION_BORDER_COLOR};
38177
+ height: 20px;
38178
+ width: 23px;
38179
+ }
38180
+ }
38181
+ .o-text-options > div {
38182
+ cursor: pointer;
38183
+ line-height: 26px;
38184
+ padding: 3px 12px;
38185
+ &:hover {
38186
+ background-color: rgba(0, 0, 0, 0.08);
38187
+ }
38188
+ }
38189
+ `;
38190
+ class FontSizeEditor extends owl.Component {
38191
+ static template = "o-spreadsheet-FontSizeEditor";
38192
+ static props = {
38193
+ currentFontSize: Number,
38194
+ onFontSizeChanged: Function,
38195
+ onToggle: { type: Function, optional: true },
38196
+ class: String,
38197
+ };
38198
+ static components = { Popover };
38199
+ fontSizes = FONT_SIZES;
38200
+ dropdown = owl.useState({ isOpen: false });
38201
+ inputRef = owl.useRef("inputFontSize");
38202
+ rootEditorRef = owl.useRef("FontSizeEditor");
38203
+ fontSizeListRef = owl.useRef("fontSizeList");
38204
+ setup() {
38205
+ owl.useExternalListener(window, "click", this.onExternalClick, { capture: true });
38206
+ }
38207
+ get popoverProps() {
38208
+ const { x, y, width, height } = this.rootEditorRef.el.getBoundingClientRect();
38209
+ return {
38210
+ anchorRect: { x, y, width, height },
38211
+ positioning: "BottomLeft",
38212
+ verticalOffset: 0,
38213
+ };
38214
+ }
38215
+ onExternalClick(ev) {
38216
+ if (!isChildEvent(this.fontSizeListRef.el, ev) && !isChildEvent(this.rootEditorRef.el, ev)) {
38217
+ this.closeFontList();
38218
+ }
38219
+ }
38220
+ toggleFontList() {
38221
+ const isOpen = this.dropdown.isOpen;
38222
+ if (!isOpen) {
38223
+ this.props.onToggle?.();
38224
+ this.inputRef.el.focus();
38225
+ }
38226
+ else {
38227
+ this.closeFontList();
38228
+ }
38229
+ }
38230
+ closeFontList() {
38231
+ this.dropdown.isOpen = false;
38232
+ }
38233
+ setSize(fontSizeStr) {
38234
+ const fontSize = clip(Math.floor(parseFloat(fontSizeStr)), 1, 400);
38235
+ this.props.onFontSizeChanged(fontSize);
38236
+ this.closeFontList();
38237
+ }
38238
+ setSizeFromInput(ev) {
38239
+ this.setSize(ev.target.value);
38240
+ }
38241
+ setSizeFromList(fontSizeStr) {
38242
+ this.setSize(fontSizeStr);
38243
+ }
38244
+ onInputFocused(ev) {
38245
+ this.dropdown.isOpen = true;
38246
+ ev.target.select();
38247
+ }
38248
+ onInputKeydown(ev) {
38249
+ if (ev.key === "Enter" || ev.key === "Escape") {
38250
+ this.closeFontList();
38251
+ const target = ev.target;
38252
+ // In the case of a ESCAPE key, we get the previous font size back
38253
+ if (ev.key === "Escape") {
38254
+ target.value = `${this.props.currentFontSize}`;
38255
+ }
38256
+ this.props.onToggle?.();
38257
+ }
38258
+ }
38259
+ }
38260
+
37414
38261
  css /* scss */ `
37415
38262
  .o-chart-title-designer {
37416
38263
  > span {
@@ -37445,7 +38292,7 @@ css /* scss */ `
37445
38292
  `;
37446
38293
  class ChartTitle extends owl.Component {
37447
38294
  static template = "o-spreadsheet.ChartTitle";
37448
- static components = { Section, ColorPickerWidget };
38295
+ static components = { Section, ColorPickerWidget, FontSizeEditor };
37449
38296
  static props = {
37450
38297
  title: { type: String, optional: true },
37451
38298
  updateTitle: Function,
@@ -37454,7 +38301,8 @@ class ChartTitle extends owl.Component {
37454
38301
  toggleBold: { type: Function, optional: true },
37455
38302
  updateAlignment: { type: Function, optional: true },
37456
38303
  updateColor: { type: Function, optional: true },
37457
- style: { type: Object, optional: true },
38304
+ style: Object,
38305
+ onFontSizeChanged: Function,
37458
38306
  };
37459
38307
  static defaultProps = {
37460
38308
  title: "",
@@ -37469,6 +38317,9 @@ class ChartTitle extends owl.Component {
37469
38317
  updateTitle(ev) {
37470
38318
  this.props.updateTitle(ev.target.value);
37471
38319
  }
38320
+ updateFontSize(fontSize) {
38321
+ this.props.onFontSizeChanged(fontSize);
38322
+ }
37472
38323
  toggleDropdownTool(tool, ev) {
37473
38324
  const isOpen = this.state.activeTool === tool;
37474
38325
  this.closeMenus();
@@ -37511,6 +38362,7 @@ class AxisDesignEditor extends owl.Component {
37511
38362
  return {
37512
38363
  color: "",
37513
38364
  align: "center",
38365
+ fontSize: CHART_AXIS_TITLE_FONT_SIZE,
37514
38366
  ...axisDesign.title,
37515
38367
  };
37516
38368
  }
@@ -37528,6 +38380,17 @@ class AxisDesignEditor extends owl.Component {
37528
38380
  };
37529
38381
  this.props.updateChart(this.props.figureId, { axesDesign });
37530
38382
  }
38383
+ updateAxisTitleFontSize(fontSize) {
38384
+ const axesDesign = this.props.definition.axesDesign ?? {};
38385
+ axesDesign[this.state.currentAxis] = {
38386
+ ...axesDesign[this.state.currentAxis],
38387
+ title: {
38388
+ ...(axesDesign[this.state.currentAxis]?.title ?? {}),
38389
+ fontSize,
38390
+ },
38391
+ };
38392
+ this.props.updateChart(this.props.figureId, { axesDesign });
38393
+ }
37531
38394
  toggleBoldAxisTitle() {
37532
38395
  const axesDesign = this.props.definition.axesDesign ?? {};
37533
38396
  const title = axesDesign[this.state.currentAxis]?.title ?? {};
@@ -37647,8 +38510,12 @@ class GeneralDesignEditor extends owl.Component {
37647
38510
  figureId: String,
37648
38511
  definition: Object,
37649
38512
  updateChart: Function,
38513
+ defaultChartTitleFontSize: { type: Number, optional: true },
37650
38514
  slots: { type: Object, optional: true },
37651
38515
  };
38516
+ static defaultProps = {
38517
+ defaultChartTitleFontSize: CHART_TITLE_FONT_SIZE,
38518
+ };
37652
38519
  state;
37653
38520
  setup() {
37654
38521
  this.state = owl.useState({
@@ -37674,6 +38541,7 @@ class GeneralDesignEditor extends owl.Component {
37674
38541
  get titleStyle() {
37675
38542
  return {
37676
38543
  align: "left",
38544
+ fontSize: this.props.defaultChartTitleFontSize,
37677
38545
  ...this.title,
37678
38546
  };
37679
38547
  }
@@ -37682,6 +38550,10 @@ class GeneralDesignEditor extends owl.Component {
37682
38550
  this.props.updateChart(this.props.figureId, { title });
37683
38551
  this.state.activeTool = "";
37684
38552
  }
38553
+ updateChartTitleFontSize(fontSize) {
38554
+ const title = { ...this.title, fontSize };
38555
+ this.props.updateChart(this.props.figureId, { title });
38556
+ }
37685
38557
  toggleBoldChartTitle() {
37686
38558
  let title = this.title;
37687
38559
  title = { ...title, bold: !title.bold };
@@ -37889,7 +38761,7 @@ class SeriesWithAxisDesignEditor extends owl.Component {
37889
38761
  case "polynomial":
37890
38762
  config = {
37891
38763
  type: "polynomial",
37892
- order: type === "linear" ? 1 : 2,
38764
+ order: type === "linear" ? 1 : this.getMaxPolynomialDegree(index),
37893
38765
  };
37894
38766
  break;
37895
38767
  case "exponential":
@@ -38160,6 +39032,104 @@ class GaugeChartDesignPanel extends owl.Component {
38160
39032
  }
38161
39033
  }
38162
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
+
38163
39133
  class LineConfigPanel extends GenericChartConfigPanel {
38164
39134
  static template = "o-spreadsheet-LineConfigPanel";
38165
39135
  get canTreatLabelsAsText() {
@@ -38349,6 +39319,9 @@ class ScorecardChartDesignPanel extends owl.Component {
38349
39319
  get humanizeNumbersLabel() {
38350
39320
  return _t("Humanize numbers");
38351
39321
  }
39322
+ get defaultScorecardTitleFontSize() {
39323
+ return SCORECARD_CHART_TITLE_FONT_SIZE;
39324
+ }
38352
39325
  updateHumanizeNumbers(humanize) {
38353
39326
  this.props.updateChart(this.props.figureId, { humanize });
38354
39327
  }
@@ -38470,6 +39443,10 @@ chartSidePanelComponentRegistry
38470
39443
  .add("radar", {
38471
39444
  configuration: GenericChartConfigPanel,
38472
39445
  design: RadarChartDesignPanel,
39446
+ })
39447
+ .add("geo", {
39448
+ configuration: GeoChartConfigPanel,
39449
+ design: GeoChartDesignPanel,
38473
39450
  });
38474
39451
 
38475
39452
  css /* scss */ `
@@ -39812,6 +40789,15 @@ class StandaloneComposerStore extends AbstractComposerStore {
39812
40789
  confirmEdition(content) {
39813
40790
  this.args().onConfirm(content);
39814
40791
  }
40792
+ getTokenColor(token) {
40793
+ if (token.type === "SYMBOL") {
40794
+ const matchedColor = this.args().getContextualColoredSymbolToken?.(token);
40795
+ if (matchedColor) {
40796
+ return matchedColor;
40797
+ }
40798
+ }
40799
+ return super.getTokenColor(token);
40800
+ }
39815
40801
  }
39816
40802
 
39817
40803
  css /* scss */ `
@@ -39824,7 +40810,7 @@ css /* scss */ `
39824
40810
  border-color: ${GRAY_300};
39825
40811
 
39826
40812
  &.active {
39827
- border-color: ${SELECTION_BORDER_COLOR};
40813
+ border-color: ${ACTION_COLOR};
39828
40814
  }
39829
40815
 
39830
40816
  &.o-invalid {
@@ -39849,6 +40835,7 @@ class StandaloneComposer extends owl.Component {
39849
40835
  placeholder: { type: String, optional: true },
39850
40836
  class: { type: String, optional: true },
39851
40837
  invalid: { type: Boolean, optional: true },
40838
+ getContextualColoredSymbolToken: { type: Function, optional: true },
39852
40839
  };
39853
40840
  static components = { Composer };
39854
40841
  static defaultProps = {
@@ -39865,6 +40852,7 @@ class StandaloneComposer extends owl.Component {
39865
40852
  content: this.props.composerContent,
39866
40853
  contextualAutocomplete: this.props.contextualAutocomplete,
39867
40854
  defaultRangeSheetId: this.props.defaultRangeSheetId,
40855
+ getContextualColoredSymbolToken: this.props.getContextualColoredSymbolToken,
39868
40856
  }));
39869
40857
  this.standaloneComposerStore = standaloneComposerStore;
39870
40858
  this.composerInterface = {
@@ -40780,7 +41768,7 @@ class ConditionalFormattingEditor extends owl.Component {
40780
41768
  }
40781
41769
  const point = this.state.rules.colorScale[target];
40782
41770
  if (point) {
40783
- point.color = Number.parseInt(color.slice(1), 16);
41771
+ point.color = colorToNumber(color);
40784
41772
  }
40785
41773
  this.updateConditionalFormat({ rule: this.state.rules.colorScale });
40786
41774
  this.closeMenus();
@@ -40925,6 +41913,9 @@ class ConditionalFormattingEditor extends owl.Component {
40925
41913
  return [this.state.rules.dataBar.rangeValues || ""];
40926
41914
  }
40927
41915
  updateDataBarColor(color) {
41916
+ if (!isColorValid(color)) {
41917
+ return;
41918
+ }
40928
41919
  this.state.rules.dataBar.color = Number.parseInt(color.slice(1), 16);
40929
41920
  this.updateConditionalFormat({ rule: this.state.rules.dataBar });
40930
41921
  }
@@ -43425,7 +44416,7 @@ function createMeasureAutoComplete(pivot, forComputedMeasure) {
43425
44416
  return {
43426
44417
  text: text,
43427
44418
  description: measure.displayName,
43428
- htmlContent: [{ value: text, color: tokenColors.FUNCTION }],
44419
+ htmlContent: [{ value: text, color: PIVOT_TOKEN_COLOR }],
43429
44420
  fuzzySearchKey: measure.displayName + text + measure.fieldName,
43430
44421
  };
43431
44422
  });
@@ -43434,7 +44425,7 @@ function createMeasureAutoComplete(pivot, forComputedMeasure) {
43434
44425
  return {
43435
44426
  text: text,
43436
44427
  description: dimension.displayName,
43437
- htmlContent: [{ value: text, color: tokenColors.FUNCTION }],
44428
+ htmlContent: [{ value: text, color: PIVOT_TOKEN_COLOR }],
43438
44429
  fuzzySearchKey: dimension.displayName + text + dimension.fieldName,
43439
44430
  };
43440
44431
  });
@@ -43514,6 +44505,76 @@ class PivotMeasureEditor extends owl.Component {
43514
44505
  measure: this.props.measure,
43515
44506
  });
43516
44507
  }
44508
+ getColoredSymbolToken(token) {
44509
+ if (token.type !== "SYMBOL") {
44510
+ return undefined;
44511
+ }
44512
+ const tokenValue = unquote(token.value, "'");
44513
+ if (this.props.definition.columns.some((col) => col.nameWithGranularity === tokenValue) ||
44514
+ this.props.definition.rows.some((row) => row.nameWithGranularity === tokenValue) ||
44515
+ this.props.definition.measures.some((measure) => measure.id === tokenValue && measure.id !== this.props.measure.id)) {
44516
+ return PIVOT_TOKEN_COLOR;
44517
+ }
44518
+ return undefined;
44519
+ }
44520
+ }
44521
+
44522
+ css /* scss */ `
44523
+ .o-pivot-sort {
44524
+ .o-sort-card {
44525
+ width: fit-content;
44526
+ background-color: ${GRAY_100};
44527
+ border: 1px solid ${GRAY_300};
44528
+
44529
+ .o-sort-value {
44530
+ color: ${PRIMARY_BUTTON_BG};
44531
+ }
44532
+ }
44533
+ }
44534
+ `;
44535
+ class PivotSortSection extends owl.Component {
44536
+ static template = "o-spreadsheet-PivotSortSection";
44537
+ static components = {
44538
+ Section,
44539
+ };
44540
+ static props = {
44541
+ definition: Object,
44542
+ pivotId: String,
44543
+ };
44544
+ get hasValidSort() {
44545
+ const pivot = this.env.model.getters.getPivot(this.props.pivotId);
44546
+ return (!!this.props.definition.sortedColumn &&
44547
+ isSortedColumnValid(this.props.definition.sortedColumn, pivot));
44548
+ }
44549
+ get sortDescription() {
44550
+ const sortOrder = this.props.definition.sortedColumn?.order === "asc" ? _t("ascending") : _t("descending");
44551
+ return _t("Sorted on column (%(ascOrDesc)s):", {
44552
+ ascOrDesc: sortOrder,
44553
+ });
44554
+ }
44555
+ get sortValuesAndFields() {
44556
+ const sortedColumn = this.props.definition.sortedColumn;
44557
+ if (!sortedColumn) {
44558
+ return [];
44559
+ }
44560
+ const pivot = this.env.model.getters.getPivot(this.props.pivotId);
44561
+ const locale = this.env.model.getters.getLocale();
44562
+ const currentDomain = [];
44563
+ const sortValues = [];
44564
+ for (const domainItem of sortedColumn.domain) {
44565
+ currentDomain.push(domainItem);
44566
+ const { value, format } = pivot.getPivotHeaderValueAndFormat(currentDomain);
44567
+ const label = formatValue(value, { format, locale });
44568
+ const field = pivot.definition.getDimension(domainItem.field);
44569
+ sortValues.push({ field: getFieldDisplayName(field), value: label });
44570
+ }
44571
+ if (sortedColumn.domain.length === 0) {
44572
+ sortValues.push({ value: _t("Total") });
44573
+ }
44574
+ const measureLabel = pivot.getMeasure(sortedColumn.measure).displayName;
44575
+ sortValues.push({ value: measureLabel, field: _t("Measure") });
44576
+ return sortValues;
44577
+ }
43517
44578
  }
43518
44579
 
43519
44580
  css /* scss */ `
@@ -43529,6 +44590,7 @@ class PivotLayoutConfigurator extends owl.Component {
43529
44590
  PivotDimensionOrder,
43530
44591
  PivotDimensionGranularity,
43531
44592
  PivotMeasureEditor,
44593
+ PivotSortSection,
43532
44594
  };
43533
44595
  static props = {
43534
44596
  definition: Object,
@@ -43685,9 +44747,13 @@ class PivotLayoutConfigurator extends owl.Component {
43685
44747
  }
43686
44748
  updateMeasure(measure, newMeasure) {
43687
44749
  const { measures } = this.props.definition;
43688
- this.props.onDimensionsUpdated({
44750
+ const update = {
43689
44751
  measures: measures.map((m) => (m.id === measure.id ? newMeasure : m)),
43690
- });
44752
+ };
44753
+ if (this.props.definition.sortedColumn?.measure === measure.id) {
44754
+ update.sortedColumn = { ...this.props.definition.sortedColumn, measure: newMeasure.id };
44755
+ }
44756
+ this.props.onDimensionsUpdated(update);
43691
44757
  }
43692
44758
  getMeasureId(fieldName, aggregator) {
43693
44759
  const baseId = fieldName + (aggregator ? `:${aggregator}` : "");
@@ -43843,10 +44909,12 @@ class PivotRuntimeDefinition {
43843
44909
  measures;
43844
44910
  columns;
43845
44911
  rows;
44912
+ sortedColumn;
43846
44913
  constructor(definition, fields) {
43847
44914
  this.measures = definition.measures.map((measure) => createMeasure(fields, measure));
43848
44915
  this.columns = definition.columns.map((dimension) => createPivotDimension(fields, dimension));
43849
44916
  this.rows = definition.rows.map((dimension) => createPivotDimension(fields, dimension));
44917
+ this.sortedColumn = definition.sortedColumn;
43850
44918
  }
43851
44919
  getDimension(nameWithGranularity) {
43852
44920
  const dimension = this.columns.find((d) => d.nameWithGranularity === nameWithGranularity) ||
@@ -44000,6 +45068,7 @@ class SpreadsheetPivotTable {
44000
45068
  pivotCells = {};
44001
45069
  rowTree;
44002
45070
  colTree;
45071
+ isSorted = false;
44003
45072
  constructor(columns, rows, measures, fieldsType) {
44004
45073
  this.columns = columns.map((row) => {
44005
45074
  // offset in the pivot table
@@ -44173,6 +45242,7 @@ class SpreadsheetPivotTable {
44173
45242
  value,
44174
45243
  field: row.fields[rowDepth],
44175
45244
  children: [],
45245
+ type: this.fieldsType[fieldName] || "char",
44176
45246
  width: 0, // not used
44177
45247
  };
44178
45248
  treesAtDepth[depth].push(node);
@@ -44195,6 +45265,7 @@ class SpreadsheetPivotTable {
44195
45265
  field: leaf.fields[depth],
44196
45266
  children: [],
44197
45267
  width: leaf.width,
45268
+ type: this.fieldsType[fieldName] || "char",
44198
45269
  };
44199
45270
  if (treesAtDepth[depth]?.at(-1)?.value !== value) {
44200
45271
  treesAtDepth[depth + 1] = [];
@@ -44213,6 +45284,35 @@ class SpreadsheetPivotTable {
44213
45284
  fieldsType: this.fieldsType,
44214
45285
  };
44215
45286
  }
45287
+ sort(measure, sortedColumn, getValue) {
45288
+ if (this.isSorted) {
45289
+ return;
45290
+ }
45291
+ const getSortValue = (measure, domain) => {
45292
+ const rawValue = getValue(measure, domain).value;
45293
+ return typeof rawValue === "number" ? rawValue : -Infinity;
45294
+ };
45295
+ const sortColDomain = sortedColumn.domain;
45296
+ const sortFn = (rowDomain1, rowDomain2) => {
45297
+ const value1 = getSortValue(measure, [...rowDomain1, ...sortColDomain]);
45298
+ const value2 = getSortValue(measure, [...rowDomain2, ...sortColDomain]);
45299
+ return sortedColumn.order === "asc" ? value1 - value2 : value2 - value1;
45300
+ };
45301
+ const sortedRowTree = sortPivotTree(this.rowTree(), [], sortFn);
45302
+ this.rowTree = lazy(sortedRowTree);
45303
+ this.rows = [...this.rowTreeToRows(sortedRowTree), this.rows[this.rows.length - 1]];
45304
+ this.isSorted = true;
45305
+ }
45306
+ rowTreeToRows(tree, parentRow) {
45307
+ return tree.flatMap((node) => {
45308
+ const row = {
45309
+ indent: parentRow ? parentRow.indent + 1 : 0,
45310
+ fields: [...(parentRow?.fields || []), node.field],
45311
+ values: [...(parentRow?.values || []), node.value],
45312
+ };
45313
+ return [row, ...this.rowTreeToRows(node.children, row)];
45314
+ });
45315
+ }
44216
45316
  }
44217
45317
  const EMPTY_PIVOT_CELL = { type: "EMPTY" };
44218
45318
 
@@ -44290,6 +45390,7 @@ function dataEntriesToColumnsTree(dataEntries, columns, index) {
44290
45390
  value: groups[key]?.[0]?.[column.nameWithGranularity]?.value ?? null,
44291
45391
  field: colName,
44292
45392
  children: dataEntriesToColumnsTree(groups[key] || [], columns, index + 1),
45393
+ type: column.type,
44293
45394
  width: 0,
44294
45395
  };
44295
45396
  });
@@ -45155,6 +46256,7 @@ class PivotSidePanelStore extends SpreadsheetStore {
45155
46256
  format: measure.format,
45156
46257
  display: measure.display,
45157
46258
  })),
46259
+ sortedColumn: this.shouldKeepSortedColumn(definition) ? definition.sortedColumn : undefined,
45158
46260
  };
45159
46261
  if (!this.draft && deepEquals(coreDefinition, cleanedDefinition)) {
45160
46262
  return;
@@ -45213,6 +46315,19 @@ class PivotSidePanelStore extends SpreadsheetStore {
45213
46315
  }
45214
46316
  return granularitiesPerFields;
45215
46317
  }
46318
+ /**
46319
+ * Check if we want to keep the sorted column when updating the pivot definition. We should remove it if either
46320
+ * the measure is not in the new definition or the columns have changed.
46321
+ */
46322
+ shouldKeepSortedColumn(newDefinition) {
46323
+ const { sortedColumn } = newDefinition;
46324
+ if (!sortedColumn) {
46325
+ return true;
46326
+ }
46327
+ const oldDefinition = this.getters.getPivotCoreDefinition(this.pivotId);
46328
+ return (newDefinition.measures.find((measure) => measure.id === sortedColumn.measure) &&
46329
+ deepEquals(oldDefinition.columns, newDefinition.columns));
46330
+ }
45216
46331
  }
45217
46332
 
45218
46333
  class PivotSpreadsheetSidePanel extends owl.Component {
@@ -46314,6 +47429,7 @@ css /* scss */ `
46314
47429
  }
46315
47430
  }
46316
47431
  `;
47432
+ const DEFAULT_TABLE_STYLE_COLOR = "#3C78D8";
46317
47433
  class TableStyleEditorPanel extends owl.Component {
46318
47434
  static template = "o-spreadsheet-TableStyleEditorPanel";
46319
47435
  static components = { Section, RoundColorPicker, TableStylePreview };
@@ -46332,7 +47448,7 @@ class TableStyleEditorPanel extends owl.Component {
46332
47448
  : null;
46333
47449
  return {
46334
47450
  pickerOpened: false,
46335
- primaryColor: editedStyle?.primaryColor || "#3C78D8",
47451
+ primaryColor: editedStyle?.primaryColor || DEFAULT_TABLE_STYLE_COLOR,
46336
47452
  selectedTemplateName: editedStyle?.templateName || "lightColoredText",
46337
47453
  styleName: editedStyle?.displayName || this.env.model.getters.getNewCustomTableStyleName(),
46338
47454
  };
@@ -46341,7 +47457,7 @@ class TableStyleEditorPanel extends owl.Component {
46341
47457
  this.state.pickerOpened = !this.state.pickerOpened;
46342
47458
  }
46343
47459
  onColorPicked(color) {
46344
- this.state.primaryColor = color;
47460
+ this.state.primaryColor = isColorValid(color) ? color : DEFAULT_TABLE_STYLE_COLOR;
46345
47461
  this.state.pickerOpened = false;
46346
47462
  }
46347
47463
  onTemplatePicked(templateName) {
@@ -47957,6 +49073,7 @@ class FiguresContainer extends owl.Component {
47957
49073
  draggedFigure: undefined,
47958
49074
  horizontalSnap: undefined,
47959
49075
  verticalSnap: undefined,
49076
+ cancelDnd: undefined,
47960
49077
  });
47961
49078
  setup() {
47962
49079
  owl.onMounted(() => {
@@ -47969,12 +49086,28 @@ class FiguresContainer extends owl.Component {
47969
49086
  // new rendering
47970
49087
  this.render();
47971
49088
  });
49089
+ owl.onWillUpdateProps(() => {
49090
+ const sheetId = this.env.model.getters.getActiveSheetId();
49091
+ const draggedFigureId = this.dnd.draggedFigure?.id;
49092
+ if (draggedFigureId && !this.env.model.getters.getFigure(sheetId, draggedFigureId)) {
49093
+ if (this.dnd.cancelDnd) {
49094
+ this.dnd.cancelDnd();
49095
+ }
49096
+ this.dnd.draggedFigure = undefined;
49097
+ this.dnd.horizontalSnap = undefined;
49098
+ this.dnd.verticalSnap = undefined;
49099
+ this.dnd.cancelDnd = undefined;
49100
+ }
49101
+ });
47972
49102
  }
47973
49103
  getVisibleFigures() {
47974
49104
  const visibleFigures = this.env.model.getters.getVisibleFigures();
47975
49105
  if (this.dnd.draggedFigure &&
47976
49106
  !visibleFigures.some((figure) => figure.id === this.dnd.draggedFigure?.id)) {
47977
- visibleFigures.push(this.env.model.getters.getFigure(this.env.model.getters.getActiveSheetId(), this.dnd.draggedFigure?.id));
49107
+ const draggedFigure = this.env.model.getters.getFigure(this.env.model.getters.getActiveSheetId(), this.dnd.draggedFigure?.id);
49108
+ if (draggedFigure) {
49109
+ visibleFigures.push(draggedFigure);
49110
+ }
47978
49111
  }
47979
49112
  return visibleFigures;
47980
49113
  }
@@ -48093,7 +49226,7 @@ class FiguresContainer extends owl.Component {
48093
49226
  this.dnd.verticalSnap = undefined;
48094
49227
  this.env.model.dispatch("UPDATE_FIGURE", { sheetId, id: figure.id, x, y });
48095
49228
  };
48096
- startDnd(onMouseMove, onMouseUp);
49229
+ this.dnd.cancelDnd = startDnd(onMouseMove, onMouseUp);
48097
49230
  }
48098
49231
  /**
48099
49232
  * Initialize the resize of a figure with mouse movements
@@ -48141,7 +49274,7 @@ class FiguresContainer extends owl.Component {
48141
49274
  this.dnd.horizontalSnap = undefined;
48142
49275
  this.dnd.verticalSnap = undefined;
48143
49276
  };
48144
- startDnd(onMouseMove, onMouseUp);
49277
+ this.dnd.cancelDnd = startDnd(onMouseMove, onMouseUp);
48145
49278
  }
48146
49279
  getOtherFigures(figId) {
48147
49280
  return this.getVisibleFigures().filter((f) => f.id !== figId);
@@ -49248,9 +50381,11 @@ class HeadersOverlay extends owl.Component {
49248
50381
  class GridRenderer {
49249
50382
  getters;
49250
50383
  renderer;
50384
+ fingerprints;
49251
50385
  constructor(get) {
49252
50386
  this.getters = get(ModelStore).getters;
49253
50387
  this.renderer = get(RendererStore);
50388
+ this.fingerprints = get(FormulaFingerprintStore);
49254
50389
  this.renderer.register(this);
49255
50390
  }
49256
50391
  get renderingLayers() {
@@ -49728,14 +50863,22 @@ class GridRenderer {
49728
50863
  const showFormula = this.getters.shouldShowFormulas();
49729
50864
  const { x, y, width, height } = this.getters.getVisibleRect(zone);
49730
50865
  const { verticalAlign } = this.getters.getCellStyle(position);
50866
+ let style = this.getters.getCellComputedStyle(position);
50867
+ if (this.fingerprints.isEnabled) {
50868
+ const fingerprintColor = this.fingerprints.colors.get(position);
50869
+ style = { ...style, fillColor: fingerprintColor };
50870
+ }
50871
+ const dataBarFill = this.fingerprints.isEnabled
50872
+ ? undefined
50873
+ : this.getters.getConditionalDataBar(position);
49731
50874
  const box = {
49732
50875
  x,
49733
50876
  y,
49734
50877
  width,
49735
50878
  height,
49736
50879
  border: this.getters.getCellComputedBorder(position) || undefined,
49737
- style: this.getters.getCellComputedStyle(position),
49738
- dataBarFill: this.getters.getConditionalDataBar(position),
50880
+ style,
50881
+ dataBarFill,
49739
50882
  verticalAlign,
49740
50883
  isError: (cell.type === CellValueType.error && !!cell.message) ||
49741
50884
  this.getters.isDataValidationInvalid(position),
@@ -49760,7 +50903,6 @@ class GridRenderer {
49760
50903
  box.hasIcon = this.getters.doesCellHaveGridIcon(position);
49761
50904
  const headerIconWidth = box.hasIcon ? GRID_ICON_EDGE_LENGTH + GRID_ICON_MARGIN : 0;
49762
50905
  /** Content */
49763
- const style = this.getters.getCellComputedStyle(position);
49764
50906
  const wrapping = style.wrapping || "overflow";
49765
50907
  const wrapText = wrapping === "wrap" && !showFormula;
49766
50908
  const maxWidth = width - 2 * MIN_CELL_TEXT_MARGIN;
@@ -51738,62 +52880,6 @@ class BordersPlugin extends CorePlugin {
51738
52880
  }
51739
52881
  }
51740
52882
 
51741
- class PositionMap {
51742
- map = {};
51743
- constructor(entries = []) {
51744
- for (const [position, value] of entries) {
51745
- this.set(position, value);
51746
- }
51747
- }
51748
- set({ sheetId, col, row }, value) {
51749
- const map = this.map;
51750
- if (!map[sheetId]) {
51751
- map[sheetId] = {};
51752
- }
51753
- if (!map[sheetId][col]) {
51754
- map[sheetId][col] = {};
51755
- }
51756
- map[sheetId][col][row] = value;
51757
- }
51758
- get({ sheetId, col, row }) {
51759
- return this.map[sheetId]?.[col]?.[row];
51760
- }
51761
- getSheet(sheetId) {
51762
- return this.map[sheetId];
51763
- }
51764
- has({ sheetId, col, row }) {
51765
- return this.map[sheetId]?.[col]?.[row] !== undefined;
51766
- }
51767
- delete({ sheetId, col, row }) {
51768
- delete this.map[sheetId]?.[col]?.[row];
51769
- }
51770
- keys() {
51771
- const map = this.map;
51772
- const keys = [];
51773
- for (const sheetId in map) {
51774
- for (const col in map[sheetId]) {
51775
- for (const row in map[sheetId][col]) {
51776
- keys.push({ sheetId, col: parseInt(col), row: parseInt(row) });
51777
- }
51778
- }
51779
- }
51780
- return keys;
51781
- }
51782
- keysForSheet(sheetId) {
51783
- const map = this.map[sheetId];
51784
- if (!map) {
51785
- return [];
51786
- }
51787
- const keys = [];
51788
- for (const col in map) {
51789
- for (const row in map[col]) {
51790
- keys.push({ sheetId, col: parseInt(col), row: parseInt(row) });
51791
- }
51792
- }
51793
- return keys;
51794
- }
51795
- }
51796
-
51797
52883
  /**
51798
52884
  * Core Plugin
51799
52885
  *
@@ -51888,7 +52974,7 @@ class CellPlugin extends CorePlugin {
51888
52974
  for (let col = zone.left; col <= zone.right; col++) {
51889
52975
  for (let row = zone.top; row <= zone.bottom; row++) {
51890
52976
  const cell = this.getters.getCell({ sheetId, col, row });
51891
- if (cell) {
52977
+ if (cell?.isFormula || cell?.content) {
51892
52978
  this.dispatch("UPDATE_CELL", {
51893
52979
  sheetId: sheetId,
51894
52980
  content: "",
@@ -51924,7 +53010,6 @@ class CellPlugin extends CorePlugin {
51924
53010
  for (let zone of recomputeZones(zones)) {
51925
53011
  for (let col = zone.left; col <= zone.right; col++) {
51926
53012
  for (let row = zone.top; row <= zone.bottom; row++) {
51927
- // commandHelpers.updateCell(sheetId, col, row, { style: undefined});
51928
53013
  this.dispatch("UPDATE_CELL", {
51929
53014
  sheetId,
51930
53015
  col,
@@ -52233,7 +53318,7 @@ class CellPlugin extends CorePlugin {
52233
53318
  const before = this.getters.getCell({ sheetId, col, row });
52234
53319
  const hasContent = "content" in after || "formula" in after;
52235
53320
  // Compute the new cell properties
52236
- const afterContent = hasContent ? replaceSpecialSpaces(after?.content) : before?.content || "";
53321
+ const afterContent = hasContent ? replaceNewLines(after?.content) : before?.content || "";
52237
53322
  let style;
52238
53323
  if (after.style !== undefined) {
52239
53324
  style = after.style || undefined;
@@ -55178,6 +56263,9 @@ class SheetPlugin extends CorePlugin {
55178
56263
  };
55179
56264
  }
55180
56265
  getUnboundedZone(sheetId, zone) {
56266
+ if (zone.bottom === undefined || zone.right === undefined) {
56267
+ return zone;
56268
+ }
55181
56269
  const isFullRow = zone.left === 0 && zone.right === this.getNumberCols(sheetId) - 1;
55182
56270
  const isFullCol = zone.top === 0 && zone.bottom === this.getNumberRows(sheetId) - 1;
55183
56271
  return {
@@ -55343,7 +56431,7 @@ class SheetPlugin extends CorePlugin {
55343
56431
  if (orderedSheetIds.find((id) => sheets[id]?.name.toLowerCase() === name && id !== cmd.sheetId)) {
55344
56432
  return "DuplicatedSheetName" /* CommandResult.DuplicatedSheetName */;
55345
56433
  }
55346
- if (FORBIDDEN_IN_EXCEL_REGEX.test(name)) {
56434
+ if (FORBIDDEN_SHEETNAME_CHARS_IN_EXCEL_REGEX.test(name)) {
55347
56435
  return "ForbiddenCharactersInSheetName" /* CommandResult.ForbiddenCharactersInSheetName */;
55348
56436
  }
55349
56437
  return "Success" /* CommandResult.Success */;
@@ -59461,66 +60549,25 @@ class EvaluationConditionalFormatPlugin extends UIPlugin {
59461
60549
  return;
59462
60550
  }
59463
60551
  const zone = this.getters.getRangeFromSheetXC(sheetId, range).zone;
59464
- const colorCellArgs = [];
60552
+ const colorThresholds = [{ value: minValue, color: rule.minimum.color }];
59465
60553
  if (rule.midpoint && midValue) {
59466
- colorCellArgs.push({
59467
- minValue,
59468
- minColor: rule.minimum.color,
59469
- colorDiffUnit: this.computeColorDiffUnits(minValue, midValue, rule.minimum.color, rule.midpoint.color),
59470
- });
59471
- colorCellArgs.push({
59472
- minValue: midValue,
59473
- minColor: rule.midpoint.color,
59474
- colorDiffUnit: this.computeColorDiffUnits(midValue, maxValue, rule.midpoint.color, rule.maximum.color),
59475
- });
59476
- }
59477
- else {
59478
- colorCellArgs.push({
59479
- minValue,
59480
- minColor: rule.minimum.color,
59481
- colorDiffUnit: this.computeColorDiffUnits(minValue, maxValue, rule.minimum.color, rule.maximum.color),
59482
- });
60554
+ colorThresholds.push({ value: midValue, color: rule.midpoint.color });
59483
60555
  }
60556
+ colorThresholds.push({ value: maxValue, color: rule.maximum.color });
60557
+ const colorScale = getColorScale(colorThresholds);
59484
60558
  for (let row = zone.top; row <= zone.bottom; row++) {
59485
60559
  for (let col = zone.left; col <= zone.right; col++) {
59486
60560
  const cell = this.getters.getEvaluatedCell({ sheetId, col, row });
59487
60561
  if (cell.type === CellValueType.number) {
59488
60562
  const value = clip(cell.value, minValue, maxValue);
59489
- let color;
59490
- if (colorCellArgs.length === 2 && midValue) {
59491
- color =
59492
- value <= midValue
59493
- ? this.colorCell(value, colorCellArgs[0].minValue, colorCellArgs[0].minColor, colorCellArgs[0].colorDiffUnit)
59494
- : this.colorCell(value, colorCellArgs[1].minValue, colorCellArgs[1].minColor, colorCellArgs[1].colorDiffUnit);
59495
- }
59496
- else {
59497
- color = this.colorCell(value, colorCellArgs[0].minValue, colorCellArgs[0].minColor, colorCellArgs[0].colorDiffUnit);
59498
- }
59499
60563
  if (!computedStyle[col])
59500
60564
  computedStyle[col] = [];
59501
60565
  computedStyle[col][row] = computedStyle[col]?.[row] || {};
59502
- computedStyle[col][row].fillColor = colorNumberString(color);
60566
+ computedStyle[col][row].fillColor = colorScale(value);
59503
60567
  }
59504
60568
  }
59505
60569
  }
59506
60570
  }
59507
- computeColorDiffUnits(minValue, maxValue, minColor, maxColor) {
59508
- const deltaValue = maxValue - minValue;
59509
- const deltaColorR = ((minColor >> 16) % 256) - ((maxColor >> 16) % 256);
59510
- const deltaColorG = ((minColor >> 8) % 256) - ((maxColor >> 8) % 256);
59511
- const deltaColorB = (minColor % 256) - (maxColor % 256);
59512
- const colorDiffUnitR = deltaColorR / deltaValue;
59513
- const colorDiffUnitG = deltaColorG / deltaValue;
59514
- const colorDiffUnitB = deltaColorB / deltaValue;
59515
- return [colorDiffUnitR, colorDiffUnitG, colorDiffUnitB];
59516
- }
59517
- colorCell(value, minValue, minColor, colorDiffUnit) {
59518
- const [colorDiffUnitR, colorDiffUnitG, colorDiffUnitB] = colorDiffUnit;
59519
- const r = Math.round(((minColor >> 16) % 256) - colorDiffUnitR * (value - minValue));
59520
- const g = Math.round(((minColor >> 8) % 256) - colorDiffUnitG * (value - minValue));
59521
- const b = Math.round((minColor % 256) - colorDiffUnitB * (value - minValue));
59522
- return (r << 16) | (g << 8) | b;
59523
- }
59524
60571
  /**
59525
60572
  * Execute the predicate to know if a conditional formatting rule should be applied to a cell
59526
60573
  */
@@ -60146,7 +61193,7 @@ function withPivotPresentationLayer (PivotClass) {
60146
61193
  }
60147
61194
  getValuesToAggregate(measure, domain) {
60148
61195
  const { rowDomain, colDomain } = domainToColRowDomain(this, domain);
60149
- const table = this.getTableStructure();
61196
+ const table = super.getTableStructure();
60150
61197
  const values = [];
60151
61198
  if (colDomain.length === 0 &&
60152
61199
  rowDomain.length < this.definition.rows.length &&
@@ -60580,6 +61627,21 @@ function withPivotPresentationLayer (PivotClass) {
60580
61627
  }
60581
61628
  throw new Error(`Value ${result.value} is not a number`);
60582
61629
  }
61630
+ getTableStructure() {
61631
+ const table = super.getTableStructure();
61632
+ this.sortTableStructure(table);
61633
+ return table;
61634
+ }
61635
+ sortTableStructure(table) {
61636
+ if (!this.definition.sortedColumn || table.isSorted) {
61637
+ return;
61638
+ }
61639
+ const measure = this.definition.sortedColumn.measure;
61640
+ const isSortValid = isSortedColumnValid(this.definition.sortedColumn, this);
61641
+ if (isSortValid) {
61642
+ table.sort(measure, this.definition.sortedColumn, (measure, domain) => this._getPivotCellValueAndFormat(measure, domain));
61643
+ }
61644
+ }
60583
61645
  }
60584
61646
  return PivotPresentationLayer;
60585
61647
  }
@@ -60751,11 +61813,13 @@ class PivotUIPlugin extends UIPlugin {
60751
61813
  return EMPTY_PIVOT_CELL;
60752
61814
  }
60753
61815
  if (functionName === "PIVOT") {
60754
- const includeTotal = args[2] === false ? false : undefined;
60755
- const includeColumnHeaders = args[3] === false ? false : undefined;
61816
+ const includeTotal = toScalar(args[2]);
61817
+ const shouldIncludeTotal = includeTotal === undefined ? true : toBoolean(includeTotal);
61818
+ const includeColumnHeaders = toScalar(args[3]);
61819
+ const shouldIncludeColumnHeaders = includeColumnHeaders === undefined ? true : toBoolean(includeColumnHeaders);
60756
61820
  const pivotCells = pivot
60757
61821
  .getTableStructure()
60758
- .getPivotCells(includeTotal, includeColumnHeaders);
61822
+ .getPivotCells(shouldIncludeTotal, shouldIncludeColumnHeaders);
60759
61823
  const pivotCol = position.col - mainPosition.col;
60760
61824
  const pivotRow = position.row - mainPosition.row;
60761
61825
  return pivotCells[pivotCol][pivotRow];
@@ -62353,23 +63417,6 @@ class Session extends EventBus {
62353
63417
  }
62354
63418
  }
62355
63419
 
62356
- function randomChoice(arr) {
62357
- return arr[Math.floor(Math.random() * arr.length)];
62358
- }
62359
- const colors = [
62360
- "#ff851b",
62361
- "#0074d9",
62362
- "#7fdbff",
62363
- "#b10dc9",
62364
- "#39cccc",
62365
- "#f012be",
62366
- "#3d9970",
62367
- "#111111",
62368
- "#ff4136",
62369
- "#aaaaaa",
62370
- "#85144b",
62371
- "#001f3f",
62372
- ];
62373
63420
  class CollaborativePlugin extends UIPlugin {
62374
63421
  static getters = [
62375
63422
  "getClientsToDisplay",
@@ -62378,7 +63425,7 @@ class CollaborativePlugin extends UIPlugin {
62378
63425
  "isFullySynchronized",
62379
63426
  ];
62380
63427
  static layers = ["Selection"];
62381
- availableColors = new Set(colors);
63428
+ availableColors = new AlternatingColorGenerator(12);
62382
63429
  colors = {};
62383
63430
  session;
62384
63431
  constructor(config) {
@@ -62389,14 +63436,6 @@ class CollaborativePlugin extends UIPlugin {
62389
63436
  return (position.row < this.getters.getNumberRows(position.sheetId) &&
62390
63437
  position.col < this.getters.getNumberCols(position.sheetId));
62391
63438
  }
62392
- chooseNewColor() {
62393
- if (this.availableColors.size === 0) {
62394
- this.availableColors = new Set(colors);
62395
- }
62396
- const color = randomChoice([...this.availableColors.values()]);
62397
- this.availableColors.delete(color);
62398
- return color;
62399
- }
62400
63439
  getClient() {
62401
63440
  return this.session.getClient();
62402
63441
  }
@@ -62431,7 +63470,7 @@ class CollaborativePlugin extends UIPlugin {
62431
63470
  this.isPositionValid(client.position)) {
62432
63471
  const position = client.position;
62433
63472
  if (!this.colors[client.id]) {
62434
- this.colors[client.id] = this.chooseNewColor();
63473
+ this.colors[client.id] = this.availableColors.next();
62435
63474
  }
62436
63475
  const color = this.colors[client.id];
62437
63476
  clients.push({ ...client, position, color });
@@ -62668,7 +63707,7 @@ class FormatPlugin extends UIPlugin {
62668
63707
  for (let row = zone.top; row <= zone.bottom; row++) {
62669
63708
  const position = { sheetId, col, row };
62670
63709
  const pivotCell = this.getters.getPivotCellFromPosition(position);
62671
- if (pivotCell.type === "VALUE") {
63710
+ if (this.isSpilledPivotValueFormula(position, pivotCell)) {
62672
63711
  measurePositions.push(position);
62673
63712
  const pivotId = this.getters.getPivotIdFromPosition(position) || "";
62674
63713
  measuresByPivotId[pivotId] ??= new Set();
@@ -62705,6 +63744,10 @@ class FormatPlugin extends UIPlugin {
62705
63744
  format,
62706
63745
  });
62707
63746
  }
63747
+ isSpilledPivotValueFormula(position, pivotCell) {
63748
+ const cell = this.getters.getCell(position);
63749
+ return pivotCell.type === "VALUE" && !cell?.isFormula;
63750
+ }
62708
63751
  /**
62709
63752
  * This function allows to adjust the quantity of decimal places after a decimal
62710
63753
  * point on cells containing number value. It does this by changing the cells
@@ -62755,6 +63798,69 @@ class FormatPlugin extends UIPlugin {
62755
63798
  }
62756
63799
  }
62757
63800
 
63801
+ class GeoFeaturePlugin extends UIPlugin {
63802
+ static getters = [
63803
+ "getGeoJsonFeatures",
63804
+ "geoFeatureNameToId",
63805
+ "getGeoChartAvailableRegions",
63806
+ ];
63807
+ geoJsonService;
63808
+ geoJsonCache = {};
63809
+ constructor(config) {
63810
+ super(config);
63811
+ this.geoJsonService = config.external.geoJsonService;
63812
+ }
63813
+ getGeoChartAvailableRegions() {
63814
+ if (!this.geoJsonService) {
63815
+ console.error("No geoJsonService provided to the model");
63816
+ return [];
63817
+ }
63818
+ return this.geoJsonService.getAvailableRegions() || [];
63819
+ }
63820
+ getGeoJsonFeatures(region) {
63821
+ if (!this.geoJsonService) {
63822
+ console.error("No geoJsonService provided to the model");
63823
+ return;
63824
+ }
63825
+ const cachedGeoJson = this.geoJsonCache[region];
63826
+ if (cachedGeoJson instanceof Promise) {
63827
+ return undefined;
63828
+ }
63829
+ if (cachedGeoJson !== undefined) {
63830
+ return cachedGeoJson ?? undefined;
63831
+ }
63832
+ this.geoJsonCache[region] = new Promise(async (resolve) => {
63833
+ const json = await this.geoJsonService?.getTopoJson(region);
63834
+ this.geoJsonCache[region] = this.convertToGeoJson(json);
63835
+ this.dispatch("EVALUATE_CHARTS");
63836
+ resolve();
63837
+ });
63838
+ return undefined;
63839
+ }
63840
+ geoFeatureNameToId(region, featureName) {
63841
+ if (!this.geoJsonService) {
63842
+ console.error("No geoJsonService provided to the model");
63843
+ return;
63844
+ }
63845
+ return this.geoJsonService.geoFeatureNameToId(region, featureName);
63846
+ }
63847
+ convertToGeoJson(json) {
63848
+ if (!json) {
63849
+ return null;
63850
+ }
63851
+ // TopoJSON
63852
+ if (json.type === "Topology") {
63853
+ const features = window.ChartGeo.topojson.feature(json, Object.values(json.objects)[0]);
63854
+ return features.type === "FeatureCollection" ? features.features : [features];
63855
+ }
63856
+ // GeoJSON
63857
+ else if (json.type === "FeatureCollection") {
63858
+ return json.features;
63859
+ }
63860
+ throw new Error("Invalid TopoJSON");
63861
+ }
63862
+ }
63863
+
62758
63864
  class HeaderVisibilityUIPlugin extends UIPlugin {
62759
63865
  static getters = [
62760
63866
  "getNextVisibleCellPosition",
@@ -62909,7 +64015,7 @@ class InsertPivotPlugin extends UIPlugin {
62909
64015
  getPivotDuplicateSheetName(pivotName) {
62910
64016
  let i = 1;
62911
64017
  const names = this.getters.getSheetIds().map((id) => this.getters.getSheetName(id));
62912
- const sanitizedName = pivotName.replace(new RegExp(FORBIDDEN_IN_EXCEL_REGEX, "g"), " ");
64018
+ const sanitizedName = sanitizeSheetName(pivotName);
62913
64019
  let name = sanitizedName;
62914
64020
  while (names.includes(name)) {
62915
64021
  name = `${sanitizedName} (${i})`;
@@ -64292,10 +65398,13 @@ class TableResizeUI extends UIPlugin {
64292
65398
  case "RESIZE_TABLE": {
64293
65399
  const table = this.getters.getCoreTableMatchingTopLeft(cmd.sheetId, cmd.zone);
64294
65400
  this.dispatch("UPDATE_TABLE", { ...cmd });
64295
- if (!table || !table.config.automaticAutofill)
65401
+ if (!table)
64296
65402
  return;
64297
- const oldTableZone = table.range.zone;
64298
65403
  const newTableZone = this.getters.getRangeFromRangeData(cmd.newTableRange).zone;
65404
+ this.selection.selectCell(newTableZone.right, newTableZone.bottom);
65405
+ if (!table.config.automaticAutofill)
65406
+ return;
65407
+ const oldTableZone = table.range.zone;
64299
65408
  if (newTableZone.bottom >= oldTableZone.bottom) {
64300
65409
  for (let col = newTableZone.left; col <= newTableZone.right; col++) {
64301
65410
  const autofillSource = { col, row: oldTableZone.bottom, sheetId: cmd.sheetId };
@@ -65501,15 +66610,23 @@ class GridSelectionPlugin extends UIPlugin {
65501
66610
  handler.paste({ zones: pasteTarget, sheetId }, data, { isCutOperation: true });
65502
66611
  const toRemove = isBasedBefore ? cmd.elements.map((el) => el + thickness) : cmd.elements;
65503
66612
  let currentIndex = cmd.base;
66613
+ const resizingGroups = {};
65504
66614
  for (const element of toRemove) {
65505
66615
  const size = this.getters.getHeaderSize(cmd.sheetId, cmd.dimension, element);
66616
+ const currentSize = this.getters.getHeaderSize(cmd.sheetId, cmd.dimension, currentIndex);
66617
+ if (size != currentSize) {
66618
+ resizingGroups[size] ??= [];
66619
+ resizingGroups[size].push(currentIndex);
66620
+ currentIndex += 1;
66621
+ }
66622
+ }
66623
+ for (const size in resizingGroups) {
65506
66624
  this.dispatch("RESIZE_COLUMNS_ROWS", {
65507
66625
  dimension: cmd.dimension,
65508
66626
  sheetId: cmd.sheetId,
65509
- size,
65510
- elements: [currentIndex],
66627
+ size: parseInt(size, 10),
66628
+ elements: resizingGroups[size],
65511
66629
  });
65512
- currentIndex += 1;
65513
66630
  }
65514
66631
  this.dispatch("REMOVE_COLUMNS_ROWS", {
65515
66632
  dimension: cmd.dimension,
@@ -66091,6 +67208,7 @@ class SheetViewPlugin extends UIPlugin {
66091
67208
  break;
66092
67209
  case "DELETE_SHEET":
66093
67210
  this.cleanViewports();
67211
+ this.sheetsWithDirtyViewports.delete(cmd.sheetId);
66094
67212
  break;
66095
67213
  case "ACTIVATE_SHEET":
66096
67214
  this.sheetsWithDirtyViewports.add(cmd.sheetIdTo);
@@ -66741,7 +67859,8 @@ const featurePluginRegistry = new Registry()
66741
67859
  .add("data_cleanup", DataCleanupPlugin)
66742
67860
  .add("table_autofill", TableAutofillPlugin)
66743
67861
  .add("table_ui_resize", TableResizeUI)
66744
- .add("datavalidation_insert", DataValidationInsertionPlugin);
67862
+ .add("datavalidation_insert", DataValidationInsertionPlugin)
67863
+ .add("geo_features", GeoFeaturePlugin);
66745
67864
  // Plugins which have a state, but which should not be shared in collaborative
66746
67865
  const statefulUIPluginRegistry = new Registry()
66747
67866
  .add("selection", GridSelectionPlugin)
@@ -66985,7 +68104,7 @@ function interactiveRenameSheet(env, sheetId, name, errorCallback) {
66985
68104
  env.raiseError(_t("A sheet with the name %s already exists. Please select another name.", name), errorCallback);
66986
68105
  }
66987
68106
  else if (result.reasons.includes("ForbiddenCharactersInSheetName" /* CommandResult.ForbiddenCharactersInSheetName */)) {
66988
- env.raiseError(_t("Some used characters are not allowed in a sheet name (Forbidden characters are %s).", FORBIDDEN_SHEET_CHARS.join(" ")), errorCallback);
68107
+ env.raiseError(_t("Some used characters are not allowed in a sheet name (Forbidden characters are %s).", FORBIDDEN_SHEETNAME_CHARS.join(" ")), errorCallback);
66989
68108
  }
66990
68109
  }
66991
68110
 
@@ -68215,7 +69334,7 @@ class ActionButton extends owl.Component {
68215
69334
  setup() {
68216
69335
  owl.onWillUpdateProps((nextProps) => {
68217
69336
  if (nextProps.action !== this.props.action) {
68218
- this.actionButton = createAction(this.props.action);
69337
+ this.actionButton = createAction(nextProps.action);
68219
69338
  }
68220
69339
  });
68221
69340
  }
@@ -68485,8 +69604,8 @@ css /* scss */ `
68485
69604
  .o-topbar-composer {
68486
69605
  height: fit-content;
68487
69606
  margin-top: -1px;
69607
+ margin-bottom: -1px;
68488
69608
  border: 1px solid;
68489
- z-index: ${ComponentsImportance.TopBarComposer};
68490
69609
  font-family: ${DEFAULT_FONT};
68491
69610
 
68492
69611
  .o-composer:empty:not(:focus):not(.active)::before {
@@ -68544,6 +69663,7 @@ class TopBarComposer extends owl.Component {
68544
69663
  }
68545
69664
  return cssPropertiesToCss({
68546
69665
  "border-color": SELECTION_BORDER_COLOR,
69666
+ "z-index": String(ComponentsImportance.TopBarComposer),
68547
69667
  });
68548
69668
  }
68549
69669
  onFocus(selection) {
@@ -68551,88 +69671,6 @@ class TopBarComposer extends owl.Component {
68551
69671
  }
68552
69672
  }
68553
69673
 
68554
- css /* scss */ `
68555
- .o-font-size-editor {
68556
- height: calc(100% - 4px);
68557
- input.o-font-size {
68558
- outline-color: ${SELECTION_BORDER_COLOR};
68559
- height: 20px;
68560
- width: 23px;
68561
- }
68562
- }
68563
- .o-text-options > div {
68564
- cursor: pointer;
68565
- line-height: 26px;
68566
- padding: 3px 12px;
68567
- &:hover {
68568
- background-color: rgba(0, 0, 0, 0.08);
68569
- }
68570
- }
68571
- `;
68572
- class FontSizeEditor extends owl.Component {
68573
- static template = "o-spreadsheet-FontSizeEditor";
68574
- static props = {
68575
- onToggle: Function,
68576
- dropdownStyle: String,
68577
- class: String,
68578
- };
68579
- static components = {};
68580
- fontSizes = FONT_SIZES;
68581
- dropdown = owl.useState({ isOpen: false });
68582
- inputRef = owl.useRef("inputFontSize");
68583
- rootEditorRef = owl.useRef("FontSizeEditor");
68584
- setup() {
68585
- owl.useExternalListener(window, "click", this.onExternalClick, { capture: true });
68586
- }
68587
- onExternalClick(ev) {
68588
- if (!isChildEvent(this.rootEditorRef.el, ev)) {
68589
- this.closeFontList();
68590
- }
68591
- }
68592
- get currentFontSize() {
68593
- return this.env.model.getters.getCurrentStyle().fontSize || DEFAULT_FONT_SIZE;
68594
- }
68595
- toggleFontList() {
68596
- const isOpen = this.dropdown.isOpen;
68597
- if (!isOpen) {
68598
- this.props.onToggle();
68599
- this.inputRef.el.focus();
68600
- }
68601
- else {
68602
- this.closeFontList();
68603
- }
68604
- }
68605
- closeFontList() {
68606
- this.dropdown.isOpen = false;
68607
- }
68608
- setSize(fontSizeStr) {
68609
- const fontSize = clip(Math.floor(parseFloat(fontSizeStr)), 1, 400);
68610
- setStyle(this.env, { fontSize });
68611
- this.closeFontList();
68612
- }
68613
- setSizeFromInput(ev) {
68614
- this.setSize(ev.target.value);
68615
- }
68616
- setSizeFromList(fontSizeStr) {
68617
- this.setSize(fontSizeStr);
68618
- }
68619
- onInputFocused(ev) {
68620
- this.dropdown.isOpen = true;
68621
- ev.target.select();
68622
- }
68623
- onInputKeydown(ev) {
68624
- if (ev.key === "Enter" || ev.key === "Escape") {
68625
- this.closeFontList();
68626
- const target = ev.target;
68627
- // In the case of a ESCAPE key, we get the previous font size back
68628
- if (ev.key === "Escape") {
68629
- target.value = `${this.currentFontSize}`;
68630
- }
68631
- this.props.onToggle();
68632
- }
68633
- }
68634
- }
68635
-
68636
69674
  class TableDropdownButton extends owl.Component {
68637
69675
  static template = "o-spreadsheet-TableDropdownButton";
68638
69676
  static components = { TableStylesPopover, ActionButton };
@@ -68734,6 +69772,15 @@ css /* scss */ `
68734
69772
  }
68735
69773
  }
68736
69774
 
69775
+ .irregularity-map {
69776
+ border-top: 1px solid ${SEPARATOR_COLOR};
69777
+ height: ${TOPBAR_TOOLBAR_HEIGHT}px;
69778
+
69779
+ .alert-info {
69780
+ border-left: 3px solid ${ALERT_INFO_BORDER};
69781
+ }
69782
+ }
69783
+
68737
69784
  .o-topbar-composer {
68738
69785
  flex-grow: 1;
68739
69786
  }
@@ -68801,9 +69848,6 @@ class TopBar extends owl.Component {
68801
69848
  onClick: Function,
68802
69849
  dropdownMaxHeight: Number,
68803
69850
  };
68804
- get dropdownStyle() {
68805
- return `max-height:${this.props.dropdownMaxHeight}px`;
68806
- }
68807
69851
  static components = {
68808
69852
  ColorPickerWidget,
68809
69853
  ColorPicker,
@@ -68830,8 +69874,10 @@ class TopBar extends owl.Component {
68830
69874
  formatNumberMenuItemSpec = formatNumberMenuItemSpec;
68831
69875
  isntToolbarMenu = false;
68832
69876
  composerFocusStore;
69877
+ fingerprints;
68833
69878
  setup() {
68834
69879
  this.composerFocusStore = useStore(ComposerFocusStore);
69880
+ this.fingerprints = useStore(FormulaFingerprintStore);
68835
69881
  owl.useExternalListener(window, "click", this.onExternalClick);
68836
69882
  owl.onWillStart(() => this.updateCellState());
68837
69883
  owl.onWillUpdateProps(() => this.updateCellState());
@@ -68841,6 +69887,9 @@ class TopBar extends owl.Component {
68841
69887
  .getAllOrdered()
68842
69888
  .filter((item) => !item.isVisible || item.isVisible(this.env));
68843
69889
  }
69890
+ get currentFontSize() {
69891
+ return this.env.model.getters.getCurrentStyle().fontSize || DEFAULT_FONT_SIZE;
69892
+ }
68844
69893
  onExternalClick(ev) {
68845
69894
  // TODO : manage click events better. We need this piece of code
68846
69895
  // otherwise the event opening the menu would close it on the same frame.
@@ -68919,6 +69968,9 @@ class TopBar extends owl.Component {
68919
69968
  setStyle(this.env, { [target]: color });
68920
69969
  this.onClick();
68921
69970
  }
69971
+ setFontSize(fontSize) {
69972
+ setStyle(this.env, { fontSize });
69973
+ }
68922
69974
  }
68923
69975
 
68924
69976
  function instantiateClipboard() {
@@ -69296,7 +70348,7 @@ class Spreadsheet extends owl.Component {
69296
70348
  properties["grid-template-rows"] = `auto`;
69297
70349
  }
69298
70350
  else {
69299
- properties["grid-template-rows"] = `${TOPBAR_HEIGHT}px auto ${BOTTOMBAR_HEIGHT + 1}px`;
70351
+ properties["grid-template-rows"] = `max-content auto ${BOTTOMBAR_HEIGHT + 1}px`;
69300
70352
  }
69301
70353
  properties["grid-template-columns"] = `auto ${this.sidePanel.panelSize}px`;
69302
70354
  return cssPropertiesToCss(properties);
@@ -70374,9 +71426,6 @@ class SelectionStreamProcessorImpl {
70374
71426
  getBackToDefault() {
70375
71427
  this.stream.getBackToDefault();
70376
71428
  }
70377
- getAnchor() {
70378
- return this.anchor;
70379
- }
70380
71429
  modifyAnchor(anchor, mode, options) {
70381
71430
  const sheetId = this.getters.getActiveSheetId();
70382
71431
  anchor = {
@@ -70919,12 +71968,11 @@ function createChart(chart, chartSheetIndex, data) {
70919
71968
  // <manualLayout/> to manually position the chart in the figure container
70920
71969
  let title = escapeXml ``;
70921
71970
  if (chart.data.title?.text) {
70922
- const color = chart.data.title.color
70923
- ? toXlsxHexColor(chart.data.title.color)
70924
- : chart.data.fontColor;
71971
+ const titleColor = toXlsxHexColor(chartMutedFontColor(chart.data.backgroundColor));
71972
+ const fontSize = chart.data.title.fontSize ?? CHART_TITLE_FONT_SIZE;
70925
71973
  title = escapeXml /*xml*/ `
70926
71974
  <c:title>
70927
- ${insertText(chart.data.title.text, color, DEFAULT_CHART_FONT_SIZE, chart.data.title)}
71975
+ ${insertText(chart.data.title.text, titleColor, fontSize, chart.data.title)}
70928
71976
  <c:overlay val="0" />
70929
71977
  </c:title>
70930
71978
  `;
@@ -71014,7 +72062,7 @@ function lineAttributes(params) {
71014
72062
  </a:ln>
71015
72063
  `;
71016
72064
  }
71017
- function insertText(text, fontColor = "000000", fontsize = DEFAULT_CHART_FONT_SIZE, style = {}) {
72065
+ function insertText(text, fontColor = "000000", fontsize = CHART_TITLE_FONT_SIZE, style = {}) {
71018
72066
  return escapeXml /*xml*/ `
71019
72067
  <c:tx>
71020
72068
  <c:rich>
@@ -71515,6 +72563,7 @@ function addAx(position, axisName, axId, crossAxId, title, defaultFontColor, del
71515
72563
  // Each Axis present inside a graph needs to be identified by an unsigned integer in order to be referenced by its crossAxis.
71516
72564
  // I.e. x-axis, will reference y-axis and vice-versa.
71517
72565
  const color = title?.color ? toXlsxHexColor(title.color) : defaultFontColor;
72566
+ const fontSize = title?.fontSize ?? CHART_AXIS_TITLE_FONT_SIZE;
71518
72567
  return escapeXml /*xml*/ `
71519
72568
  <${axisName}>
71520
72569
  <c:axId val="${axId}"/>
@@ -71530,7 +72579,7 @@ function addAx(position, axisName, axId, crossAxId, title, defaultFontColor, del
71530
72579
  <c:minorTickMark val="none" />
71531
72580
  <c:numFmt formatCode="General" sourceLinked="1" />
71532
72581
  <c:title>
71533
- ${insertText(title?.text ?? "", color, 10, title)}
72582
+ ${insertText(title?.text ?? "", color, fontSize, title)}
71534
72583
  </c:title>
71535
72584
  ${insertTextProperties(10, defaultFontColor)}
71536
72585
  </${axisName}>
@@ -73308,6 +74357,7 @@ class Model extends EventBus {
73308
74357
  session: this.session,
73309
74358
  defaultCurrency: this.config.defaultCurrency,
73310
74359
  customColors: this.config.customColors || [],
74360
+ external: this.config.external,
73311
74361
  };
73312
74362
  }
73313
74363
  // ---------------------------------------------------------------------------
@@ -73539,7 +74589,6 @@ const SPREADSHEET_DIMENSIONS = {
73539
74589
  MIN_COL_WIDTH,
73540
74590
  HEADER_HEIGHT,
73541
74591
  HEADER_WIDTH,
73542
- TOPBAR_HEIGHT,
73543
74592
  BOTTOMBAR_HEIGHT,
73544
74593
  DEFAULT_CELL_WIDTH,
73545
74594
  DEFAULT_CELL_HEIGHT,
@@ -73641,6 +74690,7 @@ const helpers = {
73641
74690
  createPivotFormula,
73642
74691
  areDomainArgsFieldsValid,
73643
74692
  splitReference,
74693
+ sanitizeSheetName,
73644
74694
  };
73645
74695
  const links = {
73646
74696
  isMarkdownLink,
@@ -73779,6 +74829,6 @@ exports.tokenColors = tokenColors;
73779
74829
  exports.tokenize = tokenize;
73780
74830
 
73781
74831
 
73782
- __info__.version = "18.1.0-alpha.6";
73783
- __info__.date = "2024-11-28T09:06:59.527Z";
73784
- __info__.hash = "875c901";
74832
+ __info__.version = "18.1.0-alpha.8";
74833
+ __info__.date = "2024-12-19T07:49:54.421Z";
74834
+ __info__.hash = "87a1567";