@odoo/o-spreadsheet 18.1.0-alpha.0 → 18.1.0-alpha.2

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,13 +2,119 @@
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.0
6
- * @date 2024-09-25T13:17:49.636Z
7
- * @hash 288f0b7
5
+ * @version 18.1.0-alpha.2
6
+ * @date 2024-10-24T08:53:21.828Z
7
+ * @hash 2a01250
8
8
  */
9
9
 
10
10
  import { useEnv, useSubEnv, onWillUnmount, useComponent, status, Component, useRef, onMounted, useEffect, useState, onPatched, onWillPatch, onWillUpdateProps, useExternalListener, onWillStart, xml, useChildSubEnv, markRaw, toRaw } from '@odoo/owl';
11
11
 
12
+ function createActions(menuItems) {
13
+ return menuItems.map(createAction).sort((a, b) => a.sequence - b.sequence);
14
+ }
15
+ let nextItemId = 1;
16
+ function createAction(item) {
17
+ const name = item.name;
18
+ const children = item.children;
19
+ const description = item.description;
20
+ const icon = item.icon;
21
+ const secondaryIcon = item.secondaryIcon;
22
+ const itemId = item.id || nextItemId++;
23
+ return {
24
+ id: itemId.toString(),
25
+ name: typeof name === "function" ? name : () => name,
26
+ isVisible: item.isVisible ? item.isVisible : () => true,
27
+ isEnabled: item.isEnabled ? item.isEnabled : () => true,
28
+ isActive: item.isActive,
29
+ execute: item.execute,
30
+ children: children
31
+ ? (env) => {
32
+ return children
33
+ .map((child) => (typeof child === "function" ? child(env) : child))
34
+ .flat()
35
+ .map(createAction);
36
+ }
37
+ : () => [],
38
+ isReadonlyAllowed: item.isReadonlyAllowed || false,
39
+ separator: item.separator || false,
40
+ icon: typeof icon === "function" ? icon : () => icon || "",
41
+ iconColor: item.iconColor,
42
+ secondaryIcon: typeof secondaryIcon === "function" ? secondaryIcon : () => secondaryIcon || "",
43
+ description: typeof description === "function" ? description : () => description || "",
44
+ textColor: item.textColor,
45
+ sequence: item.sequence || 0,
46
+ onStartHover: item.onStartHover,
47
+ onStopHover: item.onStopHover,
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Registry
53
+ *
54
+ * The Registry class is basically just a mapping from a string key to an object.
55
+ * It is really not much more than an object. It is however useful for the
56
+ * following reasons:
57
+ *
58
+ * 1. it let us react and execute code when someone add something to the registry
59
+ * (for example, the FunctionRegistry subclass this for this purpose)
60
+ * 2. it throws an error when the get operation fails
61
+ * 3. it provides a chained API to add items to the registry.
62
+ */
63
+ class Registry {
64
+ content = {};
65
+ /**
66
+ * Add an item to the registry
67
+ *
68
+ * Note that this also returns the registry, so another add method call can
69
+ * be chained
70
+ */
71
+ add(key, value) {
72
+ this.content[key] = value;
73
+ return this;
74
+ }
75
+ /**
76
+ * Get an item from the registry
77
+ */
78
+ get(key) {
79
+ /**
80
+ * Note: key in {} is ~12 times slower than {}[key].
81
+ * So, we check the absence of key only when the direct access returns
82
+ * a falsy value. It's done to ensure that the registry can contains falsy values
83
+ */
84
+ const content = this.content[key];
85
+ if (!content) {
86
+ if (!(key in this.content)) {
87
+ throw new Error(`Cannot find ${key} in this registry!`);
88
+ }
89
+ }
90
+ return content;
91
+ }
92
+ /**
93
+ * Check if the key is already in the registry
94
+ */
95
+ contains(key) {
96
+ return key in this.content;
97
+ }
98
+ /**
99
+ * Get a list of all elements in the registry
100
+ */
101
+ getAll() {
102
+ return Object.values(this.content);
103
+ }
104
+ /**
105
+ * Get a list of all keys in the registry
106
+ */
107
+ getKeys() {
108
+ return Object.keys(this.content);
109
+ }
110
+ /**
111
+ * Remove an item from the registry
112
+ */
113
+ remove(key) {
114
+ delete this.content[key];
115
+ }
116
+ }
117
+
12
118
  const CANVAS_SHIFT = 0.5;
13
119
  // Colors
14
120
  const HIGHLIGHT_COLOR = "#37A850";
@@ -223,6 +329,7 @@ const DEFAULT_SCORECARD_BASELINE_MODE = "difference";
223
329
  const DEFAULT_SCORECARD_BASELINE_COLOR_UP = "#43C5B1";
224
330
  const DEFAULT_SCORECARD_BASELINE_COLOR_DOWN = "#EA6175";
225
331
  const LINE_FILL_TRANSPARENCY = 0.4;
332
+ const DEFAULT_WINDOW_SIZE = 2;
226
333
  // session
227
334
  const DEBOUNCE_TIME = 200;
228
335
  const MESSAGE_VERSION = 1;
@@ -268,7 +375,7 @@ const PIVOT_TABLE_CONFIG = {
268
375
  bandedRows: true,
269
376
  bandedColumns: false,
270
377
  styleId: "TableStyleMedium5",
271
- automaticAutofill: true,
378
+ automaticAutofill: false,
272
379
  };
273
380
  const DEFAULT_CURRENCY = {
274
381
  symbol: "$",
@@ -484,7 +591,7 @@ function buildSheetLink(sheetId) {
484
591
  */
485
592
  function parseSheetUrl(sheetLink) {
486
593
  if (sheetLink.startsWith(O_SPREADSHEET_LINK_PREFIX)) {
487
- return sheetLink.substr(O_SPREADSHEET_LINK_PREFIX.length);
594
+ return sheetLink.slice(O_SPREADSHEET_LINK_PREFIX.length);
488
595
  }
489
596
  throw new Error(`${sheetLink} is not a valid sheet link`);
490
597
  }
@@ -3049,8 +3156,7 @@ const getNumberRegex = memoize(function getNumberRegex(locale) {
3049
3156
  const p2 = pMinus + pNumber + pCurrencyFormat;
3050
3157
  const p3 = pCurrencyFormat + pMinus + pNumber;
3051
3158
  const pNumberExp = "^(?:(?:" + [p1, p2, p3].join(")|(?:") + "))$";
3052
- const numberRegexp = new RegExp(pNumberExp, "i");
3053
- return numberRegexp;
3159
+ return new RegExp(pNumberExp, "i");
3054
3160
  });
3055
3161
  /**
3056
3162
  * Return true if the argument is a "number string".
@@ -5724,8 +5830,7 @@ function computeCachedTextWidth(context, text) {
5724
5830
  textWidthCache[font] = {};
5725
5831
  }
5726
5832
  if (textWidthCache[font][text] === undefined) {
5727
- const textWidth = context.measureText(text).width;
5728
- textWidthCache[font][text] = textWidth;
5833
+ textWidthCache[font][text] = context.measureText(text).width;
5729
5834
  }
5730
5835
  return textWidthCache[font][text];
5731
5836
  }
@@ -5968,136 +6073,22 @@ function drawDecoratedText(context, text, position, underline = false, strikethr
5968
6073
  * https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
5969
6074
  * */
5970
6075
  class UuidGenerator {
5971
- isFastIdStrategy = false;
5972
- fastIdStart = 0;
5973
- setIsFastStrategy(isFast) {
5974
- this.isFastIdStrategy = isFast;
5975
- }
5976
6076
  uuidv4() {
5977
- if (this.isFastIdStrategy) {
5978
- this.fastIdStart++;
5979
- return String(this.fastIdStart);
5980
- //@ts-ignore
5981
- }
5982
- else if (window.crypto && window.crypto.getRandomValues) {
6077
+ //@ts-ignore
6078
+ if (window.crypto && window.crypto.getRandomValues) {
5983
6079
  //@ts-ignore
5984
6080
  return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) => (c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16));
5985
6081
  }
5986
6082
  else {
5987
6083
  // mainly for jest and other browsers that do not have the crypto functionality
5988
6084
  return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
5989
- var r = (Math.random() * 16) | 0, v = c === "x" ? r : (r & 0x3) | 0x8;
6085
+ const r = (Math.random() * 16) | 0, v = c === "x" ? r : (r & 0x3) | 0x8;
5990
6086
  return v.toString(16);
5991
6087
  });
5992
6088
  }
5993
6089
  }
5994
6090
  }
5995
6091
 
5996
- function createActions(menuItems) {
5997
- return menuItems.map(createAction).sort((a, b) => a.sequence - b.sequence);
5998
- }
5999
- const uuidGenerator$1 = new UuidGenerator();
6000
- function createAction(item) {
6001
- const name = item.name;
6002
- const children = item.children;
6003
- const description = item.description;
6004
- const icon = item.icon;
6005
- const secondaryIcon = item.secondaryIcon;
6006
- return {
6007
- id: item.id || uuidGenerator$1.uuidv4(),
6008
- name: typeof name === "function" ? name : () => name,
6009
- isVisible: item.isVisible ? item.isVisible : () => true,
6010
- isEnabled: item.isEnabled ? item.isEnabled : () => true,
6011
- isActive: item.isActive,
6012
- execute: item.execute,
6013
- children: children
6014
- ? (env) => {
6015
- return children
6016
- .map((child) => (typeof child === "function" ? child(env) : child))
6017
- .flat()
6018
- .map(createAction);
6019
- }
6020
- : () => [],
6021
- isReadonlyAllowed: item.isReadonlyAllowed || false,
6022
- separator: item.separator || false,
6023
- icon: typeof icon === "function" ? icon : () => icon || "",
6024
- iconColor: item.iconColor,
6025
- secondaryIcon: typeof secondaryIcon === "function" ? secondaryIcon : () => secondaryIcon || "",
6026
- description: typeof description === "function" ? description : () => description || "",
6027
- textColor: item.textColor,
6028
- sequence: item.sequence || 0,
6029
- onStartHover: item.onStartHover,
6030
- onStopHover: item.onStopHover,
6031
- };
6032
- }
6033
-
6034
- /**
6035
- * Registry
6036
- *
6037
- * The Registry class is basically just a mapping from a string key to an object.
6038
- * It is really not much more than an object. It is however useful for the
6039
- * following reasons:
6040
- *
6041
- * 1. it let us react and execute code when someone add something to the registry
6042
- * (for example, the FunctionRegistry subclass this for this purpose)
6043
- * 2. it throws an error when the get operation fails
6044
- * 3. it provides a chained API to add items to the registry.
6045
- */
6046
- class Registry {
6047
- content = {};
6048
- /**
6049
- * Add an item to the registry
6050
- *
6051
- * Note that this also returns the registry, so another add method call can
6052
- * be chained
6053
- */
6054
- add(key, value) {
6055
- this.content[key] = value;
6056
- return this;
6057
- }
6058
- /**
6059
- * Get an item from the registry
6060
- */
6061
- get(key) {
6062
- /**
6063
- * Note: key in {} is ~12 times slower than {}[key].
6064
- * So, we check the absence of key only when the direct access returns
6065
- * a falsy value. It's done to ensure that the registry can contains falsy values
6066
- */
6067
- const content = this.content[key];
6068
- if (!content) {
6069
- if (!(key in this.content)) {
6070
- throw new Error(`Cannot find ${key} in this registry!`);
6071
- }
6072
- }
6073
- return content;
6074
- }
6075
- /**
6076
- * Check if the key is already in the registry
6077
- */
6078
- contains(key) {
6079
- return key in this.content;
6080
- }
6081
- /**
6082
- * Get a list of all elements in the registry
6083
- */
6084
- getAll() {
6085
- return Object.values(this.content);
6086
- }
6087
- /**
6088
- * Get a list of all keys in the registry
6089
- */
6090
- getKeys() {
6091
- return Object.keys(this.content);
6092
- }
6093
- /**
6094
- * Remove an item from the registry
6095
- */
6096
- remove(key) {
6097
- delete this.content[key];
6098
- }
6099
- }
6100
-
6101
6092
  function getClipboardDataPositions(sheetId, zones) {
6102
6093
  const lefts = new Set(zones.map((z) => z.left));
6103
6094
  const rights = new Set(zones.map((z) => z.right));
@@ -6588,10 +6579,9 @@ function localizeNumberLiteral(literal, locale) {
6588
6579
  return literal;
6589
6580
  }
6590
6581
  const decimalNumberRegex = getDecimalNumberRegex(DEFAULT_LOCALE);
6591
- const localized = literal.replace(decimalNumberRegex, (match) => {
6582
+ return literal.replace(decimalNumberRegex, (match) => {
6592
6583
  return match.replace(".", locale.decimalSeparator);
6593
6584
  });
6594
- return localized;
6595
6585
  }
6596
6586
  /**
6597
6587
  * Change a literal string from its canonical form (en_US locale) to the given locale. Also convert date string.
@@ -7076,6 +7066,21 @@ function predictLinearValues(Y, X, newX, computeIntercept) {
7076
7066
  });
7077
7067
  return newY.length === newX.length ? newY : transposeMatrix(newY);
7078
7068
  }
7069
+ function getMovingAverageValues(dataset, windowSize = DEFAULT_WINDOW_SIZE) {
7070
+ const values = [];
7071
+ // Fill the starting values with null until we have a full window
7072
+ for (let i = 0; i < windowSize - 1; i++) {
7073
+ values.push(null);
7074
+ }
7075
+ for (let i = 0; i <= dataset.length - windowSize; i++) {
7076
+ let sum = 0;
7077
+ for (let j = i; j < i + windowSize; j++) {
7078
+ sum += dataset[j];
7079
+ }
7080
+ values.push(sum / windowSize);
7081
+ }
7082
+ return values;
7083
+ }
7079
7084
 
7080
7085
  const PREVIOUS_VALUE = "(previous)";
7081
7086
  const NEXT_VALUE = "(next)";
@@ -7611,8 +7616,7 @@ function getMaxObjectId(o) {
7611
7616
  return 0;
7612
7617
  }
7613
7618
  const nums = keys.map((id) => parseInt(id, 10));
7614
- const max = Math.max(...nums);
7615
- return max;
7619
+ return Math.max(...nums);
7616
7620
  }
7617
7621
  const ALL_PERIODS = {
7618
7622
  year: _t("Year"),
@@ -8452,31 +8456,30 @@ class TableClipboardHandler extends AbstractCellClipboardHandler {
8452
8456
  for (let col of columnsIndexes) {
8453
8457
  const position = { col, row, sheetId };
8454
8458
  const table = this.getters.getTable(position);
8455
- if (!table || copiedTablesIds.has(table.id)) {
8459
+ if (!table) {
8456
8460
  tableCellsInRow.push({});
8457
8461
  continue;
8458
8462
  }
8459
8463
  const coreTable = this.getters.getCoreTable(position);
8460
8464
  const tableZone = coreTable?.range.zone;
8465
+ let copiedTable = undefined;
8461
8466
  // Copy whole table
8462
- if (coreTable && tableZone && zones.some((z) => isZoneInside(tableZone, z))) {
8463
- copiedTablesIds.add(coreTable.id);
8464
- const values = [];
8465
- for (const col of range(tableZone.left, tableZone.right + 1)) {
8466
- values.push(this.getters.getFilterHiddenValues({ sheetId, col, row: tableZone.top }));
8467
- }
8468
- tableCellsInRow.push({
8469
- table: {
8470
- range: coreTable.range.rangeData,
8471
- config: coreTable.config,
8472
- type: coreTable.type,
8473
- },
8474
- });
8475
- }
8476
- // Copy only style of cell
8477
- else if (table) {
8478
- tableCellsInRow.push({ style: this.getTableStyleToCopy(position) });
8467
+ if (!copiedTablesIds.has(table.id) &&
8468
+ coreTable &&
8469
+ tableZone &&
8470
+ zones.some((z) => isZoneInside(tableZone, z))) {
8471
+ copiedTablesIds.add(table.id);
8472
+ copiedTable = {
8473
+ range: coreTable.range.rangeData,
8474
+ config: coreTable.config,
8475
+ type: coreTable.type,
8476
+ };
8479
8477
  }
8478
+ tableCellsInRow.push({
8479
+ table: copiedTable,
8480
+ style: this.getTableStyleToCopy(position),
8481
+ isWholeTableCopied: copiedTablesIds.has(table.id),
8482
+ });
8480
8483
  }
8481
8484
  }
8482
8485
  return {
@@ -8557,11 +8560,14 @@ class TableClipboardHandler extends AbstractCellClipboardHandler {
8557
8560
  tableType: tableCell.table.type,
8558
8561
  });
8559
8562
  }
8560
- // Do not paste table style if we're inside another table
8561
8563
  // We cannot check for dynamic tables, because at this point the paste can have changed the evaluation, and the
8562
8564
  // dynamic tables are not yet computed
8563
- if (!this.getters.getCoreTable(position)) {
8564
- if (tableCell.style?.style && options?.pasteOption !== "asValue") {
8565
+ if (this.getters.getCoreTable(position) || options?.pasteOption === "asValue") {
8566
+ return;
8567
+ }
8568
+ if ((!options?.pasteOption && !tableCell.isWholeTableCopied) ||
8569
+ options?.pasteOption === "onlyFormat") {
8570
+ if (tableCell.style?.style) {
8565
8571
  this.dispatch("UPDATE_CELL", { ...position, style: tableCell.style.style });
8566
8572
  }
8567
8573
  if (tableCell.style?.border) {
@@ -9243,11 +9249,10 @@ function getChartPositionAtCenterOfViewport(getters, chartSize) {
9243
9249
  const { x, y } = getters.getMainViewportCoordinates();
9244
9250
  const { scrollX, scrollY } = getters.getActiveSheetScrollInfo();
9245
9251
  const { width, height } = getters.getVisibleRect(getters.getActiveMainViewport());
9246
- const position = {
9252
+ return {
9247
9253
  x: x + scrollX + Math.max(0, (width - chartSize.width) / 2),
9248
9254
  y: y + scrollY + Math.max(0, (height - chartSize.height) / 2),
9249
9255
  }; // Position at the center of the scrollable viewport
9250
- return position;
9251
9256
  }
9252
9257
  function getChartAxisTitleRuntime(design) {
9253
9258
  if (design?.title?.text) {
@@ -9281,6 +9286,49 @@ function getDefinedAxis(definition) {
9281
9286
  useLeftAxis ||= !useRightAxis;
9282
9287
  return { useLeftAxis, useRightAxis };
9283
9288
  }
9289
+ function getChartAxis(definition, position, type, options) {
9290
+ const { useLeftAxis, useRightAxis } = getDefinedAxis(definition);
9291
+ if ((position === "left" && !useLeftAxis) || (position === "right" && !useRightAxis)) {
9292
+ return undefined;
9293
+ }
9294
+ const fontColor = chartFontColor(definition.background);
9295
+ let design;
9296
+ if (position === "bottom") {
9297
+ design = definition.axesDesign?.x;
9298
+ }
9299
+ else if (position === "left") {
9300
+ design = definition.axesDesign?.y;
9301
+ }
9302
+ else {
9303
+ design = definition.axesDesign?.y1;
9304
+ }
9305
+ if (type === "values") {
9306
+ const displayGridLines = position === "left" || (position === "right" && !useLeftAxis);
9307
+ return {
9308
+ position: position,
9309
+ title: getChartAxisTitleRuntime(design),
9310
+ grid: {
9311
+ display: displayGridLines,
9312
+ },
9313
+ beginAtZero: true,
9314
+ stacked: options?.stacked,
9315
+ ticks: {
9316
+ color: fontColor,
9317
+ callback: formatTickValue(options),
9318
+ },
9319
+ };
9320
+ }
9321
+ else {
9322
+ return {
9323
+ ticks: {
9324
+ padding: 5,
9325
+ color: fontColor,
9326
+ },
9327
+ stacked: options?.stacked,
9328
+ title: getChartAxisTitleRuntime(design),
9329
+ };
9330
+ }
9331
+ }
9284
9332
  function computeChartPadding({ displayTitle, displayLegend, }) {
9285
9333
  let top = 25;
9286
9334
  if (displayTitle) {
@@ -9321,7 +9369,7 @@ function getFullTrendingLineDataSet(dataset, config, data) {
9321
9369
  return {
9322
9370
  ...dataset,
9323
9371
  type: "line",
9324
- xAxisID: TREND_LINE_XAXIS_ID,
9372
+ xAxisID: config.type !== "trailingMovingAverage" ? TREND_LINE_XAXIS_ID : "x",
9325
9373
  label: dataset.label ? _t("Trend line for %s", dataset.label) : "",
9326
9374
  data,
9327
9375
  order: -1,
@@ -9363,10 +9411,19 @@ function interpolateData(config, values, labels, newLabels) {
9363
9411
  case "logarithmic": {
9364
9412
  return predictLinearValues([values], logM([labels]), logM([newLabels]), true)[0];
9365
9413
  }
9414
+ case "trailingMovingAverage": {
9415
+ return getMovingAverageValues(values, config.window);
9416
+ }
9366
9417
  default:
9367
9418
  return [];
9368
9419
  }
9369
9420
  }
9421
+ function formatChartDatasetValue(axisFormats, locale) {
9422
+ return (value, axisId) => {
9423
+ const format = axisId ? axisFormats?.[axisId] : undefined;
9424
+ return formatTickValue({ format, locale })(value);
9425
+ };
9426
+ }
9370
9427
  function formatTickValue(localeFormat) {
9371
9428
  return (value) => {
9372
9429
  value = Number(value);
@@ -9402,70 +9459,115 @@ const chartShowValuesPlugin = {
9402
9459
  ctx.save();
9403
9460
  ctx.textAlign = "center";
9404
9461
  ctx.textBaseline = "middle";
9405
- ctx.fillStyle = chartFontColor(options.background);
9406
- ctx.strokeStyle = chartFontColor(ctx.fillStyle);
9407
- chart._metasets.forEach(function (dataset) {
9408
- if (dataset.xAxisID === TREND_LINE_XAXIS_ID) {
9409
- return; // ignore trend lines
9410
- }
9411
- switch (dataset.type) {
9412
- case "doughnut":
9413
- case "pie": {
9414
- for (let i = 0; i < dataset._parsed.length; i++) {
9415
- const bar = dataset.data[i];
9416
- const { startAngle, endAngle, innerRadius, outerRadius } = bar;
9417
- const midAngle = (startAngle + endAngle) / 2;
9418
- const midRadius = (innerRadius + outerRadius) / 2;
9419
- const x = bar.x + midRadius * Math.cos(midAngle);
9420
- const y = bar.y + midRadius * Math.sin(midAngle) + 7;
9421
- ctx.fillStyle = chartFontColor(bar.options.backgroundColor);
9422
- ctx.strokeStyle = chartFontColor(ctx.fillStyle);
9423
- const value = options.callback(dataset._parsed[i]);
9424
- ctx.strokeText(value, x, y);
9425
- ctx.fillText(value, x, y);
9426
- }
9427
- break;
9428
- }
9429
- case "bar":
9430
- case "line": {
9431
- const yOffset = dataset.type === "bar" && !options.horizontal ? 0 : 3;
9432
- for (let i = 0; i < dataset._parsed.length; i++) {
9433
- const point = dataset.data[i];
9434
- const value = options.horizontal ? dataset._parsed[i].x : dataset._parsed[i].y;
9435
- const displayedValue = options.callback(value - 0);
9436
- let xPosition = 0, yPosition = 0;
9437
- if (options.horizontal) {
9438
- yPosition = point.y;
9439
- if (value < 0) {
9440
- ctx.textAlign = "right";
9441
- xPosition = point.x - yOffset;
9442
- }
9443
- else {
9444
- ctx.textAlign = "left";
9445
- xPosition = point.x + yOffset;
9446
- }
9447
- }
9448
- else {
9449
- xPosition = point.x;
9450
- if (value < 0) {
9451
- ctx.textBaseline = "top";
9452
- yPosition = point.y + yOffset;
9453
- }
9454
- else {
9455
- ctx.textBaseline = "bottom";
9456
- yPosition = point.y - yOffset;
9457
- }
9458
- }
9459
- ctx.strokeText(displayedValue, xPosition, yPosition);
9460
- ctx.fillText(displayedValue, xPosition, yPosition);
9461
- }
9462
- break;
9463
- }
9464
- }
9465
- });
9462
+ ctx.miterLimit = 1; // Avoid sharp artifacts on strokeText
9463
+ switch (chart.config.type) {
9464
+ case "pie":
9465
+ case "doughnut":
9466
+ drawPieChartValues(chart, options, ctx);
9467
+ break;
9468
+ case "bar":
9469
+ case "line":
9470
+ options.horizontal
9471
+ ? drawHorizontalBarChartValues(chart, options, ctx)
9472
+ : drawLineOrBarChartValues(chart, options, ctx);
9473
+ break;
9474
+ }
9466
9475
  ctx.restore();
9467
9476
  },
9468
9477
  };
9478
+ function drawTextWithBackground(text, x, y, ctx) {
9479
+ ctx.lineWidth = 3; // Stroke the text with a big lineWidth width to have some kind of background
9480
+ ctx.strokeText(text, x, y);
9481
+ ctx.lineWidth = 1;
9482
+ ctx.fillText(text, x, y);
9483
+ }
9484
+ function drawLineOrBarChartValues(chart, options, ctx) {
9485
+ const yMax = chart.chartArea.bottom;
9486
+ const yMin = chart.chartArea.top;
9487
+ const textsPositions = {};
9488
+ for (const dataset of chart._metasets) {
9489
+ if (dataset.xAxisID === TREND_LINE_XAXIS_ID) {
9490
+ return; // ignore trend lines
9491
+ }
9492
+ for (let i = 0; i < dataset._parsed.length; i++) {
9493
+ const value = dataset._parsed[i].y;
9494
+ const displayValue = options.callback(value - 0, dataset.yAxisID);
9495
+ const point = dataset.data[i];
9496
+ const xPosition = point.x;
9497
+ let yPosition = 0;
9498
+ if (chart.config.type === "line") {
9499
+ yPosition = point.y - 10;
9500
+ }
9501
+ else {
9502
+ yPosition = value < 0 ? point.y - point.height / 2 : point.y + point.height / 2;
9503
+ }
9504
+ yPosition = Math.min(yPosition, yMax);
9505
+ yPosition = Math.max(yPosition, yMin);
9506
+ // Avoid overlapping texts with same X
9507
+ if (!textsPositions[xPosition]) {
9508
+ textsPositions[xPosition] = [];
9509
+ }
9510
+ for (const otherPosition of textsPositions[xPosition] || []) {
9511
+ if (Math.abs(otherPosition - yPosition) < 13) {
9512
+ yPosition = otherPosition - 13;
9513
+ }
9514
+ }
9515
+ textsPositions[xPosition].push(yPosition);
9516
+ ctx.fillStyle = point.options.backgroundColor;
9517
+ ctx.strokeStyle = options.background || "#ffffff";
9518
+ drawTextWithBackground(displayValue, xPosition, yPosition, ctx);
9519
+ }
9520
+ }
9521
+ }
9522
+ function drawHorizontalBarChartValues(chart, options, ctx) {
9523
+ const xMax = chart.chartArea.right;
9524
+ const xMin = chart.chartArea.left;
9525
+ const textsPositions = {};
9526
+ for (const dataset of chart._metasets) {
9527
+ if (dataset.xAxisID === TREND_LINE_XAXIS_ID) {
9528
+ return; // ignore trend lines
9529
+ }
9530
+ for (let i = 0; i < dataset._parsed.length; i++) {
9531
+ const value = dataset._parsed[i].x;
9532
+ const displayValue = options.callback(value - 0, dataset.xAxisID);
9533
+ const point = dataset.data[i];
9534
+ const yPosition = point.y;
9535
+ let xPosition = value < 0 ? point.x + point.width / 2 : point.x - point.width / 2;
9536
+ xPosition = Math.min(xPosition, xMax);
9537
+ xPosition = Math.max(xPosition, xMin);
9538
+ // Avoid overlapping texts with same Y
9539
+ if (!textsPositions[yPosition]) {
9540
+ textsPositions[yPosition] = [];
9541
+ }
9542
+ const textWidth = computeTextWidth(ctx, displayValue, { fontSize: 12 }, "px");
9543
+ for (const otherPosition of textsPositions[yPosition]) {
9544
+ if (Math.abs(otherPosition - xPosition) < textWidth) {
9545
+ xPosition = otherPosition + textWidth + 3;
9546
+ }
9547
+ }
9548
+ textsPositions[yPosition].push(xPosition);
9549
+ ctx.fillStyle = point.options.backgroundColor;
9550
+ ctx.strokeStyle = options.background || "#ffffff";
9551
+ drawTextWithBackground(displayValue, xPosition, yPosition, ctx);
9552
+ }
9553
+ }
9554
+ }
9555
+ function drawPieChartValues(chart, options, ctx) {
9556
+ for (const dataset of chart._metasets) {
9557
+ for (let i = 0; i < dataset._parsed.length; i++) {
9558
+ const bar = dataset.data[i];
9559
+ const { startAngle, endAngle, innerRadius, outerRadius } = bar;
9560
+ const midAngle = (startAngle + endAngle) / 2;
9561
+ const midRadius = (innerRadius + outerRadius) / 2;
9562
+ const x = bar.x + midRadius * Math.cos(midAngle);
9563
+ const y = bar.y + midRadius * Math.sin(midAngle) + 7;
9564
+ ctx.fillStyle = chartFontColor(options.background);
9565
+ ctx.strokeStyle = options.background || "#ffffff";
9566
+ const value = options.callback(dataset._parsed[i]);
9567
+ drawTextWithBackground(value, x, y, ctx);
9568
+ }
9569
+ }
9570
+ }
9469
9571
 
9470
9572
  /** This is a chartJS plugin that will draw connector lines between the bars of a Waterfall chart */
9471
9573
  const waterfallLinesPlugin = {
@@ -10243,8 +10345,7 @@ function getHtmlContentFromPattern(pattern, value, highlightColor, className) {
10243
10345
  value = value.slice(index + 1);
10244
10346
  }
10245
10347
  pendingHtmlContent.push({ value });
10246
- const htmlContent = pendingHtmlContent.filter((content) => content.value);
10247
- return htmlContent;
10348
+ return pendingHtmlContent.filter((content) => content.value);
10248
10349
  }
10249
10350
 
10250
10351
  //------------------------------------------------------------------------------
@@ -10648,7 +10749,7 @@ const MINVERSE = {
10648
10749
  assertSquareMatrix(_t("The argument square_matrix must have the same number of columns and rows."), _matrix);
10649
10750
  const { inverted } = invertMatrix(_matrix);
10650
10751
  if (!inverted) {
10651
- throw new EvaluationError(_t("The matrix is not invertible."));
10752
+ return new EvaluationError(_t("The matrix is not invertible."));
10652
10753
  }
10653
10754
  return inverted;
10654
10755
  },
@@ -10727,7 +10828,7 @@ function getSumXAndY(arrayX, arrayY, cb) {
10727
10828
  }
10728
10829
  }
10729
10830
  if (!validPairFound) {
10730
- throw new EvaluationError(_t("The arguments array_x and array_y must contain at least one pair of numbers."));
10831
+ return new EvaluationError(_t("The arguments array_x and array_y must contain at least one pair of numbers."));
10731
10832
  }
10732
10833
  return result;
10733
10834
  }
@@ -10808,7 +10909,7 @@ const TOCOL = {
10808
10909
  .flat()
10809
10910
  .filter(shouldKeepValue(_ignore));
10810
10911
  if (result.length === 0) {
10811
- throw new NotAvailableError(_t("No results for the given arguments of TOCOL."));
10912
+ return new NotAvailableError(_t("No results for the given arguments of TOCOL."));
10812
10913
  }
10813
10914
  return [result];
10814
10915
  },
@@ -10829,7 +10930,7 @@ const TOROW = {
10829
10930
  .filter(shouldKeepValue(_ignore))
10830
10931
  .map((item) => [item]);
10831
10932
  if (result.length === 0 || result[0].length === 0) {
10832
- throw new NotAvailableError(_t("No results for the given arguments of TOROW."));
10933
+ return new NotAvailableError(_t("No results for the given arguments of TOROW."));
10833
10934
  }
10834
10935
  return result;
10835
10936
  },
@@ -11389,7 +11490,7 @@ const DECIMAL = {
11389
11490
  * Return error if 'value' is positive.
11390
11491
  * Remove '-?' in the next regex to catch this error.
11391
11492
  */
11392
- assert(() => !!DECIMAL_REPRESENTATION.test(_value), _t("The value (%s) must be a valid base %s representation.", _value, _base.toString()));
11493
+ assert(() => DECIMAL_REPRESENTATION.test(_value), _t("The value (%s) must be a valid base %s representation.", _value, _base.toString()));
11393
11494
  const deci = parseInt(_value, _base);
11394
11495
  assert(() => !isNaN(deci), _t("The value (%s) must be a valid base %s representation.", _value, _base.toString()));
11395
11496
  return deci;
@@ -11657,7 +11758,7 @@ const PRODUCT = {
11657
11758
  count += 1;
11658
11759
  }
11659
11760
  if (isEvaluationError(f)) {
11660
- throw j;
11761
+ return j;
11661
11762
  }
11662
11763
  }
11663
11764
  }
@@ -12160,9 +12261,8 @@ function covariance(dataY, dataX, isSample) {
12160
12261
  }
12161
12262
  function variance(args, isSample, textAs0, locale) {
12162
12263
  let count = 0;
12163
- let sum = 0;
12164
12264
  const reduceFunction = textAs0 ? reduceNumbersTextAs0 : reduceNumbers;
12165
- sum = reduceFunction(args, (acc, a) => {
12265
+ const sum = reduceFunction(args, (acc, a) => {
12166
12266
  count += 1;
12167
12267
  return acc + a;
12168
12268
  }, 0, locale);
@@ -12559,7 +12659,7 @@ const MATTHEWS = {
12559
12659
  const flatY = dataY.flat();
12560
12660
  assertSameNumberOfElements(flatX, flatY);
12561
12661
  if (flatX.length === 0) {
12562
- throw new EvaluationError(_t("[[FUNCTION_NAME]] expects non-empty ranges for both parameters."));
12662
+ return new EvaluationError(_t("[[FUNCTION_NAME]] expects non-empty ranges for both parameters."));
12563
12663
  }
12564
12664
  const n = flatX.length;
12565
12665
  let trueN = 0, trueP = 0, falseP = 0, falseN = 0;
@@ -13359,9 +13459,8 @@ function getMatchingCells(database, field, criteria, locale) {
13359
13459
  // 4 - return for each database row corresponding, the cells corresponding to the field parameter
13360
13460
  const fieldCol = database[index];
13361
13461
  // Example continuation:: fieldCol = ["C", "j", "k", 7]
13362
- const matchingCells = [...matchingRows].map((x) => fieldCol[x + 1]);
13363
13462
  // Example continuation:: matchingCells = ["j", 7]
13364
- return matchingCells;
13463
+ return [...matchingRows].map((x) => fieldCol[x + 1]);
13365
13464
  }
13366
13465
  const databaseArgs = [
13367
13466
  arg("database (range)", _t("The array or range containing the data to consider, structured in such a way that the first row contains the labels for each column's values.")),
@@ -14546,7 +14645,7 @@ const FILTER = {
14546
14645
  }
14547
14646
  }
14548
14647
  if (!result.length) {
14549
- throw new NotAvailableError(_t("No match found in FILTER evaluation"));
14648
+ return new NotAvailableError(_t("No match found in FILTER evaluation"));
14550
14649
  }
14551
14650
  return mode === "row" ? transposeMatrix(result) : result;
14552
14651
  },
@@ -17244,7 +17343,7 @@ function mapParentFunction(tokens) {
17244
17343
  argsTokens[argPosition].push({ value: token.value, type: token.type });
17245
17344
  }
17246
17345
  }
17247
- const res = tokens.map((token, i) => {
17346
+ return tokens.map((token, i) => {
17248
17347
  if (!["SPACE", "LEFT_PAREN"].includes(token.type)) {
17249
17348
  functionStarted = "";
17250
17349
  }
@@ -17282,7 +17381,6 @@ function mapParentFunction(tokens) {
17282
17381
  }
17283
17382
  return token;
17284
17383
  });
17285
- return res;
17286
17384
  }
17287
17385
  /**
17288
17386
  * Parse the list of tokens that compose the arguments of a function to
@@ -17738,7 +17836,7 @@ const IFS = {
17738
17836
  return result;
17739
17837
  }
17740
17838
  }
17741
- throw new EvaluationError(_t("No match."));
17839
+ return new EvaluationError(_t("No match."));
17742
17840
  },
17743
17841
  isExported: true,
17744
17842
  };
@@ -17841,7 +17939,7 @@ function assertDomainLength(domain) {
17841
17939
  throw new EvaluationError(_t("Function PIVOT takes an even number of arguments."));
17842
17940
  }
17843
17941
  }
17844
- function addPivotDependencies(evalContext, coreDefinition) {
17942
+ function addPivotDependencies(evalContext, coreDefinition, forMeasures) {
17845
17943
  //TODO This function can be very costly when used with PIVOT.VALUE and PIVOT.HEADER
17846
17944
  const dependencies = [];
17847
17945
  if (coreDefinition.type === "SPREADSHEET" && coreDefinition.dataSet) {
@@ -17853,7 +17951,7 @@ function addPivotDependencies(evalContext, coreDefinition) {
17853
17951
  }
17854
17952
  dependencies.push(range);
17855
17953
  }
17856
- for (const measure of coreDefinition.measures) {
17954
+ for (const measure of forMeasures) {
17857
17955
  if (measure.computedBy) {
17858
17956
  const formula = evalContext.getters.getMeasureCompiledFormula(measure);
17859
17957
  dependencies.push(...formula.dependencies.filter((range) => !range.invalidXc));
@@ -17902,8 +18000,8 @@ const ADDRESS = {
17902
18000
  let cellReference;
17903
18001
  if (_useA1Notation) {
17904
18002
  const rangePart = {
17905
- rowFixed: [1, 2].includes(_absoluteRelativeMode) ? true : false,
17906
- colFixed: [1, 3].includes(_absoluteRelativeMode) ? true : false,
18003
+ rowFixed: [1, 2].includes(_absoluteRelativeMode),
18004
+ colFixed: [1, 3].includes(_absoluteRelativeMode),
17907
18005
  };
17908
18006
  cellReference = toXC(colNumber - 1, rowNumber - 1, rangePart);
17909
18007
  }
@@ -17929,7 +18027,7 @@ const COLUMN = {
17929
18027
  ],
17930
18028
  compute: function (cellReference) {
17931
18029
  if (isEvaluationError(cellReference?.value)) {
17932
- throw cellReference;
18030
+ return cellReference;
17933
18031
  }
17934
18032
  const column = cellReference === undefined
17935
18033
  ? this.__originCellPosition?.col
@@ -17947,7 +18045,7 @@ const COLUMNS = {
17947
18045
  args: [arg("range (meta)", _t("The range whose column count will be returned."))],
17948
18046
  compute: function (range) {
17949
18047
  if (isEvaluationError(range?.value)) {
17950
- throw range;
18048
+ return range;
17951
18049
  }
17952
18050
  const zone = toZone(range.value);
17953
18051
  return zone.right - zone.left + 1;
@@ -18027,11 +18125,11 @@ const INDIRECT = {
18027
18125
  compute: function (reference, useA1Notation = { value: true }) {
18028
18126
  let _reference = reference?.value?.toString();
18029
18127
  if (!_reference) {
18030
- throw new InvalidReferenceError(_t("Reference should be defined."));
18128
+ return new InvalidReferenceError(_t("Reference should be defined."));
18031
18129
  }
18032
18130
  const _useA1Notation = toBoolean(useA1Notation);
18033
18131
  if (!_useA1Notation) {
18034
- throw new EvaluationError(_t("R1C1 notation is not supported."));
18132
+ return new EvaluationError(_t("R1C1 notation is not supported."));
18035
18133
  }
18036
18134
  const sheetId = this.__originSheetId;
18037
18135
  const originPosition = this.__originCellPosition;
@@ -18043,7 +18141,7 @@ const INDIRECT = {
18043
18141
  }
18044
18142
  const range = this.getters.getRangeFromSheetXC(sheetId, _reference);
18045
18143
  if (range === undefined || range.invalidXc || range.invalidSheetName) {
18046
- throw new InvalidReferenceError();
18144
+ return new InvalidReferenceError();
18047
18145
  }
18048
18146
  if (originPosition) {
18049
18147
  this.addDependencies?.(originPosition, [range]);
@@ -18151,7 +18249,7 @@ const ROW = {
18151
18249
  ],
18152
18250
  compute: function (cellReference) {
18153
18251
  if (isEvaluationError(cellReference?.value)) {
18154
- throw cellReference;
18252
+ return cellReference;
18155
18253
  }
18156
18254
  const row = cellReference === undefined
18157
18255
  ? this.__originCellPosition?.row
@@ -18169,7 +18267,7 @@ const ROWS = {
18169
18267
  args: [arg("range (meta)", _t("The range whose row count will be returned."))],
18170
18268
  compute: function (range) {
18171
18269
  if (isEvaluationError(range?.value)) {
18172
- throw range;
18270
+ return range;
18173
18271
  }
18174
18272
  const zone = toZone(range.value);
18175
18273
  return zone.bottom - zone.top + 1;
@@ -18286,7 +18384,7 @@ const PIVOT_VALUE = {
18286
18384
  assertDomainLength(domainArgs);
18287
18385
  const pivot = this.getters.getPivot(pivotId);
18288
18386
  const coreDefinition = this.getters.getPivotCoreDefinition(pivotId);
18289
- addPivotDependencies(this, coreDefinition);
18387
+ addPivotDependencies(this, coreDefinition, coreDefinition.measures.filter((m) => m.id === _measure));
18290
18388
  pivot.init({ reload: pivot.needsReevaluation });
18291
18389
  const error = pivot.assertIsValid({ throwOnError: false });
18292
18390
  if (error) {
@@ -18316,7 +18414,7 @@ const PIVOT_HEADER = {
18316
18414
  assertDomainLength(domainArgs);
18317
18415
  const pivot = this.getters.getPivot(_pivotId);
18318
18416
  const coreDefinition = this.getters.getPivotCoreDefinition(_pivotId);
18319
- addPivotDependencies(this, coreDefinition);
18417
+ addPivotDependencies(this, coreDefinition, []);
18320
18418
  pivot.init({ reload: pivot.needsReevaluation });
18321
18419
  const error = pivot.assertIsValid({ throwOnError: false });
18322
18420
  if (error) {
@@ -18356,18 +18454,18 @@ const PIVOT = {
18356
18454
  const _pivotFormulaId = toString(pivotFormulaId);
18357
18455
  const _rowCount = toNumber(rowCount, this.locale);
18358
18456
  if (_rowCount < 0) {
18359
- throw new EvaluationError(_t("The number of rows must be positive."));
18457
+ return new EvaluationError(_t("The number of rows must be positive."));
18360
18458
  }
18361
18459
  const _columnCount = toNumber(columnCount, this.locale);
18362
18460
  if (_columnCount < 0) {
18363
- throw new EvaluationError(_t("The number of columns must be positive."));
18461
+ return new EvaluationError(_t("The number of columns must be positive."));
18364
18462
  }
18365
18463
  const _includeColumnHeaders = toBoolean(includeColumnHeaders);
18366
18464
  const _includedTotal = toBoolean(includeTotal);
18367
18465
  const pivotId = getPivotId(_pivotFormulaId, this.getters);
18368
18466
  const pivot = this.getters.getPivot(pivotId);
18369
18467
  const coreDefinition = this.getters.getPivotCoreDefinition(pivotId);
18370
- addPivotDependencies(this, coreDefinition);
18468
+ addPivotDependencies(this, coreDefinition, coreDefinition.measures);
18371
18469
  pivot.init({ reload: pivot.needsReevaluation });
18372
18470
  const error = pivot.assertIsValid({ throwOnError: false });
18373
18471
  if (error) {
@@ -18559,6 +18657,12 @@ const EQ = {
18559
18657
  arg("value2 (any)", _t("The value to test against value1 for equality.")),
18560
18658
  ],
18561
18659
  compute: function (value1, value2) {
18660
+ if (isEvaluationError(value1?.value)) {
18661
+ return value1;
18662
+ }
18663
+ if (isEvaluationError(value2?.value)) {
18664
+ return value2;
18665
+ }
18562
18666
  let _value1 = isEmpty(value1) ? getNeutral[typeof value2?.value] : value1?.value;
18563
18667
  let _value2 = isEmpty(value2) ? getNeutral[typeof value1?.value] : value2?.value;
18564
18668
  if (typeof _value1 === "string") {
@@ -18567,27 +18671,21 @@ const EQ = {
18567
18671
  if (typeof _value2 === "string") {
18568
18672
  _value2 = _value2.toUpperCase();
18569
18673
  }
18570
- if (isEvaluationError(_value1)) {
18571
- throw value1;
18572
- }
18573
- if (isEvaluationError(_value2)) {
18574
- throw value2;
18575
- }
18576
- return _value1 === _value2;
18674
+ return { value: _value1 === _value2 };
18577
18675
  },
18578
18676
  };
18579
18677
  // -----------------------------------------------------------------------------
18580
18678
  // GT
18581
18679
  // -----------------------------------------------------------------------------
18582
18680
  function applyRelationalOperator(value1, value2, cb) {
18583
- let _value1 = isEmpty(value1) ? getNeutral[typeof value2?.value] : value1?.value;
18584
- let _value2 = isEmpty(value2) ? getNeutral[typeof value1?.value] : value2?.value;
18585
- if (isEvaluationError(_value1)) {
18586
- throw value1;
18681
+ if (isEvaluationError(value1?.value)) {
18682
+ return value1;
18587
18683
  }
18588
- if (isEvaluationError(_value2)) {
18589
- throw value2;
18684
+ if (isEvaluationError(value2?.value)) {
18685
+ return value2;
18590
18686
  }
18687
+ let _value1 = isEmpty(value1) ? getNeutral[typeof value2?.value] : value1?.value;
18688
+ let _value2 = isEmpty(value2) ? getNeutral[typeof value1?.value] : value2?.value;
18591
18689
  if (typeof _value1 !== "number") {
18592
18690
  _value1 = toString(_value1).toUpperCase();
18593
18691
  }
@@ -18597,12 +18695,12 @@ function applyRelationalOperator(value1, value2, cb) {
18597
18695
  const tV1 = typeof _value1;
18598
18696
  const tV2 = typeof _value2;
18599
18697
  if (tV1 === "string" && tV2 === "number") {
18600
- return true;
18698
+ return { value: true };
18601
18699
  }
18602
18700
  if (tV2 === "string" && tV1 === "number") {
18603
- return false;
18701
+ return { value: false };
18604
18702
  }
18605
- return cb(_value1, _value2);
18703
+ return { value: cb(_value1, _value2) };
18606
18704
  }
18607
18705
  const GT = {
18608
18706
  description: _t("Strictly greater than."),
@@ -18641,7 +18739,11 @@ const LT = {
18641
18739
  arg("value2 (any)", _t("The second value.")),
18642
18740
  ],
18643
18741
  compute: function (value1, value2) {
18644
- return !GTE.compute.bind(this)(value1, value2);
18742
+ const result = GTE.compute.bind(this)(value1, value2);
18743
+ if (isEvaluationError(result.value)) {
18744
+ return result;
18745
+ }
18746
+ return { value: !result.value };
18645
18747
  },
18646
18748
  };
18647
18749
  // -----------------------------------------------------------------------------
@@ -18654,7 +18756,11 @@ const LTE = {
18654
18756
  arg("value2 (any)", _t("The second value.")),
18655
18757
  ],
18656
18758
  compute: function (value1, value2) {
18657
- return !GT.compute.bind(this)(value1, value2);
18759
+ const result = GT.compute.bind(this)(value1, value2);
18760
+ if (isEvaluationError(result.value)) {
18761
+ return result;
18762
+ }
18763
+ return { value: !result.value };
18658
18764
  },
18659
18765
  };
18660
18766
  // -----------------------------------------------------------------------------
@@ -18699,7 +18805,11 @@ const NE = {
18699
18805
  arg("value2 (any)", _t("The value to test against value1 for inequality.")),
18700
18806
  ],
18701
18807
  compute: function (value1, value2) {
18702
- return !EQ.compute.bind(this)(value1, value2);
18808
+ const result = EQ.compute.bind(this)(value1, value2);
18809
+ if (isEvaluationError(result.value)) {
18810
+ return result;
18811
+ }
18812
+ return { value: !result.value };
18703
18813
  },
18704
18814
  };
18705
18815
  // -----------------------------------------------------------------------------
@@ -19858,13 +19968,12 @@ function cssPropertiesToCss(attributes) {
19858
19968
  }
19859
19969
  function getElementMargins(el) {
19860
19970
  const style = window.getComputedStyle(el);
19861
- const margins = {
19971
+ return {
19862
19972
  top: parseInt(style.marginTop, 10) || 0,
19863
19973
  bottom: parseInt(style.marginBottom, 10) || 0,
19864
19974
  left: parseInt(style.marginLeft, 10) || 0,
19865
19975
  right: parseInt(style.marginRight, 10) || 0,
19866
19976
  };
19867
- return margins;
19868
19977
  }
19869
19978
 
19870
19979
  const macRegex = /Mac/i;
@@ -20163,7 +20272,7 @@ class ContentEditableHelper {
20163
20272
  let startNode = this.findChildAtCharacterIndex(start);
20164
20273
  let endNode = this.findChildAtCharacterIndex(end);
20165
20274
  range.setStart(startNode.node, startNode.offset);
20166
- selection.extend(endNode.node, endNode.offset);
20275
+ range.setEnd(endNode.node, endNode.offset);
20167
20276
  }
20168
20277
  }
20169
20278
  /**
@@ -20661,7 +20770,7 @@ class Composer extends Component {
20661
20770
  "Alt+Enter": this.processNewLineEvent,
20662
20771
  "Ctrl+Enter": this.processNewLineEvent,
20663
20772
  Escape: this.processEscapeKey,
20664
- F2: () => console.warn("Not implemented"),
20773
+ F2: (ev) => this.toggleEditionMode(ev),
20665
20774
  F4: (ev) => this.processF4Key(ev),
20666
20775
  Tab: (ev) => this.processTabKey(ev, "right"),
20667
20776
  "Shift+Tab": (ev) => this.processTabKey(ev, "left"),
@@ -20780,6 +20889,11 @@ class Composer extends Component {
20780
20889
  this.props.composerStore.cycleReferences();
20781
20890
  this.processContent();
20782
20891
  }
20892
+ toggleEditionMode(ev) {
20893
+ ev.stopPropagation();
20894
+ this.props.composerStore.toggleEditionMode();
20895
+ this.processContent();
20896
+ }
20783
20897
  processNumpadDecimal(ev) {
20784
20898
  ev.stopPropagation();
20785
20899
  ev.preventDefault();
@@ -21005,7 +21119,13 @@ class Composer extends Component {
21005
21119
  break;
21006
21120
  case "REFERENCE":
21007
21121
  const { xc, sheetName } = splitReference(token.value);
21008
- result.push({ value: token.value, color: this.rangeColor(xc, sheetName) || "#000" });
21122
+ result.push({
21123
+ value: token.value,
21124
+ color: this.rangeColor(xc, sheetName) || "#000",
21125
+ class: tokenAtCursor === token && this.props.composerStore.editionMode === "selecting"
21126
+ ? "text-decoration-underline"
21127
+ : undefined,
21128
+ });
21009
21129
  break;
21010
21130
  case "SYMBOL":
21011
21131
  const value = token.value;
@@ -21576,6 +21696,7 @@ function insertTokenAfterArgSeparator(tokenAtCursor, value) {
21576
21696
  // replace the whole token
21577
21697
  start = tokenAtCursor.start;
21578
21698
  }
21699
+ this.composer.stopComposerRangeSelection();
21579
21700
  this.composer.changeComposerCursorSelection(start, end);
21580
21701
  this.composer.replaceComposerCursorSelection(value);
21581
21702
  }
@@ -21593,6 +21714,7 @@ function insertTokenAfterLeftParenthesis(tokenAtCursor, value) {
21593
21714
  // replace the whole token
21594
21715
  start = tokenAtCursor.start;
21595
21716
  }
21717
+ this.composer.stopComposerRangeSelection();
21596
21718
  this.composer.changeComposerCursorSelection(start, end);
21597
21719
  this.composer.replaceComposerCursorSelection(value);
21598
21720
  }
@@ -21726,9 +21848,13 @@ autoCompleteProviders.add("pivot_group_fields", {
21726
21848
  const colFields = columns.map((groupBy) => groupBy.nameWithGranularity);
21727
21849
  const rowFields = rows.map((groupBy) => groupBy.nameWithGranularity);
21728
21850
  const proposals = [];
21729
- const previousGroupBy = ["ARG_SEPARATOR", "SPACE"].includes(tokenAtCursor.type)
21851
+ let previousGroupBy = ["ARG_SEPARATOR", "SPACE"].includes(tokenAtCursor.type)
21730
21852
  ? argGroupBys.at(-1)
21731
21853
  : argGroupBys.at(-2);
21854
+ const isPositionalSupported = supportedPivotPositionalFormulaRegistry.get(pivot.type);
21855
+ if (isPositionalSupported && previousGroupBy?.startsWith("#")) {
21856
+ previousGroupBy = previousGroupBy.slice(1);
21857
+ }
21732
21858
  if (previousGroupBy === undefined) {
21733
21859
  proposals.push(colFields[0]);
21734
21860
  proposals.push(rowFields[0]);
@@ -21750,7 +21876,7 @@ autoCompleteProviders.add("pivot_group_fields", {
21750
21876
  return field ? makeFieldProposal(field, granularity) : undefined;
21751
21877
  })
21752
21878
  .concat(groupBys.map((groupBy) => {
21753
- if (!supportedPivotPositionalFormulaRegistry.get(pivot.type)) {
21879
+ if (!isPositionalSupported) {
21754
21880
  return undefined;
21755
21881
  }
21756
21882
  const fieldName = groupBy.split(":")[0];
@@ -21759,13 +21885,12 @@ autoCompleteProviders.add("pivot_group_fields", {
21759
21885
  return undefined;
21760
21886
  }
21761
21887
  const positionalFieldArg = `"#${groupBy}"`;
21762
- const positionalProposal = {
21888
+ return {
21763
21889
  text: positionalFieldArg,
21764
21890
  description: _t("%s (positional)", field.string) + (field.help ? ` (${field.help})` : ""),
21765
21891
  htmlContent: [{ value: positionalFieldArg, color: tokenColors.STRING }],
21766
21892
  fuzzySearchKey: field.string + positionalFieldArg, // search on translated name and on technical name
21767
21893
  };
21768
- return positionalProposal;
21769
21894
  }))
21770
21895
  .filter(isDefined);
21771
21896
  },
@@ -22034,7 +22159,7 @@ function parseLiteral(content, locale) {
22034
22159
  return internalDate.value;
22035
22160
  }
22036
22161
  if (isBoolean(content)) {
22037
- return content.toUpperCase() === "TRUE" ? true : false;
22162
+ return content.toUpperCase() === "TRUE";
22038
22163
  }
22039
22164
  return content;
22040
22165
  }
@@ -22185,6 +22310,27 @@ autofillModifiersRegistry
22185
22310
  tooltip: content ? { props: { content: tooltipValue } } : undefined,
22186
22311
  };
22187
22312
  },
22313
+ })
22314
+ .add("DATE_INCREMENT_MODIFIER", {
22315
+ apply: (rule, data, getters) => {
22316
+ const date = toJsDate(rule.current, getters.getLocale());
22317
+ date.setFullYear(date.getFullYear() + rule.increment.years || 0);
22318
+ date.setMonth(date.getMonth() + rule.increment.months || 0);
22319
+ date.setDate(date.getDate() + rule.increment.days || 0);
22320
+ const value = jsDateToNumber(date);
22321
+ rule.current = value;
22322
+ const locale = getters.getLocale();
22323
+ const tooltipValue = formatValue(value, { format: data.cell?.format, locale });
22324
+ return {
22325
+ cellData: {
22326
+ border: data.border,
22327
+ style: data.cell && data.cell.style,
22328
+ format: data.cell && data.cell.format,
22329
+ content: value.toString(),
22330
+ },
22331
+ tooltip: value ? { props: { content: tooltipValue } } : undefined,
22332
+ };
22333
+ },
22188
22334
  })
22189
22335
  .add("COPY_MODIFIER", {
22190
22336
  apply: (rule, data, getters) => {
@@ -22265,7 +22411,9 @@ function getGroup(cell, cells, filter) {
22265
22411
  if (x === cell) {
22266
22412
  found = true;
22267
22413
  }
22268
- const cellValue = x === undefined || x.isFormula ? undefined : evaluateLiteral(x, { locale: DEFAULT_LOCALE });
22414
+ const cellValue = x === undefined || x.isFormula
22415
+ ? undefined
22416
+ : evaluateLiteral(x, { locale: DEFAULT_LOCALE, format: x.format });
22269
22417
  if (cellValue && filter(cellValue)) {
22270
22418
  group.push(cellValue);
22271
22419
  }
@@ -22301,6 +22449,71 @@ function calculateIncrementBasedOnGroup(group) {
22301
22449
  }
22302
22450
  return increment;
22303
22451
  }
22452
+ /**
22453
+ * Iterates on a list of date intervals.
22454
+ * if every interval is the same, return the interval
22455
+ * Otherwise return undefined
22456
+ *
22457
+ */
22458
+ function getEqualInterval(intervals) {
22459
+ if (intervals.length < 2) {
22460
+ return intervals[0] || { years: 0, months: 0, days: 0 };
22461
+ }
22462
+ const equal = intervals.every((interval) => interval.years === intervals[0].years &&
22463
+ interval.months === intervals[0].months &&
22464
+ interval.days === intervals[0].days);
22465
+ return equal ? intervals[0] : undefined;
22466
+ }
22467
+ /**
22468
+ * Based on a group of dates, calculate the increment that should be applied
22469
+ * to the next date.
22470
+ *
22471
+ * This will compute the date difference in calendar terms (years, months, days)
22472
+ * In order to make abstraction of leap years and months with different number of days.
22473
+ *
22474
+ * In case the dates are not equidistant in calendar terms, no rule can be extrapolated
22475
+ * In case of equidistant dates, we either have in that order:
22476
+ * - exact date interval (e.g. +n year OR +n month OR +n day) in which case we increment by the same interval
22477
+ * - exact day interval (e.g. +n days) in which case we increment by the same day interval
22478
+ * - equidistant dates but not the same interval, in which case we return increment of the same interval
22479
+ *
22480
+ * */
22481
+ function calculateDateIncrementBasedOnGroup(group) {
22482
+ if (group.length < 2) {
22483
+ return 1;
22484
+ }
22485
+ const jsDates = group.map((date) => toJsDate(date, DEFAULT_LOCALE));
22486
+ const datesIntervals = getDateIntervals(jsDates);
22487
+ const datesEquidistantInterval = getEqualInterval(datesIntervals);
22488
+ if (datesEquidistantInterval === undefined) {
22489
+ // dates are not equidistant in terms of years, months or days, thus no rule can be extrapolated
22490
+ return undefined;
22491
+ }
22492
+ // The dates are apart by an exact interval of years, months or days
22493
+ // but not a combination of them
22494
+ const exactDateInterval = Object.values(datesEquidistantInterval).filter((value) => value !== 0).length === 1;
22495
+ const isSameDay = Object.values(datesEquidistantInterval).every((el) => el === 0); // handles time values (strict decimals)
22496
+ if (!exactDateInterval || isSameDay) {
22497
+ const timeIntervals = jsDates
22498
+ .map((date, index) => {
22499
+ if (index === 0) {
22500
+ return 0;
22501
+ }
22502
+ const previous = jsDates[index - 1];
22503
+ return Math.floor(date.getTime()) - Math.floor(previous.getTime());
22504
+ })
22505
+ .slice(1);
22506
+ const equidistantDates = timeIntervals.every((interval) => interval === timeIntervals[0]);
22507
+ if (equidistantDates) {
22508
+ return group.length * (group[1] - group[0]);
22509
+ }
22510
+ }
22511
+ return {
22512
+ years: datesEquidistantInterval.years * group.length,
22513
+ months: datesEquidistantInterval.months * group.length,
22514
+ days: datesEquidistantInterval.days * group.length,
22515
+ };
22516
+ }
22304
22517
  autofillRulesRegistry
22305
22518
  .add("simple_value_copy", {
22306
22519
  condition: (cell, cells) => {
@@ -22348,12 +22561,47 @@ autofillRulesRegistry
22348
22561
  return { type: "FORMULA_MODIFIER", increment: cells.length, current: 0 };
22349
22562
  },
22350
22563
  sequence: 30,
22564
+ })
22565
+ .add("increment_dates", {
22566
+ condition: (cell, cells) => {
22567
+ return (!cell.isFormula &&
22568
+ evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.number &&
22569
+ !!cell.format &&
22570
+ isDateTimeFormat(cell.format));
22571
+ },
22572
+ generateRule: (cell, cells) => {
22573
+ const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number &&
22574
+ !!evaluatedCell.format &&
22575
+ isDateTimeFormat(evaluatedCell.format)).map((cell) => Number(cell.value));
22576
+ const increment = calculateDateIncrementBasedOnGroup(group);
22577
+ if (increment === undefined) {
22578
+ return { type: "COPY_MODIFIER" };
22579
+ }
22580
+ /** requires to detect the current date (requires to be an integer value with the right format)
22581
+ * detect if year or if month or if day then extrapolate increment required (+1 month, +1 year + 1 day)
22582
+ */
22583
+ const evaluation = evaluateLiteral(cell, { locale: DEFAULT_LOCALE });
22584
+ if (typeof increment === "object") {
22585
+ return {
22586
+ type: "DATE_INCREMENT_MODIFIER",
22587
+ increment,
22588
+ current: evaluation.type === CellValueType.number ? evaluation.value : 0,
22589
+ };
22590
+ }
22591
+ return {
22592
+ type: "INCREMENT_MODIFIER",
22593
+ increment,
22594
+ current: evaluation.type === CellValueType.number ? evaluation.value : 0,
22595
+ };
22596
+ },
22597
+ sequence: 25,
22351
22598
  })
22352
22599
  .add("increment_number", {
22353
22600
  condition: (cell) => !cell.isFormula &&
22354
22601
  evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.number,
22355
22602
  generateRule: (cell, cells) => {
22356
- const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number).map((cell) => Number(cell.value));
22603
+ const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number &&
22604
+ !isDateTimeFormat(evaluatedCell.format || "")).map((cell) => Number(cell.value));
22357
22605
  const increment = calculateIncrementBasedOnGroup(group);
22358
22606
  const evaluation = evaluateLiteral(cell, { locale: DEFAULT_LOCALE });
22359
22607
  return {
@@ -22364,6 +22612,37 @@ autofillRulesRegistry
22364
22612
  },
22365
22613
  sequence: 40,
22366
22614
  });
22615
+ /**
22616
+ * Returns the date intervals between consecutive dates of an array
22617
+ * in the format of { years: number, months: number, days: number }
22618
+ *
22619
+ * The split is necessary to make abstraction of leap years and
22620
+ * months with different number of days.
22621
+ *
22622
+ * @param dates
22623
+ */
22624
+ function getDateIntervals(dates) {
22625
+ if (dates.length < 2) {
22626
+ return [{ years: 0, months: 0, days: 0 }];
22627
+ }
22628
+ const res = dates.map((date, index) => {
22629
+ if (index === 0) {
22630
+ return { years: 0, months: 0, days: 0 };
22631
+ }
22632
+ const previous = DateTime.fromTimestamp(dates[index - 1].getTime());
22633
+ const years = getTimeDifferenceInWholeYears(previous, date);
22634
+ const months = getTimeDifferenceInWholeMonths(previous, date) % 12;
22635
+ previous.setFullYear(previous.getFullYear() + years);
22636
+ previous.setMonth(previous.getMonth() + months);
22637
+ const days = getTimeDifferenceInWholeDays(previous, date);
22638
+ return {
22639
+ years,
22640
+ months,
22641
+ days,
22642
+ };
22643
+ });
22644
+ return res.slice(1);
22645
+ }
22367
22646
 
22368
22647
  const cellPopoverRegistry = new Registry();
22369
22648
 
@@ -22780,6 +23059,7 @@ const XLSX_CHART_TYPES = [
22780
23059
  "surface3DChart",
22781
23060
  "bubbleChart",
22782
23061
  "comboChart",
23062
+ "radarChart",
22783
23063
  ];
22784
23064
 
22785
23065
  /** In XLSX color format (no #) */
@@ -23320,7 +23600,7 @@ const CHART_TYPE_CONVERSION_MAP = {
23320
23600
  lineChart: "line",
23321
23601
  line3DChart: undefined,
23322
23602
  stockChart: undefined,
23323
- radarChart: undefined,
23603
+ radarChart: "radar",
23324
23604
  scatterChart: "scatter",
23325
23605
  pieChart: "pie",
23326
23606
  pie3DChart: undefined,
@@ -24733,7 +25013,7 @@ const TABLE_STYLE_CATEGORIES = {
24733
25013
  custom: _t("Custom"),
24734
25014
  };
24735
25015
  const DEFAULT_TABLE_CONFIG = {
24736
- hasFilters: true,
25016
+ hasFilters: false,
24737
25017
  totalRow: false,
24738
25018
  firstColumn: false,
24739
25019
  lastColumn: false,
@@ -25402,12 +25682,11 @@ class XlsxBaseExtractor {
25402
25682
  * Get the list of all the XLSX files in the XLSX file structure
25403
25683
  */
25404
25684
  getListOfXMLFiles() {
25405
- const XMLFiles = Object.entries(this.xlsxFileStructure)
25685
+ return Object.entries(this.xlsxFileStructure)
25406
25686
  .filter(([key]) => key !== "images")
25407
25687
  .map(([_, value]) => value)
25408
25688
  .flat()
25409
25689
  .filter(isDefined);
25410
- return XMLFiles;
25411
25690
  }
25412
25691
  /**
25413
25692
  * Return an array containing the return value of the given function applied to all the XML elements
@@ -25571,13 +25850,12 @@ class XlsxBaseExtractor {
25571
25850
  rgb = this.extractAttr(colorElement, "rgb")?.asString();
25572
25851
  rgb = rgb === DEFAULT_SYSTEM_COLOR ? undefined : rgb;
25573
25852
  }
25574
- const color = {
25853
+ return {
25575
25854
  rgb: rgb || defaultColor,
25576
25855
  auto: this.extractAttr(colorElement, "auto")?.asBool(),
25577
25856
  indexed: this.extractAttr(colorElement, "indexed")?.asNum(),
25578
25857
  tint: this.extractAttr(colorElement, "tint")?.asNum(),
25579
25858
  };
25580
- return color;
25581
25859
  }
25582
25860
  /**
25583
25861
  * Returns the xml file targeted by a relationship.
@@ -26169,7 +26447,7 @@ class XlsxSheetExtractor extends XlsxBaseExtractor {
26169
26447
  hyperlinks: this.extractHyperLinks(sheetElement),
26170
26448
  tables: this.extractTables(sheetElement),
26171
26449
  pivotTables: this.extractPivotTables(),
26172
- isVisible: sheetWorkbookInfo.state === "visible" ? true : false,
26450
+ isVisible: sheetWorkbookInfo.state === "visible",
26173
26451
  };
26174
26452
  })[0];
26175
26453
  }
@@ -26240,8 +26518,7 @@ class XlsxSheetExtractor extends XlsxBaseExtractor {
26240
26518
  const figures = this.mapOnElements({ parent: worksheet, query: "drawing" }, (drawingElement) => {
26241
26519
  const drawingId = this.extractAttr(drawingElement, "r:id", { required: true })?.asString();
26242
26520
  const drawingFile = this.getTargetXmlFile(this.relationships[drawingId]);
26243
- const figures = new XlsxFigureExtractor(drawingFile, this.xlsxFileStructure, this.warningManager).extractFigures();
26244
- return figures;
26521
+ return new XlsxFigureExtractor(drawingFile, this.xlsxFileStructure, this.warningManager).extractFigures();
26245
26522
  })[0];
26246
26523
  return figures || [];
26247
26524
  }
@@ -26259,8 +26536,7 @@ class XlsxSheetExtractor extends XlsxBaseExtractor {
26259
26536
  .filter((relationship) => relationship.type.endsWith("pivotTable"))
26260
26537
  .map((pivotRelationship) => {
26261
26538
  const pivotFile = this.getTargetXmlFile(pivotRelationship);
26262
- const pivot = new XlsxPivotExtractor(pivotFile, this.xlsxFileStructure, this.warningManager).getPivotTable();
26263
- return pivot;
26539
+ return new XlsxPivotExtractor(pivotFile, this.xlsxFileStructure, this.warningManager).getPivotTable();
26264
26540
  });
26265
26541
  }
26266
26542
  catch (e) {
@@ -26659,8 +26935,7 @@ class XlsxReader {
26659
26935
  }
26660
26936
  convertXlsx() {
26661
26937
  const xlsxData = this.getXlsxData();
26662
- const convertedData = this.convertImportedData(xlsxData);
26663
- return convertedData;
26938
+ return this.convertImportedData(xlsxData);
26664
26939
  }
26665
26940
  // ---------------------------------------------------------------------------
26666
26941
  // Parsing XMLs
@@ -27251,6 +27526,20 @@ migrationStepRegistry
27251
27526
  }
27252
27527
  return data;
27253
27528
  },
27529
+ })
27530
+ .add("migration_22", {
27531
+ // "tables are no longer inserted with filters by default",
27532
+ versionFrom: "22",
27533
+ migrate(data) {
27534
+ for (const sheet of data.sheets || []) {
27535
+ for (const table of sheet.tables || []) {
27536
+ if (!table.config) {
27537
+ table.config = { ...DEFAULT_TABLE_CONFIG, hasFilters: true };
27538
+ }
27539
+ }
27540
+ }
27541
+ return data;
27542
+ },
27254
27543
  });
27255
27544
  function fixOverlappingFilters(data) {
27256
27545
  for (let sheet of data.sheets || []) {
@@ -27278,7 +27567,7 @@ function fixOverlappingFilters(data) {
27278
27567
  * a breaking change is made in the way the state is handled, and an upgrade
27279
27568
  * function should be defined
27280
27569
  */
27281
- const CURRENT_VERSION = 22;
27570
+ const CURRENT_VERSION = 23;
27282
27571
  const INITIAL_SHEET_ID = "Sheet1";
27283
27572
  /**
27284
27573
  * This function tries to load anything that could look like a valid
@@ -27291,7 +27580,7 @@ function load(data, verboseImport) {
27291
27580
  if (!data) {
27292
27581
  return createEmptyWorkbookData();
27293
27582
  }
27294
- console.group("Loading data");
27583
+ console.debug("### Loading data ###");
27295
27584
  const start = performance.now();
27296
27585
  if (data["[Content_Types].xml"]) {
27297
27586
  const reader = new XlsxReader(data);
@@ -27305,13 +27594,13 @@ function load(data, verboseImport) {
27305
27594
  // apply migrations, if needed
27306
27595
  if ("version" in data) {
27307
27596
  if (data.version < CURRENT_VERSION) {
27308
- console.info("Migrating data from version", data.version);
27597
+ console.debug("Migrating data from version", data.version);
27309
27598
  data = migrate(data);
27310
27599
  }
27311
27600
  }
27312
27601
  data = repairData(data);
27313
- console.info("Data loaded in", performance.now() - start, "ms");
27314
- console.groupEnd();
27602
+ console.debug("Data loaded in", performance.now() - start, "ms");
27603
+ console.debug("###");
27315
27604
  return data;
27316
27605
  }
27317
27606
  // -----------------------------------------------------------------------------
@@ -27341,7 +27630,7 @@ function migrate(data) {
27341
27630
  for (let i = index; i < steps.length; i++) {
27342
27631
  data = steps[i].migrate(data);
27343
27632
  }
27344
- console.info("Data migrated in", performance.now() - start, "ms");
27633
+ console.debug("Data migrated in", performance.now() - start, "ms");
27345
27634
  return data;
27346
27635
  }
27347
27636
  /**
@@ -27523,7 +27812,7 @@ function createEmptySheet(sheetId, name) {
27523
27812
  };
27524
27813
  }
27525
27814
  function createEmptyWorkbookData(sheetName = "Sheet1") {
27526
- const data = {
27815
+ return {
27527
27816
  version: CURRENT_VERSION,
27528
27817
  sheets: [createEmptySheet(INITIAL_SHEET_ID, sheetName)],
27529
27818
  styles: {},
@@ -27536,7 +27825,6 @@ function createEmptyWorkbookData(sheetName = "Sheet1") {
27536
27825
  pivotNextId: 1,
27537
27826
  customTableStyles: {},
27538
27827
  };
27539
- return data;
27540
27828
  }
27541
27829
  function createEmptyExcelSheet(sheetId, name) {
27542
27830
  return {
@@ -27874,9 +28162,9 @@ function truncateLabel(label) {
27874
28162
  /**
27875
28163
  * Get a default chart js configuration
27876
28164
  */
27877
- function getDefaultChartJsRuntime(chart, labels, fontColor, { format, locale, truncateLabels = true, horizontalChart, }) {
28165
+ function getDefaultChartJsRuntime(chart, labels, fontColor, { axisFormats, locale, truncateLabels = true, horizontalChart }) {
27878
28166
  const chartTitle = chart.title.text ? chart.title : { ...chart.title, content: "" };
27879
- const options = {
28167
+ const chartOptions = {
27880
28168
  // https://www.chartjs.org/docs/latest/general/responsive.html
27881
28169
  responsive: true, // will resize when its container is resized
27882
28170
  maintainAspectRatio: false, // doesn't maintain the aspect ration (width/height =2 by default) so the user has the choice of the exact layout
@@ -27920,11 +28208,13 @@ function getDefaultChartJsRuntime(chart, labels, fontColor, { format, locale, tr
27920
28208
  const xLabel = tooltipItem.dataset?.label || tooltipItem.label;
27921
28209
  // tooltipItem.parsed can be an object or a number for pie charts
27922
28210
  let yLabel = horizontalChart ? tooltipItem.parsed.x : tooltipItem.parsed.y;
27923
- if (!yLabel) {
28211
+ if (yLabel === undefined || yLabel === null) {
27924
28212
  yLabel = tooltipItem.parsed;
27925
28213
  }
27926
- const toolTipFormat = !format && Math.abs(yLabel) >= 1000 ? "#,##" : format;
27927
- const yLabelStr = formatValue(yLabel, { format: toolTipFormat, locale });
28214
+ const axisId = horizontalChart
28215
+ ? tooltipItem.dataset.xAxisID
28216
+ : tooltipItem.dataset.yAxisID;
28217
+ const yLabelStr = formatChartDatasetValue(axisFormats, locale)(yLabel, axisId);
27928
28218
  return xLabel ? `${xLabel}: ${yLabelStr}` : yLabelStr;
27929
28219
  },
27930
28220
  },
@@ -27933,7 +28223,7 @@ function getDefaultChartJsRuntime(chart, labels, fontColor, { format, locale, tr
27933
28223
  };
27934
28224
  return {
27935
28225
  type: chart.type,
27936
- options,
28226
+ options: chartOptions,
27937
28227
  data: {
27938
28228
  labels: truncateLabels ? labels.map(truncateLabel) : labels,
27939
28229
  datasets: [],
@@ -27992,7 +28282,8 @@ function getChartLabelValues(getters, dataSets, labelRange) {
27992
28282
  * Get the format to apply to the the dataset values. This format is defined as the first format
27993
28283
  * found in the dataset ranges that isn't a date format.
27994
28284
  */
27995
- function getChartDatasetFormat(getters, dataSets) {
28285
+ function getChartDatasetFormat(getters, allDataSets, axis) {
28286
+ const dataSets = allDataSets.filter((ds) => (axis === "right") === !!ds.rightYAxis);
27996
28287
  for (const ds of dataSets) {
27997
28288
  const formatsInDataset = getters.getRangeFormats(ds.dataRange);
27998
28289
  const format = formatsInDataset.find((f) => f !== undefined && !isDateTimeFormat(f));
@@ -28246,12 +28537,16 @@ function createBarChartRuntime(chart, getters) {
28246
28537
  if (chart.aggregated) {
28247
28538
  ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));
28248
28539
  }
28249
- const dataSetFormat = getChartDatasetFormat(getters, chart.dataSets);
28540
+ const leftAxisFormat = getChartDatasetFormat(getters, chart.dataSets, "left");
28541
+ const rightAxisFormat = getChartDatasetFormat(getters, chart.dataSets, "right");
28250
28542
  const locale = getters.getLocale();
28251
- const localeFormat = { format: dataSetFormat, locale };
28252
28543
  const fontColor = chartFontColor(chart.background);
28544
+ const axisFormats = chart.horizontal
28545
+ ? { x: leftAxisFormat || rightAxisFormat }
28546
+ : { y: leftAxisFormat, y1: rightAxisFormat };
28253
28547
  const config = getDefaultChartJsRuntime(chart, labels, fontColor, {
28254
- ...localeFormat,
28548
+ locale,
28549
+ axisFormats,
28255
28550
  horizontalChart: chart.horizontal,
28256
28551
  });
28257
28552
  const legend = {
@@ -28272,51 +28567,27 @@ function createBarChartRuntime(chart, getters) {
28272
28567
  };
28273
28568
  config.options.indexAxis = chart.horizontal ? "y" : "x";
28274
28569
  config.options.scales = {};
28275
- const labelsAxis = { ticks: { padding: 5, color: fontColor } };
28276
- const valuesAxis = {
28277
- beginAtZero: true, // the origin of the y axis is always zero
28278
- ticks: {
28279
- color: fontColor,
28280
- callback: formatTickValue(localeFormat),
28281
- },
28282
- };
28283
- const xAxis = chart.horizontal ? valuesAxis : labelsAxis;
28284
- const yAxis = chart.horizontal ? labelsAxis : valuesAxis;
28285
- const { useLeftAxis, useRightAxis } = getDefinedAxis(chart.getDefinition());
28286
- config.options.scales.x = { ...xAxis, title: getChartAxisTitleRuntime(chart.axesDesign?.x) };
28287
- if (useLeftAxis) {
28288
- config.options.scales.y = {
28289
- ...yAxis,
28290
- position: "left",
28291
- title: getChartAxisTitleRuntime(chart.axesDesign?.y),
28292
- };
28293
- }
28294
- if (useRightAxis) {
28295
- config.options.scales.y1 = {
28296
- ...yAxis,
28297
- position: "right",
28298
- title: getChartAxisTitleRuntime(chart.axesDesign?.y1),
28299
- };
28570
+ const definition = chart.getDefinition();
28571
+ const options = { stacked: chart.stacked, locale };
28572
+ if (chart.horizontal) {
28573
+ const format = leftAxisFormat || rightAxisFormat;
28574
+ config.options.scales.x = getChartAxis(definition, "bottom", "values", { ...options, format });
28575
+ config.options.scales.y = getChartAxis(definition, "left", "labels", options);
28300
28576
  }
28301
- if (chart.stacked) {
28302
- // @ts-ignore chart.js type is broken
28303
- config.options.scales.x.stacked = true;
28304
- if (useLeftAxis) {
28305
- // @ts-ignore chart.js type is broken
28306
- config.options.scales.y.stacked = true;
28307
- }
28308
- if (useRightAxis) {
28309
- // @ts-ignore chart.js type is broken
28310
- config.options.scales.y1.stacked = true;
28311
- }
28577
+ else {
28578
+ config.options.scales.x = getChartAxis(definition, "bottom", "labels", options);
28579
+ const leftAxisOptions = { ...options, format: leftAxisFormat };
28580
+ config.options.scales.y = getChartAxis(definition, "left", "values", leftAxisOptions);
28581
+ const rightAxisOptions = { ...options, format: rightAxisFormat };
28582
+ config.options.scales.y1 = getChartAxis(definition, "right", "values", rightAxisOptions);
28312
28583
  }
28584
+ config.options.scales = removeFalsyAttributes(config.options.scales);
28313
28585
  config.options.plugins.chartShowValuesPlugin = {
28314
28586
  showValues: chart.showValues,
28315
28587
  background: chart.background,
28316
28588
  horizontal: chart.horizontal,
28317
- callback: formatTickValue(localeFormat),
28589
+ callback: formatChartDatasetValue(axisFormats, locale),
28318
28590
  };
28319
- const definition = chart.getDefinition();
28320
28591
  const colors = getChartColorsGenerator(definition, dataSetsValues.length);
28321
28592
  const trendDatasets = [];
28322
28593
  for (const index in dataSetsValues) {
@@ -28331,12 +28602,10 @@ function createBarChartRuntime(chart, getters) {
28331
28602
  };
28332
28603
  config.data.datasets.push(dataset);
28333
28604
  if (definition.dataSets?.[index]?.label) {
28334
- const label = definition.dataSets[index].label;
28335
- dataset.label = label;
28336
- }
28337
- if (definition.dataSets?.[index]?.yAxisId && !chart.horizontal) {
28338
- dataset["yAxisID"] = definition.dataSets[index].yAxisId;
28605
+ dataset.label = definition.dataSets[index].label;
28339
28606
  }
28607
+ dataset.yAxisID = chart.horizontal ? "y" : definition.dataSets[index].yAxisId || "y";
28608
+ dataset.xAxisID = "x";
28340
28609
  const trend = definition.dataSets?.[index].trend;
28341
28610
  if (!trend?.display || chart.horizontal) {
28342
28611
  continue;
@@ -28352,7 +28621,7 @@ function createBarChartRuntime(chart, getters) {
28352
28621
  */
28353
28622
  const maxLength = Math.max(...trendDatasets.map((trendDataset) => trendDataset.data.length));
28354
28623
  config.options.scales[TREND_LINE_XAXIS_ID] = {
28355
- ...xAxis,
28624
+ ...config.options.scales.x,
28356
28625
  labels: Array(maxLength).fill(""),
28357
28626
  offset: false,
28358
28627
  display: false,
@@ -28361,13 +28630,10 @@ function createBarChartRuntime(chart, getters) {
28361
28630
  * datasets to ensure the way we distinguish the originals and trendLine datasets after
28362
28631
  */
28363
28632
  trendDatasets.forEach((x) => config.data.datasets.push(x));
28364
- const originalTooltipTitle = config.options.plugins.tooltip.callbacks.title;
28365
28633
  config.options.plugins.tooltip.callbacks.title = function (tooltipItems) {
28366
- if (tooltipItems.some((item) => item.dataset.xAxisID !== TREND_LINE_XAXIS_ID)) {
28367
- // @ts-expect-error
28368
- return originalTooltipTitle?.(tooltipItems);
28369
- }
28370
- return "";
28634
+ return tooltipItems.some((item) => item.dataset.xAxisID !== TREND_LINE_XAXIS_ID)
28635
+ ? undefined
28636
+ : "";
28371
28637
  };
28372
28638
  }
28373
28639
  return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
@@ -28642,8 +28908,10 @@ function createLineOrScatterChartRuntime(chart, getters) {
28642
28908
  }
28643
28909
  const locale = getters.getLocale();
28644
28910
  const truncateLabels = axisType === "category";
28645
- const dataSetFormat = getChartDatasetFormat(getters, chart.dataSets);
28646
- const options = { format: dataSetFormat, locale, truncateLabels };
28911
+ const leftAxisFormat = getChartDatasetFormat(getters, chart.dataSets, "left");
28912
+ const rightAxisFormat = getChartDatasetFormat(getters, chart.dataSets, "right");
28913
+ const axisFormats = { y: leftAxisFormat, y1: rightAxisFormat };
28914
+ const options = { locale, truncateLabels, axisFormats };
28647
28915
  const fontColor = chartFontColor(chart.background);
28648
28916
  const config = getDefaultChartJsRuntime(chart, labels, fontColor, options);
28649
28917
  const legend = {
@@ -28673,52 +28941,18 @@ function createLineOrScatterChartRuntime(chart, getters) {
28673
28941
  displayLegend: chart.legendPosition === "top",
28674
28942
  }),
28675
28943
  };
28676
- const xAxis = {
28677
- ticks: {
28678
- padding: 5,
28679
- color: fontColor,
28680
- },
28681
- title: getChartAxisTitleRuntime(chart.axesDesign?.x),
28682
- };
28944
+ const definition = chart.getDefinition();
28945
+ const stacked = "stacked" in chart && chart.stacked;
28683
28946
  config.options.scales = {
28684
- x: xAxis,
28685
- };
28686
- const yAxis = {
28687
- beginAtZero: true, // the origin of the y axis is always zero
28688
- ticks: {
28689
- color: fontColor,
28690
- callback: formatTickValue(options),
28691
- },
28947
+ x: getChartAxis(definition, "bottom", "labels", { locale }),
28948
+ y: getChartAxis(definition, "left", "values", { locale, stacked, format: leftAxisFormat }),
28949
+ y1: getChartAxis(definition, "right", "values", { locale, stacked, format: rightAxisFormat }),
28692
28950
  };
28693
- const { useLeftAxis, useRightAxis } = getDefinedAxis(chart.getDefinition());
28694
- if (useLeftAxis) {
28695
- config.options.scales.y = {
28696
- ...yAxis,
28697
- position: "left",
28698
- title: getChartAxisTitleRuntime(chart.axesDesign?.y),
28699
- };
28700
- }
28701
- if (useRightAxis) {
28702
- config.options.scales.y1 = {
28703
- ...yAxis,
28704
- position: "right",
28705
- title: getChartAxisTitleRuntime(chart.axesDesign?.y1),
28706
- };
28707
- }
28708
- if ("stacked" in chart && chart.stacked) {
28709
- if (useLeftAxis) {
28710
- // @ts-ignore chart.js type is broken
28711
- config.options.scales.y.stacked = true;
28712
- }
28713
- if (useRightAxis) {
28714
- // @ts-ignore chart.js type is broken
28715
- config.options.scales.y1.stacked = true;
28716
- }
28717
- }
28951
+ config.options.scales = removeFalsyAttributes(config.options.scales);
28718
28952
  config.options.plugins.chartShowValuesPlugin = {
28719
28953
  showValues: chart.showValues,
28720
28954
  background: chart.background,
28721
- callback: formatTickValue(options),
28955
+ callback: formatChartDatasetValue(axisFormats, locale),
28722
28956
  };
28723
28957
  if (chart.dataSetsHaveTitle &&
28724
28958
  dataSetsValues[0] &&
@@ -28737,7 +28971,6 @@ function createLineOrScatterChartRuntime(chart, getters) {
28737
28971
  else if (axisType === "linear") {
28738
28972
  config.options.scales.x.type = "linear";
28739
28973
  config.options.scales.x.ticks.callback = (value) => formatValue(value, { format: labelFormat, locale });
28740
- config.options.plugins.tooltip.callbacks.title = () => "";
28741
28974
  config.options.plugins.tooltip.callbacks.label = (tooltipItem) => {
28742
28975
  const dataSetPoint = dataSetsValues[tooltipItem.datasetIndex].data[tooltipItem.dataIndex];
28743
28976
  let label = tooltipItem.label || labelValues.values[tooltipItem.dataIndex];
@@ -28745,7 +28978,7 @@ function createLineOrScatterChartRuntime(chart, getters) {
28745
28978
  label = toNumber(label, locale);
28746
28979
  }
28747
28980
  const formattedX = formatValue(label, { locale, format: labelFormat });
28748
- const formattedY = formatValue(dataSetPoint, { locale, format: dataSetFormat });
28981
+ const formattedY = formatValue(dataSetPoint, { locale, format: leftAxisFormat });
28749
28982
  const dataSetTitle = tooltipItem.dataset.label;
28750
28983
  return formattedX
28751
28984
  ? `${dataSetTitle}: (${formattedX}, ${formattedY})`
@@ -28755,7 +28988,6 @@ function createLineOrScatterChartRuntime(chart, getters) {
28755
28988
  const areaChart = "fillArea" in chart ? chart.fillArea : false;
28756
28989
  const stackedChart = "stacked" in chart ? chart.stacked : false;
28757
28990
  const cumulative = "cumulative" in chart ? chart.cumulative : false;
28758
- const definition = chart.getDefinition();
28759
28991
  const colors = getChartColorsGenerator(definition, dataSetsValues.length);
28760
28992
  for (let [index, { label, data }] of dataSetsValues.entries()) {
28761
28993
  const color = colors.next();
@@ -28793,12 +29025,9 @@ function createLineOrScatterChartRuntime(chart, getters) {
28793
29025
  const trendDatasets = [];
28794
29026
  for (const [index, dataset] of config.data.datasets.entries()) {
28795
29027
  if (definition.dataSets?.[index]?.label) {
28796
- const label = definition.dataSets[index].label;
28797
- dataset.label = label;
28798
- }
28799
- if (definition.dataSets?.[index]?.yAxisId) {
28800
- dataset["yAxisID"] = definition.dataSets[index].yAxisId;
29028
+ dataset.label = definition.dataSets[index].label;
28801
29029
  }
29030
+ dataset["yAxisID"] = definition.dataSets[index].yAxisId || "y";
28802
29031
  const trend = definition.dataSets?.[index].trend;
28803
29032
  if (!trend?.display) {
28804
29033
  continue;
@@ -28815,7 +29044,7 @@ function createLineOrScatterChartRuntime(chart, getters) {
28815
29044
  * set so that the second axis points match the classical x axis
28816
29045
  */
28817
29046
  config.options.scales[TREND_LINE_XAXIS_ID] = {
28818
- ...xAxis,
29047
+ ...config.options.scales.x,
28819
29048
  type: "category",
28820
29049
  labels: range(0, maxLength).map((x) => x.toString()),
28821
29050
  offset: false,
@@ -28825,22 +29054,15 @@ function createLineOrScatterChartRuntime(chart, getters) {
28825
29054
  * distinguish the originals and trendLine datasets after
28826
29055
  */
28827
29056
  trendDatasets.forEach((x) => config.data.datasets.push(x));
28828
- const originalTooltipTitle = config.options.plugins.tooltip.callbacks.title;
28829
- config.options.plugins.tooltip.callbacks.title = function (tooltipItems) {
28830
- if (tooltipItems.some((item) => item.dataset.xAxisID !== TREND_LINE_XAXIS_ID)) {
28831
- // @ts-expect-error
28832
- return originalTooltipTitle?.(tooltipItems);
28833
- }
28834
- return "";
28835
- };
28836
29057
  }
29058
+ config.options.plugins.tooltip.callbacks.title = function (tooltipItems) {
29059
+ const displayTooltipTitle = axisType !== "linear" &&
29060
+ tooltipItems.some((item) => item.dataset.xAxisID !== TREND_LINE_XAXIS_ID);
29061
+ return displayTooltipTitle ? undefined : "";
29062
+ };
28837
29063
  return {
28838
29064
  chartJsConfig: config,
28839
29065
  background: chart.background || BACKGROUND_CHART_COLOR,
28840
- dataSetsValues,
28841
- labelValues,
28842
- dataSetFormat,
28843
- labelFormat,
28844
29066
  };
28845
29067
  }
28846
29068
 
@@ -28898,6 +29120,7 @@ class ComboChart extends AbstractChart {
28898
29120
  ranges.push({
28899
29121
  ...this.dataSetDesign?.[i],
28900
29122
  dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId),
29123
+ type: this.dataSetDesign?.[i]?.type ?? (i ? "line" : "bar"),
28901
29124
  });
28902
29125
  }
28903
29126
  return {
@@ -28943,9 +29166,13 @@ class ComboChart extends AbstractChart {
28943
29166
  return new ComboChart(definition, this.sheetId, this.getters);
28944
29167
  }
28945
29168
  static getDefinitionFromContextCreation(context) {
29169
+ const dataSets = (context.range ?? []).map((ds, index) => ({
29170
+ ...ds,
29171
+ type: index ? "line" : "bar",
29172
+ }));
28946
29173
  return {
28947
29174
  background: context.background,
28948
- dataSets: context.range ?? [],
29175
+ dataSets,
28949
29176
  dataSetsHaveTitle: context.dataSetsHaveTitle ?? false,
28950
29177
  aggregated: context.aggregated,
28951
29178
  legendPosition: context.legendPosition ?? "top",
@@ -28968,10 +29195,8 @@ class ComboChart extends AbstractChart {
28968
29195
  }
28969
29196
  }
28970
29197
  function createComboChartRuntime(chart, getters) {
28971
- const mainDataSetFormat = chart.dataSets.length
28972
- ? getChartDatasetFormat(getters, [chart.dataSets[0]])
28973
- : undefined;
28974
- const lineDataSetsFormat = getChartDatasetFormat(getters, chart.dataSets.slice(1));
29198
+ const mainDataSetFormat = getChartDatasetFormat(getters, chart.dataSets, "left");
29199
+ const lineDataSetsFormat = getChartDatasetFormat(getters, chart.dataSets, "right");
28975
29200
  const locale = getters.getLocale();
28976
29201
  const labelValues = getChartLabelValues(getters, chart.dataSets, chart.labelRange);
28977
29202
  let labels = labelValues.formattedValues;
@@ -28985,12 +29210,11 @@ function createComboChartRuntime(chart, getters) {
28985
29210
  if (chart.aggregated) {
28986
29211
  ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));
28987
29212
  }
28988
- const localeFormat = { format: mainDataSetFormat, locale };
28989
29213
  const fontColor = chartFontColor(chart.background);
28990
- const config = getDefaultChartJsRuntime(chart, labels, fontColor, localeFormat);
29214
+ const axisFormats = { y: mainDataSetFormat, y1: lineDataSetsFormat };
29215
+ const config = getDefaultChartJsRuntime(chart, labels, fontColor, { locale, axisFormats });
28991
29216
  const legend = {
28992
29217
  labels: { color: fontColor },
28993
- reverse: true,
28994
29218
  };
28995
29219
  if (chart.legendPosition === "none") {
28996
29220
  legend.display = false;
@@ -29005,52 +29229,17 @@ function createComboChartRuntime(chart, getters) {
29005
29229
  displayLegend: chart.legendPosition === "top",
29006
29230
  }),
29007
29231
  };
29232
+ const definition = chart.getDefinition();
29008
29233
  config.options.scales = {
29009
- x: {
29010
- ticks: {
29011
- padding: 5,
29012
- color: fontColor,
29013
- },
29014
- title: getChartAxisTitleRuntime(chart.axesDesign?.x),
29015
- },
29234
+ x: getChartAxis(definition, "bottom", "labels", { locale }),
29235
+ y: getChartAxis(definition, "left", "values", { locale, format: mainDataSetFormat }),
29236
+ y1: getChartAxis(definition, "right", "values", { locale, format: lineDataSetsFormat }),
29016
29237
  };
29017
- const leftVerticalAxis = {
29018
- beginAtZero: true, // the origin of the y axis is always zero
29019
- ticks: {
29020
- color: fontColor,
29021
- callback: formatTickValue({ format: mainDataSetFormat, locale }),
29022
- },
29023
- };
29024
- const rightVerticalAxis = {
29025
- beginAtZero: true, // the origin of the y axis is always zero
29026
- ticks: {
29027
- color: fontColor,
29028
- callback: formatTickValue({ format: lineDataSetsFormat, locale }),
29029
- },
29030
- };
29031
- const definition = chart.getDefinition();
29032
- const { useLeftAxis, useRightAxis } = getDefinedAxis(definition);
29033
- if (useLeftAxis) {
29034
- config.options.scales.y = {
29035
- ...leftVerticalAxis,
29036
- position: "left",
29037
- title: getChartAxisTitleRuntime(chart.axesDesign?.y),
29038
- };
29039
- }
29040
- if (useRightAxis) {
29041
- config.options.scales.y1 = {
29042
- ...rightVerticalAxis,
29043
- position: "right",
29044
- grid: {
29045
- display: false,
29046
- },
29047
- title: getChartAxisTitleRuntime(chart.axesDesign?.y1),
29048
- };
29049
- }
29238
+ config.options.scales = removeFalsyAttributes(config.options.scales);
29050
29239
  config.options.plugins.chartShowValuesPlugin = {
29051
29240
  showValues: chart.showValues,
29052
29241
  background: chart.background,
29053
- callback: formatTickValue({ format: mainDataSetFormat, locale }),
29242
+ callback: formatChartDatasetValue(axisFormats, locale),
29054
29243
  };
29055
29244
  const colors = getChartColorsGenerator(definition, dataSetsValues.length);
29056
29245
  let maxLength = 0;
@@ -29058,14 +29247,15 @@ function createComboChartRuntime(chart, getters) {
29058
29247
  for (let [index, { label, data }] of dataSetsValues.entries()) {
29059
29248
  const design = definition.dataSets[index];
29060
29249
  const color = colors.next();
29250
+ const type = design?.type ?? "line";
29061
29251
  const dataset = {
29062
29252
  label: design?.label ?? label,
29063
29253
  data,
29064
29254
  borderColor: color,
29065
29255
  backgroundColor: color,
29066
29256
  yAxisID: design?.yAxisId ?? "y",
29067
- type: index === 0 ? "bar" : "line",
29068
- order: -index,
29257
+ type,
29258
+ order: type === "bar" ? dataSetsValues.length + index : index,
29069
29259
  };
29070
29260
  config.data.datasets.push(dataset);
29071
29261
  const trend = definition.dataSets?.[index].trend;
@@ -29092,13 +29282,10 @@ function createComboChartRuntime(chart, getters) {
29092
29282
  * distinguish the originals and trendLine datasets after
29093
29283
  */
29094
29284
  trendDatasets.forEach((x) => config.data.datasets.push(x));
29095
- const originalTooltipTitle = config.options.plugins.tooltip.callbacks.title;
29096
29285
  config.options.plugins.tooltip.callbacks.title = function (tooltipItems) {
29097
- if (tooltipItems.some((item) => item.dataset.xAxisID !== TREND_LINE_XAXIS_ID)) {
29098
- // @ts-expect-error
29099
- return originalTooltipTitle?.(tooltipItems);
29100
- }
29101
- return "";
29286
+ return tooltipItems.some((item) => item.dataset.xAxisID !== TREND_LINE_XAXIS_ID)
29287
+ ? undefined
29288
+ : "";
29102
29289
  };
29103
29290
  }
29104
29291
  return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
@@ -29678,7 +29865,7 @@ function createPieChartRuntime(chart, getters) {
29678
29865
  ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));
29679
29866
  }
29680
29867
  ({ dataSetsValues, labels } = filterNegativeValues(labels, dataSetsValues));
29681
- const dataSetFormat = getChartDatasetFormat(getters, chart.dataSets);
29868
+ const dataSetFormat = getChartDatasetFormat(getters, chart.dataSets, "left");
29682
29869
  const locale = getters.getLocale();
29683
29870
  const config = getPieConfiguration(chart, labels, { format: dataSetFormat, locale });
29684
29871
  const dataSetsLength = Math.max(0, ...dataSetsValues.map((ds) => ds?.data?.length ?? 0));
@@ -29833,7 +30020,199 @@ function createPyramidChartRuntime(chart, getters) {
29833
30020
  return tooltipLabelCallback(tooltipItem);
29834
30021
  };
29835
30022
  const callback = config.options.plugins.chartShowValuesPlugin.callback;
29836
- config.options.plugins.chartShowValuesPlugin.callback = (x) => callback(Math.abs(x));
30023
+ config.options.plugins.chartShowValuesPlugin.callback = (x, axisId) => callback(Math.abs(x), axisId);
30024
+ return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
30025
+ }
30026
+
30027
+ class RadarChart extends AbstractChart {
30028
+ dataSets;
30029
+ labelRange;
30030
+ background;
30031
+ legendPosition;
30032
+ stacked;
30033
+ aggregated;
30034
+ type = "radar";
30035
+ dataSetsHaveTitle;
30036
+ dataSetDesign;
30037
+ fillArea;
30038
+ constructor(definition, sheetId, getters) {
30039
+ super(definition, sheetId, getters);
30040
+ this.dataSets = createDataSets(getters, definition.dataSets, sheetId, definition.dataSetsHaveTitle);
30041
+ this.labelRange = createValidRange(getters, sheetId, definition.labelRange);
30042
+ this.background = definition.background;
30043
+ this.legendPosition = definition.legendPosition;
30044
+ this.stacked = definition.stacked;
30045
+ this.aggregated = definition.aggregated;
30046
+ this.dataSetsHaveTitle = definition.dataSetsHaveTitle;
30047
+ this.dataSetDesign = definition.dataSets;
30048
+ this.fillArea = definition.fillArea;
30049
+ }
30050
+ static transformDefinition(definition, executed) {
30051
+ return transformChartDefinitionWithDataSetsWithZone(definition, executed);
30052
+ }
30053
+ static validateChartDefinition(validator, definition) {
30054
+ return validator.checkValidations(definition, checkDataset, checkLabelRange);
30055
+ }
30056
+ static getDefinitionFromContextCreation(context) {
30057
+ return {
30058
+ background: context.background,
30059
+ dataSets: context.range ?? [],
30060
+ dataSetsHaveTitle: context.dataSetsHaveTitle ?? false,
30061
+ stacked: context.stacked ?? false,
30062
+ aggregated: context.aggregated ?? false,
30063
+ legendPosition: context.legendPosition ?? "top",
30064
+ title: context.title || { text: "" },
30065
+ type: "radar",
30066
+ labelRange: context.auxiliaryRange || undefined,
30067
+ fillArea: context.fillArea ?? false,
30068
+ };
30069
+ }
30070
+ getContextCreation() {
30071
+ const range = [];
30072
+ for (const [i, dataSet] of this.dataSets.entries()) {
30073
+ range.push({
30074
+ ...this.dataSetDesign?.[i],
30075
+ dataRange: this.getters.getRangeString(dataSet.dataRange, this.sheetId),
30076
+ });
30077
+ }
30078
+ return {
30079
+ ...this,
30080
+ range,
30081
+ auxiliaryRange: this.labelRange
30082
+ ? this.getters.getRangeString(this.labelRange, this.sheetId)
30083
+ : undefined,
30084
+ };
30085
+ }
30086
+ copyForSheetId(sheetId) {
30087
+ const dataSets = copyDataSetsWithNewSheetId(this.sheetId, sheetId, this.dataSets);
30088
+ const labelRange = copyLabelRangeWithNewSheetId(this.sheetId, sheetId, this.labelRange);
30089
+ const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange, sheetId);
30090
+ return new RadarChart(definition, sheetId, this.getters);
30091
+ }
30092
+ copyInSheetId(sheetId) {
30093
+ const definition = this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange, sheetId);
30094
+ return new RadarChart(definition, sheetId, this.getters);
30095
+ }
30096
+ getDefinition() {
30097
+ return this.getDefinitionWithSpecificDataSets(this.dataSets, this.labelRange);
30098
+ }
30099
+ getDefinitionWithSpecificDataSets(dataSets, labelRange, targetSheetId) {
30100
+ const ranges = [];
30101
+ for (const [i, dataSet] of dataSets.entries()) {
30102
+ ranges.push({
30103
+ ...this.dataSetDesign?.[i],
30104
+ dataRange: this.getters.getRangeString(dataSet.dataRange, targetSheetId || this.sheetId),
30105
+ });
30106
+ }
30107
+ return {
30108
+ type: "radar",
30109
+ dataSetsHaveTitle: dataSets.length ? Boolean(dataSets[0].labelCell) : false,
30110
+ background: this.background,
30111
+ dataSets: ranges,
30112
+ legendPosition: this.legendPosition,
30113
+ labelRange: labelRange
30114
+ ? this.getters.getRangeString(labelRange, targetSheetId || this.sheetId)
30115
+ : undefined,
30116
+ title: this.title,
30117
+ stacked: this.stacked,
30118
+ aggregated: this.aggregated,
30119
+ fillArea: this.fillArea,
30120
+ };
30121
+ }
30122
+ getDefinitionForExcel() {
30123
+ if (this.aggregated) {
30124
+ return undefined;
30125
+ }
30126
+ const dataSets = this.dataSets
30127
+ .map((ds) => toExcelDataset(this.getters, ds))
30128
+ .filter((ds) => ds.range !== "" && ds.range !== CellErrorType.InvalidReference);
30129
+ const labelRange = toExcelLabelRange(this.getters, this.labelRange, shouldRemoveFirstLabel(this.labelRange, this.dataSets[0], this.dataSetsHaveTitle));
30130
+ const definition = this.getDefinition();
30131
+ return {
30132
+ ...definition,
30133
+ backgroundColor: toXlsxHexColor(this.background || BACKGROUND_CHART_COLOR),
30134
+ fontColor: toXlsxHexColor(chartFontColor(this.background)),
30135
+ dataSets,
30136
+ labelRange,
30137
+ };
30138
+ }
30139
+ updateRanges(applyChange) {
30140
+ const { dataSets, labelRange, isStale } = updateChartRangesWithDataSets(this.getters, applyChange, this.dataSets, this.labelRange);
30141
+ if (!isStale) {
30142
+ return this;
30143
+ }
30144
+ const definition = this.getDefinitionWithSpecificDataSets(dataSets, labelRange);
30145
+ return new RadarChart(definition, this.sheetId, this.getters);
30146
+ }
30147
+ }
30148
+ function createRadarChartRuntime(chart, getters) {
30149
+ const definition = chart.getDefinition();
30150
+ const labelValues = getChartLabelValues(getters, chart.dataSets, chart.labelRange);
30151
+ let labels = labelValues.formattedValues;
30152
+ let dataSetsValues = getChartDatasetValues(getters, chart.dataSets);
30153
+ if (chart.dataSetsHaveTitle &&
30154
+ dataSetsValues[0] &&
30155
+ labels.length > dataSetsValues[0].data.length) {
30156
+ labels.shift();
30157
+ }
30158
+ ({ labels, dataSetsValues } = filterEmptyDataPoints(labels, dataSetsValues));
30159
+ if (chart.aggregated) {
30160
+ ({ labels, dataSetsValues } = aggregateDataForLabels(labels, dataSetsValues));
30161
+ }
30162
+ const leftAxisFormat = getChartDatasetFormat(getters, chart.dataSets, "left");
30163
+ const rightAxisFormat = getChartDatasetFormat(getters, chart.dataSets, "right");
30164
+ const axisFormats = { y: leftAxisFormat, y1: rightAxisFormat };
30165
+ const locale = getters.getLocale();
30166
+ const fontColor = chartFontColor(chart.background);
30167
+ const config = getDefaultChartJsRuntime(chart, labels, fontColor, {
30168
+ axisFormats,
30169
+ locale,
30170
+ });
30171
+ const legend = {
30172
+ labels: { color: fontColor },
30173
+ };
30174
+ if ((!chart.labelRange && chart.dataSets.length === 1) || chart.legendPosition === "none") {
30175
+ legend.display = false;
30176
+ }
30177
+ else {
30178
+ legend.position = chart.legendPosition;
30179
+ }
30180
+ const fill = definition.fillArea ?? false;
30181
+ if (!fill) {
30182
+ legend.labels["boxHeight"] = 0;
30183
+ }
30184
+ config.options.plugins.legend = { ...config.options.plugins?.legend, ...legend };
30185
+ config.options.plugins.tooltip = {
30186
+ ...config.options.plugins?.tooltip,
30187
+ callbacks: {
30188
+ label: function (tooltipItem) {
30189
+ const xLabel = tooltipItem.dataset?.label || tooltipItem.label;
30190
+ const yLabel = tooltipItem.parsed.r;
30191
+ return xLabel ? `${xLabel}: ${yLabel}` : yLabel.toString();
30192
+ },
30193
+ },
30194
+ };
30195
+ config.options.layout = {
30196
+ padding: { left: 20, right: 20, top: chart.title ? 10 : 25, bottom: 10 },
30197
+ };
30198
+ const colorGenerator = getChartColorsGenerator(definition, dataSetsValues.length);
30199
+ for (let i = 0; i < dataSetsValues.length; i++) {
30200
+ let { label, data } = dataSetsValues[i];
30201
+ if (definition.dataSets?.[i]?.label) {
30202
+ label = definition.dataSets[i].label;
30203
+ }
30204
+ const borderColor = colorGenerator.next();
30205
+ const dataset = {
30206
+ label,
30207
+ data,
30208
+ borderColor,
30209
+ };
30210
+ if (fill) {
30211
+ dataset.backgroundColor = setColorAlpha(borderColor, 0.3);
30212
+ dataset["fill"] = true;
30213
+ }
30214
+ config.data.datasets.push(dataset);
30215
+ }
29837
30216
  return { chartJsConfig: config, background: chart.background || BACKGROUND_CHART_COLOR };
29838
30217
  }
29839
30218
 
@@ -30224,7 +30603,8 @@ function createWaterfallChartRuntime(chart, getters) {
30224
30603
  if (chart.showSubTotals) {
30225
30604
  labels.push(_t("Subtotal"));
30226
30605
  }
30227
- const dataSetFormat = getChartDatasetFormat(getters, chart.dataSets);
30606
+ const dataSetFormat = getChartDatasetFormat(getters, chart.dataSets, "left") ||
30607
+ getChartDatasetFormat(getters, chart.dataSets, "right");
30228
30608
  const locale = getters.getLocale();
30229
30609
  const dataSeriesLabels = dataSetsValues.map((dataSet) => dataSet.label);
30230
30610
  const config = getWaterfallConfiguration(chart, labels, dataSeriesLabels, {
@@ -30358,6 +30738,15 @@ chartRegistry.add("pyramid", {
30358
30738
  getChartDefinitionFromContextCreation: PyramidChart.getDefinitionFromContextCreation,
30359
30739
  sequence: 80,
30360
30740
  });
30741
+ chartRegistry.add("radar", {
30742
+ match: (type) => type === "radar",
30743
+ createChart: (definition, sheetId, getters) => new RadarChart(definition, sheetId, getters),
30744
+ getChartRuntime: createRadarChartRuntime,
30745
+ validateChartDefinition: RadarChart.validateChartDefinition,
30746
+ transformDefinition: RadarChart.transformDefinition,
30747
+ getChartDefinitionFromContextCreation: RadarChart.getDefinitionFromContextCreation,
30748
+ sequence: 80,
30749
+ });
30361
30750
  const chartComponentRegistry = new Registry();
30362
30751
  chartComponentRegistry.add("line", ChartJsComponent);
30363
30752
  chartComponentRegistry.add("bar", ChartJsComponent);
@@ -30368,6 +30757,7 @@ chartComponentRegistry.add("scatter", ChartJsComponent);
30368
30757
  chartComponentRegistry.add("scorecard", ScorecardChart);
30369
30758
  chartComponentRegistry.add("waterfall", ChartJsComponent);
30370
30759
  chartComponentRegistry.add("pyramid", ChartJsComponent);
30760
+ chartComponentRegistry.add("radar", ChartJsComponent);
30371
30761
  const chartCategories = {
30372
30762
  line: _t("Line"),
30373
30763
  column: _t("Column"),
@@ -30509,6 +30899,24 @@ chartSubtypeRegistry
30509
30899
  chartType: "pyramid",
30510
30900
  category: "misc",
30511
30901
  preview: "o-spreadsheet-ChartPreview.POPULATION_PYRAMID_CHART",
30902
+ })
30903
+ .add("radar", {
30904
+ matcher: (definition) => definition.type === "radar" && !definition.fillArea,
30905
+ displayName: _t("Radar"),
30906
+ chartSubtype: "radar",
30907
+ chartType: "radar",
30908
+ subtypeDefinition: { fillArea: false },
30909
+ category: "misc",
30910
+ preview: "o-spreadsheet-ChartPreview.RADAR_CHART",
30911
+ })
30912
+ .add("filled_radar", {
30913
+ matcher: (definition) => definition.type === "radar" && !!definition.fillArea,
30914
+ displayName: _t("Filled Radar"),
30915
+ chartType: "radar",
30916
+ chartSubtype: "filled_radar",
30917
+ subtypeDefinition: { fillArea: true },
30918
+ category: "misc",
30919
+ preview: "o-spreadsheet-ChartPreview.FILLED_RADAR_CHART",
30512
30920
  });
30513
30921
 
30514
30922
  /**
@@ -30576,11 +30984,10 @@ function centerFigurePosition(getters, size) {
30576
30984
  const rect = getters.getVisibleRect(getters.getActiveMainViewport());
30577
30985
  const scrollableViewportWidth = Math.min(rect.width, dim.width - offsetCorrectionX);
30578
30986
  const scrollableViewportHeight = Math.min(rect.height, dim.height - offsetCorrectionY);
30579
- const position = {
30987
+ return {
30580
30988
  x: offsetCorrectionX + scrollX + Math.max(0, (scrollableViewportWidth - size.width) / 2),
30581
30989
  y: offsetCorrectionY + scrollY + Math.max(0, (scrollableViewportHeight - size.height) / 2),
30582
30990
  }; // Position at the center of the scrollable viewport
30583
- return position;
30584
30991
  }
30585
30992
  function getMaxFigureSize(getters, figureSize) {
30586
30993
  const size = deepCopy(figureSize);
@@ -31153,14 +31560,13 @@ class PopoverPositionContext {
31153
31560
  const shouldRenderAtBottom = this.shouldRenderAtBottom(elDims.height);
31154
31561
  const shouldRenderAtRight = this.shouldRenderAtRight(elDims.width);
31155
31562
  verticalOffset = shouldRenderAtBottom ? verticalOffset : -verticalOffset;
31156
- const cssProperties = {
31563
+ return {
31157
31564
  top: this.getTopCoordinate(actualHeight, shouldRenderAtBottom) -
31158
31565
  this.spreadsheetOffset.y -
31159
31566
  verticalOffset +
31160
31567
  "px",
31161
31568
  left: this.getLeftCoordinate(actualWidth, shouldRenderAtRight) - this.spreadsheetOffset.x + "px",
31162
31569
  };
31163
- return cssProperties;
31164
31570
  }
31165
31571
  getCurrentPosition(elDims) {
31166
31572
  const shouldRenderAtBottom = this.shouldRenderAtBottom(elDims.height);
@@ -32388,7 +32794,7 @@ function getSmartChartDefinition(zone, getters) {
32388
32794
  * Create a table on the selected zone, with UI warnings to the user if the creation fails.
32389
32795
  * If a single cell is selected, expand the selection to non-empty adjacent cells to create a table.
32390
32796
  */
32391
- function interactiveCreateTable(env, sheetId, tableConfig) {
32797
+ function interactiveCreateTable(env, sheetId, tableConfig = DEFAULT_TABLE_CONFIG) {
32392
32798
  let target = env.model.getters.getSelectedZones();
32393
32799
  let isDynamic = env.model.getters.canCreateDynamicTableOnZones(sheetId, target);
32394
32800
  if (target.length === 1 && !isDynamic && getZoneArea(target[0]) === 1) {
@@ -33447,7 +33853,7 @@ function getColumnsNumber(env) {
33447
33853
  }
33448
33854
 
33449
33855
  const pivotProperties = {
33450
- name: _t("Edit Pivot"),
33856
+ name: _t("See pivot properties"),
33451
33857
  execute(env) {
33452
33858
  const position = env.model.getters.getActivePosition();
33453
33859
  const pivotId = env.model.getters.getPivotIdFromPosition(position);
@@ -33601,8 +34007,7 @@ cellMenuRegistry
33601
34007
  })
33602
34008
  .add("pivot_properties", {
33603
34009
  ...pivotProperties,
33604
- sequence: 160,
33605
- separator: true,
34010
+ sequence: 170,
33606
34011
  });
33607
34012
 
33608
34013
  const sortRange = {
@@ -35294,6 +35699,7 @@ const CHECK_SVG = /*xml*/ `
35294
35699
  <path fill='none' stroke='#FFF' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/>
35295
35700
  </svg>
35296
35701
  `;
35702
+ const CHECKBOX_WIDTH = 14;
35297
35703
  css /* scss */ `
35298
35704
  label.o-checkbox {
35299
35705
  input {
@@ -35301,8 +35707,8 @@ css /* scss */ `
35301
35707
  -webkit-appearance: none;
35302
35708
  -moz-appearance: none;
35303
35709
  border-radius: 0;
35304
- width: 14px;
35305
- height: 14px;
35710
+ width: ${CHECKBOX_WIDTH}px;
35711
+ height: ${CHECKBOX_WIDTH}px;
35306
35712
  vertical-align: top;
35307
35713
  box-sizing: border-box;
35308
35714
  outline: none;
@@ -35348,6 +35754,7 @@ class Section extends Component {
35348
35754
  static template = "o_spreadsheet.Section";
35349
35755
  static props = {
35350
35756
  class: { type: String, optional: true },
35757
+ title: { type: String, optional: true },
35351
35758
  slots: Object,
35352
35759
  };
35353
35760
  }
@@ -36077,17 +36484,11 @@ class BarConfigPanel extends GenericChartConfigPanel {
36077
36484
  stacked,
36078
36485
  });
36079
36486
  }
36080
- onUpdateAggregated(aggregated) {
36081
- this.props.updateChart(this.props.figureId, {
36082
- aggregated,
36083
- });
36084
- }
36085
36487
  }
36086
36488
 
36087
36489
  css /* scss */ `
36088
36490
  .o_side_panel_collapsible_title {
36089
36491
  font-size: 16px;
36090
- font-weight: bold;
36091
36492
  cursor: pointer;
36092
36493
  padding: 6px 0px 6px 6px !important;
36093
36494
 
@@ -36124,49 +36525,40 @@ class SidePanelCollapsible extends Component {
36124
36525
  static template = "o-spreadsheet-SidePanelCollapsible";
36125
36526
  static props = {
36126
36527
  slots: Object,
36528
+ title: { type: String, optional: true },
36127
36529
  collapsedAtInit: { type: Boolean, optional: true },
36128
36530
  class: { type: String, optional: true },
36129
36531
  };
36130
36532
  currentId = (CURRENT_COLLAPSIBLE_ID++).toString();
36131
36533
  }
36132
36534
 
36133
- const CIRCLE_SVG = /*xml*/ `
36134
- <svg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'>
36135
- <circle r="2" fill="#FFF"/>
36136
- </svg>
36137
- `;
36138
36535
  css /* scss */ `
36139
- .o-radio {
36140
- input {
36141
- appearance: none;
36142
- -webkit-appearance: none;
36143
- -moz-appearance: none;
36144
- width: 14px;
36145
- height: 14px;
36146
- border: 1px solid ${GRAY_300};
36147
- box-sizing: border-box;
36148
- outline: none;
36149
- border-radius: 8px;
36150
-
36151
- &:checked {
36152
- background: url("data:image/svg+xml,${encodeURIComponent(CIRCLE_SVG)}");
36153
- background-color: ${ACTION_COLOR};
36536
+ .o-badge-selection {
36537
+ gap: 1px;
36538
+ button.o-button {
36539
+ border-radius: 0;
36540
+ &.selected {
36541
+ color: ${GRAY_900};
36154
36542
  border-color: ${ACTION_COLOR};
36543
+ background: ${BADGE_SELECTED_COLOR};
36544
+ font-weight: 600;
36545
+ }
36546
+
36547
+ &:first-child {
36548
+ border-radius: 4px 0 0 4px;
36549
+ }
36550
+ &:last-child {
36551
+ border-radius: 0 4px 4px 0;
36155
36552
  }
36156
36553
  }
36157
36554
  }
36158
36555
  `;
36159
- class RadioSelection extends Component {
36160
- static template = "o-spreadsheet.RadioSelection";
36556
+ class BadgeSelection extends Component {
36557
+ static template = "o-spreadsheet.BadgeSelection";
36161
36558
  static props = {
36162
36559
  choices: Array,
36163
36560
  onChange: Function,
36164
- selectedValue: { optional: false },
36165
- name: String,
36166
- direction: { type: String, optional: true },
36167
- };
36168
- static defaultProps = {
36169
- direction: "horizontal",
36561
+ selectedValue: String,
36170
36562
  };
36171
36563
  }
36172
36564
 
@@ -36711,86 +37103,6 @@ class ColorPickerWidget extends Component {
36711
37103
  }
36712
37104
  }
36713
37105
 
36714
- const TRANSPARENT_BACKGROUND_SVG = /*xml*/ `
36715
- <svg xmlns="http://www.w3.org/2000/svg" width="10" height="10">
36716
- <path fill="#d9d9d9" d="M5 5h5v5H5zH0V0h5"/>
36717
- </svg>
36718
- `;
36719
- css /* scss */ `
36720
- .o-round-color-picker-button {
36721
- width: 18px;
36722
- height: 18px;
36723
- cursor: pointer;
36724
- border: 1px solid ${GRAY_300};
36725
- background-position: 1px 1px;
36726
- background-image: url("data:image/svg+xml,${encodeURIComponent(TRANSPARENT_BACKGROUND_SVG)}");
36727
- }
36728
- `;
36729
- class RoundColorPicker extends Component {
36730
- static template = "o-spreadsheet.RoundColorPicker";
36731
- static components = { ColorPickerWidget, Section, ColorPicker };
36732
- static props = {
36733
- currentColor: { type: String, optional: true },
36734
- title: { type: String, optional: true },
36735
- onColorPicked: Function,
36736
- };
36737
- colorPickerButtonRef = useRef("colorPickerButton");
36738
- state;
36739
- setup() {
36740
- this.state = useState({ pickerOpened: false });
36741
- useExternalListener(window, "click", this.closePicker);
36742
- }
36743
- closePicker() {
36744
- this.state.pickerOpened = false;
36745
- }
36746
- togglePicker() {
36747
- this.state.pickerOpened = !this.state.pickerOpened;
36748
- }
36749
- onColorPicked(color) {
36750
- this.props.onColorPicked(color);
36751
- this.state.pickerOpened = false;
36752
- }
36753
- get colorPickerAnchorRect() {
36754
- const button = this.colorPickerButtonRef.el;
36755
- return getBoundingRectAsPOJO(button);
36756
- }
36757
- get buttonStyle() {
36758
- return cssPropertiesToCss({
36759
- background: this.props.currentColor,
36760
- });
36761
- }
36762
- }
36763
-
36764
- css /* scss */ `
36765
- .o-badge-selection {
36766
- gap: 1px;
36767
- button.o-button {
36768
- border-radius: 0;
36769
- &.selected {
36770
- color: ${GRAY_900};
36771
- border-color: ${ACTION_COLOR};
36772
- background: ${BADGE_SELECTED_COLOR};
36773
- font-weight: 600;
36774
- }
36775
-
36776
- &:first-child {
36777
- border-radius: 4px 0 0 4px;
36778
- }
36779
- &:last-child {
36780
- border-radius: 0 4px 4px 0;
36781
- }
36782
- }
36783
- }
36784
- `;
36785
- class BadgeSelection extends Component {
36786
- static template = "o-spreadsheet.BadgeSelection";
36787
- static props = {
36788
- choices: Array,
36789
- onChange: Function,
36790
- selectedValue: String,
36791
- };
36792
- }
36793
-
36794
37106
  css /* scss */ `
36795
37107
  .o-chart-title-designer {
36796
37108
  > span {
@@ -36945,8 +37257,7 @@ class AxisDesignEditor extends Component {
36945
37257
  this.props.updateChart(this.props.figureId, { axesDesign });
36946
37258
  }
36947
37259
  updateAxisEditor(ev) {
36948
- const axis = ev.target.value;
36949
- this.state.currentAxis = axis;
37260
+ this.state.currentAxis = ev.target.value;
36950
37261
  }
36951
37262
  getAxisTitle() {
36952
37263
  const axesDesign = this.props.definition.axesDesign ?? {};
@@ -36965,6 +37276,56 @@ class AxisDesignEditor extends Component {
36965
37276
  }
36966
37277
  }
36967
37278
 
37279
+ const TRANSPARENT_BACKGROUND_SVG = /*xml*/ `
37280
+ <svg xmlns="http://www.w3.org/2000/svg" width="10" height="10">
37281
+ <path fill="#d9d9d9" d="M5 5h5v5H5zH0V0h5"/>
37282
+ </svg>
37283
+ `;
37284
+ css /* scss */ `
37285
+ .o-round-color-picker-button {
37286
+ width: 18px;
37287
+ height: 18px;
37288
+ cursor: pointer;
37289
+ border: 1px solid ${GRAY_300};
37290
+ background-position: 1px 1px;
37291
+ background-image: url("data:image/svg+xml,${encodeURIComponent(TRANSPARENT_BACKGROUND_SVG)}");
37292
+ }
37293
+ `;
37294
+ class RoundColorPicker extends Component {
37295
+ static template = "o-spreadsheet.RoundColorPicker";
37296
+ static components = { Section, ColorPicker };
37297
+ static props = {
37298
+ currentColor: { type: String, optional: true },
37299
+ title: { type: String, optional: true },
37300
+ onColorPicked: Function,
37301
+ };
37302
+ colorPickerButtonRef = useRef("colorPickerButton");
37303
+ state;
37304
+ setup() {
37305
+ this.state = useState({ pickerOpened: false });
37306
+ useExternalListener(window, "click", this.closePicker);
37307
+ }
37308
+ closePicker() {
37309
+ this.state.pickerOpened = false;
37310
+ }
37311
+ togglePicker() {
37312
+ this.state.pickerOpened = !this.state.pickerOpened;
37313
+ }
37314
+ onColorPicked(color) {
37315
+ this.props.onColorPicked(color);
37316
+ this.state.pickerOpened = false;
37317
+ }
37318
+ get colorPickerAnchorRect() {
37319
+ const button = this.colorPickerButtonRef.el;
37320
+ return getBoundingRectAsPOJO(button);
37321
+ }
37322
+ get buttonStyle() {
37323
+ return cssPropertiesToCss({
37324
+ background: this.props.currentColor,
37325
+ });
37326
+ }
37327
+ }
37328
+
36968
37329
  class GeneralDesignEditor extends Component {
36969
37330
  static template = "o-spreadsheet-GeneralDesignEditor";
36970
37331
  static components = {
@@ -37029,58 +37390,75 @@ class GeneralDesignEditor extends Component {
37029
37390
  }
37030
37391
  }
37031
37392
 
37032
- class ChartWithAxisDesignPanel extends Component {
37033
- static template = "o-spreadsheet-ChartWithAxisDesignPanel";
37393
+ const CIRCLE_SVG = /*xml*/ `
37394
+ <svg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'>
37395
+ <circle r="2" fill="#FFF"/>
37396
+ </svg>
37397
+ `;
37398
+ css /* scss */ `
37399
+ .o-radio {
37400
+ input {
37401
+ appearance: none;
37402
+ -webkit-appearance: none;
37403
+ -moz-appearance: none;
37404
+ width: 14px;
37405
+ height: 14px;
37406
+ border: 1px solid ${GRAY_300};
37407
+ box-sizing: border-box;
37408
+ outline: none;
37409
+ border-radius: 8px;
37410
+
37411
+ &:checked {
37412
+ background: url("data:image/svg+xml,${encodeURIComponent(CIRCLE_SVG)}");
37413
+ background-color: ${ACTION_COLOR};
37414
+ border-color: ${ACTION_COLOR};
37415
+ }
37416
+ }
37417
+ }
37418
+ `;
37419
+ class RadioSelection extends Component {
37420
+ static template = "o-spreadsheet.RadioSelection";
37421
+ static props = {
37422
+ choices: Array,
37423
+ onChange: Function,
37424
+ selectedValue: { optional: false },
37425
+ name: String,
37426
+ direction: { type: String, optional: true },
37427
+ };
37428
+ static defaultProps = {
37429
+ direction: "horizontal",
37430
+ };
37431
+ }
37432
+
37433
+ class SeriesDesignEditor extends Component {
37434
+ static template = "o-spreadsheet-SeriesDesignEditor";
37034
37435
  static components = {
37035
- GeneralDesignEditor,
37036
37436
  SidePanelCollapsible,
37037
37437
  Section,
37038
- AxisDesignEditor,
37039
37438
  RoundColorPicker,
37040
- Checkbox,
37041
- RadioSelection,
37042
37439
  };
37043
37440
  static props = {
37044
37441
  figureId: String,
37045
37442
  definition: Object,
37046
- canUpdateChart: Function,
37047
37443
  updateChart: Function,
37444
+ canUpdateChart: Function,
37445
+ slots: { type: Object, optional: true },
37048
37446
  };
37049
- axisChoices = CHART_AXIS_CHOICES;
37050
37447
  state = useState({ index: 0 });
37051
- get axesList() {
37052
- const { useLeftAxis, useRightAxis } = getDefinedAxis(this.props.definition);
37053
- let axes = [{ id: "x", name: _t("Horizontal axis") }];
37054
- if (useLeftAxis) {
37055
- axes.push({ id: "y", name: useRightAxis ? _t("Left axis") : _t("Vertical axis") });
37056
- }
37057
- if (useRightAxis) {
37058
- axes.push({ id: "y1", name: useLeftAxis ? _t("Right axis") : _t("Vertical axis") });
37059
- }
37060
- return axes;
37061
- }
37062
- updateLegendPosition(ev) {
37063
- this.props.updateChart(this.props.figureId, {
37064
- legendPosition: ev.target.value,
37065
- });
37066
- }
37067
37448
  getDataSeries() {
37068
- return this.props.definition.dataSets.map((d, i) => d.label ?? `${ChartTerms.Series} ${i + 1}`);
37449
+ const runtime = this.env.model.getters.getChartRuntime(this.props.figureId);
37450
+ if (!runtime || !("chartJsConfig" in runtime)) {
37451
+ return [];
37452
+ }
37453
+ return runtime.chartJsConfig.data.datasets.map((d) => d.label);
37069
37454
  }
37070
37455
  updateSerieEditor(ev) {
37071
- const chartId = this.props.figureId;
37072
- const selectedIndex = ev.target.selectedIndex;
37073
- const runtime = this.env.model.getters.getChartRuntime(chartId);
37074
- if (!runtime) {
37075
- return;
37076
- }
37077
- this.state.index = selectedIndex;
37456
+ this.state.index = ev.target.selectedIndex;
37078
37457
  }
37079
37458
  updateDataSeriesColor(color) {
37080
- const dataSets = [...this.props.definition.dataSets];
37081
- if (!dataSets?.[this.state.index]) {
37459
+ const dataSets = this.props.definition.dataSets;
37460
+ if (!dataSets?.[this.state.index])
37082
37461
  return;
37083
- }
37084
37462
  dataSets[this.state.index] = {
37085
37463
  ...dataSets[this.state.index],
37086
37464
  backgroundColor: color,
@@ -37089,71 +37467,87 @@ class ChartWithAxisDesignPanel extends Component {
37089
37467
  }
37090
37468
  getDataSerieColor() {
37091
37469
  const dataSets = this.props.definition.dataSets;
37092
- if (!dataSets?.[this.state.index]) {
37470
+ if (!dataSets?.[this.state.index])
37093
37471
  return "";
37094
- }
37095
37472
  const color = dataSets[this.state.index].backgroundColor;
37096
- return color ? toHex(color) : getNthColor(this.state.index, getColorsPalette(dataSets.length));
37473
+ return color
37474
+ ? toHex(color)
37475
+ : getNthColor(this.state.index, getColorsPalette(this.props.definition.dataSets.length));
37097
37476
  }
37098
- updateDataSeriesAxis(axis) {
37099
- const dataSets = [...this.props.definition.dataSets];
37100
- if (!dataSets?.[this.state.index]) {
37477
+ updateDataSeriesLabel(ev) {
37478
+ const label = ev.target.value;
37479
+ const dataSets = this.props.definition.dataSets;
37480
+ if (!dataSets?.[this.state.index])
37101
37481
  return;
37102
- }
37103
37482
  dataSets[this.state.index] = {
37104
37483
  ...dataSets[this.state.index],
37105
- yAxisId: axis === "left" ? "y" : "y1",
37484
+ label,
37106
37485
  };
37107
37486
  this.props.updateChart(this.props.figureId, { dataSets });
37108
37487
  }
37109
- getDataSerieAxis() {
37488
+ getDataSerieLabel() {
37110
37489
  const dataSets = this.props.definition.dataSets;
37111
- if (!dataSets?.[this.state.index]) {
37112
- return "left";
37113
- }
37114
- return dataSets[this.state.index].yAxisId === "y1" ? "right" : "left";
37115
- }
37116
- get canHaveTwoVerticalAxis() {
37117
- return "horizontal" in this.props.definition ? !this.props.definition.horizontal : true;
37490
+ return dataSets[this.state.index]?.label || this.getDataSeries()[this.state.index];
37118
37491
  }
37119
- updateDataSeriesLabel(ev) {
37120
- const label = ev.target.value;
37492
+ }
37493
+
37494
+ class SeriesWithAxisDesignEditor extends Component {
37495
+ static template = "o-spreadsheet-SeriesWithAxisDesignEditor";
37496
+ static components = {
37497
+ SeriesDesignEditor,
37498
+ Checkbox,
37499
+ RadioSelection,
37500
+ Section,
37501
+ RoundColorPicker,
37502
+ };
37503
+ static props = {
37504
+ figureId: String,
37505
+ definition: Object,
37506
+ canUpdateChart: Function,
37507
+ updateChart: Function,
37508
+ slots: { type: Object, optional: true },
37509
+ };
37510
+ axisChoices = CHART_AXIS_CHOICES;
37511
+ updateDataSeriesAxis(index, axis) {
37121
37512
  const dataSets = [...this.props.definition.dataSets];
37122
- if (!dataSets?.[this.state.index]) {
37513
+ if (!dataSets?.[index]) {
37123
37514
  return;
37124
37515
  }
37125
- dataSets[this.state.index] = {
37126
- ...dataSets[this.state.index],
37127
- label,
37516
+ dataSets[index] = {
37517
+ ...dataSets[index],
37518
+ yAxisId: axis === "left" ? "y" : "y1",
37128
37519
  };
37129
37520
  this.props.updateChart(this.props.figureId, { dataSets });
37130
37521
  }
37131
- getDataSerieLabel() {
37522
+ getDataSerieAxis(index) {
37132
37523
  const dataSets = this.props.definition.dataSets;
37133
- return dataSets[this.state.index]?.label || this.getDataSeries()[this.state.index];
37524
+ if (!dataSets?.[index]) {
37525
+ return "left";
37526
+ }
37527
+ return dataSets[index].yAxisId === "y1" ? "right" : "left";
37134
37528
  }
37135
- updateShowValues(showValues) {
37136
- this.props.updateChart(this.props.figureId, { showValues });
37529
+ get canHaveTwoVerticalAxis() {
37530
+ return !("horizontal" in this.props.definition && this.props.definition.horizontal);
37137
37531
  }
37138
- toggleDataTrend(display) {
37532
+ toggleDataTrend(index, display) {
37139
37533
  const dataSets = [...this.props.definition.dataSets];
37140
- if (!dataSets?.[this.state.index]) {
37534
+ if (!dataSets?.[index]) {
37141
37535
  return;
37142
37536
  }
37143
- dataSets[this.state.index] = {
37144
- ...dataSets[this.state.index],
37537
+ dataSets[index] = {
37538
+ ...dataSets[index],
37145
37539
  trend: {
37146
37540
  type: "polynomial",
37147
37541
  order: 1,
37148
- ...dataSets[this.state.index].trend,
37542
+ ...dataSets[index].trend,
37149
37543
  display,
37150
37544
  },
37151
37545
  };
37152
37546
  this.props.updateChart(this.props.figureId, { dataSets });
37153
37547
  }
37154
- getTrendLineConfiguration() {
37548
+ getTrendLineConfiguration(index) {
37155
37549
  const dataSets = this.props.definition.dataSets;
37156
- return dataSets?.[this.state.index]?.trend;
37550
+ return dataSets?.[index]?.trend;
37157
37551
  }
37158
37552
  getTrendType(config) {
37159
37553
  if (!config) {
@@ -37161,7 +37555,7 @@ class ChartWithAxisDesignPanel extends Component {
37161
37555
  }
37162
37556
  return config.type === "polynomial" && config.order === 1 ? "linear" : config.type;
37163
37557
  }
37164
- onChangeTrendType(ev) {
37558
+ onChangeTrendType(index, ev) {
37165
37559
  const type = ev.target.value;
37166
37560
  let config;
37167
37561
  switch (type) {
@@ -37174,37 +37568,59 @@ class ChartWithAxisDesignPanel extends Component {
37174
37568
  break;
37175
37569
  case "exponential":
37176
37570
  case "logarithmic":
37571
+ case "trailingMovingAverage":
37177
37572
  config = { type };
37178
37573
  break;
37179
37574
  default:
37180
37575
  return;
37181
37576
  }
37182
- this.updateTrendLineValue(config);
37577
+ this.updateTrendLineValue(index, config);
37183
37578
  }
37184
- onChangePolynomialDegree(ev) {
37579
+ onChangePolynomialDegree(index, ev) {
37185
37580
  const element = ev.target;
37186
37581
  const order = parseInt(element.value || "1");
37187
37582
  if (order < 2) {
37188
- element.value = `${this.getTrendLineConfiguration()?.order ?? 2}`;
37583
+ element.value = `${this.getTrendLineConfiguration(index)?.order ?? 2}`;
37189
37584
  return;
37190
37585
  }
37191
- this.updateTrendLineValue({ order });
37586
+ this.updateTrendLineValue(index, { order });
37587
+ }
37588
+ get defaultWindowSize() {
37589
+ return DEFAULT_WINDOW_SIZE;
37590
+ }
37591
+ onChangeMovingAverageWindow(index, ev) {
37592
+ const element = ev.target;
37593
+ let window = parseInt(element.value) || DEFAULT_WINDOW_SIZE;
37594
+ if (window <= 1) {
37595
+ window = DEFAULT_WINDOW_SIZE;
37596
+ }
37597
+ this.updateTrendLineValue(index, { window });
37192
37598
  }
37193
- getTrendLineColor() {
37194
- return this.getTrendLineConfiguration()?.color ?? setColorAlpha(this.getDataSerieColor(), 0.5);
37599
+ getDataSerieColor(index) {
37600
+ const dataSets = this.props.definition.dataSets;
37601
+ if (!dataSets?.[index])
37602
+ return "";
37603
+ const color = dataSets[index].backgroundColor;
37604
+ return color
37605
+ ? toHex(color)
37606
+ : getNthColor(index, getColorsPalette(this.props.definition.dataSets.length));
37607
+ }
37608
+ getTrendLineColor(index) {
37609
+ return (this.getTrendLineConfiguration(index)?.color ??
37610
+ setColorAlpha(this.getDataSerieColor(index), 0.5));
37195
37611
  }
37196
- updateTrendLineColor(color) {
37197
- this.updateTrendLineValue({ color });
37612
+ updateTrendLineColor(index, color) {
37613
+ this.updateTrendLineValue(index, { color });
37198
37614
  }
37199
- updateTrendLineValue(config) {
37615
+ updateTrendLineValue(index, config) {
37200
37616
  const dataSets = [...this.props.definition.dataSets];
37201
- if (!dataSets?.[this.state.index]) {
37617
+ if (!dataSets?.[index]) {
37202
37618
  return;
37203
37619
  }
37204
- dataSets[this.state.index] = {
37205
- ...dataSets[this.state.index],
37620
+ dataSets[index] = {
37621
+ ...dataSets[index],
37206
37622
  trend: {
37207
- ...dataSets[this.state.index].trend,
37623
+ ...dataSets[index].trend,
37208
37624
  ...config,
37209
37625
  },
37210
37626
  };
@@ -37212,6 +37628,70 @@ class ChartWithAxisDesignPanel extends Component {
37212
37628
  }
37213
37629
  }
37214
37630
 
37631
+ class ChartWithAxisDesignPanel extends Component {
37632
+ static template = "o-spreadsheet-ChartWithAxisDesignPanel";
37633
+ static components = {
37634
+ GeneralDesignEditor,
37635
+ SidePanelCollapsible,
37636
+ Section,
37637
+ AxisDesignEditor,
37638
+ Checkbox,
37639
+ SeriesWithAxisDesignEditor,
37640
+ };
37641
+ static props = {
37642
+ figureId: String,
37643
+ definition: Object,
37644
+ canUpdateChart: Function,
37645
+ updateChart: Function,
37646
+ };
37647
+ get axesList() {
37648
+ const { useLeftAxis, useRightAxis } = getDefinedAxis(this.props.definition);
37649
+ let axes = [{ id: "x", name: _t("Horizontal axis") }];
37650
+ if (useLeftAxis) {
37651
+ axes.push({ id: "y", name: useRightAxis ? _t("Left axis") : _t("Vertical axis") });
37652
+ }
37653
+ if (useRightAxis) {
37654
+ axes.push({ id: "y1", name: useLeftAxis ? _t("Right axis") : _t("Vertical axis") });
37655
+ }
37656
+ return axes;
37657
+ }
37658
+ updateLegendPosition(ev) {
37659
+ this.props.updateChart(this.props.figureId, {
37660
+ legendPosition: ev.target.value,
37661
+ });
37662
+ }
37663
+ }
37664
+
37665
+ class ComboChartDesignPanel extends ChartWithAxisDesignPanel {
37666
+ static template = "o-spreadsheet-ComboChartDesignPanel";
37667
+ static components = {
37668
+ ...ChartWithAxisDesignPanel.components,
37669
+ RadioSelection,
37670
+ };
37671
+ seriesTypeChoices = [
37672
+ { value: "bar", label: _t("Bar") },
37673
+ { value: "line", label: _t("Line") },
37674
+ ];
37675
+ updateDataSeriesType(index, type) {
37676
+ const dataSets = [...this.props.definition.dataSets];
37677
+ if (!dataSets?.[index]) {
37678
+ return;
37679
+ }
37680
+ dataSets[index] = {
37681
+ ...dataSets[index],
37682
+ type,
37683
+ };
37684
+ this.props.updateChart(this.props.figureId, { dataSets });
37685
+ }
37686
+ getDataSeriesType(index) {
37687
+ const dataSets = this.props.definition.dataSets;
37688
+ if (!dataSets?.[index]) {
37689
+ return "bar";
37690
+ }
37691
+ return dataSets[index].type ?? "line";
37692
+ }
37693
+ }
37694
+
37215
37695
  class GaugeChartConfigPanel extends Component {
37216
37696
  static template = "o-spreadsheet-GaugeChartConfigPanel";
37217
37697
  static components = { ChartErrorSection, ChartDataSeries };
@@ -37391,11 +37871,6 @@ class LineConfigPanel extends GenericChartConfigPanel {
37391
37871
  stacked,
37392
37872
  });
37393
37873
  }
37394
- onUpdateAggregated(aggregated) {
37395
- this.props.updateChart(this.props.figureId, {
37396
- aggregated,
37397
- });
37398
- }
37399
37874
  onUpdateCumulative(cumulative) {
37400
37875
  this.props.updateChart(this.props.figureId, {
37401
37876
  cumulative,
@@ -37423,6 +37898,27 @@ class PieChartDesignPanel extends Component {
37423
37898
  }
37424
37899
  }
37425
37900
 
37901
+ class RadarChartDesignPanel extends Component {
37902
+ static template = "o-spreadsheet-RadarChartDesignPanel";
37903
+ static components = {
37904
+ GeneralDesignEditor,
37905
+ SeriesDesignEditor,
37906
+ Section,
37907
+ Checkbox,
37908
+ };
37909
+ static props = {
37910
+ figureId: String,
37911
+ definition: Object,
37912
+ canUpdateChart: Function,
37913
+ updateChart: Function,
37914
+ };
37915
+ updateLegendPosition(ev) {
37916
+ this.props.updateChart(this.props.figureId, {
37917
+ legendPosition: ev.target.value,
37918
+ });
37919
+ }
37920
+ }
37921
+
37426
37922
  class ScatterConfigPanel extends GenericChartConfigPanel {
37427
37923
  static template = "o-spreadsheet-ScatterConfigPanel";
37428
37924
  get canTreatLabelsAsText() {
@@ -37617,9 +38113,6 @@ class WaterfallChartDesignPanel extends Component {
37617
38113
  verticalAxisPosition: value,
37618
38114
  });
37619
38115
  }
37620
- updateShowValues(showValues) {
37621
- this.props.updateChart(this.props.figureId, { showValues });
37622
- }
37623
38116
  }
37624
38117
 
37625
38118
  const chartSidePanelComponentRegistry = new Registry();
@@ -37638,7 +38131,7 @@ chartSidePanelComponentRegistry
37638
38131
  })
37639
38132
  .add("combo", {
37640
38133
  configuration: GenericChartConfigPanel,
37641
- design: ChartWithAxisDesignPanel,
38134
+ design: ComboChartDesignPanel,
37642
38135
  })
37643
38136
  .add("pie", {
37644
38137
  configuration: GenericChartConfigPanel,
@@ -37659,6 +38152,10 @@ chartSidePanelComponentRegistry
37659
38152
  .add("pyramid", {
37660
38153
  configuration: GenericChartConfigPanel,
37661
38154
  design: ChartWithAxisDesignPanel,
38155
+ })
38156
+ .add("radar", {
38157
+ configuration: GenericChartConfigPanel,
38158
+ design: RadarChartDesignPanel,
37662
38159
  });
37663
38160
 
37664
38161
  css /* scss */ `
@@ -37924,6 +38421,7 @@ class AbstractComposerStore extends SpreadsheetStore {
37924
38421
  "stopComposerRangeSelection",
37925
38422
  "cancelEdition",
37926
38423
  "cycleReferences",
38424
+ "toggleEditionMode",
37927
38425
  "changeComposerCursorSelection",
37928
38426
  "replaceComposerCursorSelection",
37929
38427
  ];
@@ -38079,6 +38577,42 @@ class AbstractComposerStore extends SpreadsheetStore {
38079
38577
  }
38080
38578
  this.setCurrentContent(updated.content, updated.selection);
38081
38579
  }
38580
+ toggleEditionMode() {
38581
+ if (this.editionMode === "inactive")
38582
+ return;
38583
+ const start = Math.min(this.selectionStart, this.selectionEnd);
38584
+ const end = Math.max(this.selectionStart, this.selectionEnd);
38585
+ const refToken = [...this.currentTokens]
38586
+ .reverse()
38587
+ .find((tk) => tk.end >= start && end >= tk.start && tk.type === "REFERENCE");
38588
+ if (this.editionMode === "editing" && refToken) {
38589
+ const currentSheetId = this.getters.getActiveSheetId();
38590
+ const { sheetName, xc } = splitReference(refToken.value);
38591
+ const sheetId = this.getters.getSheetIdByName(sheetName);
38592
+ if (sheetId && sheetId !== currentSheetId) {
38593
+ this.model.dispatch("ACTIVATE_SHEET", { sheetIdFrom: currentSheetId, sheetIdTo: sheetId });
38594
+ }
38595
+ // move cursor to the right part of the token
38596
+ this.selectionStart = this.selectionEnd = refToken.end;
38597
+ const zone = this.getters.getRangeFromSheetXC(this.sheetId, xc).zone;
38598
+ this.captureSelection(zone);
38599
+ this.editionMode = "selecting";
38600
+ }
38601
+ else {
38602
+ this.editionMode = "editing";
38603
+ }
38604
+ }
38605
+ captureSelection(zone, col, row) {
38606
+ this.model.selection.capture(this, {
38607
+ cell: { col: col || zone.left, row: row || zone.right },
38608
+ zone,
38609
+ }, {
38610
+ handleEvent: this.handleEvent.bind(this),
38611
+ release: () => {
38612
+ this._stopEdition();
38613
+ },
38614
+ });
38615
+ }
38082
38616
  isSelectionValid(length, start, end) {
38083
38617
  return start >= 0 && start <= length && end >= 0 && end <= length;
38084
38618
  }
@@ -38117,12 +38651,7 @@ class AbstractComposerStore extends SpreadsheetStore {
38117
38651
  this.setContent(str || this.initialContent, selection);
38118
38652
  this.colorIndexByRange = {};
38119
38653
  const zone = positionToZone({ col: this.col, row: this.row });
38120
- this.model.selection.capture(this, { cell: { col: this.col, row: this.row }, zone }, {
38121
- handleEvent: this.handleEvent.bind(this),
38122
- release: () => {
38123
- this._stopEdition();
38124
- },
38125
- });
38654
+ this.captureSelection(zone, col, row);
38126
38655
  }
38127
38656
  _stopEdition() {
38128
38657
  if (this.editionMode !== "inactive") {
@@ -38829,13 +39358,6 @@ class DOMDndHelper {
38829
39358
  return;
38830
39359
  this.edgeScrollIntervalId = window.setInterval(() => {
38831
39360
  const offset = direction * 3;
38832
- let newPosition = this.currentMousePosition + offset;
38833
- if (newPosition < Math.min(this.container.start, this.minPosition)) {
38834
- newPosition = Math.min(this.container.start, this.minPosition);
38835
- }
38836
- else if (newPosition > Math.max(this.container.end, this.maxPosition)) {
38837
- newPosition = Math.max(this.container.end, this.maxPosition);
38838
- }
38839
39361
  this.container.scroll += offset;
38840
39362
  }, 5);
38841
39363
  }
@@ -39012,7 +39534,6 @@ css /* scss */ `
39012
39534
  width: 142px;
39013
39535
  .o-cf-preview-description-rule {
39014
39536
  margin-bottom: 4px;
39015
- font-weight: 600;
39016
39537
  max-height: 2.8em;
39017
39538
  line-height: 1.4em;
39018
39539
  }
@@ -39484,7 +40005,7 @@ class ConditionalFormattingEditor extends Component {
39484
40005
  setColorScaleColor(target, color) {
39485
40006
  const point = this.state.rules.colorScale[target];
39486
40007
  if (point) {
39487
- point.color = Number.parseInt(color.substr(1), 16);
40008
+ point.color = Number.parseInt(color.slice(1), 16);
39488
40009
  }
39489
40010
  this.closeMenus();
39490
40011
  }
@@ -39595,7 +40116,7 @@ class ConditionalFormattingEditor extends Component {
39595
40116
  return [this.state.rules.dataBar.rangeValues || ""];
39596
40117
  }
39597
40118
  updateDataBarColor(color) {
39598
- this.state.rules.dataBar.color = Number.parseInt(color.substr(1), 16);
40119
+ this.state.rules.dataBar.color = Number.parseInt(color.slice(1), 16);
39599
40120
  }
39600
40121
  onDataBarRangeUpdate(ranges) {
39601
40122
  this.state.rules.dataBar.rangeValues = ranges[0];
@@ -41008,10 +41529,11 @@ class FindAndReplaceStore extends SpreadsheetStore {
41008
41529
  activeSheetMatches = [];
41009
41530
  specificRangeMatches = [];
41010
41531
  currentSearchRegex = null;
41011
- isSearchDirty = false;
41012
41532
  initialShowFormulaState;
41013
41533
  preserveSelectedMatchIndex = false;
41014
41534
  irreplaceableMatchCount = 0;
41535
+ isSearchDirty = false;
41536
+ shouldFinalizeUpdateSelection = false;
41015
41537
  notificationStore = this.get(NotificationStore);
41016
41538
  // fixme: why do we make selectedMatchIndex on top of a selected
41017
41539
  // property in the matches?
@@ -41057,10 +41579,13 @@ class FindAndReplaceStore extends SpreadsheetStore {
41057
41579
  this.updateSearchOptions({ searchFormulas: showFormula });
41058
41580
  }
41059
41581
  selectPreviousMatch() {
41060
- this.selectNextCell(Direction.previous);
41582
+ this.selectNextCell(Direction.previous, {
41583
+ jumpToMatchSheet: true,
41584
+ updateSelection: true,
41585
+ });
41061
41586
  }
41062
41587
  selectNextMatch() {
41063
- this.selectNextCell(Direction.next);
41588
+ this.selectNextCell(Direction.next, { jumpToMatchSheet: true, updateSelection: true });
41064
41589
  }
41065
41590
  handle(cmd) {
41066
41591
  switch (cmd.type) {
@@ -41077,8 +41602,11 @@ class FindAndReplaceStore extends SpreadsheetStore {
41077
41602
  case "ADD_COLUMNS_ROWS":
41078
41603
  case "EVALUATE_CELLS":
41079
41604
  case "UPDATE_CELL":
41605
+ this.isSearchDirty = true;
41606
+ break;
41080
41607
  case "ACTIVATE_SHEET":
41081
41608
  this.isSearchDirty = true;
41609
+ this.shouldFinalizeUpdateSelection = true;
41082
41610
  break;
41083
41611
  case "REPLACE_SEARCH":
41084
41612
  for (const match of cmd.matches) {
@@ -41093,7 +41621,11 @@ class FindAndReplaceStore extends SpreadsheetStore {
41093
41621
  }
41094
41622
  finalize() {
41095
41623
  if (this.isSearchDirty) {
41096
- this.refreshSearch(false);
41624
+ this.refreshSearch({
41625
+ jumpToMatchSheet: false,
41626
+ updateSelection: this.shouldFinalizeUpdateSelection,
41627
+ });
41628
+ this.shouldFinalizeUpdateSelection = false;
41097
41629
  this.isSearchDirty = false;
41098
41630
  }
41099
41631
  }
@@ -41120,17 +41652,17 @@ class FindAndReplaceStore extends SpreadsheetStore {
41120
41652
  }
41121
41653
  this.toSearch = toSearch;
41122
41654
  this.currentSearchRegex = getSearchRegex(this.toSearch, this.searchOptions);
41123
- this.refreshSearch();
41655
+ this.refreshSearch({ jumpToMatchSheet: true, updateSelection: true });
41124
41656
  }
41125
41657
  /**
41126
41658
  * refresh the matches according to the current search options
41127
41659
  */
41128
- refreshSearch(jumpToMatchSheet = true) {
41660
+ refreshSearch(options) {
41129
41661
  if (!this.preserveSelectedMatchIndex) {
41130
41662
  this.selectedMatchIndex = null;
41131
41663
  }
41132
41664
  this.findMatches();
41133
- this.selectNextCell(Direction.current, jumpToMatchSheet);
41665
+ this.selectNextCell(Direction.current, options);
41134
41666
  }
41135
41667
  getSheetsInSearchOrder() {
41136
41668
  switch (this.searchOptions.searchScope) {
@@ -41200,7 +41732,7 @@ class FindAndReplaceStore extends SpreadsheetStore {
41200
41732
  * It is also used to keep coherence between the selected searchMatch
41201
41733
  * and selectedMatchIndex.
41202
41734
  */
41203
- selectNextCell(indexChange, jumpToMatchSheet = true) {
41735
+ selectNextCell(indexChange, options) {
41204
41736
  const matches = this.searchMatches;
41205
41737
  if (!matches.length) {
41206
41738
  this.selectedMatchIndex = null;
@@ -41226,7 +41758,7 @@ class FindAndReplaceStore extends SpreadsheetStore {
41226
41758
  this.selectedMatchIndex = nextIndex;
41227
41759
  const selectedMatch = matches[nextIndex];
41228
41760
  // Switch to the sheet where the match is located
41229
- if (jumpToMatchSheet && this.getters.getActiveSheetId() !== selectedMatch.sheetId) {
41761
+ if (options.jumpToMatchSheet && this.getters.getActiveSheetId() !== selectedMatch.sheetId) {
41230
41762
  // We set `preserveSelectedMatchIndex` to true to avoid resetting the selected search
41231
41763
  // index in the `refreshSearch` function when a new sheet is activated. The reason being
41232
41764
  // that, when we automatically go back to previous sheet while performing a search, the
@@ -41242,7 +41774,9 @@ class FindAndReplaceStore extends SpreadsheetStore {
41242
41774
  }
41243
41775
  // we want grid selection to capture the selection stream
41244
41776
  this.model.selection.getBackToDefault();
41245
- this.model.selection.selectCell(selectedMatch.col, selectedMatch.row);
41777
+ if (options.updateSelection) {
41778
+ this.model.selection.selectCell(selectedMatch.col, selectedMatch.row);
41779
+ }
41246
41780
  }
41247
41781
  /**
41248
41782
  * Replace the value of the currently selected match
@@ -41257,7 +41791,7 @@ class FindAndReplaceStore extends SpreadsheetStore {
41257
41791
  matches: [this.searchMatches[this.selectedMatchIndex]],
41258
41792
  searchOptions: this.searchOptions,
41259
41793
  });
41260
- this.selectNextCell(Direction.next);
41794
+ this.selectNextCell(Direction.next, { jumpToMatchSheet: true, updateSelection: true });
41261
41795
  }
41262
41796
  /**
41263
41797
  * Apply the replace function to all the matches one time.
@@ -41439,6 +41973,14 @@ class FindAndReplacePanel extends Component {
41439
41973
  onMounted(() => this.searchInput.el?.focus());
41440
41974
  onWillUnmount(() => this.updateSearchContent.stopDebounce());
41441
41975
  this.updateSearchContent = debounce(this.store.updateSearchContent, 200);
41976
+ useExternalListener(window, "keydown", (ev) => {
41977
+ const code = keyboardEventToShortcutString(ev);
41978
+ if (code === "Ctrl+F" || code === "Ctrl+H") {
41979
+ this.searchInput.el?.focus();
41980
+ ev.preventDefault();
41981
+ ev.stopPropagation();
41982
+ }
41983
+ }, { capture: true });
41442
41984
  }
41443
41985
  onFocusSearch() {
41444
41986
  this.updateDataRange();
@@ -42014,7 +42556,6 @@ function createMeasureAutoComplete(pivot, forComputedMeasure) {
42014
42556
  sequence: 0,
42015
42557
  autoSelectFirstProposal: true,
42016
42558
  getProposals(tokenAtCursor) {
42017
- // return []
42018
42559
  const measureProposals = pivot.measures
42019
42560
  .filter((m) => m !== forComputedMeasure)
42020
42561
  .map((measure) => {
@@ -42817,8 +43358,9 @@ const EMPTY_PIVOT_CELL = { type: "EMPTY" };
42817
43358
  * This function converts a list of data entry into a spreadsheet pivot table.
42818
43359
  */
42819
43360
  function dataEntriesToSpreadsheetPivotTable(dataEntries, definition) {
43361
+ const measureIds = definition.measures.filter((measure) => !measure.isHidden).map((m) => m.id);
42820
43362
  const columnsTree = dataEntriesToColumnsTree(dataEntries, definition.columns, 0);
42821
- computeWidthOfColumnsNodes(columnsTree, definition.measures.length);
43363
+ computeWidthOfColumnsNodes(columnsTree, measureIds.length);
42822
43364
  const cols = columnsTreeToColumns(columnsTree, definition);
42823
43365
  const rows = dataEntriesToRows(dataEntries, 0, definition.rows, [], []);
42824
43366
  // Add the total row
@@ -42827,7 +43369,6 @@ function dataEntriesToSpreadsheetPivotTable(dataEntries, definition) {
42827
43369
  values: [],
42828
43370
  indent: 0,
42829
43371
  });
42830
- const measureIds = definition.measures.filter((measure) => !measure.isHidden).map((m) => m.id);
42831
43372
  const fieldsType = {};
42832
43373
  for (const columns of definition.columns) {
42833
43374
  fieldsType[columns.fieldName] = columns.type;
@@ -43588,20 +44129,22 @@ pivotRegistry.add("SPREADSHEET", {
43588
44129
  onIterationEndEvaluation: (pivot) => pivot.markAsDirtyForEvaluation(),
43589
44130
  dateGranularities: [...dateGranularities],
43590
44131
  datetimeGranularities: [...dateGranularities, "hour_number", "minute_number", "second_number"],
43591
- isMeasureCandidate: (field) => !["date", "boolean"].includes(field.type),
44132
+ isMeasureCandidate: (field) => !["datetime", "boolean"].includes(field.type),
43592
44133
  isGroupable: () => true,
43593
44134
  });
43594
44135
 
43595
44136
  class PivotSidePanelStore extends SpreadsheetStore {
43596
44137
  pivotId;
43597
44138
  mutators = ["reset", "deferUpdates", "applyUpdate", "discardPendingUpdate", "update"];
43598
- updatesAreDeferred = false;
44139
+ updatesAreDeferred;
43599
44140
  draft = null;
43600
44141
  notification = this.get(NotificationStore);
43601
44142
  alreadyNotified = false;
43602
44143
  constructor(get, pivotId) {
43603
44144
  super(get);
43604
44145
  this.pivotId = pivotId;
44146
+ this.updatesAreDeferred =
44147
+ this.getters.getPivotCoreDefinition(this.pivotId).deferUpdates ?? false;
43605
44148
  }
43606
44149
  handle(cmd) {
43607
44150
  switch (cmd.type) {
@@ -43689,10 +44232,14 @@ class PivotSidePanelStore extends SpreadsheetStore {
43689
44232
  this.draft = null;
43690
44233
  }
43691
44234
  deferUpdates(shouldDefer) {
43692
- this.updatesAreDeferred = shouldDefer;
43693
44235
  if (shouldDefer === false && this.draft) {
44236
+ this.draft.deferUpdates = false;
43694
44237
  this.applyUpdate();
43695
44238
  }
44239
+ else {
44240
+ this.update({ deferUpdates: shouldDefer });
44241
+ }
44242
+ this.updatesAreDeferred = shouldDefer;
43696
44243
  }
43697
44244
  applyUpdate() {
43698
44245
  if (this.draft) {
@@ -43950,7 +44497,7 @@ class RemoveDuplicatesPanel extends Component {
43950
44497
  return colLabel;
43951
44498
  }
43952
44499
  get isEveryColumnSelected() {
43953
- return Object.values(this.state.columns).every((value) => value === true);
44500
+ return Object.values(this.state.columns).every((value) => value);
43954
44501
  }
43955
44502
  get errorMessages() {
43956
44503
  const cancelledReasons = this.env.model.canDispatch("REMOVE_DUPLICATES", {
@@ -44050,8 +44597,7 @@ class SettingsPanel extends Component {
44050
44597
  const currentLocale = this.currentLocale;
44051
44598
  const localeInLoadedLocales = this.loadedLocales.find((l) => l.code === currentLocale.code);
44052
44599
  if (!localeInLoadedLocales) {
44053
- const locales = [...this.loadedLocales, currentLocale].sort((a, b) => a.name.localeCompare(b.name));
44054
- return locales;
44600
+ return [...this.loadedLocales, currentLocale].sort((a, b) => a.name.localeCompare(b.name));
44055
44601
  }
44056
44602
  else if (!deepEquals(currentLocale, localeInLoadedLocales)) {
44057
44603
  const index = this.loadedLocales.indexOf(localeInLoadedLocales);
@@ -45945,10 +46491,9 @@ class GridComposer extends Component {
45945
46491
  });
45946
46492
  }
45947
46493
  get focus() {
45948
- const focus = this.composerFocusStore.activeComposer === this.composerInterface
46494
+ return this.composerFocusStore.activeComposer === this.composerInterface
45949
46495
  ? this.composerFocusStore.focusMode
45950
46496
  : "inactive";
45951
- return focus;
45952
46497
  }
45953
46498
  get composerProps() {
45954
46499
  const { width, height } = this.env.model.getters.getSheetViewDimensionWithHeaders();
@@ -46117,13 +46662,10 @@ class GridCellIcon extends Component {
46117
46662
  }
46118
46663
  }
46119
46664
 
46120
- const CHECKBOX_WIDTH = 15;
46121
46665
  const MARGIN = (GRID_ICON_EDGE_LENGTH - CHECKBOX_WIDTH) / 2;
46122
46666
  css /* scss */ `
46123
46667
  .o-dv-checkbox {
46124
46668
  box-sizing: border-box !important;
46125
- width: ${CHECKBOX_WIDTH}px;
46126
- height: ${CHECKBOX_WIDTH}px;
46127
46669
  accent-color: #808080;
46128
46670
  margin: ${MARGIN}px;
46129
46671
  /** required to prevent the checkbox position to be sensible to the font-size (affects Firefox) */
@@ -46132,13 +46674,15 @@ css /* scss */ `
46132
46674
  `;
46133
46675
  class DataValidationCheckbox extends Component {
46134
46676
  static template = "o-spreadsheet-DataValidationCheckbox";
46677
+ static components = {
46678
+ Checkbox,
46679
+ };
46135
46680
  static props = {
46136
46681
  cellPosition: Object,
46137
46682
  };
46138
- onCheckboxChange(ev) {
46139
- const newValue = ev.target.checked;
46683
+ onCheckboxChange(value) {
46140
46684
  const { sheetId, col, row } = this.props.cellPosition;
46141
- const cellContent = newValue ? "TRUE" : "FALSE";
46685
+ const cellContent = value ? "TRUE" : "FALSE";
46142
46686
  this.env.model.dispatch("UPDATE_CELL", { sheetId, col, row, content: cellContent });
46143
46687
  }
46144
46688
  get checkBoxValue() {
@@ -46952,7 +47496,12 @@ class GridAddRowsFooter extends Component {
46952
47496
  class PaintFormatStore extends SpreadsheetStore {
46953
47497
  mutators = ["activate", "cancel", "pasteFormat"];
46954
47498
  highlightStore = this.get(HighlightStore);
46955
- cellClipboardHandler = new CellClipboardHandler(this.getters, this.model.dispatch);
47499
+ clipboardHandlers = [
47500
+ new CellClipboardHandler(this.getters, this.model.dispatch),
47501
+ new BorderClipboardHandler(this.getters, this.model.dispatch),
47502
+ new TableClipboardHandler(this.getters, this.model.dispatch),
47503
+ new ConditionalFormatClipboardHandler(this.getters, this.model.dispatch),
47504
+ ];
46956
47505
  status = "inactive";
46957
47506
  copiedData;
46958
47507
  constructor(get) {
@@ -46962,6 +47511,13 @@ class PaintFormatStore extends SpreadsheetStore {
46962
47511
  this.highlightStore.unRegister(this);
46963
47512
  });
46964
47513
  }
47514
+ handle(cmd) {
47515
+ switch (cmd.type) {
47516
+ case "PAINT_FORMAT":
47517
+ this.paintFormat(cmd.sheetId, cmd.target);
47518
+ break;
47519
+ }
47520
+ }
46965
47521
  activate(args) {
46966
47522
  this.copiedData = this.copyFormats();
46967
47523
  this.status = args.persistent ? "persistent" : "oneOff";
@@ -46971,16 +47527,7 @@ class PaintFormatStore extends SpreadsheetStore {
46971
47527
  this.copiedData = undefined;
46972
47528
  }
46973
47529
  pasteFormat(target) {
46974
- if (this.copiedData) {
46975
- const sheetId = this.getters.getActiveSheetId();
46976
- this.cellClipboardHandler.paste({ zones: target, sheetId }, this.copiedData, {
46977
- isCutOperation: false,
46978
- pasteOption: "onlyFormat",
46979
- });
46980
- }
46981
- if (this.status === "oneOff") {
46982
- this.cancel();
46983
- }
47530
+ this.model.dispatch("PAINT_FORMAT", { target, sheetId: this.getters.getActiveSheetId() });
46984
47531
  }
46985
47532
  get isActive() {
46986
47533
  return this.status !== "inactive";
@@ -46988,7 +47535,24 @@ class PaintFormatStore extends SpreadsheetStore {
46988
47535
  copyFormats() {
46989
47536
  const sheetId = this.getters.getActiveSheetId();
46990
47537
  const zones = this.getters.getSelectedZones();
46991
- return this.cellClipboardHandler.copy(getClipboardDataPositions(sheetId, zones));
47538
+ const copiedData = {};
47539
+ for (const handler of this.clipboardHandlers) {
47540
+ Object.assign(copiedData, handler.copy(getClipboardDataPositions(sheetId, zones)));
47541
+ }
47542
+ return copiedData;
47543
+ }
47544
+ paintFormat(sheetId, target) {
47545
+ if (this.copiedData) {
47546
+ for (const handler of this.clipboardHandlers) {
47547
+ handler.paste({ zones: target, sheetId }, this.copiedData, {
47548
+ isCutOperation: false,
47549
+ pasteOption: "onlyFormat",
47550
+ });
47551
+ }
47552
+ }
47553
+ if (this.status === "oneOff") {
47554
+ this.cancel();
47555
+ }
46992
47556
  }
46993
47557
  get highlights() {
46994
47558
  const data = this.copiedData;
@@ -47366,7 +47930,7 @@ class AbstractResizer extends Component {
47366
47930
  if (index < 0) {
47367
47931
  return;
47368
47932
  }
47369
- if (this.state.waitingForMove === true) {
47933
+ if (this.state.waitingForMove) {
47370
47934
  if (!this.env.model.getters.isGridSelectionActive()) {
47371
47935
  this._selectElement(index, false);
47372
47936
  }
@@ -48925,7 +49489,7 @@ class SidePanelStore extends SpreadsheetStore {
48925
49489
  }
48926
49490
  open(componentTag, panelProps = {}) {
48927
49491
  const state = this.computeState(componentTag, panelProps);
48928
- if (state.isOpen === false) {
49492
+ if (!state.isOpen) {
48929
49493
  return;
48930
49494
  }
48931
49495
  if (this.isOpen && componentTag !== this.componentTag) {
@@ -49264,6 +49828,8 @@ class Grid extends Component {
49264
49828
  },
49265
49829
  "Ctrl+D": async () => this.env.model.dispatch("COPY_PASTE_CELLS_ABOVE"),
49266
49830
  "Ctrl+R": async () => this.env.model.dispatch("COPY_PASTE_CELLS_ON_LEFT"),
49831
+ "Ctrl+H": () => this.sidePanel.open("FindAndReplace", {}),
49832
+ "Ctrl+F": () => this.sidePanel.open("FindAndReplace", {}),
49267
49833
  "Ctrl+Shift+E": () => this.setHorizontalAlign("center"),
49268
49834
  "Ctrl+Shift+L": () => this.setHorizontalAlign("left"),
49269
49835
  "Ctrl+Shift+R": () => this.setHorizontalAlign("right"),
@@ -49671,31 +50237,6 @@ class Grid extends Component {
49671
50237
  }
49672
50238
  }
49673
50239
 
49674
- /** @odoo-module */
49675
- class EditableName extends Component {
49676
- static template = "o-spreadsheet-EditableName";
49677
- static props = {
49678
- name: String,
49679
- displayName: String,
49680
- onChanged: Function,
49681
- };
49682
- state;
49683
- setup() {
49684
- this.state = useState({
49685
- isEditing: false,
49686
- name: "",
49687
- });
49688
- }
49689
- rename() {
49690
- this.state.isEditing = true;
49691
- this.state.name = this.props.name;
49692
- }
49693
- save() {
49694
- this.props.onChanged(this.state.name.trim());
49695
- this.state.isEditing = false;
49696
- }
49697
- }
49698
-
49699
50240
  /**
49700
50241
  * BasePlugin
49701
50242
  *
@@ -49796,12 +50337,10 @@ class BasePlugin {
49796
50337
  */
49797
50338
  class CorePlugin extends BasePlugin {
49798
50339
  getters;
49799
- uuidGenerator;
49800
- constructor({ getters, stateObserver, range, dispatch, canDispatch, uuidGenerator, }) {
50340
+ constructor({ getters, stateObserver, range, dispatch, canDispatch }) {
49801
50341
  super(stateObserver, dispatch, canDispatch);
49802
50342
  range.addRangeProvider(this.adaptRanges.bind(this));
49803
50343
  this.getters = getters;
49804
- this.uuidGenerator = uuidGenerator;
49805
50344
  }
49806
50345
  // ---------------------------------------------------------------------------
49807
50346
  // Import/Export
@@ -53615,7 +54154,7 @@ class SheetPlugin extends CorePlugin {
53615
54154
  this.sheetIdsMapName[sheet.name] = sheet.id;
53616
54155
  }
53617
54156
  for (let sheetData of data.sheets) {
53618
- const name = sheetData.name || _t("Sheet") + (Object.keys(this.sheets).length + 1);
54157
+ const name = sheetData.name || "Sheet" + (Object.keys(this.sheets).length + 1);
53619
54158
  const { colNumber, rowNumber } = this.getImportedSheetSize(sheetData);
53620
54159
  const sheet = {
53621
54160
  id: sheetData.id,
@@ -54299,6 +54838,7 @@ class SheetPlugin extends CorePlugin {
54299
54838
  }
54300
54839
  }
54301
54840
 
54841
+ let nextTableId = 1;
54302
54842
  class TablePlugin extends CorePlugin {
54303
54843
  static getters = ["getCoreTable", "getCoreTables", "getCoreTableMatchingTopLeft"];
54304
54844
  tables = {};
@@ -54366,7 +54906,7 @@ class TablePlugin extends CorePlugin {
54366
54906
  const union = this.getters.getRangesUnion(ranges);
54367
54907
  const mergesInTarget = this.getters.getMergesInZone(cmd.sheetId, union.zone);
54368
54908
  this.dispatch("REMOVE_MERGE", { sheetId: cmd.sheetId, target: mergesInTarget });
54369
- const id = this.uuidGenerator.uuidv4();
54909
+ const id = `${nextTableId++}`;
54370
54910
  const config = cmd.config || DEFAULT_TABLE_CONFIG;
54371
54911
  const newTable = cmd.tableType === "dynamic"
54372
54912
  ? this.createDynamicTable(id, union, config)
@@ -54519,7 +55059,7 @@ class TablePlugin extends CorePlugin {
54519
55059
  filters = [];
54520
55060
  for (const i of range(zone.left, zone.right + 1)) {
54521
55061
  const filterZone = { ...zone, left: i, right: i };
54522
- const uid = this.uuidGenerator.uuidv4();
55062
+ const uid = `${nextTableId++}`;
54523
55063
  filters.push(this.createFilterFromZone(uid, tableRange.sheetId, filterZone, config));
54524
55064
  }
54525
55065
  }
@@ -54584,7 +55124,7 @@ class TablePlugin extends CorePlugin {
54584
55124
  ? table.filters.find((f) => f.col === i)
54585
55125
  : undefined;
54586
55126
  const filterZone = { ...tableZone, left: i, right: i };
54587
- const filterId = oldFilter?.id || this.uuidGenerator.uuidv4();
55127
+ const filterId = oldFilter?.id || `${nextTableId++}`;
54588
55128
  filters.push(this.createFilterFromZone(filterId, tableRange.sheetId, filterZone, config));
54589
55129
  }
54590
55130
  }
@@ -54685,7 +55225,7 @@ class TablePlugin extends CorePlugin {
54685
55225
  if (filters.length < zoneToDimension(tableZone).numberOfCols) {
54686
55226
  for (let col = tableZone.left; col <= tableZone.right; col++) {
54687
55227
  if (!filters.find((filter) => filter.col === col)) {
54688
- const uid = this.uuidGenerator.uuidv4();
55228
+ const uid = `${nextTableId++}`;
54689
55229
  const filterZone = { ...tableZone, left: col, right: col };
54690
55230
  filters.push(this.createFilterFromZone(uid, sheetId, filterZone, table.config));
54691
55231
  }
@@ -54701,7 +55241,7 @@ class TablePlugin extends CorePlugin {
54701
55241
  import(data) {
54702
55242
  for (const sheet of data.sheets) {
54703
55243
  for (const tableData of sheet.tables || []) {
54704
- const uuid = this.uuidGenerator.uuidv4();
55244
+ const uuid = `${nextTableId++}`;
54705
55245
  const tableConfig = tableData.config || DEFAULT_TABLE_CONFIG;
54706
55246
  const range = this.getters.getRangeFromSheetXC(sheet.id, tableData.range);
54707
55247
  const tableType = tableData.type || "static";
@@ -55233,7 +55773,7 @@ class PivotCorePlugin extends CorePlugin {
55233
55773
  case "DUPLICATE_PIVOT": {
55234
55774
  const { pivotId, newPivotId } = cmd;
55235
55775
  const pivot = deepCopy(this.getPivotCore(pivotId).definition);
55236
- pivot.name = _t("%s (copy)", pivot.name);
55776
+ pivot.name = cmd.duplicatedPivotName ?? pivot.name + " (copy)";
55237
55777
  this.addPivot(newPivotId, pivot);
55238
55778
  break;
55239
55779
  }
@@ -55273,7 +55813,7 @@ class PivotCorePlugin extends CorePlugin {
55273
55813
  return `(#${formulaId}) ${this.getPivotName(pivotId)}`;
55274
55814
  }
55275
55815
  getPivotName(pivotId) {
55276
- return _t(this.getPivotCore(pivotId).definition.name);
55816
+ return this.getPivotCore(pivotId).definition.name;
55277
55817
  }
55278
55818
  /**
55279
55819
  * Returns the pivot core definition of the pivot with the given id.
@@ -56915,7 +57455,7 @@ class Evaluator {
56915
57455
  cellsToCompute.addMany(arrayFormulasPositions);
56916
57456
  cellsToCompute.addMany(this.getCellsDependingOn(arrayFormulasPositions));
56917
57457
  this.evaluate(cellsToCompute);
56918
- console.info("evaluate Cells", performance.now() - start, "ms");
57458
+ console.debug("evaluate Cells", performance.now() - start, "ms");
56919
57459
  }
56920
57460
  getArrayFormulasImpactedByChangesOf(positions) {
56921
57461
  const impactedPositions = this.createEmptyPositionSet();
@@ -56959,7 +57499,7 @@ class Evaluator {
56959
57499
  const start = performance.now();
56960
57500
  this.evaluatedCells = new PositionMap();
56961
57501
  this.evaluate(this.getAllCells());
56962
- console.info("evaluate all cells", performance.now() - start, "ms");
57502
+ console.debug("evaluate all cells", performance.now() - start, "ms");
56963
57503
  }
56964
57504
  evaluateFormulaResult(sheetId, formulaString) {
56965
57505
  const compiledFormula = compile(formulaString);
@@ -57965,8 +58505,7 @@ class EvaluationConditionalFormatPlugin extends UIPlugin {
57965
58505
  .map((cell) => cell.value);
57966
58506
  switch (threshold.type) {
57967
58507
  case "value":
57968
- const result = functionName === "max" ? largeMax(rangeValues) : largeMin(rangeValues);
57969
- return result;
58508
+ return functionName === "max" ? largeMax(rangeValues) : largeMin(rangeValues);
57970
58509
  case "number":
57971
58510
  return Number(threshold.value);
57972
58511
  case "percentage":
@@ -59050,7 +59589,7 @@ function withPivotPresentationLayer (PivotClass) {
59050
59589
  const ranking = {};
59051
59590
  const mainDimension = getFieldDimensionType(this, fieldNameWithGranularity);
59052
59591
  const secondaryDimension = mainDimension === "row" ? "column" : "row";
59053
- let pivotCells = this.getPivotValueCells();
59592
+ let pivotCells = this.getPivotValueCells(measure.id);
59054
59593
  if (mainDimension === "column") {
59055
59594
  // Transpose the pivot cells so we can do the same operations on the columns as on the rows
59056
59595
  // This means that we need to transpose back the ranking at the end
@@ -59094,7 +59633,7 @@ function withPivotPresentationLayer (PivotClass) {
59094
59633
  const cellsRunningTotals = {};
59095
59634
  const mainDimension = getFieldDimensionType(this, fieldNameWithGranularity);
59096
59635
  const secondaryDimension = mainDimension === "row" ? "column" : "row";
59097
- let pivotCells = this.getPivotValueCells();
59636
+ let pivotCells = this.getPivotValueCells(measure.id);
59098
59637
  if (mainDimension === "column") {
59099
59638
  // Transpose the pivot cells so we can do the same operations on the columns as on the rows
59100
59639
  // This means that we need to transpose back the totals at the end
@@ -59170,13 +59709,12 @@ function withPivotPresentationLayer (PivotClass) {
59170
59709
  throw new NotAvailableError();
59171
59710
  }
59172
59711
  const comparedValue = this._getPivotCellValueAndFormat(measure.id, comparedDomain);
59173
- const comparedValueNumber = this.strictMeasureValueToNumber(comparedValue);
59174
- return comparedValueNumber;
59712
+ return this.strictMeasureValueToNumber(comparedValue);
59175
59713
  }
59176
- getPivotValueCells() {
59714
+ getPivotValueCells(measureId) {
59177
59715
  return this.getTableStructure()
59178
59716
  .getPivotCells()
59179
- .map((col) => col.filter((cell) => cell.type === "VALUE"))
59717
+ .map((col) => col.filter((cell) => cell.type === "VALUE" && cell.measure === measureId))
59180
59718
  .filter((col) => col.length > 0);
59181
59719
  }
59182
59720
  measureValueToNumber(result) {
@@ -60705,7 +61243,7 @@ class Session extends EventBus {
60705
61243
  this.onMessageReceived(message);
60706
61244
  }
60707
61245
  this.isReplayingInitialRevisions = false;
60708
- console.info("Replayed", numberOfCommands, "commands in", performance.now() - start, "ms");
61246
+ console.debug("Replayed", numberOfCommands, "commands in", performance.now() - start, "ms");
60709
61247
  }
60710
61248
  /**
60711
61249
  * Notify the server that the user client left the collaborative session
@@ -60877,7 +61415,6 @@ class Session extends EventBus {
60877
61415
  if (this.waitingAck) {
60878
61416
  return;
60879
61417
  }
60880
- this.waitingAck = true;
60881
61418
  this.sendPendingMessage();
60882
61419
  }
60883
61420
  /**
@@ -60914,6 +61451,7 @@ class Session extends EventBus {
60914
61451
  throw new Error(`Trying to send a new revision while replaying initial revision. This can lead to endless dispatches every time the spreadsheet is open.
60915
61452
  ${JSON.stringify(message)}`);
60916
61453
  }
61454
+ this.waitingAck = true;
60917
61455
  this.transportService.sendMessage({
60918
61456
  ...message,
60919
61457
  serverRevisionId: this.serverRevisionId,
@@ -60944,6 +61482,7 @@ class Session extends EventBus {
60944
61482
  case "REMOTE_REVISION":
60945
61483
  case "REVISION_REDONE":
60946
61484
  case "REVISION_UNDONE":
61485
+ case "SNAPSHOT_CREATED":
60947
61486
  return this.processedRevisions.has(message.nextRevisionId);
60948
61487
  default:
60949
61488
  return false;
@@ -61072,8 +61611,7 @@ class CollaborativePlugin extends UIPlugin {
61072
61611
  }
61073
61612
  const color = client.color;
61074
61613
  /* Cell background */
61075
- const cellBackgroundColor = `${color}10`;
61076
- ctx.fillStyle = cellBackgroundColor;
61614
+ ctx.fillStyle = `${color}10`;
61077
61615
  ctx.lineWidth = 4 * thinLineWidth;
61078
61616
  ctx.strokeStyle = color;
61079
61617
  ctx.globalCompositeOperation = "multiply";
@@ -61440,8 +61978,7 @@ class HeaderVisibilityUIPlugin extends UIPlugin {
61440
61978
  exportForExcel(data) {
61441
61979
  for (const sheetData of data.sheets) {
61442
61980
  for (const [row, rowData] of Object.entries(sheetData.rows)) {
61443
- const isHidden = this.isRowHidden(sheetData.id, Number(row));
61444
- rowData.isHidden = isHidden;
61981
+ rowData.isHidden = this.isRowHidden(sheetData.id, Number(row));
61445
61982
  }
61446
61983
  }
61447
61984
  }
@@ -61501,12 +62038,13 @@ class InsertPivotPlugin extends UIPlugin {
61501
62038
  this.dispatch("DUPLICATE_PIVOT", {
61502
62039
  pivotId,
61503
62040
  newPivotId,
62041
+ duplicatedPivotName: _t("%s (copy)", this.getters.getPivotCoreDefinition(pivotId).name),
61504
62042
  });
61505
62043
  const activeSheetId = this.getters.getActiveSheetId();
61506
62044
  const position = this.getters.getSheetIds().indexOf(activeSheetId) + 1;
61507
62045
  const formulaId = this.getters.getPivotFormulaId(newPivotId);
61508
62046
  const newPivotName = this.getters.getPivotName(newPivotId);
61509
- this.dispatch("CREATE_SHEET", {
62047
+ const result = this.dispatch("CREATE_SHEET", {
61510
62048
  sheetId: newSheetId,
61511
62049
  name: this.getPivotDuplicateSheetName(_t("%(newPivotName)s (Pivot #%(formulaId)s)", {
61512
62050
  newPivotName,
@@ -61514,20 +62052,19 @@ class InsertPivotPlugin extends UIPlugin {
61514
62052
  })),
61515
62053
  position,
61516
62054
  });
61517
- this.dispatch("ACTIVATE_SHEET", { sheetIdFrom: activeSheetId, sheetIdTo: newSheetId });
61518
- this.dispatch("UPDATE_CELL", {
61519
- sheetId: newSheetId,
61520
- col: 0,
61521
- row: 0,
61522
- content: `=PIVOT(${formulaId})`,
61523
- });
62055
+ if (result.isSuccessful) {
62056
+ this.dispatch("ACTIVATE_SHEET", { sheetIdFrom: activeSheetId, sheetIdTo: newSheetId });
62057
+ const pivot = this.getters.getPivot(pivotId);
62058
+ this.insertPivotWithTable(newSheetId, 0, 0, newPivotId, pivot.getTableStructure().export(), "dynamic");
62059
+ }
61524
62060
  }
61525
62061
  getPivotDuplicateSheetName(pivotName) {
61526
62062
  let i = 1;
61527
62063
  const names = this.getters.getSheetIds().map((id) => this.getters.getSheetName(id));
61528
- let name = pivotName;
62064
+ const sanitizedName = pivotName.replace(new RegExp(FORBIDDEN_IN_EXCEL_REGEX, "g"), " ");
62065
+ let name = sanitizedName;
61529
62066
  while (names.includes(name)) {
61530
- name = `${pivotName} (${i})`;
62067
+ name = `${sanitizedName} (${i})`;
61531
62068
  i++;
61532
62069
  }
61533
62070
  return name;
@@ -61966,7 +62503,6 @@ class SheetUIPlugin extends UIPlugin {
61966
62503
  if (!isEqual(zone, newZone)) {
61967
62504
  hasExpanded = true;
61968
62505
  zone = newZone;
61969
- continue;
61970
62506
  }
61971
62507
  } while (hasExpanded);
61972
62508
  return zone;
@@ -63059,7 +63595,7 @@ class ClipboardPlugin extends UIPlugin {
63059
63595
  case "ADD_COLUMNS_ROWS": {
63060
63596
  this.status = "invisible";
63061
63597
  // If we add a col/row inside or before the cut area, we invalidate the clipboard
63062
- if (this._isCutOperation !== true || cmd.sheetId !== this.copiedData?.sheetId) {
63598
+ if (!this._isCutOperation || cmd.sheetId !== this.copiedData?.sheetId) {
63063
63599
  return;
63064
63600
  }
63065
63601
  const isClipboardDirty = this.isColRowDirtyingClipboard(cmd.position === "before" ? cmd.base : cmd.base + 1, cmd.dimension);
@@ -63071,7 +63607,7 @@ class ClipboardPlugin extends UIPlugin {
63071
63607
  case "REMOVE_COLUMNS_ROWS": {
63072
63608
  this.status = "invisible";
63073
63609
  // If we remove a col/row inside or before the cut area, we invalidate the clipboard
63074
- if (this._isCutOperation !== true || cmd.sheetId !== this.copiedData?.sheetId) {
63610
+ if (!this._isCutOperation || cmd.sheetId !== this.copiedData?.sheetId) {
63075
63611
  return;
63076
63612
  }
63077
63613
  for (let el of cmd.elements) {
@@ -63093,7 +63629,7 @@ class ClipboardPlugin extends UIPlugin {
63093
63629
  break;
63094
63630
  }
63095
63631
  case "DELETE_SHEET":
63096
- if (this._isCutOperation !== true) {
63632
+ if (!this._isCutOperation) {
63097
63633
  return;
63098
63634
  }
63099
63635
  if (this.originSheetId === cmd.sheetId) {
@@ -63986,8 +64522,7 @@ class GridSelectionPlugin extends UIPlugin {
63986
64522
  this.setSelectionMixin({ zone, cell: { col, row } }, [zone]);
63987
64523
  }
63988
64524
  setActiveSheet(id) {
63989
- const sheet = this.getters.getSheet(id);
63990
- this.activeSheet = sheet;
64525
+ this.activeSheet = this.getters.getSheet(id);
63991
64526
  }
63992
64527
  activateNextSheet(direction) {
63993
64528
  const sheetIds = this.getters.getSheetIds();
@@ -64681,9 +65216,6 @@ class SheetViewPlugin extends UIPlugin {
64681
65216
  case "UNFREEZE_COLUMNS_ROWS":
64682
65217
  this.resetViewports(this.getters.getActiveSheetId());
64683
65218
  break;
64684
- case "DELETE_SHEET":
64685
- this.sheetsWithDirtyViewports.delete(cmd.sheetId);
64686
- break;
64687
65219
  case "SCROLL_TO_CELL":
64688
65220
  this.refreshViewport(this.getters.getActiveSheetId(), { col: cmd.col, row: cmd.row });
64689
65221
  break;
@@ -66635,10 +67167,9 @@ css /* scss */ `
66635
67167
  user-select: none;
66636
67168
  color: ${TEXT_BODY};
66637
67169
 
66638
- .o-heading-3 {
67170
+ .o-sidePanelTitle {
66639
67171
  line-height: 20px;
66640
67172
  font-size: 16px;
66641
- font-weight: 600;
66642
67173
  }
66643
67174
 
66644
67175
  .o-sidePanelHeader {
@@ -66723,6 +67254,10 @@ css /* scss */ `
66723
67254
  }
66724
67255
  }
66725
67256
  }
67257
+
67258
+ .o-fw-bold {
67259
+ font-weight: 500;
67260
+ }
66726
67261
  `;
66727
67262
  class SidePanel extends Component {
66728
67263
  static template = "o-spreadsheet-SidePanel";
@@ -67554,8 +68089,7 @@ class WebClipboardWrapper {
67554
68089
  for (const item of clipboardItems) {
67555
68090
  for (const type of item.types) {
67556
68091
  const blob = await item.getType(type);
67557
- const text = await blob.text();
67558
- clipboardContent[type] = text;
68092
+ clipboardContent[type] = await blob.text();
67559
68093
  }
67560
68094
  }
67561
68095
  return { status: "ok", content: clipboardContent };
@@ -67867,7 +68401,6 @@ class Spreadsheet extends Component {
67867
68401
  spreadsheetRef = useRef("spreadsheet");
67868
68402
  spreadsheetRect = useSpreadsheetRect();
67869
68403
  _focusGrid;
67870
- keyDownMapping;
67871
68404
  isViewportTooSmall = false;
67872
68405
  notificationStore;
67873
68406
  composerFocusStore;
@@ -67891,10 +68424,6 @@ class Spreadsheet extends Component {
67891
68424
  this.notificationStore = useStore(NotificationStore);
67892
68425
  this.composerFocusStore = useStore(ComposerFocusStore);
67893
68426
  this.sidePanel = useStore(SidePanelStore);
67894
- this.keyDownMapping = {
67895
- "CTRL+H": () => this.sidePanel.toggle("FindAndReplace", {}),
67896
- "CTRL+F": () => this.sidePanel.toggle("FindAndReplace", {}),
67897
- };
67898
68427
  const fileStore = this.model.config.external.fileStore;
67899
68428
  useSubEnv({
67900
68429
  model: this.model,
@@ -67994,20 +68523,6 @@ class Spreadsheet extends Component {
67994
68523
  }
67995
68524
  this._focusGrid();
67996
68525
  }
67997
- onKeydown(ev) {
67998
- let keyDownString = "";
67999
- if (isCtrlKey(ev)) {
68000
- keyDownString += "CTRL+";
68001
- }
68002
- keyDownString += ev.key.toUpperCase();
68003
- let handler = this.keyDownMapping[keyDownString];
68004
- if (handler) {
68005
- ev.preventDefault();
68006
- ev.stopPropagation();
68007
- handler();
68008
- return;
68009
- }
68010
- }
68011
68526
  get gridHeight() {
68012
68527
  const { height } = this.env.model.getters.getSheetViewDimension();
68013
68528
  return height;
@@ -69422,10 +69937,10 @@ class SelectionStreamProcessorImpl {
69422
69937
  getNextCellPosition(currentPosition, dimension, direction) {
69423
69938
  const dimOfInterest = dimension === "cols" ? "col" : "row";
69424
69939
  const startingPosition = { ...currentPosition };
69425
- const nextCoord = dimension === "cols"
69426
- ? this.getNextAvailableCol(direction, startingPosition.col, startingPosition.row)
69427
- : this.getNextAvailableRow(direction, startingPosition.col, startingPosition.row);
69428
- startingPosition[dimOfInterest] = nextCoord;
69940
+ startingPosition[dimOfInterest] =
69941
+ dimension === "cols"
69942
+ ? this.getNextAvailableCol(direction, startingPosition.col, startingPosition.row)
69943
+ : this.getNextAvailableRow(direction, startingPosition.col, startingPosition.row);
69429
69944
  return { col: startingPosition.col, row: startingPosition.row };
69430
69945
  }
69431
69946
  getPosition() {
@@ -69549,6 +70064,8 @@ function createChart(chart, chartSheetIndex, data) {
69549
70064
  case "pie":
69550
70065
  plot = addDoughnutChart(chart.data, chartSheetIndex, data, { holeSize: 0 });
69551
70066
  break;
70067
+ case "radar":
70068
+ plot = addRadarChart(chart.data);
69552
70069
  }
69553
70070
  let position = "t";
69554
70071
  switch (chart.data.legendPosition) {
@@ -70007,6 +70524,53 @@ function addScatterChart(chart) {
70007
70524
  `
70008
70525
  : ""}`;
70009
70526
  }
70527
+ function addRadarChart(chart) {
70528
+ const dataSetsColors = chart.dataSets.map((ds) => ds.backgroundColor ?? "");
70529
+ const colors = new ColorGenerator(chart.dataSets.length, dataSetsColors);
70530
+ const dataSetsNodes = [];
70531
+ for (const [dsIndex, dataset] of Object.entries(chart.dataSets)) {
70532
+ const color = toXlsxHexColor(colors.next());
70533
+ const dataShapeProperty = shapeProperty({
70534
+ line: {
70535
+ width: 2.5,
70536
+ style: "solid",
70537
+ color,
70538
+ },
70539
+ });
70540
+ const dataSetNode = escapeXml /*xml*/ `
70541
+ <c:ser>
70542
+ <c:idx val="${dsIndex}"/>
70543
+ <c:order val="${dsIndex}"/>
70544
+ <c:smooth val="0"/>
70545
+ <c:marker>
70546
+ <c:symbol val="circle" />
70547
+ <c:size val="5"/>
70548
+ ${shapeProperty({ backgroundColor: color, line: { color } })}
70549
+ </c:marker>
70550
+ ${extractDataSetLabel(dataset.label)}
70551
+ ${dataShapeProperty}
70552
+ ${chart.labelRange ? escapeXml `<c:cat>${stringRef(chart.labelRange)}</c:cat>` : ""} <!-- x-coordinate values -->
70553
+ <c:val> <!-- x-coordinate values -->
70554
+ ${numberRef(dataset.range)}
70555
+ </c:val>
70556
+ </c:ser>
70557
+ `;
70558
+ dataSetsNodes.push(dataSetNode);
70559
+ }
70560
+ return escapeXml /*xml*/ `
70561
+ ${escapeXml /*xml*/ `
70562
+ <c:radarChart>
70563
+ <c:radarStyle val="marker"/>
70564
+ <c:varyColors val="0"/>
70565
+ ${joinXmlNodes(dataSetsNodes)}
70566
+ <c:axId val="${catAxId}" />
70567
+ <c:axId val="${valAxId}" />
70568
+ </c:radarChart>
70569
+ ${addAx("b", "c:catAx", catAxId, valAxId, chart.axesDesign?.x?.title, chart.fontColor)}
70570
+ ${addAx("l", "c:valAx", valAxId, catAxId, chart.axesDesign?.y?.title, chart.fontColor)}
70571
+ `}
70572
+ `;
70573
+ }
70010
70574
  function addDoughnutChart(chart, chartSheetIndex, data, { holeSize } = { holeSize: 50 }) {
70011
70575
  const maxLength = largeMax(chart.dataSets.map((ds) => getRangeSize(ds.range, chartSheetIndex, data)));
70012
70576
  const colors = new ColorGenerator(maxLength);
@@ -70880,6 +71444,15 @@ function addTableColumns(table, sheetData) {
70880
71444
  ["id", i + 1], // id cannot be 0
70881
71445
  ["name", colName],
70882
71446
  ];
71447
+ if (table.config.totalRow) {
71448
+ // Note: To be 100% complete, we could also add a `totalsRowLabel` attribute for total strings, and a tag
71449
+ // `<totalsRowFormula>` for the formula of the total. But those doesn't seem to be mandatory for Excel.
71450
+ const colTotalXc = toXC(tableZone.left + i, tableZone.bottom);
71451
+ const colTotalContent = sheetData.cells[colTotalXc]?.content;
71452
+ if (colTotalContent?.startsWith("=")) {
71453
+ colAttributes.push(["totalsRowFunction", "custom"]);
71454
+ }
71455
+ }
70883
71456
  columns.push(escapeXml /*xml*/ `<tableColumn ${formatAttributes(colAttributes)}/>`);
70884
71457
  }
70885
71458
  return escapeXml /*xml*/ `
@@ -70974,8 +71547,9 @@ function addRows(construct, data, sheet) {
70974
71547
  }
70975
71548
  else if (cell.content && cell.content !== "") {
70976
71549
  const isTableHeader = isCellTableHeader(c, r, sheet);
71550
+ const isTableTotal = isCellTableTotal(c, r, sheet);
70977
71551
  const isPlainText = !!(cell.format && isTextFormat(data.formats[cell.format]));
70978
- ({ attrs: additionalAttrs, node: cellNode } = addContent(cell.content, construct.sharedStrings, isTableHeader || isPlainText));
71552
+ ({ attrs: additionalAttrs, node: cellNode } = addContent(cell.content, construct.sharedStrings, isTableHeader || isTableTotal || isPlainText));
70979
71553
  }
70980
71554
  attributes.push(...additionalAttrs);
70981
71555
  // prettier-ignore
@@ -71009,6 +71583,16 @@ function isCellTableHeader(col, row, sheet) {
71009
71583
  return isInside(col, row, headerZone);
71010
71584
  });
71011
71585
  }
71586
+ function isCellTableTotal(col, row, sheet) {
71587
+ return sheet.tables.some((table) => {
71588
+ if (!table.config.totalRow) {
71589
+ return false;
71590
+ }
71591
+ const zone = toZone(table.range);
71592
+ const totalZone = { ...zone, top: zone.bottom };
71593
+ return isInside(col, row, totalZone);
71594
+ });
71595
+ }
71012
71596
  function addHyperlinks(construct, data, sheetIndex) {
71013
71597
  const sheet = data.sheets[sheetIndex];
71014
71598
  const cells = sheet.cells;
@@ -71094,25 +71678,23 @@ function addSheetViews(sheet) {
71094
71678
  ["showGridLines", sheet.areGridLinesVisible ? 1 : 0],
71095
71679
  ["workbookViewId", 0],
71096
71680
  ];
71097
- let sheetView = escapeXml /*xml*/ `
71681
+ return escapeXml /*xml*/ `
71098
71682
  <sheetViews>
71099
71683
  <sheetView ${formatAttributes(sheetViewAttrs)}>
71100
71684
  ${splitPanes}
71101
71685
  </sheetView>
71102
71686
  </sheetViews>
71103
71687
  `;
71104
- return sheetView;
71105
71688
  }
71106
71689
  function addSheetProperties(sheet) {
71107
71690
  if (!sheet.color) {
71108
71691
  return "";
71109
71692
  }
71110
- let sheetView = escapeXml /*xml*/ `
71693
+ return escapeXml /*xml*/ `
71111
71694
  <sheetPr>
71112
71695
  <tabColor ${formatAttributes([["rgb", toXlsxHexColor(sheet.color)]])} />
71113
71696
  </sheetPr>
71114
71697
  `;
71115
- return sheetView;
71116
71698
  }
71117
71699
 
71118
71700
  /**
@@ -71425,9 +72007,7 @@ var Status;
71425
72007
  })(Status || (Status = {}));
71426
72008
  class Model extends EventBus {
71427
72009
  corePlugins = [];
71428
- featurePlugins = [];
71429
72010
  statefulUIPlugins = [];
71430
- coreViewsPlugins = [];
71431
72011
  range;
71432
72012
  session;
71433
72013
  /**
@@ -71472,7 +72052,7 @@ class Model extends EventBus {
71472
72052
  coreHandlers = [];
71473
72053
  constructor(data = {}, config = {}, stateUpdateMessages = [], uuidGenerator = new UuidGenerator(), verboseImport = false) {
71474
72054
  const start = performance.now();
71475
- console.group("Model creation");
72055
+ console.debug("##### Model creation #####");
71476
72056
  super();
71477
72057
  setDefaultTranslationMethod();
71478
72058
  stateUpdateMessages = repairInitialMessages(data, stateUpdateMessages);
@@ -71499,7 +72079,6 @@ class Model extends EventBus {
71499
72079
  isReadonly: () => this.config.mode === "readonly" || this.config.mode === "dashboard",
71500
72080
  isDashboard: () => this.config.mode === "dashboard",
71501
72081
  };
71502
- this.uuidGenerator.setIsFastStrategy(true);
71503
72082
  // Initiate stream processor
71504
72083
  this.selection = new SelectionStreamProcessorImpl(this.getters);
71505
72084
  this.coreHandlers.push(this.range);
@@ -71514,7 +72093,6 @@ class Model extends EventBus {
71514
72093
  this.session.loadInitialMessages(stateUpdateMessages);
71515
72094
  for (let Plugin of coreViewsPluginRegistry.getAll()) {
71516
72095
  const plugin = this.setupUiPlugin(Plugin);
71517
- this.coreViewsPlugins.push(plugin);
71518
72096
  this.handlers.push(plugin);
71519
72097
  this.uiHandlers.push(plugin);
71520
72098
  this.coreHandlers.push(plugin);
@@ -71527,11 +72105,9 @@ class Model extends EventBus {
71527
72105
  }
71528
72106
  for (let Plugin of featurePluginRegistry.getAll()) {
71529
72107
  const plugin = this.setupUiPlugin(Plugin);
71530
- this.featurePlugins.push(plugin);
71531
72108
  this.handlers.push(plugin);
71532
72109
  this.uiHandlers.push(plugin);
71533
72110
  }
71534
- this.uuidGenerator.setIsFastStrategy(false);
71535
72111
  // starting plugins
71536
72112
  this.dispatch("START");
71537
72113
  // Model should be the last permanent subscriber in the list since he should render
@@ -71545,16 +72121,16 @@ class Model extends EventBus {
71545
72121
  this.joinSession();
71546
72122
  if (config.snapshotRequested) {
71547
72123
  const startSnapshot = performance.now();
71548
- console.info("Snapshot requested");
72124
+ console.debug("Snapshot requested");
71549
72125
  this.session.snapshot(this.exportData());
71550
72126
  this.garbageCollectExternalResources();
71551
- console.info("Snapshot taken in", performance.now() - startSnapshot, "ms");
72127
+ console.debug("Snapshot taken in", performance.now() - startSnapshot, "ms");
71552
72128
  }
71553
72129
  // mark all models as "raw", so they will not be turned into reactive objects
71554
72130
  // by owl, since we do not rely on reactivity
71555
72131
  markRaw(this);
71556
- console.info("Model created in", performance.now() - start, "ms");
71557
- console.groupEnd();
72132
+ console.debug("Model created in", performance.now() - start, "ms");
72133
+ console.debug("######");
71558
72134
  }
71559
72135
  joinSession() {
71560
72136
  this.session.join(this.config.client);
@@ -71613,7 +72189,7 @@ class Model extends EventBus {
71613
72189
  this.finalize();
71614
72190
  }
71615
72191
  setupSession(revisionId) {
71616
- const session = new Session(buildRevisionLog({
72192
+ return new Session(buildRevisionLog({
71617
72193
  initialRevisionId: revisionId,
71618
72194
  recordChanges: this.state.recordChanges.bind(this.state),
71619
72195
  dispatch: (command) => {
@@ -71626,7 +72202,6 @@ class Model extends EventBus {
71626
72202
  this.isReplayingCommand = false;
71627
72203
  },
71628
72204
  }), this.config.transportService, revisionId);
71629
- return session;
71630
72205
  }
71631
72206
  setupSessionEvents() {
71632
72207
  this.session.on("remote-revision-received", this, this.onRemoteRevisionReceived);
@@ -71679,7 +72254,6 @@ class Model extends EventBus {
71679
72254
  range: this.range,
71680
72255
  dispatch: this.dispatchFromCorePlugin,
71681
72256
  canDispatch: this.canDispatch,
71682
- uuidGenerator: this.uuidGenerator,
71683
72257
  custom: this.config.custom,
71684
72258
  external: this.config.external,
71685
72259
  };
@@ -71720,8 +72294,7 @@ class Model extends EventBus {
71720
72294
  return results;
71721
72295
  }
71722
72296
  checkDispatchAllowedLocalCommand(command) {
71723
- const results = this.uiHandlers.map((handler) => handler.allowDispatch(command));
71724
- return results;
72297
+ return this.uiHandlers.map((handler) => handler.allowDispatch(command));
71725
72298
  }
71726
72299
  finalize() {
71727
72300
  this.status = 3 /* Status.Finalizing */;
@@ -71778,7 +72351,7 @@ class Model extends EventBus {
71778
72351
  this.finalize();
71779
72352
  const time = performance.now() - start;
71780
72353
  if (time > 5) {
71781
- console.info(type, time, "ms");
72354
+ console.debug(type, time, "ms");
71782
72355
  }
71783
72356
  });
71784
72357
  this.session.save(command, commands, changes);
@@ -72081,7 +72654,6 @@ const components = {
72081
72654
  PivotDimensionOrder,
72082
72655
  PivotDimension,
72083
72656
  PivotLayoutConfigurator,
72084
- EditableName,
72085
72657
  PivotDeferUpdate,
72086
72658
  PivotTitleSection,
72087
72659
  CogWheelMenu,
@@ -72130,6 +72702,6 @@ const constants = {
72130
72702
  export { AbstractCellClipboardHandler, AbstractChart, AbstractFigureClipboardHandler, CellErrorType, CommandResult, CorePlugin, DispatchResult, EvaluationError, Model, PivotRuntimeDefinition, Registry, Revision, SPREADSHEET_DIMENSIONS, Spreadsheet, SpreadsheetPivotTable, UIPlugin, __info__, addFunction, addRenderingLayer, astToFormula, compile, compileTokens, components, constants, convertAstNodes, coreTypes, findCellInNewZone, functionCache, helpers, hooks, invalidateCFEvaluationCommands, invalidateDependenciesCommands, invalidateEvaluationCommands, iterateAstNodes, links, load, parse, parseTokens, readonlyAllowedCommands, registries, setDefaultSheetViewSize, setTranslationMethod, stores, tokenColors, tokenize };
72131
72703
 
72132
72704
 
72133
- __info__.version = "18.1.0-alpha.0";
72134
- __info__.date = "2024-09-25T13:17:49.636Z";
72135
- __info__.hash = "288f0b7";
72705
+ __info__.version = "18.1.0-alpha.2";
72706
+ __info__.date = "2024-10-24T08:53:21.828Z";
72707
+ __info__.hash = "2a01250";