@odoo/o-spreadsheet 18.2.1 → 18.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,9 +2,9 @@
2
2
  /**
3
3
  * This file is generated by o-spreadsheet build tools. Do not edit it.
4
4
  * @see https://github.com/odoo/o-spreadsheet
5
- * @version 18.2.1
6
- * @date 2025-02-25T06:03:13.262Z
7
- * @hash 3b4b5c9
5
+ * @version 18.2.3
6
+ * @date 2025-03-12T15:32:36.274Z
7
+ * @hash 81b0e08
8
8
  */
9
9
 
10
10
  import { useEnv, useSubEnv, onWillUnmount, useComponent, status, Component, useRef, onMounted, useEffect, App, blockDom, useState, onPatched, onWillPatch, onWillUpdateProps, useExternalListener, onWillStart, xml, useChildSubEnv, markRaw, toRaw } from '@odoo/owl';
@@ -423,7 +423,6 @@ function escapeRegExp(str) {
423
423
  * Sparse arrays remain sparse.
424
424
  */
425
425
  function deepCopy(obj) {
426
- const result = Array.isArray(obj) ? [] : {};
427
426
  switch (typeof obj) {
428
427
  case "object": {
429
428
  if (obj === null) {
@@ -435,8 +434,18 @@ function deepCopy(obj) {
435
434
  else if (!(isPlainObject(obj) || obj instanceof Array)) {
436
435
  throw new Error("Unsupported type: only objects and arrays are supported");
437
436
  }
438
- for (const key in obj) {
439
- result[key] = deepCopy(obj[key]);
437
+ const result = Array.isArray(obj) ? new Array(obj.length) : {};
438
+ if (Array.isArray(obj)) {
439
+ for (let i = 0, len = obj.length; i < len; i++) {
440
+ if (i in obj) {
441
+ result[i] = deepCopy(obj[i]);
442
+ }
443
+ }
444
+ }
445
+ else {
446
+ for (const key in obj) {
447
+ result[key] = deepCopy(obj[key]);
448
+ }
440
449
  }
441
450
  return result;
442
451
  }
@@ -2699,21 +2708,30 @@ function mergeContiguousZones(zones) {
2699
2708
  return mergedZones;
2700
2709
  }
2701
2710
 
2711
+ const globalReverseLookup$1 = new WeakMap();
2712
+ const globalIdCounter = new WeakMap();
2702
2713
  /**
2703
2714
  * Get the id of the given item (its key in the given dictionary).
2704
2715
  * If the given item does not exist in the dictionary, it creates one with a new id.
2705
2716
  */
2706
2717
  function getItemId(item, itemsDic) {
2707
- for (const key in itemsDic) {
2708
- if (deepEquals(itemsDic[key], item)) {
2709
- return parseInt(key, 10);
2710
- }
2718
+ if (!globalReverseLookup$1.has(itemsDic)) {
2719
+ globalReverseLookup$1.set(itemsDic, new Map());
2720
+ globalIdCounter.set(itemsDic, 0);
2721
+ }
2722
+ const reverseLookup = globalReverseLookup$1.get(itemsDic);
2723
+ const canonical = getCanonicalRepresentation(item);
2724
+ if (reverseLookup.has(canonical)) {
2725
+ const id = reverseLookup.get(canonical);
2726
+ itemsDic[id] = item;
2727
+ return id;
2711
2728
  }
2712
2729
  // Generate new Id if the item didn't exist in the dictionary
2713
- const ids = Object.keys(itemsDic);
2714
- const maxId = ids.length === 0 ? 0 : largeMax(ids.map((id) => parseInt(id, 10)));
2715
- itemsDic[maxId + 1] = item;
2716
- return maxId + 1;
2730
+ const newId = globalIdCounter.get(itemsDic) + 1;
2731
+ reverseLookup.set(canonical, newId);
2732
+ globalIdCounter.set(itemsDic, newId);
2733
+ itemsDic[newId] = item;
2734
+ return newId;
2717
2735
  }
2718
2736
  function groupItemIdsByZones(positionsByItemId) {
2719
2737
  const result = {};
@@ -2737,6 +2755,33 @@ function* iterateItemIdsPositions(sheetId, itemIdsByZones) {
2737
2755
  }
2738
2756
  }
2739
2757
  }
2758
+ function getCanonicalRepresentation(item) {
2759
+ if (item === null)
2760
+ return "null";
2761
+ if (item === undefined)
2762
+ return "undefined";
2763
+ if (typeof item !== "object")
2764
+ return String(item);
2765
+ if (Array.isArray(item)) {
2766
+ const len = item.length;
2767
+ let result = "[";
2768
+ for (let i = 0; i < len; i++) {
2769
+ if (i > 0)
2770
+ result += ",";
2771
+ result += getCanonicalRepresentation(item[i]);
2772
+ }
2773
+ return result + "]";
2774
+ }
2775
+ const keys = Object.keys(item).sort();
2776
+ let repr = "{";
2777
+ for (const key of keys) {
2778
+ if (item[key] !== undefined) {
2779
+ repr += `"${key}":${getCanonicalRepresentation(item[key])},`;
2780
+ }
2781
+ }
2782
+ repr += "}";
2783
+ return repr;
2784
+ }
2740
2785
 
2741
2786
  // -----------------------------------------------------------------------------
2742
2787
  // Date Type
@@ -6100,8 +6145,9 @@ function spreadRange(getters, dataSets) {
6100
6145
  if (zone.bottom !== zone.top && zone.left != zone.right) {
6101
6146
  if (zone.right) {
6102
6147
  for (let j = zone.left; j <= zone.right; ++j) {
6148
+ const datasetOptions = j === zone.left ? dataSet : { yAxisId: dataSet.yAxisId };
6103
6149
  postProcessedRanges.push({
6104
- ...dataSet,
6150
+ ...datasetOptions,
6105
6151
  dataRange: `${sheetPrefix}${zoneToXc({
6106
6152
  left: j,
6107
6153
  right: j,
@@ -6113,8 +6159,9 @@ function spreadRange(getters, dataSets) {
6113
6159
  }
6114
6160
  else {
6115
6161
  for (let j = zone.top; j <= zone.bottom; ++j) {
6162
+ const datasetOptions = j === zone.top ? dataSet : { yAxisId: dataSet.yAxisId };
6116
6163
  postProcessedRanges.push({
6117
- ...dataSet,
6164
+ ...datasetOptions,
6118
6165
  dataRange: `${sheetPrefix}${zoneToXc({
6119
6166
  left: zone.left,
6120
6167
  right: zone.right,
@@ -8287,7 +8334,8 @@ function isSortedColumnValid(sortedColumn, pivot) {
8287
8334
  const possibleValues = pivot
8288
8335
  .getPossibleFieldValues(columns[i])
8289
8336
  .map((v) => v.value);
8290
- if (!possibleValues.includes(sortedColumn.domain[i].value)) {
8337
+ if (!possibleValues.includes(sortedColumn.domain[i].value) &&
8338
+ !(sortedColumn.domain[i].value === null && possibleValues.includes(""))) {
8291
8339
  return false;
8292
8340
  }
8293
8341
  }
@@ -9532,150 +9580,6 @@ class ComposerFocusStore extends SpreadsheetStore {
9532
9580
  }
9533
9581
  }
9534
9582
 
9535
- /**
9536
- * This file is largely inspired by owl 1.
9537
- * `css` tag has been removed from owl 2 without workaround to manage css.
9538
- * So, the solution was to import the behavior of owl 1 directly in our
9539
- * codebase, with one difference: the css is added to the sheet as soon as the
9540
- * css tag is executed. In owl 1, the css was added as soon as a Component was
9541
- * created for the first time.
9542
- */
9543
- const STYLESHEETS = {};
9544
- let nextId = 0;
9545
- /**
9546
- * CSS tag helper for defining inline stylesheets. With this, one can simply define
9547
- * an inline stylesheet with just the following code:
9548
- * ```js
9549
- * css`.component-a { color: red; }`;
9550
- * ```
9551
- */
9552
- function css(strings, ...args) {
9553
- const name = `__sheet__${nextId++}`;
9554
- const value = String.raw(strings, ...args);
9555
- registerSheet(name, value);
9556
- activateSheet(name);
9557
- return name;
9558
- }
9559
- function processSheet(str) {
9560
- const tokens = str.split(/(\{|\}|;)/).map((s) => s.trim());
9561
- const selectorStack = [];
9562
- const parts = [];
9563
- let rules = [];
9564
- function generateSelector(stackIndex, parentSelector) {
9565
- const parts = [];
9566
- for (const selector of selectorStack[stackIndex]) {
9567
- let part = (parentSelector && parentSelector + " " + selector) || selector;
9568
- if (part.includes("&")) {
9569
- part = selector.replace(/&/g, parentSelector || "");
9570
- }
9571
- if (stackIndex < selectorStack.length - 1) {
9572
- part = generateSelector(stackIndex + 1, part);
9573
- }
9574
- parts.push(part);
9575
- }
9576
- return parts.join(", ");
9577
- }
9578
- function generateRules() {
9579
- if (rules.length) {
9580
- parts.push(generateSelector(0) + " {");
9581
- parts.push(...rules);
9582
- parts.push("}");
9583
- rules = [];
9584
- }
9585
- }
9586
- while (tokens.length) {
9587
- let token = tokens.shift();
9588
- if (token === "}") {
9589
- generateRules();
9590
- selectorStack.pop();
9591
- }
9592
- else {
9593
- if (tokens[0] === "{") {
9594
- generateRules();
9595
- selectorStack.push(token.split(/\s*,\s*/));
9596
- tokens.shift();
9597
- }
9598
- if (tokens[0] === ";") {
9599
- rules.push(" " + token + ";");
9600
- }
9601
- }
9602
- }
9603
- return parts.join("\n");
9604
- }
9605
- function registerSheet(id, css) {
9606
- const sheet = document.createElement("style");
9607
- sheet.textContent = processSheet(css);
9608
- STYLESHEETS[id] = sheet;
9609
- }
9610
- function activateSheet(id) {
9611
- const sheet = STYLESHEETS[id];
9612
- sheet.setAttribute("component", id);
9613
- document.head.appendChild(sheet);
9614
- }
9615
- function getTextDecoration({ strikethrough, underline, }) {
9616
- if (!strikethrough && !underline) {
9617
- return "none";
9618
- }
9619
- return `${strikethrough ? "line-through" : ""} ${underline ? "underline" : ""}`;
9620
- }
9621
- /**
9622
- * Convert the cell style to CSS properties.
9623
- */
9624
- function cellStyleToCss(style) {
9625
- const attributes = cellTextStyleToCss(style);
9626
- if (!style)
9627
- return attributes;
9628
- if (style.fillColor) {
9629
- attributes["background"] = style.fillColor;
9630
- }
9631
- return attributes;
9632
- }
9633
- /**
9634
- * Convert the cell text style to CSS properties.
9635
- */
9636
- function cellTextStyleToCss(style) {
9637
- const attributes = {};
9638
- if (!style)
9639
- return attributes;
9640
- if (style.bold) {
9641
- attributes["font-weight"] = "bold";
9642
- }
9643
- if (style.italic) {
9644
- attributes["font-style"] = "italic";
9645
- }
9646
- if (style.strikethrough || style.underline) {
9647
- let decoration = style.strikethrough ? "line-through" : "";
9648
- decoration = style.underline ? decoration + " underline" : decoration;
9649
- attributes["text-decoration"] = decoration;
9650
- }
9651
- if (style.textColor) {
9652
- attributes["color"] = style.textColor;
9653
- }
9654
- return attributes;
9655
- }
9656
- /**
9657
- * Transform CSS properties into a CSS string.
9658
- */
9659
- function cssPropertiesToCss(attributes) {
9660
- let styleStr = "";
9661
- for (const attName in attributes) {
9662
- if (!attributes[attName]) {
9663
- continue;
9664
- }
9665
- styleStr += `${attName}:${attributes[attName]}; `;
9666
- }
9667
- return styleStr;
9668
- }
9669
- function getElementMargins(el) {
9670
- const style = window.getComputedStyle(el);
9671
- return {
9672
- top: parseInt(style.marginTop, 10) || 0,
9673
- bottom: parseInt(style.marginBottom, 10) || 0,
9674
- left: parseInt(style.marginLeft, 10) || 0,
9675
- right: parseInt(style.marginRight, 10) || 0,
9676
- };
9677
- }
9678
-
9679
9583
  const TREND_LINE_XAXIS_ID = "x1";
9680
9584
  /**
9681
9585
  * This file contains helpers that are common to different charts (mainly
@@ -10220,79 +10124,341 @@ function getNextNonEmptyBar(bars, startIndex) {
10220
10124
  return bars.find((bar, i) => i > startIndex && bar.height !== 0);
10221
10125
  }
10222
10126
 
10223
- window.Chart?.register(waterfallLinesPlugin);
10224
- window.Chart?.register(chartShowValuesPlugin);
10225
- css /* scss */ `
10226
- .o-spreadsheet {
10227
- .o-chart-custom-tooltip {
10228
- font-size: 12px;
10229
- background-color: #fff;
10230
- z-index: ${ComponentsImportance.FigureTooltip};
10127
+ const GAUGE_PADDING_SIDE = 30;
10128
+ const GAUGE_PADDING_TOP = 10;
10129
+ const GAUGE_PADDING_BOTTOM = 20;
10130
+ const GAUGE_LABELS_FONT_SIZE = 12;
10131
+ const GAUGE_DEFAULT_VALUE_FONT_SIZE = 80;
10132
+ const GAUGE_BACKGROUND_COLOR = "#F3F2F1";
10133
+ const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
10134
+ const GAUGE_TITLE_SECTION_HEIGHT = 25;
10135
+ function drawGaugeChart(canvas, runtime) {
10136
+ const canvasBoundingRect = canvas.getBoundingClientRect();
10137
+ canvas.width = canvasBoundingRect.width;
10138
+ canvas.height = canvasBoundingRect.height;
10139
+ const ctx = canvas.getContext("2d");
10140
+ const config = getGaugeRenderingConfig(canvasBoundingRect, runtime, ctx);
10141
+ drawBackground(ctx, config);
10142
+ drawGauge(ctx, config);
10143
+ drawInflectionValues(ctx, config);
10144
+ drawLabels(ctx, config);
10145
+ drawTitle(ctx, config);
10146
+ }
10147
+ function drawGauge(ctx, config) {
10148
+ ctx.save();
10149
+ const gauge = config.gauge;
10150
+ const arcCenterX = gauge.rect.x + gauge.rect.width / 2;
10151
+ const arcCenterY = gauge.rect.y + gauge.rect.height;
10152
+ const arcRadius = gauge.rect.height - gauge.arcWidth / 2;
10153
+ if (arcRadius < 0) {
10154
+ return;
10231
10155
  }
10232
- }
10233
- `;
10234
- class ChartJsComponent extends Component {
10235
- static template = "o-spreadsheet-ChartJsComponent";
10236
- static props = {
10237
- figure: Object,
10156
+ const gaugeAngle = gauge.percentage === 1 ? 0 : Math.PI * (1 + gauge.percentage);
10157
+ // Gauge background
10158
+ ctx.strokeStyle = GAUGE_BACKGROUND_COLOR;
10159
+ ctx.beginPath();
10160
+ ctx.lineWidth = gauge.arcWidth;
10161
+ ctx.arc(arcCenterX, arcCenterY, arcRadius, gaugeAngle, 0);
10162
+ ctx.stroke();
10163
+ // Gauge value
10164
+ ctx.strokeStyle = gauge.color;
10165
+ ctx.beginPath();
10166
+ ctx.arc(arcCenterX, arcCenterY, arcRadius, Math.PI, gaugeAngle);
10167
+ ctx.stroke();
10168
+ ctx.restore();
10169
+ }
10170
+ function drawBackground(ctx, config) {
10171
+ ctx.save();
10172
+ ctx.fillStyle = config.backgroundColor;
10173
+ ctx.fillRect(0, 0, config.width, config.height);
10174
+ ctx.restore();
10175
+ }
10176
+ function drawLabels(ctx, config) {
10177
+ for (const label of [config.minLabel, config.maxLabel, config.gaugeValue]) {
10178
+ ctx.save();
10179
+ ctx.textAlign = "center";
10180
+ ctx.fillStyle = label.color;
10181
+ ctx.font = `${label.fontSize}px ${DEFAULT_FONT}`;
10182
+ ctx.fillText(label.label, label.textPosition.x, label.textPosition.y);
10183
+ ctx.restore();
10184
+ }
10185
+ }
10186
+ function drawInflectionValues(ctx, config) {
10187
+ const { x: rectX, y: rectY, width, height } = config.gauge.rect;
10188
+ for (const inflectionValue of config.inflectionValues) {
10189
+ ctx.save();
10190
+ ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment
10191
+ ctx.rotate(Math.PI / 2 - inflectionValue.rotation);
10192
+ ctx.lineWidth = 2;
10193
+ ctx.strokeStyle = chartMutedFontColor(config.backgroundColor) + "aa";
10194
+ ctx.beginPath();
10195
+ ctx.moveTo(0, -(height - config.gauge.arcWidth));
10196
+ ctx.lineTo(0, -height - 3);
10197
+ ctx.stroke();
10198
+ ctx.textAlign = "center";
10199
+ ctx.font = `${inflectionValue.fontSize}px ${DEFAULT_FONT}`;
10200
+ ctx.fillStyle = inflectionValue.color;
10201
+ const textY = -height - GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN - inflectionValue.offset;
10202
+ ctx.fillText(inflectionValue.label, 0, textY);
10203
+ ctx.restore();
10204
+ }
10205
+ }
10206
+ function drawTitle(ctx, config) {
10207
+ ctx.save();
10208
+ const title = config.title;
10209
+ ctx.font = getDefaultContextFont(title.fontSize, title.bold, title.italic);
10210
+ ctx.textBaseline = "middle";
10211
+ ctx.fillStyle = title.color;
10212
+ ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
10213
+ ctx.restore();
10214
+ }
10215
+ function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
10216
+ const maxValue = runtime.maxValue;
10217
+ const minValue = runtime.minValue;
10218
+ const gaugeValue = runtime.gaugeValue;
10219
+ const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
10220
+ const gaugeArcWidth = gaugeRect.width / 6;
10221
+ const gaugePercentage = gaugeValue
10222
+ ? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
10223
+ : 0;
10224
+ const gaugeValuePosition = {
10225
+ x: boundingRect.width / 2,
10226
+ y: gaugeRect.y + gaugeRect.height - gaugeRect.height / 12,
10238
10227
  };
10239
- canvas = useRef("graphContainer");
10240
- chart;
10241
- currentRuntime;
10242
- get background() {
10243
- return this.chartRuntime.background;
10228
+ let gaugeValueFontSize = GAUGE_DEFAULT_VALUE_FONT_SIZE;
10229
+ // Scale down the font size if the gaugeRect is too small
10230
+ if (gaugeRect.height < 300) {
10231
+ gaugeValueFontSize = gaugeValueFontSize * (gaugeRect.height / 300);
10244
10232
  }
10245
- get canvasStyle() {
10246
- return `background-color: ${this.background}`;
10233
+ // Scale down the font size if the text is too long
10234
+ const maxTextWidth = gaugeRect.width / 2;
10235
+ const gaugeLabel = gaugeValue?.label || "-";
10236
+ if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
10237
+ gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
10247
10238
  }
10248
- get chartRuntime() {
10249
- const runtime = this.env.model.getters.getChartRuntime(this.props.figure.id);
10250
- if (!("chartJsConfig" in runtime)) {
10251
- throw new Error("Unsupported chart runtime");
10252
- }
10253
- return runtime;
10239
+ const minLabelPosition = {
10240
+ x: gaugeRect.x + gaugeArcWidth / 2,
10241
+ y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
10242
+ };
10243
+ const maxLabelPosition = {
10244
+ x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
10245
+ y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
10246
+ };
10247
+ const textColor = chartMutedFontColor(runtime.background);
10248
+ const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);
10249
+ let x = 0, titleWidth = 0, titleHeight = 0;
10250
+ if (runtime.title.text) {
10251
+ ({ width: titleWidth, height: titleHeight } = computeTextDimension(ctx, runtime.title.text, { fontSize: CHART_TITLE_FONT_SIZE, ...runtime.title }, "px"));
10254
10252
  }
10255
- setup() {
10256
- onMounted(() => {
10257
- const runtime = this.chartRuntime;
10258
- this.currentRuntime = runtime;
10259
- // Note: chartJS modify the runtime in place, so it's important to give it a copy
10260
- this.createChart(deepCopy(runtime.chartJsConfig));
10261
- });
10262
- onWillUnmount(() => this.chart?.destroy());
10263
- useEffect(() => {
10264
- const runtime = this.chartRuntime;
10265
- if (runtime !== this.currentRuntime) {
10266
- if (runtime.chartJsConfig.type !== this.currentRuntime.chartJsConfig.type) {
10267
- this.chart?.destroy();
10268
- this.createChart(deepCopy(runtime.chartJsConfig));
10269
- }
10270
- else {
10271
- this.updateChartJs(deepCopy(runtime));
10272
- }
10273
- this.currentRuntime = runtime;
10274
- }
10253
+ switch (runtime.title.align) {
10254
+ case "right":
10255
+ x = boundingRect.width - titleWidth - CHART_PADDING$1;
10256
+ break;
10257
+ case "center":
10258
+ x = (boundingRect.width - titleWidth) / 2;
10259
+ break;
10260
+ case "left":
10261
+ default:
10262
+ x = CHART_PADDING$1;
10263
+ break;
10264
+ }
10265
+ return {
10266
+ width: boundingRect.width,
10267
+ height: boundingRect.height,
10268
+ title: {
10269
+ label: runtime.title.text ?? "",
10270
+ fontSize: runtime.title.fontSize ?? CHART_TITLE_FONT_SIZE,
10271
+ textPosition: {
10272
+ x,
10273
+ y: CHART_PADDING_TOP + titleHeight / 2,
10274
+ },
10275
+ color: runtime.title.color ?? textColor,
10276
+ bold: runtime.title.bold,
10277
+ italic: runtime.title.italic,
10278
+ },
10279
+ backgroundColor: runtime.background,
10280
+ gauge: {
10281
+ rect: gaugeRect,
10282
+ arcWidth: gaugeArcWidth,
10283
+ percentage: clip(gaugePercentage, 0, 1),
10284
+ color: getGaugeColor(runtime),
10285
+ },
10286
+ inflectionValues,
10287
+ gaugeValue: {
10288
+ label: gaugeLabel,
10289
+ textPosition: gaugeValuePosition,
10290
+ fontSize: gaugeValueFontSize,
10291
+ color: textColor,
10292
+ },
10293
+ minLabel: {
10294
+ label: runtime.minValue.label,
10295
+ textPosition: minLabelPosition,
10296
+ fontSize: GAUGE_LABELS_FONT_SIZE,
10297
+ color: textColor,
10298
+ },
10299
+ maxLabel: {
10300
+ label: runtime.maxValue.label,
10301
+ textPosition: maxLabelPosition,
10302
+ fontSize: GAUGE_LABELS_FONT_SIZE,
10303
+ color: textColor,
10304
+ },
10305
+ };
10306
+ }
10307
+ /**
10308
+ * Get the rectangle in which the gauge will be drawn, based on the bounding rectangle of the canvas and leaving
10309
+ * space for the title and labels.
10310
+ */
10311
+ function getGaugeRect(boundingRect, title) {
10312
+ const titleHeight = title ? GAUGE_TITLE_SECTION_HEIGHT : 0;
10313
+ const drawHeight = boundingRect.height - GAUGE_PADDING_BOTTOM - titleHeight - GAUGE_PADDING_TOP;
10314
+ const drawWidth = boundingRect.width - GAUGE_PADDING_SIDE * 2;
10315
+ let gaugeWidth;
10316
+ let gaugeHeight;
10317
+ if (drawWidth > 2 * drawHeight) {
10318
+ gaugeWidth = 2 * drawHeight;
10319
+ gaugeHeight = drawHeight;
10320
+ }
10321
+ else {
10322
+ gaugeWidth = drawWidth;
10323
+ gaugeHeight = drawWidth / 2;
10324
+ }
10325
+ const gaugeX = GAUGE_PADDING_SIDE + (drawWidth - gaugeWidth) / 2;
10326
+ const gaugeY = titleHeight + GAUGE_PADDING_TOP + (drawHeight - gaugeHeight) / 2;
10327
+ return {
10328
+ x: gaugeX,
10329
+ y: gaugeY,
10330
+ width: gaugeWidth,
10331
+ height: gaugeHeight,
10332
+ };
10333
+ }
10334
+ /**
10335
+ * Get the infliction values of the gauge, and where to draw them (the angle from the center of the gauge at which they are drawn).
10336
+ *
10337
+ * Also compute an offset for the text so that it doesn't overlap with other text.
10338
+ */
10339
+ function getInflectionValues(runtime, gaugeRect, textColor, ctx) {
10340
+ const maxValue = runtime.maxValue;
10341
+ const minValue = runtime.minValue;
10342
+ const gaugeCircleCenter = {
10343
+ x: gaugeRect.x + gaugeRect.width / 2,
10344
+ y: gaugeRect.y + gaugeRect.height,
10345
+ };
10346
+ const textStyle = { fontSize: GAUGE_LABELS_FONT_SIZE };
10347
+ const inflectionValues = [];
10348
+ const inflectionValuesTextRects = [];
10349
+ for (const inflectionValue of runtime.inflectionValues) {
10350
+ const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
10351
+ const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
10352
+ const angle = Math.PI - Math.PI * percentage;
10353
+ const textRect = getRectangleTangentToCircle(angle, // angle between X axis and the point where the rectangle is tangent to the circle
10354
+ gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, // radius of the gauge circle + margin below text
10355
+ gaugeCircleCenter.x, // center of the gauge circle
10356
+ gaugeCircleCenter.y, // center of the gauge circle
10357
+ labelWidth + 2, // width of the text + some margin
10358
+ GAUGE_LABELS_FONT_SIZE // height of the text
10359
+ );
10360
+ let offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect))
10361
+ ? GAUGE_LABELS_FONT_SIZE
10362
+ : 0;
10363
+ inflectionValuesTextRects.push(textRect);
10364
+ inflectionValues.push({
10365
+ rotation: angle,
10366
+ label: inflectionValue.label,
10367
+ fontSize: GAUGE_LABELS_FONT_SIZE,
10368
+ color: textColor,
10369
+ offset,
10275
10370
  });
10276
10371
  }
10277
- createChart(chartData) {
10278
- const canvas = this.canvas.el;
10279
- const ctx = canvas.getContext("2d");
10280
- this.chart = new window.Chart(ctx, chartData);
10372
+ return inflectionValues;
10373
+ }
10374
+ function getGaugeColor(runtime) {
10375
+ const gaugeValue = runtime.gaugeValue?.value;
10376
+ if (gaugeValue === undefined) {
10377
+ return GAUGE_BACKGROUND_COLOR;
10281
10378
  }
10282
- updateChartJs(chartRuntime) {
10283
- const chartData = chartRuntime.chartJsConfig;
10284
- if (chartData.data && chartData.data.datasets) {
10285
- this.chart.data = chartData.data;
10286
- if (chartData.options?.plugins?.title) {
10287
- this.chart.config.options.plugins.title = chartData.options.plugins.title;
10288
- }
10379
+ for (let i = 0; i < runtime.inflectionValues.length; i++) {
10380
+ const inflectionValue = runtime.inflectionValues[i];
10381
+ if (inflectionValue.operator === "<" && gaugeValue < inflectionValue.value) {
10382
+ return runtime.colors[i];
10289
10383
  }
10290
- else {
10291
- this.chart.data.datasets = [];
10384
+ else if (inflectionValue.operator === "<=" && gaugeValue <= inflectionValue.value) {
10385
+ return runtime.colors[i];
10292
10386
  }
10293
- this.chart.config.options = chartData.options;
10294
- this.chart.update();
10295
10387
  }
10388
+ return runtime.colors.at(-1);
10389
+ }
10390
+ function getSegmentsOfRectangle(rectangle) {
10391
+ return [
10392
+ { start: rectangle.topLeft, end: rectangle.topRight },
10393
+ { start: rectangle.topRight, end: rectangle.bottomRight },
10394
+ { start: rectangle.bottomRight, end: rectangle.bottomLeft },
10395
+ { start: rectangle.bottomLeft, end: rectangle.topLeft },
10396
+ ];
10397
+ }
10398
+ /**
10399
+ * Check if two segment intersect. The case where the segments are colinear (both segments on the same line)
10400
+ * is not handled.
10401
+ */
10402
+ function doSegmentIntersect(segment1, segment2) {
10403
+ const A = segment1.start;
10404
+ const B = segment1.end;
10405
+ const C = segment2.start;
10406
+ const D = segment2.end;
10407
+ /**
10408
+ * Line segment intersection algorithm
10409
+ * https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
10410
+ */
10411
+ function ccw(a, b, c) {
10412
+ return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
10413
+ }
10414
+ return ccw(A, C, D) !== ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D);
10415
+ }
10416
+ function doRectanglesIntersect(rect1, rect2) {
10417
+ const segments1 = getSegmentsOfRectangle(rect1);
10418
+ const segments2 = getSegmentsOfRectangle(rect2);
10419
+ for (const segment1 of segments1) {
10420
+ for (const segment2 of segments2) {
10421
+ if (doSegmentIntersect(segment1, segment2)) {
10422
+ return true;
10423
+ }
10424
+ }
10425
+ }
10426
+ return false;
10427
+ }
10428
+ /**
10429
+ * Get the rectangle that is tangent to a circle at a given angle.
10430
+ *
10431
+ * @param angle angle between X axis and the point where the rectangle is tangent to the circle
10432
+ */
10433
+ function getRectangleTangentToCircle(angle, radius, circleCenterX, circleCenterY, rectWidth, rectHeight) {
10434
+ const cos = Math.cos(angle);
10435
+ const sin = Math.sin(angle);
10436
+ // x, y are the distance from the center of the circle to the point where the rectangle is tangent to the circle
10437
+ const x = cos * radius;
10438
+ const y = sin * radius;
10439
+ // x2, y2 are the distance from the point the rectangle is tangent to the circle to the bottom left corner of the rectangle
10440
+ const x2 = sin * (rectWidth / 2); // cos(angle + 90°) = sin(angle)
10441
+ const y2 = cos * (rectWidth / 2);
10442
+ const bottomRight = {
10443
+ x: x + x2 + circleCenterX,
10444
+ y: circleCenterY - (y - y2),
10445
+ };
10446
+ const bottomLeft = {
10447
+ x: x - x2 + circleCenterX,
10448
+ y: circleCenterY - (y + y2),
10449
+ };
10450
+ // Same as above but for the top corners of the rectangle (radius + rectangle height instead of radius)
10451
+ const xp = cos * (radius + rectHeight);
10452
+ const yp = sin * (radius + rectHeight);
10453
+ const topLeft = {
10454
+ x: xp - x2 + circleCenterX,
10455
+ y: circleCenterY - (yp + y2),
10456
+ };
10457
+ const topRight = {
10458
+ x: xp + x2 + circleCenterX,
10459
+ y: circleCenterY - (yp - y2),
10460
+ };
10461
+ return { bottomLeft, bottomRight, topRight, topLeft };
10296
10462
  }
10297
10463
 
10298
10464
  /**
@@ -10874,6 +11040,299 @@ class ScorecardChartConfigBuilder {
10874
11040
  }
10875
11041
  }
10876
11042
 
11043
+ const CHART_COMMON_OPTIONS = {
11044
+ // https://www.chartjs.org/docs/latest/general/responsive.html
11045
+ responsive: true, // will resize when its container is resized
11046
+ maintainAspectRatio: false, // doesn't maintain the aspect ratio (width/height =2 by default) so the user has the choice of the exact layout
11047
+ elements: {
11048
+ line: {
11049
+ fill: false, // do not fill the area under line charts
11050
+ },
11051
+ point: {
11052
+ hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
11053
+ },
11054
+ },
11055
+ animation: false,
11056
+ };
11057
+ function chartToImage(runtime, figure, type) {
11058
+ // wrap the canvas in a div with a fixed size because chart.js would
11059
+ // fill the whole page otherwise
11060
+ const div = document.createElement("div");
11061
+ div.style.width = `${figure.width}px`;
11062
+ div.style.height = `${figure.height}px`;
11063
+ const canvas = document.createElement("canvas");
11064
+ div.append(canvas);
11065
+ canvas.setAttribute("width", figure.width.toString());
11066
+ canvas.setAttribute("height", figure.height.toString());
11067
+ // we have to add the canvas to the DOM otherwise it won't be rendered
11068
+ document.body.append(div);
11069
+ if ("chartJsConfig" in runtime) {
11070
+ const config = deepCopy(runtime.chartJsConfig);
11071
+ config.plugins = [backgroundColorChartJSPlugin];
11072
+ const Chart = getChartJSConstructor();
11073
+ const chart = new Chart(canvas, config);
11074
+ const imgContent = chart.toBase64Image();
11075
+ chart.destroy();
11076
+ div.remove();
11077
+ return imgContent;
11078
+ }
11079
+ else if (type === "scorecard") {
11080
+ const design = getScorecardConfiguration(figure, runtime);
11081
+ drawScoreChart(design, canvas);
11082
+ const imgContent = canvas.toDataURL();
11083
+ div.remove();
11084
+ return imgContent;
11085
+ }
11086
+ else if (type === "gauge") {
11087
+ drawGaugeChart(canvas, runtime);
11088
+ const imgContent = canvas.toDataURL();
11089
+ div.remove();
11090
+ return imgContent;
11091
+ }
11092
+ return undefined;
11093
+ }
11094
+ /**
11095
+ * Custom chart.js plugin to set the background color of the canvas
11096
+ * https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md
11097
+ */
11098
+ const backgroundColorChartJSPlugin = {
11099
+ id: "customCanvasBackgroundColor",
11100
+ beforeDraw: (chart) => {
11101
+ const { ctx } = chart;
11102
+ ctx.save();
11103
+ ctx.globalCompositeOperation = "destination-over";
11104
+ ctx.fillStyle = "#ffffff";
11105
+ ctx.fillRect(0, 0, chart.width, chart.height);
11106
+ ctx.restore();
11107
+ },
11108
+ };
11109
+ /** Return window.Chart, making sure all our extensions are loaded in ChartJS */
11110
+ function getChartJSConstructor() {
11111
+ if (window.Chart && !window.Chart?.registry.plugins.get("chartShowValuesPlugin")) {
11112
+ window.Chart.register(chartShowValuesPlugin);
11113
+ window.Chart.register(waterfallLinesPlugin);
11114
+ }
11115
+ return window.Chart;
11116
+ }
11117
+
11118
+ /**
11119
+ * This file is largely inspired by owl 1.
11120
+ * `css` tag has been removed from owl 2 without workaround to manage css.
11121
+ * So, the solution was to import the behavior of owl 1 directly in our
11122
+ * codebase, with one difference: the css is added to the sheet as soon as the
11123
+ * css tag is executed. In owl 1, the css was added as soon as a Component was
11124
+ * created for the first time.
11125
+ */
11126
+ const STYLESHEETS = {};
11127
+ let nextId = 0;
11128
+ /**
11129
+ * CSS tag helper for defining inline stylesheets. With this, one can simply define
11130
+ * an inline stylesheet with just the following code:
11131
+ * ```js
11132
+ * css`.component-a { color: red; }`;
11133
+ * ```
11134
+ */
11135
+ function css(strings, ...args) {
11136
+ const name = `__sheet__${nextId++}`;
11137
+ const value = String.raw(strings, ...args);
11138
+ registerSheet(name, value);
11139
+ activateSheet(name);
11140
+ return name;
11141
+ }
11142
+ function processSheet(str) {
11143
+ const tokens = str.split(/(\{|\}|;)/).map((s) => s.trim());
11144
+ const selectorStack = [];
11145
+ const parts = [];
11146
+ let rules = [];
11147
+ function generateSelector(stackIndex, parentSelector) {
11148
+ const parts = [];
11149
+ for (const selector of selectorStack[stackIndex]) {
11150
+ let part = (parentSelector && parentSelector + " " + selector) || selector;
11151
+ if (part.includes("&")) {
11152
+ part = selector.replace(/&/g, parentSelector || "");
11153
+ }
11154
+ if (stackIndex < selectorStack.length - 1) {
11155
+ part = generateSelector(stackIndex + 1, part);
11156
+ }
11157
+ parts.push(part);
11158
+ }
11159
+ return parts.join(", ");
11160
+ }
11161
+ function generateRules() {
11162
+ if (rules.length) {
11163
+ parts.push(generateSelector(0) + " {");
11164
+ parts.push(...rules);
11165
+ parts.push("}");
11166
+ rules = [];
11167
+ }
11168
+ }
11169
+ while (tokens.length) {
11170
+ let token = tokens.shift();
11171
+ if (token === "}") {
11172
+ generateRules();
11173
+ selectorStack.pop();
11174
+ }
11175
+ else {
11176
+ if (tokens[0] === "{") {
11177
+ generateRules();
11178
+ selectorStack.push(token.split(/\s*,\s*/));
11179
+ tokens.shift();
11180
+ }
11181
+ if (tokens[0] === ";") {
11182
+ rules.push(" " + token + ";");
11183
+ }
11184
+ }
11185
+ }
11186
+ return parts.join("\n");
11187
+ }
11188
+ function registerSheet(id, css) {
11189
+ const sheet = document.createElement("style");
11190
+ sheet.textContent = processSheet(css);
11191
+ STYLESHEETS[id] = sheet;
11192
+ }
11193
+ function activateSheet(id) {
11194
+ const sheet = STYLESHEETS[id];
11195
+ sheet.setAttribute("component", id);
11196
+ document.head.appendChild(sheet);
11197
+ }
11198
+ function getTextDecoration({ strikethrough, underline, }) {
11199
+ if (!strikethrough && !underline) {
11200
+ return "none";
11201
+ }
11202
+ return `${strikethrough ? "line-through" : ""} ${underline ? "underline" : ""}`;
11203
+ }
11204
+ /**
11205
+ * Convert the cell style to CSS properties.
11206
+ */
11207
+ function cellStyleToCss(style) {
11208
+ const attributes = cellTextStyleToCss(style);
11209
+ if (!style)
11210
+ return attributes;
11211
+ if (style.fillColor) {
11212
+ attributes["background"] = style.fillColor;
11213
+ }
11214
+ return attributes;
11215
+ }
11216
+ /**
11217
+ * Convert the cell text style to CSS properties.
11218
+ */
11219
+ function cellTextStyleToCss(style) {
11220
+ const attributes = {};
11221
+ if (!style)
11222
+ return attributes;
11223
+ if (style.bold) {
11224
+ attributes["font-weight"] = "bold";
11225
+ }
11226
+ if (style.italic) {
11227
+ attributes["font-style"] = "italic";
11228
+ }
11229
+ if (style.strikethrough || style.underline) {
11230
+ let decoration = style.strikethrough ? "line-through" : "";
11231
+ decoration = style.underline ? decoration + " underline" : decoration;
11232
+ attributes["text-decoration"] = decoration;
11233
+ }
11234
+ if (style.textColor) {
11235
+ attributes["color"] = style.textColor;
11236
+ }
11237
+ return attributes;
11238
+ }
11239
+ /**
11240
+ * Transform CSS properties into a CSS string.
11241
+ */
11242
+ function cssPropertiesToCss(attributes) {
11243
+ let styleStr = "";
11244
+ for (const attName in attributes) {
11245
+ if (!attributes[attName]) {
11246
+ continue;
11247
+ }
11248
+ styleStr += `${attName}:${attributes[attName]}; `;
11249
+ }
11250
+ return styleStr;
11251
+ }
11252
+ function getElementMargins(el) {
11253
+ const style = window.getComputedStyle(el);
11254
+ return {
11255
+ top: parseInt(style.marginTop, 10) || 0,
11256
+ bottom: parseInt(style.marginBottom, 10) || 0,
11257
+ left: parseInt(style.marginLeft, 10) || 0,
11258
+ right: parseInt(style.marginRight, 10) || 0,
11259
+ };
11260
+ }
11261
+
11262
+ css /* scss */ `
11263
+ .o-spreadsheet {
11264
+ .o-chart-custom-tooltip {
11265
+ font-size: 12px;
11266
+ background-color: #fff;
11267
+ z-index: ${ComponentsImportance.FigureTooltip};
11268
+ }
11269
+ }
11270
+ `;
11271
+ class ChartJsComponent extends Component {
11272
+ static template = "o-spreadsheet-ChartJsComponent";
11273
+ static props = {
11274
+ figure: Object,
11275
+ };
11276
+ canvas = useRef("graphContainer");
11277
+ chart;
11278
+ currentRuntime;
11279
+ get background() {
11280
+ return this.chartRuntime.background;
11281
+ }
11282
+ get canvasStyle() {
11283
+ return `background-color: ${this.background}`;
11284
+ }
11285
+ get chartRuntime() {
11286
+ const runtime = this.env.model.getters.getChartRuntime(this.props.figure.id);
11287
+ if (!("chartJsConfig" in runtime)) {
11288
+ throw new Error("Unsupported chart runtime");
11289
+ }
11290
+ return runtime;
11291
+ }
11292
+ setup() {
11293
+ onMounted(() => {
11294
+ const runtime = this.chartRuntime;
11295
+ this.currentRuntime = runtime;
11296
+ // Note: chartJS modify the runtime in place, so it's important to give it a copy
11297
+ this.createChart(deepCopy(runtime.chartJsConfig));
11298
+ });
11299
+ onWillUnmount(() => this.chart?.destroy());
11300
+ useEffect(() => {
11301
+ const runtime = this.chartRuntime;
11302
+ if (runtime !== this.currentRuntime) {
11303
+ if (runtime.chartJsConfig.type !== this.currentRuntime.chartJsConfig.type) {
11304
+ this.chart?.destroy();
11305
+ this.createChart(deepCopy(runtime.chartJsConfig));
11306
+ }
11307
+ else {
11308
+ this.updateChartJs(deepCopy(runtime));
11309
+ }
11310
+ this.currentRuntime = runtime;
11311
+ }
11312
+ });
11313
+ }
11314
+ createChart(chartData) {
11315
+ const canvas = this.canvas.el;
11316
+ const ctx = canvas.getContext("2d");
11317
+ const Chart = getChartJSConstructor();
11318
+ this.chart = new Chart(ctx, chartData);
11319
+ }
11320
+ updateChartJs(chartRuntime) {
11321
+ const chartData = chartRuntime.chartJsConfig;
11322
+ if (chartData.data && chartData.data.datasets) {
11323
+ this.chart.data = chartData.data;
11324
+ if (chartData.options?.plugins?.title) {
11325
+ this.chart.config.options.plugins.title = chartData.options.plugins.title;
11326
+ }
11327
+ }
11328
+ else {
11329
+ this.chart.data.datasets = [];
11330
+ }
11331
+ this.chart.config.options = chartData.options;
11332
+ this.chart.update();
11333
+ }
11334
+ }
11335
+
10877
11336
  class ScorecardChart extends Component {
10878
11337
  static template = "o-spreadsheet-ScorecardChart";
10879
11338
  static props = {
@@ -10905,6 +11364,7 @@ class ScorecardChart extends Component {
10905
11364
  const autoCompleteProviders = new Registry();
10906
11365
 
10907
11366
  autoCompleteProviders.add("dataValidation", {
11367
+ displayAllOnInitialContent: true,
10908
11368
  getProposals(tokenAtCursor, content) {
10909
11369
  if (content.startsWith("=")) {
10910
11370
  return [];
@@ -21201,6 +21661,15 @@ class AbstractComposerStore extends SpreadsheetStore {
21201
21661
  const exactMatch = proposals?.find((p) => p.text === tokenAtCursor.value);
21202
21662
  // remove tokens that are likely to be other parts of the formula that slipped in the token if it's a string
21203
21663
  const searchTerm = tokenAtCursor.value.replace(/[ ,\(\)]/g, "");
21664
+ if (this._currentContent === this.initialContent &&
21665
+ provider.displayAllOnInitialContent &&
21666
+ proposals?.length) {
21667
+ return {
21668
+ proposals,
21669
+ selectProposal: provider.selectProposal,
21670
+ autoSelectFirstProposal: provider.autoSelectFirstProposal ?? false,
21671
+ };
21672
+ }
21204
21673
  if (exactMatch && this._currentContent !== this.initialContent) {
21205
21674
  // this means the user has chosen a proposal
21206
21675
  return;
@@ -22201,586 +22670,255 @@ const alphaNumericValueRegExp = /^(.*\D+)(\d+)$/;
22201
22670
  /**
22202
22671
  * Get the consecutive evaluated cells that can pass the filter function (e.g. certain type filter).
22203
22672
  * Return the one which contains the given cell
22204
- */
22205
- function getGroup(cell, cells, filter) {
22206
- let group = [];
22207
- let found = false;
22208
- for (let x of cells) {
22209
- if (x === cell) {
22210
- found = true;
22211
- }
22212
- const cellValue = x === undefined || x.isFormula
22213
- ? undefined
22214
- : evaluateLiteral(x, { locale: DEFAULT_LOCALE, format: x.format });
22215
- if (cellValue && filter(cellValue)) {
22216
- group.push(cellValue);
22217
- }
22218
- else {
22219
- if (found) {
22220
- return group;
22221
- }
22222
- group = [];
22223
- }
22224
- }
22225
- return group;
22226
- }
22227
- /**
22228
- * Get the average steps between numbers
22229
- */
22230
- function getAverageIncrement(group) {
22231
- const averages = [];
22232
- let last = group[0];
22233
- for (let i = 1; i < group.length; i++) {
22234
- const current = group[i];
22235
- averages.push(current - last);
22236
- last = current;
22237
- }
22238
- return averages.reduce((a, b) => a + b, 0) / averages.length;
22239
- }
22240
- /**
22241
- * Get the step for a group
22242
- */
22243
- function calculateIncrementBasedOnGroup(group) {
22244
- let increment = 1;
22245
- if (group.length >= 2) {
22246
- increment = getAverageIncrement(group) * group.length;
22247
- }
22248
- return increment;
22249
- }
22250
- /**
22251
- * Iterates on a list of date intervals.
22252
- * if every interval is the same, return the interval
22253
- * Otherwise return undefined
22254
- *
22255
- */
22256
- function getEqualInterval(intervals) {
22257
- if (intervals.length < 2) {
22258
- return intervals[0] || { years: 0, months: 0, days: 0 };
22259
- }
22260
- const equal = intervals.every((interval) => interval.years === intervals[0].years &&
22261
- interval.months === intervals[0].months &&
22262
- interval.days === intervals[0].days);
22263
- return equal ? intervals[0] : undefined;
22264
- }
22265
- /**
22266
- * Based on a group of dates, calculate the increment that should be applied
22267
- * to the next date.
22268
- *
22269
- * This will compute the date difference in calendar terms (years, months, days)
22270
- * In order to make abstraction of leap years and months with different number of days.
22271
- *
22272
- * In case the dates are not equidistant in calendar terms, no rule can be extrapolated
22273
- * In case of equidistant dates, we either have in that order:
22274
- * - exact date interval (e.g. +n year OR +n month OR +n day) in which case we increment by the same interval
22275
- * - exact day interval (e.g. +n days) in which case we increment by the same day interval
22276
- * - equidistant dates but not the same interval, in which case we return increment of the same interval
22277
- *
22278
- * */
22279
- function calculateDateIncrementBasedOnGroup(group) {
22280
- if (group.length < 2) {
22281
- return 1;
22282
- }
22283
- const jsDates = group.map((date) => toJsDate(date, DEFAULT_LOCALE));
22284
- const datesIntervals = getDateIntervals(jsDates);
22285
- const datesEquidistantInterval = getEqualInterval(datesIntervals);
22286
- if (datesEquidistantInterval === undefined) {
22287
- // dates are not equidistant in terms of years, months or days, thus no rule can be extrapolated
22288
- return undefined;
22289
- }
22290
- // The dates are apart by an exact interval of years, months or days
22291
- // but not a combination of them
22292
- const exactDateInterval = Object.values(datesEquidistantInterval).filter((value) => value !== 0).length === 1;
22293
- const isSameDay = Object.values(datesEquidistantInterval).every((el) => el === 0); // handles time values (strict decimals)
22294
- if (!exactDateInterval || isSameDay) {
22295
- const timeIntervals = jsDates
22296
- .map((date, index) => {
22297
- if (index === 0) {
22298
- return 0;
22299
- }
22300
- const previous = jsDates[index - 1];
22301
- return Math.floor(date.getTime()) - Math.floor(previous.getTime());
22302
- })
22303
- .slice(1);
22304
- const equidistantDates = timeIntervals.every((interval) => interval === timeIntervals[0]);
22305
- if (equidistantDates) {
22306
- return group.length * (group[1] - group[0]);
22307
- }
22308
- }
22309
- return {
22310
- years: datesEquidistantInterval.years * group.length,
22311
- months: datesEquidistantInterval.months * group.length,
22312
- days: datesEquidistantInterval.days * group.length,
22313
- };
22314
- }
22315
- autofillRulesRegistry
22316
- .add("simple_value_copy", {
22317
- condition: (cell, cells) => {
22318
- return (cells.length === 1 && !cell.isFormula && !(cell.format && isDateTimeFormat(cell.format)));
22319
- },
22320
- generateRule: () => {
22321
- return { type: "COPY_MODIFIER" };
22322
- },
22323
- sequence: 10,
22324
- })
22325
- .add("increment_alphanumeric_value", {
22326
- condition: (cell) => !cell.isFormula &&
22327
- evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.text &&
22328
- alphaNumericValueRegExp.test(cell.content),
22329
- generateRule: (cell, cells) => {
22330
- const numberPostfix = parseInt(cell.content.match(numberPostfixRegExp)[0]);
22331
- const prefix = cell.content.match(stringPrefixRegExp)[0];
22332
- const numberPostfixLength = cell.content.length - prefix.length;
22333
- const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.text &&
22334
- alphaNumericValueRegExp.test(evaluatedCell.value)) // get consecutive alphanumeric cells, no matter what the prefix is
22335
- .filter((cell) => prefix === (cell.value ?? "").toString().match(stringPrefixRegExp)[0])
22336
- .map((cell) => parseInt((cell.value ?? "").toString().match(numberPostfixRegExp)[0]));
22337
- const increment = calculateIncrementBasedOnGroup(group);
22338
- return {
22339
- type: "ALPHANUMERIC_INCREMENT_MODIFIER",
22340
- prefix,
22341
- current: numberPostfix,
22342
- increment,
22343
- numberPostfixLength,
22344
- };
22345
- },
22346
- sequence: 15,
22347
- })
22348
- .add("copy_text", {
22349
- condition: (cell) => !cell.isFormula &&
22350
- evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.text,
22351
- generateRule: () => {
22352
- return { type: "COPY_MODIFIER" };
22353
- },
22354
- sequence: 20,
22355
- })
22356
- .add("update_formula", {
22357
- condition: (cell) => cell.isFormula,
22358
- generateRule: (_, cells) => {
22359
- return { type: "FORMULA_MODIFIER", increment: cells.length, current: 0 };
22360
- },
22361
- sequence: 30,
22362
- })
22363
- .add("increment_dates", {
22364
- condition: (cell, cells) => {
22365
- return (!cell.isFormula &&
22366
- evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.number &&
22367
- !!cell.format &&
22368
- isDateTimeFormat(cell.format));
22369
- },
22370
- generateRule: (cell, cells) => {
22371
- const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number &&
22372
- !!evaluatedCell.format &&
22373
- isDateTimeFormat(evaluatedCell.format)).map((cell) => Number(cell.value));
22374
- const increment = calculateDateIncrementBasedOnGroup(group);
22375
- if (increment === undefined) {
22376
- return { type: "COPY_MODIFIER" };
22377
- }
22378
- /** requires to detect the current date (requires to be an integer value with the right format)
22379
- * detect if year or if month or if day then extrapolate increment required (+1 month, +1 year + 1 day)
22380
- */
22381
- const evaluation = evaluateLiteral(cell, { locale: DEFAULT_LOCALE });
22382
- if (typeof increment === "object") {
22383
- return {
22384
- type: "DATE_INCREMENT_MODIFIER",
22385
- increment,
22386
- current: evaluation.type === CellValueType.number ? evaluation.value : 0,
22387
- };
22388
- }
22389
- return {
22390
- type: "INCREMENT_MODIFIER",
22391
- increment,
22392
- current: evaluation.type === CellValueType.number ? evaluation.value : 0,
22393
- };
22394
- },
22395
- sequence: 25,
22396
- })
22397
- .add("increment_number", {
22398
- condition: (cell) => !cell.isFormula &&
22399
- evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.number,
22400
- generateRule: (cell, cells) => {
22401
- const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number &&
22402
- !isDateTimeFormat(evaluatedCell.format || "")).map((cell) => Number(cell.value));
22403
- const increment = calculateIncrementBasedOnGroup(group);
22404
- const evaluation = evaluateLiteral(cell, { locale: DEFAULT_LOCALE });
22405
- return {
22406
- type: "INCREMENT_MODIFIER",
22407
- increment,
22408
- current: evaluation.type === CellValueType.number ? evaluation.value : 0,
22409
- };
22410
- },
22411
- sequence: 40,
22412
- });
22413
- /**
22414
- * Returns the date intervals between consecutive dates of an array
22415
- * in the format of { years: number, months: number, days: number }
22416
- *
22417
- * The split is necessary to make abstraction of leap years and
22418
- * months with different number of days.
22419
- *
22420
- * @param dates
22421
- */
22422
- function getDateIntervals(dates) {
22423
- if (dates.length < 2) {
22424
- return [{ years: 0, months: 0, days: 0 }];
22425
- }
22426
- const res = dates.map((date, index) => {
22427
- if (index === 0) {
22428
- return { years: 0, months: 0, days: 0 };
22429
- }
22430
- const previous = DateTime.fromTimestamp(dates[index - 1].getTime());
22431
- const years = getTimeDifferenceInWholeYears(previous, date);
22432
- const months = getTimeDifferenceInWholeMonths(previous, date) % 12;
22433
- previous.setFullYear(previous.getFullYear() + years);
22434
- previous.setMonth(previous.getMonth() + months);
22435
- const days = getTimeDifferenceInWholeDays(previous, date);
22436
- return {
22437
- years,
22438
- months,
22439
- days,
22440
- };
22441
- });
22442
- return res.slice(1);
22443
- }
22444
-
22445
- const cellPopoverRegistry = new Registry();
22446
-
22447
- const GAUGE_PADDING_SIDE = 30;
22448
- const GAUGE_PADDING_TOP = 10;
22449
- const GAUGE_PADDING_BOTTOM = 20;
22450
- const GAUGE_LABELS_FONT_SIZE = 12;
22451
- const GAUGE_DEFAULT_VALUE_FONT_SIZE = 80;
22452
- const GAUGE_BACKGROUND_COLOR = "#F3F2F1";
22453
- const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
22454
- const GAUGE_TITLE_SECTION_HEIGHT = 25;
22455
- function drawGaugeChart(canvas, runtime) {
22456
- const canvasBoundingRect = canvas.getBoundingClientRect();
22457
- canvas.width = canvasBoundingRect.width;
22458
- canvas.height = canvasBoundingRect.height;
22459
- const ctx = canvas.getContext("2d");
22460
- const config = getGaugeRenderingConfig(canvasBoundingRect, runtime, ctx);
22461
- drawBackground(ctx, config);
22462
- drawGauge(ctx, config);
22463
- drawInflectionValues(ctx, config);
22464
- drawLabels(ctx, config);
22465
- drawTitle(ctx, config);
22466
- }
22467
- function drawGauge(ctx, config) {
22468
- ctx.save();
22469
- const gauge = config.gauge;
22470
- const arcCenterX = gauge.rect.x + gauge.rect.width / 2;
22471
- const arcCenterY = gauge.rect.y + gauge.rect.height;
22472
- const arcRadius = gauge.rect.height - gauge.arcWidth / 2;
22473
- if (arcRadius < 0) {
22474
- return;
22475
- }
22476
- const gaugeAngle = gauge.percentage === 1 ? 0 : Math.PI * (1 + gauge.percentage);
22477
- // Gauge background
22478
- ctx.strokeStyle = GAUGE_BACKGROUND_COLOR;
22479
- ctx.beginPath();
22480
- ctx.lineWidth = gauge.arcWidth;
22481
- ctx.arc(arcCenterX, arcCenterY, arcRadius, gaugeAngle, 0);
22482
- ctx.stroke();
22483
- // Gauge value
22484
- ctx.strokeStyle = gauge.color;
22485
- ctx.beginPath();
22486
- ctx.arc(arcCenterX, arcCenterY, arcRadius, Math.PI, gaugeAngle);
22487
- ctx.stroke();
22488
- ctx.restore();
22489
- }
22490
- function drawBackground(ctx, config) {
22491
- ctx.save();
22492
- ctx.fillStyle = config.backgroundColor;
22493
- ctx.fillRect(0, 0, config.width, config.height);
22494
- ctx.restore();
22495
- }
22496
- function drawLabels(ctx, config) {
22497
- for (const label of [config.minLabel, config.maxLabel, config.gaugeValue]) {
22498
- ctx.save();
22499
- ctx.textAlign = "center";
22500
- ctx.fillStyle = label.color;
22501
- ctx.font = `${label.fontSize}px ${DEFAULT_FONT}`;
22502
- ctx.fillText(label.label, label.textPosition.x, label.textPosition.y);
22503
- ctx.restore();
22504
- }
22505
- }
22506
- function drawInflectionValues(ctx, config) {
22507
- const { x: rectX, y: rectY, width, height } = config.gauge.rect;
22508
- for (const inflectionValue of config.inflectionValues) {
22509
- ctx.save();
22510
- ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment
22511
- ctx.rotate(Math.PI / 2 - inflectionValue.rotation);
22512
- ctx.lineWidth = 2;
22513
- ctx.strokeStyle = chartMutedFontColor(config.backgroundColor) + "aa";
22514
- ctx.beginPath();
22515
- ctx.moveTo(0, -(height - config.gauge.arcWidth));
22516
- ctx.lineTo(0, -height - 3);
22517
- ctx.stroke();
22518
- ctx.textAlign = "center";
22519
- ctx.font = `${inflectionValue.fontSize}px ${DEFAULT_FONT}`;
22520
- ctx.fillStyle = inflectionValue.color;
22521
- const textY = -height - GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN - inflectionValue.offset;
22522
- ctx.fillText(inflectionValue.label, 0, textY);
22523
- ctx.restore();
22524
- }
22525
- }
22526
- function drawTitle(ctx, config) {
22527
- ctx.save();
22528
- const title = config.title;
22529
- ctx.font = getDefaultContextFont(title.fontSize, title.bold, title.italic);
22530
- ctx.textBaseline = "middle";
22531
- ctx.fillStyle = title.color;
22532
- ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
22533
- ctx.restore();
22534
- }
22535
- function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
22536
- const maxValue = runtime.maxValue;
22537
- const minValue = runtime.minValue;
22538
- const gaugeValue = runtime.gaugeValue;
22539
- const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
22540
- const gaugeArcWidth = gaugeRect.width / 6;
22541
- const gaugePercentage = gaugeValue
22542
- ? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
22543
- : 0;
22544
- const gaugeValuePosition = {
22545
- x: boundingRect.width / 2,
22546
- y: gaugeRect.y + gaugeRect.height - gaugeRect.height / 12,
22547
- };
22548
- let gaugeValueFontSize = GAUGE_DEFAULT_VALUE_FONT_SIZE;
22549
- // Scale down the font size if the gaugeRect is too small
22550
- if (gaugeRect.height < 300) {
22551
- gaugeValueFontSize = gaugeValueFontSize * (gaugeRect.height / 300);
22552
- }
22553
- // Scale down the font size if the text is too long
22554
- const maxTextWidth = gaugeRect.width / 2;
22555
- const gaugeLabel = gaugeValue?.label || "-";
22556
- if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
22557
- gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
22558
- }
22559
- const minLabelPosition = {
22560
- x: gaugeRect.x + gaugeArcWidth / 2,
22561
- y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
22562
- };
22563
- const maxLabelPosition = {
22564
- x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
22565
- y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
22566
- };
22567
- const textColor = chartMutedFontColor(runtime.background);
22568
- const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);
22569
- let x = 0, titleWidth = 0, titleHeight = 0;
22570
- if (runtime.title.text) {
22571
- ({ width: titleWidth, height: titleHeight } = computeTextDimension(ctx, runtime.title.text, { fontSize: CHART_TITLE_FONT_SIZE, ...runtime.title }, "px"));
22572
- }
22573
- switch (runtime.title.align) {
22574
- case "right":
22575
- x = boundingRect.width - titleWidth - CHART_PADDING$1;
22576
- break;
22577
- case "center":
22578
- x = (boundingRect.width - titleWidth) / 2;
22579
- break;
22580
- case "left":
22581
- default:
22582
- x = CHART_PADDING$1;
22583
- break;
22584
- }
22585
- return {
22586
- width: boundingRect.width,
22587
- height: boundingRect.height,
22588
- title: {
22589
- label: runtime.title.text ?? "",
22590
- fontSize: runtime.title.fontSize ?? CHART_TITLE_FONT_SIZE,
22591
- textPosition: {
22592
- x,
22593
- y: CHART_PADDING_TOP + titleHeight / 2,
22594
- },
22595
- color: runtime.title.color ?? textColor,
22596
- bold: runtime.title.bold,
22597
- italic: runtime.title.italic,
22598
- },
22599
- backgroundColor: runtime.background,
22600
- gauge: {
22601
- rect: gaugeRect,
22602
- arcWidth: gaugeArcWidth,
22603
- percentage: clip(gaugePercentage, 0, 1),
22604
- color: getGaugeColor(runtime),
22605
- },
22606
- inflectionValues,
22607
- gaugeValue: {
22608
- label: gaugeLabel,
22609
- textPosition: gaugeValuePosition,
22610
- fontSize: gaugeValueFontSize,
22611
- color: textColor,
22612
- },
22613
- minLabel: {
22614
- label: runtime.minValue.label,
22615
- textPosition: minLabelPosition,
22616
- fontSize: GAUGE_LABELS_FONT_SIZE,
22617
- color: textColor,
22618
- },
22619
- maxLabel: {
22620
- label: runtime.maxValue.label,
22621
- textPosition: maxLabelPosition,
22622
- fontSize: GAUGE_LABELS_FONT_SIZE,
22623
- color: textColor,
22624
- },
22625
- };
22626
- }
22627
- /**
22628
- * Get the rectangle in which the gauge will be drawn, based on the bounding rectangle of the canvas and leaving
22629
- * space for the title and labels.
22630
- */
22631
- function getGaugeRect(boundingRect, title) {
22632
- const titleHeight = title ? GAUGE_TITLE_SECTION_HEIGHT : 0;
22633
- const drawHeight = boundingRect.height - GAUGE_PADDING_BOTTOM - titleHeight - GAUGE_PADDING_TOP;
22634
- const drawWidth = boundingRect.width - GAUGE_PADDING_SIDE * 2;
22635
- let gaugeWidth;
22636
- let gaugeHeight;
22637
- if (drawWidth > 2 * drawHeight) {
22638
- gaugeWidth = 2 * drawHeight;
22639
- gaugeHeight = drawHeight;
22640
- }
22641
- else {
22642
- gaugeWidth = drawWidth;
22643
- gaugeHeight = drawWidth / 2;
22644
- }
22645
- const gaugeX = GAUGE_PADDING_SIDE + (drawWidth - gaugeWidth) / 2;
22646
- const gaugeY = titleHeight + GAUGE_PADDING_TOP + (drawHeight - gaugeHeight) / 2;
22647
- return {
22648
- x: gaugeX,
22649
- y: gaugeY,
22650
- width: gaugeWidth,
22651
- height: gaugeHeight,
22652
- };
22653
- }
22654
- /**
22655
- * Get the infliction values of the gauge, and where to draw them (the angle from the center of the gauge at which they are drawn).
22656
- *
22657
- * Also compute an offset for the text so that it doesn't overlap with other text.
22658
- */
22659
- function getInflectionValues(runtime, gaugeRect, textColor, ctx) {
22660
- const maxValue = runtime.maxValue;
22661
- const minValue = runtime.minValue;
22662
- const gaugeCircleCenter = {
22663
- x: gaugeRect.x + gaugeRect.width / 2,
22664
- y: gaugeRect.y + gaugeRect.height,
22665
- };
22666
- const textStyle = { fontSize: GAUGE_LABELS_FONT_SIZE };
22667
- const inflectionValues = [];
22668
- const inflectionValuesTextRects = [];
22669
- for (const inflectionValue of runtime.inflectionValues) {
22670
- const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
22671
- const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
22672
- const angle = Math.PI - Math.PI * percentage;
22673
- const textRect = getRectangleTangentToCircle(angle, // angle between X axis and the point where the rectangle is tangent to the circle
22674
- gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, // radius of the gauge circle + margin below text
22675
- gaugeCircleCenter.x, // center of the gauge circle
22676
- gaugeCircleCenter.y, // center of the gauge circle
22677
- labelWidth + 2, // width of the text + some margin
22678
- GAUGE_LABELS_FONT_SIZE // height of the text
22679
- );
22680
- let offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect))
22681
- ? GAUGE_LABELS_FONT_SIZE
22682
- : 0;
22683
- inflectionValuesTextRects.push(textRect);
22684
- inflectionValues.push({
22685
- rotation: angle,
22686
- label: inflectionValue.label,
22687
- fontSize: GAUGE_LABELS_FONT_SIZE,
22688
- color: textColor,
22689
- offset,
22690
- });
22691
- }
22692
- return inflectionValues;
22693
- }
22694
- function getGaugeColor(runtime) {
22695
- const gaugeValue = runtime.gaugeValue?.value;
22696
- if (gaugeValue === undefined) {
22697
- return GAUGE_BACKGROUND_COLOR;
22698
- }
22699
- for (let i = 0; i < runtime.inflectionValues.length; i++) {
22700
- const inflectionValue = runtime.inflectionValues[i];
22701
- if (inflectionValue.operator === "<" && gaugeValue < inflectionValue.value) {
22702
- return runtime.colors[i];
22673
+ */
22674
+ function getGroup(cell, cells, filter) {
22675
+ let group = [];
22676
+ let found = false;
22677
+ for (let x of cells) {
22678
+ if (x === cell) {
22679
+ found = true;
22703
22680
  }
22704
- else if (inflectionValue.operator === "<=" && gaugeValue <= inflectionValue.value) {
22705
- return runtime.colors[i];
22681
+ const cellValue = x === undefined || x.isFormula
22682
+ ? undefined
22683
+ : evaluateLiteral(x, { locale: DEFAULT_LOCALE, format: x.format });
22684
+ if (cellValue && filter(cellValue)) {
22685
+ group.push(cellValue);
22686
+ }
22687
+ else {
22688
+ if (found) {
22689
+ return group;
22690
+ }
22691
+ group = [];
22706
22692
  }
22707
22693
  }
22708
- return runtime.colors.at(-1);
22694
+ return group;
22709
22695
  }
22710
- function getSegmentsOfRectangle(rectangle) {
22711
- return [
22712
- { start: rectangle.topLeft, end: rectangle.topRight },
22713
- { start: rectangle.topRight, end: rectangle.bottomRight },
22714
- { start: rectangle.bottomRight, end: rectangle.bottomLeft },
22715
- { start: rectangle.bottomLeft, end: rectangle.topLeft },
22716
- ];
22696
+ /**
22697
+ * Get the average steps between numbers
22698
+ */
22699
+ function getAverageIncrement(group) {
22700
+ const averages = [];
22701
+ let last = group[0];
22702
+ for (let i = 1; i < group.length; i++) {
22703
+ const current = group[i];
22704
+ averages.push(current - last);
22705
+ last = current;
22706
+ }
22707
+ return averages.reduce((a, b) => a + b, 0) / averages.length;
22717
22708
  }
22718
22709
  /**
22719
- * Check if two segment intersect. The case where the segments are colinear (both segments on the same line)
22720
- * is not handled.
22710
+ * Get the step for a group
22721
22711
  */
22722
- function doSegmentIntersect(segment1, segment2) {
22723
- const A = segment1.start;
22724
- const B = segment1.end;
22725
- const C = segment2.start;
22726
- const D = segment2.end;
22727
- /**
22728
- * Line segment intersection algorithm
22729
- * https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
22730
- */
22731
- function ccw(a, b, c) {
22732
- return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
22712
+ function calculateIncrementBasedOnGroup(group) {
22713
+ let increment = 1;
22714
+ if (group.length >= 2) {
22715
+ increment = getAverageIncrement(group) * group.length;
22733
22716
  }
22734
- return ccw(A, C, D) !== ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D);
22717
+ return increment;
22735
22718
  }
22736
- function doRectanglesIntersect(rect1, rect2) {
22737
- const segments1 = getSegmentsOfRectangle(rect1);
22738
- const segments2 = getSegmentsOfRectangle(rect2);
22739
- for (const segment1 of segments1) {
22740
- for (const segment2 of segments2) {
22741
- if (doSegmentIntersect(segment1, segment2)) {
22742
- return true;
22719
+ /**
22720
+ * Iterates on a list of date intervals.
22721
+ * if every interval is the same, return the interval
22722
+ * Otherwise return undefined
22723
+ *
22724
+ */
22725
+ function getEqualInterval(intervals) {
22726
+ if (intervals.length < 2) {
22727
+ return intervals[0] || { years: 0, months: 0, days: 0 };
22728
+ }
22729
+ const equal = intervals.every((interval) => interval.years === intervals[0].years &&
22730
+ interval.months === intervals[0].months &&
22731
+ interval.days === intervals[0].days);
22732
+ return equal ? intervals[0] : undefined;
22733
+ }
22734
+ /**
22735
+ * Based on a group of dates, calculate the increment that should be applied
22736
+ * to the next date.
22737
+ *
22738
+ * This will compute the date difference in calendar terms (years, months, days)
22739
+ * In order to make abstraction of leap years and months with different number of days.
22740
+ *
22741
+ * In case the dates are not equidistant in calendar terms, no rule can be extrapolated
22742
+ * In case of equidistant dates, we either have in that order:
22743
+ * - exact date interval (e.g. +n year OR +n month OR +n day) in which case we increment by the same interval
22744
+ * - exact day interval (e.g. +n days) in which case we increment by the same day interval
22745
+ * - equidistant dates but not the same interval, in which case we return increment of the same interval
22746
+ *
22747
+ * */
22748
+ function calculateDateIncrementBasedOnGroup(group) {
22749
+ if (group.length < 2) {
22750
+ return 1;
22751
+ }
22752
+ const jsDates = group.map((date) => toJsDate(date, DEFAULT_LOCALE));
22753
+ const datesIntervals = getDateIntervals(jsDates);
22754
+ const datesEquidistantInterval = getEqualInterval(datesIntervals);
22755
+ if (datesEquidistantInterval === undefined) {
22756
+ // dates are not equidistant in terms of years, months or days, thus no rule can be extrapolated
22757
+ return undefined;
22758
+ }
22759
+ // The dates are apart by an exact interval of years, months or days
22760
+ // but not a combination of them
22761
+ const exactDateInterval = Object.values(datesEquidistantInterval).filter((value) => value !== 0).length === 1;
22762
+ const isSameDay = Object.values(datesEquidistantInterval).every((el) => el === 0); // handles time values (strict decimals)
22763
+ if (!exactDateInterval || isSameDay) {
22764
+ const timeIntervals = jsDates
22765
+ .map((date, index) => {
22766
+ if (index === 0) {
22767
+ return 0;
22743
22768
  }
22769
+ const previous = jsDates[index - 1];
22770
+ return Math.floor(date.getTime()) - Math.floor(previous.getTime());
22771
+ })
22772
+ .slice(1);
22773
+ const equidistantDates = timeIntervals.every((interval) => interval === timeIntervals[0]);
22774
+ if (equidistantDates) {
22775
+ return group.length * (group[1] - group[0]);
22744
22776
  }
22745
22777
  }
22746
- return false;
22778
+ return {
22779
+ years: datesEquidistantInterval.years * group.length,
22780
+ months: datesEquidistantInterval.months * group.length,
22781
+ days: datesEquidistantInterval.days * group.length,
22782
+ };
22747
22783
  }
22784
+ autofillRulesRegistry
22785
+ .add("simple_value_copy", {
22786
+ condition: (cell, cells) => {
22787
+ return (cells.length === 1 && !cell.isFormula && !(cell.format && isDateTimeFormat(cell.format)));
22788
+ },
22789
+ generateRule: () => {
22790
+ return { type: "COPY_MODIFIER" };
22791
+ },
22792
+ sequence: 10,
22793
+ })
22794
+ .add("increment_alphanumeric_value", {
22795
+ condition: (cell) => !cell.isFormula &&
22796
+ evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.text &&
22797
+ alphaNumericValueRegExp.test(cell.content),
22798
+ generateRule: (cell, cells, direction) => {
22799
+ const numberPostfix = parseInt(cell.content.match(numberPostfixRegExp)[0]);
22800
+ const prefix = cell.content.match(stringPrefixRegExp)[0];
22801
+ const numberPostfixLength = cell.content.length - prefix.length;
22802
+ const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.text &&
22803
+ alphaNumericValueRegExp.test(evaluatedCell.value)) // get consecutive alphanumeric cells, no matter what the prefix is
22804
+ .filter((cell) => prefix === (cell.value ?? "").toString().match(stringPrefixRegExp)[0])
22805
+ .map((cell) => parseInt((cell.value ?? "").toString().match(numberPostfixRegExp)[0]));
22806
+ let increment = calculateIncrementBasedOnGroup(group);
22807
+ if (["up", "left"].includes(direction) && group.length === 1) {
22808
+ increment = -increment;
22809
+ }
22810
+ return {
22811
+ type: "ALPHANUMERIC_INCREMENT_MODIFIER",
22812
+ prefix,
22813
+ current: numberPostfix,
22814
+ increment,
22815
+ numberPostfixLength,
22816
+ };
22817
+ },
22818
+ sequence: 15,
22819
+ })
22820
+ .add("copy_text", {
22821
+ condition: (cell) => !cell.isFormula &&
22822
+ evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.text,
22823
+ generateRule: () => {
22824
+ return { type: "COPY_MODIFIER" };
22825
+ },
22826
+ sequence: 20,
22827
+ })
22828
+ .add("update_formula", {
22829
+ condition: (cell) => cell.isFormula,
22830
+ generateRule: (_, cells) => {
22831
+ return { type: "FORMULA_MODIFIER", increment: cells.length, current: 0 };
22832
+ },
22833
+ sequence: 30,
22834
+ })
22835
+ .add("increment_dates", {
22836
+ condition: (cell, cells) => {
22837
+ return (!cell.isFormula &&
22838
+ evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.number &&
22839
+ !!cell.format &&
22840
+ isDateTimeFormat(cell.format));
22841
+ },
22842
+ generateRule: (cell, cells) => {
22843
+ const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number &&
22844
+ !!evaluatedCell.format &&
22845
+ isDateTimeFormat(evaluatedCell.format)).map((cell) => Number(cell.value));
22846
+ const increment = calculateDateIncrementBasedOnGroup(group);
22847
+ if (increment === undefined) {
22848
+ return { type: "COPY_MODIFIER" };
22849
+ }
22850
+ /** requires to detect the current date (requires to be an integer value with the right format)
22851
+ * detect if year or if month or if day then extrapolate increment required (+1 month, +1 year + 1 day)
22852
+ */
22853
+ const evaluation = evaluateLiteral(cell, { locale: DEFAULT_LOCALE });
22854
+ if (typeof increment === "object") {
22855
+ return {
22856
+ type: "DATE_INCREMENT_MODIFIER",
22857
+ increment,
22858
+ current: evaluation.type === CellValueType.number ? evaluation.value : 0,
22859
+ };
22860
+ }
22861
+ return {
22862
+ type: "INCREMENT_MODIFIER",
22863
+ increment,
22864
+ current: evaluation.type === CellValueType.number ? evaluation.value : 0,
22865
+ };
22866
+ },
22867
+ sequence: 25,
22868
+ })
22869
+ .add("increment_number", {
22870
+ condition: (cell) => !cell.isFormula &&
22871
+ evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.number,
22872
+ generateRule: (cell, cells, direction) => {
22873
+ const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number &&
22874
+ !isDateTimeFormat(evaluatedCell.format || "")).map((cell) => Number(cell.value));
22875
+ let increment = calculateIncrementBasedOnGroup(group);
22876
+ if (["up", "left"].includes(direction) && group.length === 1) {
22877
+ increment = -increment;
22878
+ }
22879
+ const evaluation = evaluateLiteral(cell, { locale: DEFAULT_LOCALE });
22880
+ return {
22881
+ type: "INCREMENT_MODIFIER",
22882
+ increment,
22883
+ current: evaluation.type === CellValueType.number ? evaluation.value : 0,
22884
+ };
22885
+ },
22886
+ sequence: 40,
22887
+ });
22748
22888
  /**
22749
- * Get the rectangle that is tangent to a circle at a given angle.
22889
+ * Returns the date intervals between consecutive dates of an array
22890
+ * in the format of { years: number, months: number, days: number }
22750
22891
  *
22751
- * @param angle angle between X axis and the point where the rectangle is tangent to the circle
22892
+ * The split is necessary to make abstraction of leap years and
22893
+ * months with different number of days.
22894
+ *
22895
+ * @param dates
22752
22896
  */
22753
- function getRectangleTangentToCircle(angle, radius, circleCenterX, circleCenterY, rectWidth, rectHeight) {
22754
- const cos = Math.cos(angle);
22755
- const sin = Math.sin(angle);
22756
- // x, y are the distance from the center of the circle to the point where the rectangle is tangent to the circle
22757
- const x = cos * radius;
22758
- const y = sin * radius;
22759
- // x2, y2 are the distance from the point the rectangle is tangent to the circle to the bottom left corner of the rectangle
22760
- const x2 = sin * (rectWidth / 2); // cos(angle + 90°) = sin(angle)
22761
- const y2 = cos * (rectWidth / 2);
22762
- const bottomRight = {
22763
- x: x + x2 + circleCenterX,
22764
- y: circleCenterY - (y - y2),
22765
- };
22766
- const bottomLeft = {
22767
- x: x - x2 + circleCenterX,
22768
- y: circleCenterY - (y + y2),
22769
- };
22770
- // Same as above but for the top corners of the rectangle (radius + rectangle height instead of radius)
22771
- const xp = cos * (radius + rectHeight);
22772
- const yp = sin * (radius + rectHeight);
22773
- const topLeft = {
22774
- x: xp - x2 + circleCenterX,
22775
- y: circleCenterY - (yp + y2),
22776
- };
22777
- const topRight = {
22778
- x: xp + x2 + circleCenterX,
22779
- y: circleCenterY - (yp - y2),
22780
- };
22781
- return { bottomLeft, bottomRight, topRight, topLeft };
22897
+ function getDateIntervals(dates) {
22898
+ if (dates.length < 2) {
22899
+ return [{ years: 0, months: 0, days: 0 }];
22900
+ }
22901
+ const res = dates.map((date, index) => {
22902
+ if (index === 0) {
22903
+ return { years: 0, months: 0, days: 0 };
22904
+ }
22905
+ const previous = DateTime.fromTimestamp(dates[index - 1].getTime());
22906
+ const years = getTimeDifferenceInWholeYears(previous, date);
22907
+ const months = getTimeDifferenceInWholeMonths(previous, date) % 12;
22908
+ previous.setFullYear(previous.getFullYear() + years);
22909
+ previous.setMonth(previous.getMonth() + months);
22910
+ const days = getTimeDifferenceInWholeDays(previous, date);
22911
+ return {
22912
+ years,
22913
+ months,
22914
+ days,
22915
+ };
22916
+ });
22917
+ return res.slice(1);
22782
22918
  }
22783
22919
 
22920
+ const cellPopoverRegistry = new Registry();
22921
+
22784
22922
  class GaugeChartComponent extends Component {
22785
22923
  static template = "o-spreadsheet-GaugeChartComponent";
22786
22924
  canvas = useRef("chartContainer");
@@ -22813,72 +22951,6 @@ function toXlsxHexColor(color) {
22813
22951
  return color;
22814
22952
  }
22815
22953
 
22816
- const CHART_COMMON_OPTIONS = {
22817
- // https://www.chartjs.org/docs/latest/general/responsive.html
22818
- responsive: true, // will resize when its container is resized
22819
- maintainAspectRatio: false, // doesn't maintain the aspect ratio (width/height =2 by default) so the user has the choice of the exact layout
22820
- elements: {
22821
- line: {
22822
- fill: false, // do not fill the area under line charts
22823
- },
22824
- point: {
22825
- hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
22826
- },
22827
- },
22828
- animation: false,
22829
- };
22830
- function chartToImage(runtime, figure, type) {
22831
- // wrap the canvas in a div with a fixed size because chart.js would
22832
- // fill the whole page otherwise
22833
- const div = document.createElement("div");
22834
- div.style.width = `${figure.width}px`;
22835
- div.style.height = `${figure.height}px`;
22836
- const canvas = document.createElement("canvas");
22837
- div.append(canvas);
22838
- canvas.setAttribute("width", figure.width.toString());
22839
- canvas.setAttribute("height", figure.height.toString());
22840
- // we have to add the canvas to the DOM otherwise it won't be rendered
22841
- document.body.append(div);
22842
- if ("chartJsConfig" in runtime) {
22843
- const config = deepCopy(runtime.chartJsConfig);
22844
- config.plugins = [backgroundColorChartJSPlugin];
22845
- const chart = new window.Chart(canvas, config);
22846
- const imgContent = chart.toBase64Image();
22847
- chart.destroy();
22848
- div.remove();
22849
- return imgContent;
22850
- }
22851
- else if (type === "scorecard") {
22852
- const design = getScorecardConfiguration(figure, runtime);
22853
- drawScoreChart(design, canvas);
22854
- const imgContent = canvas.toDataURL();
22855
- div.remove();
22856
- return imgContent;
22857
- }
22858
- else if (type === "gauge") {
22859
- drawGaugeChart(canvas, runtime);
22860
- const imgContent = canvas.toDataURL();
22861
- div.remove();
22862
- return imgContent;
22863
- }
22864
- return undefined;
22865
- }
22866
- /**
22867
- * Custom chart.js plugin to set the background color of the canvas
22868
- * https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md
22869
- */
22870
- const backgroundColorChartJSPlugin = {
22871
- id: "customCanvasBackgroundColor",
22872
- beforeDraw: (chart) => {
22873
- const { ctx } = chart;
22874
- ctx.save();
22875
- ctx.globalCompositeOperation = "destination-over";
22876
- ctx.fillStyle = "#ffffff";
22877
- ctx.fillRect(0, 0, chart.width, chart.height);
22878
- ctx.restore();
22879
- },
22880
- };
22881
-
22882
22954
  /**
22883
22955
  * Represent a raw XML string
22884
22956
  */
@@ -22944,6 +23016,7 @@ const CONTENT_TYPES = {
22944
23016
  macroEnabledTemplateWorkbook: "application/vnd.ms-excel.template.macroEnabled.main+xml",
22945
23017
  excelAddInWorkbook: "application/vnd.ms-excel.addin.macroEnabled.main+xml",
22946
23018
  sheet: "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml",
23019
+ metadata: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheetMetadata+xml",
22947
23020
  sharedStrings: "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml",
22948
23021
  styles: "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml",
22949
23022
  drawing: "application/vnd.openxmlformats-officedocument.drawing+xml",
@@ -22956,6 +23029,7 @@ const CONTENT_TYPES = {
22956
23029
  const XLSX_RELATION_TYPE = {
22957
23030
  document: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument",
22958
23031
  sheet: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet",
23032
+ metadata: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sheetMetadata",
22959
23033
  sharedStrings: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings",
22960
23034
  styles: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles",
22961
23035
  drawing: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing",
@@ -22965,6 +23039,7 @@ const XLSX_RELATION_TYPE = {
22965
23039
  hyperlink: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
22966
23040
  image: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
22967
23041
  };
23042
+ const ARRAY_FORMULA_URI = "bdbb8cdc-fa1e-496e-a857-3c3f30c029c3";
22968
23043
  const RELATIONSHIP_NSR = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
22969
23044
  const HEIGHT_FACTOR = 0.75; // 100px => 75 u
22970
23045
  /**
@@ -24340,16 +24415,25 @@ function addRelsToFile(relsFiles, path, rel) {
24340
24415
  }
24341
24416
  return id;
24342
24417
  }
24418
+ const globalReverseLookup = new WeakMap();
24343
24419
  function pushElement(property, propertyList) {
24344
- let len = propertyList.length;
24345
- const operator = typeof property === "object" ? deepEquals : (a, b) => a === b;
24346
- for (let i = 0; i < len; i++) {
24347
- if (operator(property, propertyList[i])) {
24348
- return i;
24420
+ let reverseLookup = globalReverseLookup.get(propertyList);
24421
+ if (!reverseLookup) {
24422
+ reverseLookup = new Map();
24423
+ for (let i = 0; i < propertyList.length; i++) {
24424
+ const canonical = getCanonicalRepresentation(propertyList[i]);
24425
+ reverseLookup.set(canonical, i);
24349
24426
  }
24427
+ globalReverseLookup.set(propertyList, reverseLookup);
24428
+ }
24429
+ const canonical = getCanonicalRepresentation(property);
24430
+ if (reverseLookup.has(canonical)) {
24431
+ return reverseLookup.get(canonical);
24350
24432
  }
24351
- propertyList[propertyList.length] = property;
24352
- return propertyList.length - 1;
24433
+ const maxId = propertyList.length;
24434
+ propertyList.push(property);
24435
+ reverseLookup.set(canonical, maxId);
24436
+ return maxId;
24353
24437
  }
24354
24438
  const chartIds = [];
24355
24439
  /**
@@ -25386,29 +25470,34 @@ function convertPivotTableConfig(pivotTable) {
25386
25470
  * In all the sheets, replace the table-only references in the formula cells with standard references.
25387
25471
  */
25388
25472
  function convertTableFormulaReferences(convertedSheets, xlsxSheets) {
25389
- for (let sheet of convertedSheets) {
25390
- const tables = xlsxSheets.find((s) => s.sheetName === sheet.name).tables;
25473
+ for (let tableSheet of convertedSheets) {
25474
+ const tables = xlsxSheets.find((s) => s.sheetName === tableSheet.name).tables;
25391
25475
  for (let table of tables) {
25392
25476
  const tabRef = table.name + "[";
25393
- for (let position of positions(toZone(table.ref))) {
25394
- const xc = toXC(position.col, position.row);
25395
- let cellContent = sheet.cells[xc];
25396
- if (cellContent?.startsWith("=")) {
25397
- let refIndex;
25398
- while ((refIndex = cellContent.indexOf(tabRef)) !== -1) {
25399
- let reference = cellContent.slice(refIndex + tabRef.length);
25400
- // Expression can either be tableName[colName] or tableName[[#This Row], [colName]]
25401
- let endIndex = reference.indexOf("]");
25402
- if (reference.startsWith(`[`)) {
25403
- endIndex = reference.indexOf("]", endIndex + 1);
25404
- endIndex = reference.indexOf("]", endIndex + 1);
25477
+ for (let sheet of convertedSheets) {
25478
+ for (let xc in sheet.cells) {
25479
+ const cell = sheet.cells[xc];
25480
+ let cellContent = sheet.cells[xc];
25481
+ if (cell && cellContent && cellContent.startsWith("=")) {
25482
+ let refIndex;
25483
+ while ((refIndex = cellContent.indexOf(tabRef)) !== -1) {
25484
+ let endIndex = refIndex + tabRef.length;
25485
+ let openBrackets = 1;
25486
+ while (openBrackets > 0 && endIndex < cellContent.length) {
25487
+ if (cellContent[endIndex] === "[") {
25488
+ openBrackets++;
25489
+ }
25490
+ else if (cellContent[endIndex] === "]") {
25491
+ openBrackets--;
25492
+ }
25493
+ endIndex++;
25494
+ }
25495
+ let reference = cellContent.slice(refIndex + tabRef.length, endIndex - 1);
25496
+ const sheetPrefix = tableSheet.id === sheet.id ? "" : tableSheet.name + "!";
25497
+ const convertedRef = convertTableReference(sheetPrefix, reference, table, xc);
25498
+ cellContent =
25499
+ cellContent.slice(0, refIndex) + convertedRef + cellContent.slice(endIndex);
25405
25500
  }
25406
- reference = reference.slice(0, endIndex);
25407
- const convertedRef = convertTableReference(reference, table, xc);
25408
- cellContent =
25409
- cellContent.slice(0, refIndex) +
25410
- convertedRef +
25411
- cellContent.slice(tabRef.length + refIndex + endIndex + 1);
25412
25501
  }
25413
25502
  sheet.cells[xc] = cellContent;
25414
25503
  }
@@ -25417,11 +25506,17 @@ function convertTableFormulaReferences(convertedSheets, xlsxSheets) {
25417
25506
  }
25418
25507
  }
25419
25508
  /**
25420
- * Convert table-specific references in formulas into standard references.
25509
+ * Convert table-specific references in formulas into standard references. A table reference is composed of columns names,
25510
+ * and of keywords determining the rows of the table to reference.
25421
25511
  *
25422
25512
  * A reference in a table can have the form (only the part between brackets should be given to this function):
25423
25513
  * - tableName[colName] : reference to the whole column "colName"
25514
+ * - tableName[#keyword] : reference to the whatever row the keyword refers to
25424
25515
  * - tableName[[#keyword], [colName]] : reference to some of the element(s) of the column colName
25516
+ * - tableName[[#keyword], [colName]:[col2Name]] : reference to some of the element(s) of the columns colName to col2Name
25517
+ * - tableName[[#keyword1], [#keyword2], [colName]] : reference to all the rows referenced by the keywords in the column colName
25518
+ * - tableName[[#keyword1], [colName], [#keyword2]]: the keywords and colName can be in any order
25519
+ *
25425
25520
  *
25426
25521
  * The available keywords are :
25427
25522
  * - #All : all the column (including totals)
@@ -25429,58 +25524,109 @@ function convertTableFormulaReferences(convertedSheets, xlsxSheets) {
25429
25524
  * - #Headers : only the header of the column
25430
25525
  * - #Totals : only the totals of the column
25431
25526
  * - #This Row : only the element in the same row as the cell
25527
+ *
25528
+ * Note that the only valid combination of multiple keywords are #Data + #Totals and #Headers + #Data.
25432
25529
  */
25433
- function convertTableReference(expr, table, cellXc) {
25434
- const refElements = expr.split(",");
25530
+ function convertTableReference(sheetPrefix, expr, table, cellXc) {
25531
+ // TODO: Ideally we'd want to make a real tokenizer, this simple approach won't work if for example the column name
25532
+ // contain # or , characters. But that's probably an edge case that we can ignore for now.
25533
+ const parts = expr.split(",").map((part) => part.trim());
25435
25534
  const tableZone = toZone(table.ref);
25436
- const refZone = { ...tableZone };
25437
- let isReferencedZoneValid = true;
25438
- // Single column reference
25439
- if (refElements.length === 1) {
25440
- const colRelativeIndex = table.cols.findIndex((col) => col.name === refElements[0]);
25441
- refZone.left = refZone.right = colRelativeIndex + tableZone.left;
25442
- if (table.headerRowCount) {
25443
- refZone.top += table.headerRowCount;
25444
- }
25445
- if (table.totalsRowCount) {
25446
- refZone.bottom -= 1;
25535
+ const colIndexes = [];
25536
+ const rowIndexes = [];
25537
+ const foundKeywords = [];
25538
+ for (const part of parts) {
25539
+ if (removeBrackets(part).startsWith("#")) {
25540
+ const keyWord = removeBrackets(part);
25541
+ foundKeywords.push(keyWord);
25542
+ switch (keyWord) {
25543
+ case "#All":
25544
+ rowIndexes.push(tableZone.top, tableZone.bottom);
25545
+ break;
25546
+ case "#Data":
25547
+ const top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
25548
+ const bottom = table.totalsRowCount
25549
+ ? tableZone.bottom - table.totalsRowCount
25550
+ : tableZone.bottom;
25551
+ rowIndexes.push(top, bottom);
25552
+ break;
25553
+ case "#This Row":
25554
+ rowIndexes.push(toCartesian(cellXc).row);
25555
+ break;
25556
+ case "#Headers":
25557
+ if (!table.headerRowCount) {
25558
+ return CellErrorType.InvalidReference;
25559
+ }
25560
+ rowIndexes.push(tableZone.top);
25561
+ break;
25562
+ case "#Totals":
25563
+ if (!table.totalsRowCount) {
25564
+ return CellErrorType.InvalidReference;
25565
+ }
25566
+ rowIndexes.push(tableZone.bottom);
25567
+ break;
25568
+ }
25447
25569
  }
25448
- }
25449
- // Other references
25450
- else {
25451
- switch (refElements[0].slice(1, refElements[0].length - 1)) {
25452
- case "#All":
25453
- refZone.top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
25454
- refZone.bottom = tableZone.bottom;
25455
- break;
25456
- case "#Data":
25457
- refZone.top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
25458
- refZone.bottom = table.totalsRowCount ? tableZone.bottom + 1 : tableZone.bottom;
25459
- break;
25460
- case "#This Row":
25461
- refZone.top = refZone.bottom = toCartesian(cellXc).row;
25462
- break;
25463
- case "#Headers":
25464
- refZone.top = refZone.bottom = tableZone.top;
25465
- if (!table.headerRowCount) {
25466
- isReferencedZoneValid = false;
25467
- }
25468
- break;
25469
- case "#Totals":
25470
- refZone.top = refZone.bottom = tableZone.bottom;
25471
- if (!table.totalsRowCount) {
25472
- isReferencedZoneValid = false;
25570
+ else {
25571
+ const columns = part
25572
+ .split(":")
25573
+ .map((part) => part.trim())
25574
+ .map(removeBrackets);
25575
+ if (colIndexes.length) {
25576
+ return CellErrorType.InvalidReference;
25577
+ }
25578
+ const colRelativeIndex = table.cols.findIndex((col) => col.name === columns[0]);
25579
+ if (colRelativeIndex === -1) {
25580
+ return CellErrorType.InvalidReference;
25581
+ }
25582
+ colIndexes.push(colRelativeIndex + tableZone.left);
25583
+ if (columns[1]) {
25584
+ const colRelativeIndex2 = table.cols.findIndex((col) => col.name === columns[1]);
25585
+ if (colRelativeIndex2 === -1) {
25586
+ return CellErrorType.InvalidReference;
25473
25587
  }
25474
- break;
25588
+ colIndexes.push(colRelativeIndex2 + tableZone.left);
25589
+ }
25475
25590
  }
25476
- const colRef = refElements[1].slice(1, refElements[1].length - 1);
25477
- const colRelativeIndex = table.cols.findIndex((col) => col.name === colRef);
25478
- refZone.left = refZone.right = colRelativeIndex + tableZone.left;
25479
25591
  }
25480
- if (!isReferencedZoneValid) {
25592
+ if (!areKeywordsCompatible(foundKeywords)) {
25481
25593
  return CellErrorType.InvalidReference;
25482
25594
  }
25483
- return refZone.top !== refZone.bottom ? zoneToXc(refZone) : toXC(refZone.left, refZone.top);
25595
+ if (rowIndexes.length === 0) {
25596
+ const top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
25597
+ const bottom = table.totalsRowCount
25598
+ ? tableZone.bottom - table.totalsRowCount
25599
+ : tableZone.bottom;
25600
+ rowIndexes.push(top, bottom);
25601
+ }
25602
+ if (colIndexes.length === 0) {
25603
+ colIndexes.push(tableZone.left, tableZone.right);
25604
+ }
25605
+ const refZone = {
25606
+ top: Math.min(...rowIndexes),
25607
+ left: Math.min(...colIndexes),
25608
+ bottom: Math.max(...rowIndexes),
25609
+ right: Math.max(...colIndexes),
25610
+ };
25611
+ return sheetPrefix + zoneToXc(refZone);
25612
+ }
25613
+ function removeBrackets(str) {
25614
+ return str.startsWith("[") && str.endsWith("]") ? str.slice(1, str.length - 1) : str;
25615
+ }
25616
+ function areKeywordsCompatible(keywords) {
25617
+ if (keywords.length < 2) {
25618
+ return true;
25619
+ }
25620
+ else if (keywords.length > 2) {
25621
+ return false;
25622
+ }
25623
+ else if (keywords.includes("#Data") && keywords.includes("#Totals")) {
25624
+ return true;
25625
+ }
25626
+ else if (keywords.includes("#Headers") && keywords.includes("#Data")) {
25627
+ return true;
25628
+ }
25629
+ return false;
25484
25630
  }
25485
25631
 
25486
25632
  // -------------------------------------
@@ -26134,7 +26280,7 @@ class XlsxChartExtractor extends XlsxBaseExtractor {
26134
26280
  title: { text: chartTitle },
26135
26281
  type: CHART_TYPE_CONVERSION_MAP[chartType],
26136
26282
  dataSets: this.extractChartDatasets(this.querySelectorAll(rootChartElement, `c:${chartType}`), chartType),
26137
- labelRange: this.extractChildTextContent(rootChartElement, `c:ser ${chartType === "scatterChart" ? "c:numRef" : "c:cat"} c:f`),
26283
+ labelRange: this.extractLabelRange(chartType, rootChartElement),
26138
26284
  backgroundColor: this.extractChildAttr(rootChartElement, "c:chartSpace > c:spPr a:srgbClr", "val", {
26139
26285
  default: "ffffff",
26140
26286
  }).asString(),
@@ -26146,6 +26292,13 @@ class XlsxChartExtractor extends XlsxBaseExtractor {
26146
26292
  };
26147
26293
  })[0];
26148
26294
  }
26295
+ extractLabelRange(chartType, rootChartElement) {
26296
+ if (chartType === "scatterChart") {
26297
+ return (this.extractChildTextContent(rootChartElement, `c:ser c:strRef c:f`) ||
26298
+ this.extractChildTextContent(rootChartElement, `c:ser c:numRef c:f`));
26299
+ }
26300
+ return this.extractChildTextContent(rootChartElement, `c:ser c:cat c:f`);
26301
+ }
26149
26302
  extractComboChart(chartElement) {
26150
26303
  // Title can be separated into multiple xml elements (for styling and such), we only import the text
26151
26304
  const chartTitle = this.mapOnElements({ parent: chartElement, query: "c:title a:t" }, (textElement) => {
@@ -28648,11 +28801,12 @@ function canBeLinearChart(definition, dataSets, labelRange, getters) {
28648
28801
  }
28649
28802
  let missingTimeAdapterAlreadyWarned = false;
28650
28803
  function isLuxonTimeAdapterInstalled() {
28651
- if (!window.Chart) {
28804
+ const Chart = getChartJSConstructor();
28805
+ if (!Chart) {
28652
28806
  return false;
28653
28807
  }
28654
28808
  // @ts-ignore
28655
- const adapter = new window.Chart._adapters._date({});
28809
+ const adapter = new Chart._adapters._date({});
28656
28810
  const isInstalled = adapter._id === "luxon";
28657
28811
  if (!isInstalled && !missingTimeAdapterAlreadyWarned) {
28658
28812
  missingTimeAdapterAlreadyWarned = true;
@@ -32506,10 +32660,6 @@ class Popover extends Component {
32506
32660
  this.currentDisplayValue = newDisplay;
32507
32661
  if (!anchor)
32508
32662
  return;
32509
- el.style.top = "";
32510
- el.style.left = "";
32511
- el.style["max-height"] = "";
32512
- el.style["max-width"] = "";
32513
32663
  const propsMaxSize = { width: this.props.maxWidth, height: this.props.maxHeight };
32514
32664
  let elDims = {
32515
32665
  width: el.getBoundingClientRect().width,
@@ -34061,6 +34211,7 @@ var CHART_HELPERS = /*#__PURE__*/Object.freeze({
34061
34211
  duplicateLabelRangeInDuplicatedSheet: duplicateLabelRangeInDuplicatedSheet,
34062
34212
  formatChartDatasetValue: formatChartDatasetValue,
34063
34213
  formatTickValue: formatTickValue,
34214
+ getChartJSConstructor: getChartJSConstructor,
34064
34215
  getChartPositionAtCenterOfViewport: getChartPositionAtCenterOfViewport,
34065
34216
  getDefinedAxis: getDefinedAxis,
34066
34217
  getPieColors: getPieColors,
@@ -36320,6 +36471,7 @@ const irregularityMap = {
36320
36471
  fingerprintStore.enable();
36321
36472
  }
36322
36473
  },
36474
+ isReadonlyAllowed: true,
36323
36475
  icon: "o-spreadsheet-Icon.IRREGULARITY_MAP",
36324
36476
  };
36325
36477
  const viewFormulas = {
@@ -37861,6 +38013,11 @@ class SelectionInputStore extends SpreadsheetStore {
37861
38013
  }
37862
38014
  updateColors(colors) {
37863
38015
  this.colors = colors;
38016
+ const colorGenerator = new ColorGenerator(this.ranges.length, this.colors);
38017
+ this.ranges = this.ranges.map((range) => ({
38018
+ ...range,
38019
+ color: colorGenerator.next(),
38020
+ }));
37864
38021
  }
37865
38022
  confirm() {
37866
38023
  for (const range of this.selectionInputs) {
@@ -37895,12 +38052,11 @@ class SelectionInputStore extends SpreadsheetStore {
37895
38052
  * e.g. ["A1", "Sheet2!B3", "E12"]
37896
38053
  */
37897
38054
  get selectionInputs() {
37898
- const generator = new ColorGenerator(this.ranges.length, this.colors);
37899
38055
  return this.ranges.map((input, index) => Object.assign({}, input, {
37900
38056
  color: this.hasMainFocus &&
37901
38057
  this.focusedRangeIndex !== null &&
37902
38058
  this.getters.isRangeValid(input.xc)
37903
- ? generator.next()
38059
+ ? input.color
37904
38060
  : null,
37905
38061
  isFocused: this.hasMainFocus && this.focusedRangeIndex === index,
37906
38062
  isValidRange: input.xc === "" || this.getters.isRangeValid(input.xc),
@@ -38170,10 +38326,10 @@ class SelectionInput extends Component {
38170
38326
  if (originalIndex === finalIndex) {
38171
38327
  return;
38172
38328
  }
38173
- const draggedItems = [...draggableIds];
38174
- draggedItems.splice(originalIndex, 1);
38175
- draggedItems.splice(finalIndex, 0, rangeId);
38176
- this.props.onSelectionReordered?.(this.store.selectionInputs.map((range) => draggedItems.indexOf(range.id)));
38329
+ const indexes = range(0, draggableIds.length);
38330
+ indexes.splice(originalIndex, 1);
38331
+ indexes.splice(finalIndex, 0, originalIndex);
38332
+ this.props.onSelectionReordered?.(indexes);
38177
38333
  this.props.onSelectionConfirmed?.();
38178
38334
  this.store.confirm();
38179
38335
  },
@@ -38451,6 +38607,9 @@ class GenericChartConfigPanel extends Component {
38451
38607
  this.state.datasetDispatchResult = this.props.updateChart(this.props.figureId, {
38452
38608
  dataSets: this.dataSets,
38453
38609
  });
38610
+ if (this.state.datasetDispatchResult.isSuccessful) {
38611
+ this.dataSets = this.env.model.getters.getChartDefinition(this.props.figureId).dataSets;
38612
+ }
38454
38613
  }
38455
38614
  getDataSeriesRanges() {
38456
38615
  return this.dataSets;
@@ -39867,8 +40026,16 @@ class ContentEditableHelper {
39867
40026
  }
39868
40027
  let startNode = this.findChildAtCharacterIndex(start);
39869
40028
  let endNode = this.findChildAtCharacterIndex(end);
39870
- range.setStart(startNode.node, startNode.offset);
39871
- range.setEnd(endNode.node, endNode.offset);
40029
+ // setEnd (setStart) will result in a collapsed range if the end point is before the start point
40030
+ // https://developer.mozilla.org/en-US/docs/Web/API/Range/setEnd
40031
+ if (start <= end) {
40032
+ range.setStart(startNode.node, startNode.offset);
40033
+ range.setEnd(endNode.node, endNode.offset);
40034
+ }
40035
+ else {
40036
+ range.setStart(endNode.node, endNode.offset);
40037
+ range.setEnd(startNode.node, startNode.offset);
40038
+ }
39872
40039
  }
39873
40040
  }
39874
40041
  /**
@@ -40170,8 +40337,7 @@ css /* scss */ `
40170
40337
  }
40171
40338
 
40172
40339
  .o-composer-assistant {
40173
- position: absolute;
40174
- margin: 1px 4px;
40340
+ margin-top: 1px;
40175
40341
 
40176
40342
  .o-semi-bold {
40177
40343
  /* FIXME: to remove in favor of Bootstrap
@@ -40222,10 +40388,11 @@ class Composer extends Component {
40222
40388
  });
40223
40389
  compositionActive = false;
40224
40390
  spreadsheetRect = useSpreadsheetRect();
40225
- get assistantStyle() {
40391
+ get assistantStyleProperties() {
40226
40392
  const composerRect = this.composerRef.el.getBoundingClientRect();
40227
40393
  const assistantStyle = {};
40228
- assistantStyle["min-width"] = `${this.props.rect?.width || ASSISTANT_WIDTH}px`;
40394
+ const minWidth = Math.min(this.props.rect?.width || Infinity, ASSISTANT_WIDTH);
40395
+ assistantStyle["min-width"] = `${minWidth}px`;
40229
40396
  const proposals = this.autoCompleteState.provider?.proposals;
40230
40397
  const proposalsHaveDescription = proposals?.some((proposal) => proposal.description);
40231
40398
  if (this.functionDescriptionState.showDescription || proposalsHaveDescription) {
@@ -40249,13 +40416,29 @@ class Composer extends Component {
40249
40416
  }
40250
40417
  }
40251
40418
  else {
40252
- assistantStyle["max-height"] = `${this.spreadsheetRect.height - composerRect.bottom}px`;
40419
+ assistantStyle["max-height"] = `${this.spreadsheetRect.height - composerRect.bottom - 1}px`; // -1: margin
40253
40420
  if (composerRect.left + ASSISTANT_WIDTH + SCROLLBAR_WIDTH + CLOSE_ICON_RADIUS >
40254
40421
  this.spreadsheetRect.width) {
40255
40422
  assistantStyle.right = `${CLOSE_ICON_RADIUS}px`;
40256
40423
  }
40257
40424
  }
40258
- return cssPropertiesToCss(assistantStyle);
40425
+ return assistantStyle;
40426
+ }
40427
+ get assistantStyle() {
40428
+ const allProperties = this.assistantStyleProperties;
40429
+ return cssPropertiesToCss({
40430
+ "max-height": allProperties["max-height"],
40431
+ width: allProperties["width"],
40432
+ "min-width": allProperties["min-width"],
40433
+ });
40434
+ }
40435
+ get assistantContainerStyle() {
40436
+ const allProperties = this.assistantStyleProperties;
40437
+ return cssPropertiesToCss({
40438
+ top: allProperties["top"],
40439
+ right: allProperties["right"],
40440
+ transform: allProperties["transform"],
40441
+ });
40259
40442
  }
40260
40443
  // we can't allow input events to be triggered while we remove and add back the content of the composer in processContent
40261
40444
  shouldProcessInputEvents = false;
@@ -46729,9 +46912,7 @@ class PivotSidePanelStore extends SpreadsheetStore {
46729
46912
  pivot: this.draft,
46730
46913
  });
46731
46914
  this.draft = null;
46732
- if (!this.alreadyNotified &&
46733
- !this.isDynamicPivotInViewport() &&
46734
- this.isStaticPivotInViewport()) {
46915
+ if (!this.alreadyNotified && this.isUpdatedPivotVisibleInViewportOnlyAsStaticPivot()) {
46735
46916
  const formulaId = this.getters.getPivotFormulaId(this.pivotId);
46736
46917
  const pivotExample = `=PIVOT(${formulaId})`;
46737
46918
  this.alreadyNotified = true;
@@ -46787,26 +46968,33 @@ class PivotSidePanelStore extends SpreadsheetStore {
46787
46968
  this.applyUpdate();
46788
46969
  }
46789
46970
  }
46790
- isDynamicPivotInViewport() {
46791
- for (const position of this.getters.getVisibleCellPositions()) {
46792
- const isDynamicPivot = this.getters.isSpillPivotFormula(position);
46793
- if (isDynamicPivot) {
46794
- return true;
46795
- }
46796
- }
46797
- return false;
46798
- }
46799
- isStaticPivotInViewport() {
46971
+ /**
46972
+ * @returns true if the updated pivot is visible in the viewport only as a
46973
+ * static pivot and not as a dynamic pivot
46974
+ */
46975
+ isUpdatedPivotVisibleInViewportOnlyAsStaticPivot() {
46976
+ let staticPivotCount = 0;
46977
+ const updatedPivotFormulaId = this.getters.getPivotFormulaId(this.pivotId);
46800
46978
  for (const position of this.getters.getVisibleCellPositions()) {
46801
46979
  const cell = this.getters.getCell(position);
46802
46980
  if (cell?.isFormula) {
46803
46981
  const pivotFunction = getFirstPivotFunction(cell.compiledFormula.tokens);
46804
- if (pivotFunction && pivotFunction.functionName !== "PIVOT") {
46805
- return true;
46982
+ const pivotFormulaId = pivotFunction?.args[0]?.value;
46983
+ if (pivotFunction && updatedPivotFormulaId === pivotFormulaId.toString()) {
46984
+ if (pivotFunction.functionName === "PIVOT") {
46985
+ // if we have at least one dynamic pivot visible inserted the viewport
46986
+ // we return false
46987
+ return false;
46988
+ }
46989
+ else {
46990
+ staticPivotCount++;
46991
+ }
46806
46992
  }
46807
46993
  }
46808
46994
  }
46809
- return false;
46995
+ // we return true if there are only static pivots visible inserted the viewport,
46996
+ // otherwise false
46997
+ return staticPivotCount > 0;
46810
46998
  }
46811
46999
  addDefaultDateTimeGranularity(fields, definition) {
46812
47000
  const { columns, rows } = definition;
@@ -50345,6 +50533,71 @@ class GridPopover extends Component {
50345
50533
  }
50346
50534
  }
50347
50535
 
50536
+ class UnhideRowHeaders extends Component {
50537
+ static template = "o-spreadsheet-UnhideRowHeaders";
50538
+ static props = {
50539
+ headersGroups: Array,
50540
+ headerRange: Object,
50541
+ offset: { type: Number, optional: true },
50542
+ };
50543
+ static defaultProps = { offset: 0 };
50544
+ get sheetId() {
50545
+ return this.env.model.getters.getActiveSheetId();
50546
+ }
50547
+ getUnhidePreviousButtonStyle(hiddenIndex) {
50548
+ const rect = this.env.model.getters.getRect(positionToZone({ col: 0, row: hiddenIndex }));
50549
+ const y = rect.y + rect.height - HEADER_HEIGHT;
50550
+ return cssPropertiesToCss({ top: y - this.props.offset + "px", "margin-right": "1px" });
50551
+ }
50552
+ getUnhideNextButtonStyle(hiddenIndex) {
50553
+ const rect = this.env.model.getters.getRect(positionToZone({ col: 0, row: hiddenIndex }));
50554
+ const y = rect.y - HEADER_HEIGHT;
50555
+ return cssPropertiesToCss({ top: y - this.props.offset + "px", "margin-right": "1px" });
50556
+ }
50557
+ unhide(hiddenElements) {
50558
+ this.env.model.dispatch("UNHIDE_COLUMNS_ROWS", {
50559
+ sheetId: this.sheetId,
50560
+ dimension: "ROW",
50561
+ elements: hiddenElements,
50562
+ });
50563
+ }
50564
+ isVisible(header) {
50565
+ return header >= this.props.headerRange.start && header <= this.props.headerRange.end;
50566
+ }
50567
+ }
50568
+ class UnhideColumnHeaders extends Component {
50569
+ static template = "o-spreadsheet-UnhideColumnHeaders";
50570
+ static props = {
50571
+ headersGroups: Array,
50572
+ headerRange: Object,
50573
+ offset: { type: Number, optional: true },
50574
+ };
50575
+ static defaultProps = { offset: 0 };
50576
+ get sheetId() {
50577
+ return this.env.model.getters.getActiveSheetId();
50578
+ }
50579
+ getUnhidePreviousButtonStyle(hiddenIndex) {
50580
+ const rect = this.env.model.getters.getRect(positionToZone({ col: hiddenIndex, row: 0 }));
50581
+ const x = rect.x + rect.width - HEADER_WIDTH;
50582
+ return cssPropertiesToCss({ left: x - this.props.offset + "px" });
50583
+ }
50584
+ getUnhideNextButtonStyle(hiddenIndex) {
50585
+ const rect = this.env.model.getters.getRect(positionToZone({ col: hiddenIndex, row: 0 }));
50586
+ const x = rect.x - HEADER_WIDTH;
50587
+ return cssPropertiesToCss({ left: x - this.props.offset + "px" });
50588
+ }
50589
+ unhide(hiddenElements) {
50590
+ this.env.model.dispatch("UNHIDE_COLUMNS_ROWS", {
50591
+ sheetId: this.sheetId,
50592
+ dimension: "COL",
50593
+ elements: hiddenElements,
50594
+ });
50595
+ }
50596
+ isVisible(header) {
50597
+ return header >= this.props.headerRange.start && header <= this.props.headerRange.end;
50598
+ }
50599
+ }
50600
+
50348
50601
  class AbstractResizer extends Component {
50349
50602
  static props = {
50350
50603
  onOpenContextMenu: Function,
@@ -50563,6 +50816,7 @@ css /* scss */ `
50563
50816
  left: ${HEADER_WIDTH}px;
50564
50817
  right: 0;
50565
50818
  height: ${HEADER_HEIGHT}px;
50819
+ width: calc(100% - ${HEADER_WIDTH + SCROLLBAR_WIDTH}px);
50566
50820
  &.o-dragging {
50567
50821
  cursor: grabbing;
50568
50822
  }
@@ -50612,6 +50866,7 @@ class ColResizer extends AbstractResizer {
50612
50866
  onOpenContextMenu: Function,
50613
50867
  };
50614
50868
  static template = "o-spreadsheet-ColResizer";
50869
+ static components = { UnhideColumnHeaders };
50615
50870
  colResizerRef;
50616
50871
  setup() {
50617
50872
  super.setup();
@@ -50620,6 +50875,9 @@ class ColResizer extends AbstractResizer {
50620
50875
  this.MAX_SIZE_MARGIN = 90;
50621
50876
  this.MIN_ELEMENT_SIZE = MIN_COL_WIDTH;
50622
50877
  }
50878
+ get sheetId() {
50879
+ return this.env.model.getters.getActiveSheetId();
50880
+ }
50623
50881
  _getEvOffset(ev) {
50624
50882
  return ev.offsetX;
50625
50883
  }
@@ -50642,10 +50900,10 @@ class ColResizer extends AbstractResizer {
50642
50900
  return this.env.model.getters.getEdgeScrollCol(position, position, position);
50643
50901
  }
50644
50902
  _getDimensionsInViewport(index) {
50645
- return this.env.model.getters.getColDimensionsInViewport(this.env.model.getters.getActiveSheetId(), index);
50903
+ return this.env.model.getters.getColDimensionsInViewport(this.sheetId, index);
50646
50904
  }
50647
50905
  _getElementSize(index) {
50648
- return this.env.model.getters.getColSize(this.env.model.getters.getActiveSheetId(), index);
50906
+ return this.env.model.getters.getColSize(this.sheetId, index);
50649
50907
  }
50650
50908
  _getMaxSize() {
50651
50909
  return this.colResizerRef.el.clientWidth;
@@ -50656,7 +50914,7 @@ class ColResizer extends AbstractResizer {
50656
50914
  const cols = this.env.model.getters.getActiveCols();
50657
50915
  this.env.model.dispatch("RESIZE_COLUMNS_ROWS", {
50658
50916
  dimension: "COL",
50659
- sheetId: this.env.model.getters.getActiveSheetId(),
50917
+ sheetId: this.sheetId,
50660
50918
  elements: cols.has(index) ? [...cols] : [index],
50661
50919
  size,
50662
50920
  });
@@ -50669,7 +50927,7 @@ class ColResizer extends AbstractResizer {
50669
50927
  elements.push(colIndex);
50670
50928
  }
50671
50929
  const result = this.env.model.dispatch("MOVE_COLUMNS_ROWS", {
50672
- sheetId: this.env.model.getters.getActiveSheetId(),
50930
+ sheetId: this.sheetId,
50673
50931
  dimension: "COL",
50674
50932
  base: this.state.base,
50675
50933
  elements,
@@ -50688,7 +50946,7 @@ class ColResizer extends AbstractResizer {
50688
50946
  _fitElementSize(index) {
50689
50947
  const cols = this.env.model.getters.getActiveCols();
50690
50948
  this.env.model.dispatch("AUTORESIZE_COLUMNS", {
50691
- sheetId: this.env.model.getters.getActiveSheetId(),
50949
+ sheetId: this.sheetId,
50692
50950
  cols: cols.has(index) ? [...cols] : [index],
50693
50951
  });
50694
50952
  }
@@ -50699,7 +50957,7 @@ class ColResizer extends AbstractResizer {
50699
50957
  return this.env.model.getters.getActiveCols();
50700
50958
  }
50701
50959
  _getPreviousVisibleElement(index) {
50702
- const sheetId = this.env.model.getters.getActiveSheetId();
50960
+ const sheetId = this.sheetId;
50703
50961
  let row;
50704
50962
  for (row = index - 1; row >= 0; row--) {
50705
50963
  if (!this.env.model.getters.isColHidden(sheetId, row)) {
@@ -50710,13 +50968,38 @@ class ColResizer extends AbstractResizer {
50710
50968
  }
50711
50969
  unhide(hiddenElements) {
50712
50970
  this.env.model.dispatch("UNHIDE_COLUMNS_ROWS", {
50713
- sheetId: this.env.model.getters.getActiveSheetId(),
50971
+ sheetId: this.sheetId,
50714
50972
  elements: hiddenElements,
50715
50973
  dimension: "COL",
50716
50974
  });
50717
50975
  }
50718
- getUnhideButtonStyle(hiddenIndex) {
50719
- return cssPropertiesToCss({ left: this._getDimensionsInViewport(hiddenIndex).start + "px" });
50976
+ get mainUnhideHeadersProps() {
50977
+ const { left, right } = this.env.model.getters.getActiveMainViewport();
50978
+ const { xSplit } = this.env.model.getters.getPaneDivisions(this.sheetId);
50979
+ const hiddenGroups = this.env.model.getters.getHiddenColsGroups(this.sheetId);
50980
+ const index = hiddenGroups.findIndex((group) => group[0] >= xSplit - 1);
50981
+ return {
50982
+ headersGroups: hiddenGroups.slice(index),
50983
+ offset: this.env.model.getters.getMainViewportCoordinates().x,
50984
+ headerRange: { start: left, end: right },
50985
+ };
50986
+ }
50987
+ get frozenUnhideHeadersProps() {
50988
+ const { xSplit } = this.env.model.getters.getPaneDivisions(this.sheetId);
50989
+ const hiddenGroups = this.env.model.getters.getHiddenColsGroups(this.sheetId);
50990
+ const index = hiddenGroups.findIndex((group) => group[0] >= xSplit - 1);
50991
+ return {
50992
+ headersGroups: hiddenGroups.slice(0, index + 1),
50993
+ headerRange: { start: 0, end: xSplit - 1 },
50994
+ };
50995
+ }
50996
+ get frozenContainerStyle() {
50997
+ return cssPropertiesToCss({
50998
+ width: this.env.model.getters.getMainViewportCoordinates().x + "px",
50999
+ });
51000
+ }
51001
+ get hasFrozenPane() {
51002
+ return this.env.model.getters.getPaneDivisions(this.sheetId).xSplit > 0;
50720
51003
  }
50721
51004
  }
50722
51005
  css /* scss */ `
@@ -50726,7 +51009,7 @@ css /* scss */ `
50726
51009
  left: 0;
50727
51010
  right: 0;
50728
51011
  width: ${HEADER_WIDTH}px;
50729
- height: 100%;
51012
+ height: calc(100% - ${HEADER_HEIGHT + SCROLLBAR_WIDTH}px);
50730
51013
  &.o-dragging {
50731
51014
  cursor: grabbing;
50732
51015
  }
@@ -50776,6 +51059,7 @@ class RowResizer extends AbstractResizer {
50776
51059
  onOpenContextMenu: Function,
50777
51060
  };
50778
51061
  static template = "o-spreadsheet-RowResizer";
51062
+ static components = { UnhideRowHeaders };
50779
51063
  setup() {
50780
51064
  super.setup();
50781
51065
  this.rowResizerRef = useRef("rowResizer");
@@ -50784,6 +51068,9 @@ class RowResizer extends AbstractResizer {
50784
51068
  this.MIN_ELEMENT_SIZE = MIN_ROW_HEIGHT;
50785
51069
  }
50786
51070
  rowResizerRef;
51071
+ get sheetId() {
51072
+ return this.env.model.getters.getActiveSheetId();
51073
+ }
50787
51074
  _getEvOffset(ev) {
50788
51075
  return ev.offsetY;
50789
51076
  }
@@ -50806,10 +51093,10 @@ class RowResizer extends AbstractResizer {
50806
51093
  return this.env.model.getters.getEdgeScrollRow(position, position, position);
50807
51094
  }
50808
51095
  _getDimensionsInViewport(index) {
50809
- return this.env.model.getters.getRowDimensionsInViewport(this.env.model.getters.getActiveSheetId(), index);
51096
+ return this.env.model.getters.getRowDimensionsInViewport(this.sheetId, index);
50810
51097
  }
50811
51098
  _getElementSize(index) {
50812
- return this.env.model.getters.getRowSize(this.env.model.getters.getActiveSheetId(), index);
51099
+ return this.env.model.getters.getRowSize(this.sheetId, index);
50813
51100
  }
50814
51101
  _getMaxSize() {
50815
51102
  return this.rowResizerRef.el.clientHeight;
@@ -50820,7 +51107,7 @@ class RowResizer extends AbstractResizer {
50820
51107
  const rows = this.env.model.getters.getActiveRows();
50821
51108
  this.env.model.dispatch("RESIZE_COLUMNS_ROWS", {
50822
51109
  dimension: "ROW",
50823
- sheetId: this.env.model.getters.getActiveSheetId(),
51110
+ sheetId: this.sheetId,
50824
51111
  elements: rows.has(index) ? [...rows] : [index],
50825
51112
  size,
50826
51113
  });
@@ -50833,7 +51120,7 @@ class RowResizer extends AbstractResizer {
50833
51120
  elements.push(rowIndex);
50834
51121
  }
50835
51122
  const result = this.env.model.dispatch("MOVE_COLUMNS_ROWS", {
50836
- sheetId: this.env.model.getters.getActiveSheetId(),
51123
+ sheetId: this.sheetId,
50837
51124
  dimension: "ROW",
50838
51125
  base: this.state.base,
50839
51126
  elements,
@@ -50852,7 +51139,7 @@ class RowResizer extends AbstractResizer {
50852
51139
  _fitElementSize(index) {
50853
51140
  const rows = this.env.model.getters.getActiveRows();
50854
51141
  this.env.model.dispatch("AUTORESIZE_ROWS", {
50855
- sheetId: this.env.model.getters.getActiveSheetId(),
51142
+ sheetId: this.sheetId,
50856
51143
  rows: rows.has(index) ? [...rows] : [index],
50857
51144
  });
50858
51145
  }
@@ -50863,7 +51150,7 @@ class RowResizer extends AbstractResizer {
50863
51150
  return this.env.model.getters.getActiveRows();
50864
51151
  }
50865
51152
  _getPreviousVisibleElement(index) {
50866
- const sheetId = this.env.model.getters.getActiveSheetId();
51153
+ const sheetId = this.sheetId;
50867
51154
  let row;
50868
51155
  for (row = index - 1; row >= 0; row--) {
50869
51156
  if (!this.env.model.getters.isRowHidden(sheetId, row)) {
@@ -50872,15 +51159,33 @@ class RowResizer extends AbstractResizer {
50872
51159
  }
50873
51160
  return row;
50874
51161
  }
50875
- unhide(hiddenElements) {
50876
- this.env.model.dispatch("UNHIDE_COLUMNS_ROWS", {
50877
- sheetId: this.env.model.getters.getActiveSheetId(),
50878
- dimension: "ROW",
50879
- elements: hiddenElements,
51162
+ get mainUnhideHeadersProps() {
51163
+ const { top, bottom } = this.env.model.getters.getActiveMainViewport();
51164
+ const { ySplit } = this.env.model.getters.getPaneDivisions(this.sheetId);
51165
+ const hiddenGroups = this.env.model.getters.getHiddenRowsGroups(this.sheetId);
51166
+ const index = hiddenGroups.findIndex((group) => group[0] >= ySplit - 1);
51167
+ return {
51168
+ headersGroups: hiddenGroups.slice(index),
51169
+ offset: this.env.model.getters.getMainViewportCoordinates().y,
51170
+ headerRange: { start: top, end: bottom },
51171
+ };
51172
+ }
51173
+ get frozenUnhideHeadersProps() {
51174
+ const { ySplit } = this.env.model.getters.getPaneDivisions(this.sheetId);
51175
+ const hiddenGroups = this.env.model.getters.getHiddenRowsGroups(this.sheetId);
51176
+ const index = hiddenGroups.findIndex((group) => group[0] >= ySplit - 1);
51177
+ return {
51178
+ headersGroups: hiddenGroups.slice(0, index + 1),
51179
+ headerRange: { start: 0, end: ySplit - 1 },
51180
+ };
51181
+ }
51182
+ get frozenContainerStyle() {
51183
+ return cssPropertiesToCss({
51184
+ height: this.env.model.getters.getMainViewportCoordinates().y + "px",
50880
51185
  });
50881
51186
  }
50882
- getUnhideButtonStyle(hiddenIndex) {
50883
- return cssPropertiesToCss({ top: this._getDimensionsInViewport(hiddenIndex).start + "px" });
51187
+ get hasFrozenPane() {
51188
+ return this.env.model.getters.getPaneDivisions(this.sheetId).ySplit > 0;
50884
51189
  }
50885
51190
  }
50886
51191
  css /* scss */ `
@@ -60660,6 +60965,7 @@ class EvaluationPlugin extends CoreViewPlugin {
60660
60965
  exportForExcel(data) {
60661
60966
  for (const sheet of data.sheets) {
60662
60967
  sheet.cellValues = {};
60968
+ sheet.formulaSpillRanges = {};
60663
60969
  }
60664
60970
  for (const position of this.evaluator.getEvaluatedPositions()) {
60665
60971
  const evaluatedCell = this.evaluator.getEvaluatedCell(position);
@@ -60671,8 +60977,9 @@ class EvaluationPlugin extends CoreViewPlugin {
60671
60977
  const exportedSheetData = data.sheets.find((sheet) => sheet.id === position.sheetId);
60672
60978
  const formulaCell = this.getCorrespondingFormulaCell(position);
60673
60979
  if (formulaCell) {
60980
+ const cell = this.getters.getCell(position);
60674
60981
  isExported = isExportableToExcel(formulaCell.compiledFormula.tokens);
60675
- isFormula = isExported;
60982
+ isFormula = isExported && cell?.content === formulaCell.content;
60676
60983
  // If the cell contains a non-exported formula and that is evaluates to
60677
60984
  // nothing* ,we don't export it.
60678
60985
  // * non-falsy value are relevant and so are 0 and FALSE, which only leaves
@@ -60695,7 +61002,11 @@ class EvaluationPlugin extends CoreViewPlugin {
60695
61002
  content = !isExported ? newContent : exportedCellData;
60696
61003
  }
60697
61004
  exportedSheetData.cells[xc] = content;
60698
- exportedSheetData.cellValues[xc] = value;
61005
+ exportedSheetData.cellValues[xc] = evaluatedCell.type !== "error" ? value : undefined;
61006
+ const spillZone = this.getSpreadZone(position);
61007
+ if (spillZone) {
61008
+ exportedSheetData.formulaSpillRanges[xc] = this.getters.getRangeString(this.getters.getRangeFromZone(position.sheetId, spillZone), position.sheetId);
61009
+ }
60699
61010
  }
60700
61011
  }
60701
61012
  /**
@@ -62923,7 +63234,7 @@ class AutofillPlugin extends UIPlugin {
62923
63234
  getRule(cell, cells) {
62924
63235
  const rules = autofillRulesRegistry.getAll().sort((a, b) => a.sequence - b.sequence);
62925
63236
  const rule = rules.find((rule) => rule.condition(cell, cells));
62926
- return rule && rule.generateRule(cell, cells);
63237
+ return rule && this.direction && rule.generateRule(cell, cells, this.direction);
62927
63238
  }
62928
63239
  /**
62929
63240
  * Create the generator to be able to autofill the next cells.
@@ -67644,7 +67955,7 @@ class InternalViewport {
67644
67955
  *
67645
67956
  */
67646
67957
  getFullRect(zone) {
67647
- const targetZone = intersection(zone, this);
67958
+ const targetZone = intersection(zone, this.boundaries);
67648
67959
  const scrollDeltaX = this.snapCorrection.x;
67649
67960
  const scrollDeltaY = this.snapCorrection.y;
67650
67961
  if (targetZone) {
@@ -68112,7 +68423,8 @@ class SheetViewPlugin extends UIPlugin {
68112
68423
  ? this.getters.getSheetViewVisibleCols()
68113
68424
  : this.getters.getSheetViewVisibleRows();
68114
68425
  const startIndex = visibleHeaders.findIndex((header) => referenceHeaderIndex >= header);
68115
- const endIndex = visibleHeaders.findIndex((header) => targetHeaderIndex <= header);
68426
+ let endIndex = visibleHeaders.findIndex((header) => targetHeaderIndex <= header);
68427
+ endIndex = endIndex === -1 ? visibleHeaders.length : endIndex;
68116
68428
  const relevantIndexes = visibleHeaders.slice(startIndex, endIndex);
68117
68429
  let offset = 0;
68118
68430
  for (const i of relevantIndexes) {
@@ -68237,11 +68549,12 @@ class SheetViewPlugin extends UIPlugin {
68237
68549
  * column of the current viewport
68238
68550
  */
68239
68551
  getColDimensionsInViewport(sheetId, col) {
68552
+ const { top } = this.getMainInternalViewport(sheetId);
68240
68553
  const zone = {
68241
68554
  left: col,
68242
68555
  right: col,
68243
- top: 0,
68244
- bottom: this.getters.getNumberRows(sheetId) - 1,
68556
+ top: top,
68557
+ bottom: top,
68245
68558
  };
68246
68559
  const { x, width } = this.getVisibleRect(zone);
68247
68560
  const start = x - this.gridOffsetX;
@@ -68252,9 +68565,10 @@ class SheetViewPlugin extends UIPlugin {
68252
68565
  * of the current viewport
68253
68566
  */
68254
68567
  getRowDimensionsInViewport(sheetId, row) {
68568
+ const { left } = this.getMainInternalViewport(sheetId);
68255
68569
  const zone = {
68256
68570
  left: 0,
68257
- right: this.getters.getNumberCols(sheetId) - 1,
68571
+ right: left,
68258
68572
  top: row,
68259
68573
  bottom: row,
68260
68574
  };
@@ -73405,7 +73719,7 @@ function numberRef(reference) {
73405
73719
  `;
73406
73720
  }
73407
73721
 
73408
- function addFormula(formula, value) {
73722
+ function addFormula(formula, value, formulaSpillRange) {
73409
73723
  if (!formula) {
73410
73724
  return { attrs: [], node: escapeXml `` };
73411
73725
  }
@@ -73413,10 +73727,17 @@ function addFormula(formula, value) {
73413
73727
  if (type === undefined) {
73414
73728
  return { attrs: [], node: escapeXml `` };
73415
73729
  }
73416
- const attrs = [["t", type]];
73730
+ const attrs = [
73731
+ ["cm", "1"],
73732
+ ["t", type],
73733
+ ];
73417
73734
  const XlsxFormula = adaptFormulaToExcel(formula);
73418
73735
  const exportedValue = adaptFormulaValueToExcel(value);
73419
- const node = escapeXml /*xml*/ `<f>${XlsxFormula}</f><v>${exportedValue}</v>`;
73736
+ // We treat all formulas as array formulas (a simple formula
73737
+ // is an array formula that spills on only one cell) to avoid
73738
+ // trying to detect spilling sub-formulas which is not a trivial task.
73739
+ let node;
73740
+ node = escapeXml /*xml*/ `<f t="array" ref="${formulaSpillRange}">${XlsxFormula}</f><v>${exportedValue}</v>`;
73420
73741
  return { attrs, node };
73421
73742
  }
73422
73743
  function addContent(content, sharedStrings, forceString = false) {
@@ -74199,7 +74520,7 @@ function addStyles(styles) {
74199
74520
  }
74200
74521
  if (alignAttrs.length > 0) {
74201
74522
  attributes.push(["applyAlignment", "1"]); // for Libre Office
74202
- styleNodes.push(escapeXml /*xml*/ `<xf ${formatAttributes(attributes)}>${escapeXml /*xml*/ `<alignment ${formatAttributes(alignAttrs)} />`}</xf> `);
74523
+ styleNodes.push(escapeXml /*xml*/ `<xf ${formatAttributes(attributes)}><alignment ${formatAttributes(alignAttrs)} /></xf> `);
74203
74524
  }
74204
74525
  else {
74205
74526
  styleNodes.push(escapeXml /*xml*/ `<xf ${formatAttributes(attributes)} />`);
@@ -74367,6 +74688,9 @@ function addColumns(cols) {
74367
74688
  }
74368
74689
  function addRows(construct, data, sheet) {
74369
74690
  const rowNodes = [];
74691
+ const styles = new PositionMap(iterateItemIdsPositions(sheet.id, sheet.styles));
74692
+ const borders = new PositionMap(iterateItemIdsPositions(sheet.id, sheet.borders));
74693
+ const formats = new PositionMap(iterateItemIdsPositions(sheet.id, sheet.formats));
74370
74694
  for (let r = 0; r < sheet.rowNumber; r++) {
74371
74695
  const rowAttrs = [["r", r + 1]];
74372
74696
  const row = sheet.rows[r] || {};
@@ -74382,9 +74706,6 @@ function addRows(construct, data, sheet) {
74382
74706
  if (row.collapsed) {
74383
74707
  rowAttrs.push(["collapsed", 1]);
74384
74708
  }
74385
- const styles = new PositionMap(iterateItemIdsPositions(sheet.id, sheet.styles));
74386
- const borders = new PositionMap(iterateItemIdsPositions(sheet.id, sheet.borders));
74387
- const formats = new PositionMap(iterateItemIdsPositions(sheet.id, sheet.formats));
74388
74709
  const cellNodes = [];
74389
74710
  for (let c = 0; c < sheet.colNumber; c++) {
74390
74711
  const xc = toXC(c, r);
@@ -74406,7 +74727,7 @@ function addRows(construct, data, sheet) {
74406
74727
  let cellNode = escapeXml ``;
74407
74728
  // Either formula or static value inside the cell
74408
74729
  if (content?.startsWith("=") && value !== undefined) {
74409
- const res = addFormula(content, value);
74730
+ const res = addFormula(content, value, sheet.formulaSpillRanges[xc] ?? xc);
74410
74731
  if (!res) {
74411
74732
  continue;
74412
74733
  }
@@ -74692,6 +75013,30 @@ function createWorksheets(data, construct) {
74692
75013
  `;
74693
75014
  files.push(createXMLFile(parseXML(sheetXml), `xl/worksheets/sheet${sheetIndex}.xml`, "sheet"));
74694
75015
  }
75016
+ const sheetMetadataXml = escapeXml /*xml*/ `
75017
+ <metadata xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:xda="http://schemas.microsoft.com/office/spreadsheetml/2017/dynamicarray">
75018
+ <metadataTypes count="1">
75019
+ <metadataType name="XLDAPR" minSupportedVersion="120000" copy="1" pasteAll="1"
75020
+ pasteValues="1" merge="1" splitFirst="1" rowColShift="1" clearFormats="1"
75021
+ clearComments="1" assign="1" coerce="1" cellMeta="1" />
75022
+ </metadataTypes>
75023
+ <futureMetadata name="XLDAPR" count="1">
75024
+ <bk>
75025
+ <extLst>
75026
+ <ext uri="{${ARRAY_FORMULA_URI}}">
75027
+ <xda:dynamicArrayProperties fDynamic="1" fCollapsed="0" />
75028
+ </ext>
75029
+ </extLst>
75030
+ </bk>
75031
+ </futureMetadata>
75032
+ <cellMetadata count="1">
75033
+ <bk>
75034
+ <rc t="1" v="0" />
75035
+ </bk>
75036
+ </cellMetadata>
75037
+ </metadata>
75038
+ `;
75039
+ files.push(createXMLFile(parseXML(sheetMetadataXml), "xl/metadata.xml", "metadata"));
74695
75040
  addRelsToFile(construct.relsFiles, "xl/_rels/workbook.xml.rels", {
74696
75041
  type: XLSX_RELATION_TYPE.sharedStrings,
74697
75042
  target: "sharedStrings.xml",
@@ -74700,6 +75045,10 @@ function createWorksheets(data, construct) {
74700
75045
  type: XLSX_RELATION_TYPE.styles,
74701
75046
  target: "styles.xml",
74702
75047
  });
75048
+ addRelsToFile(construct.relsFiles, "xl/_rels/workbook.xml.rels", {
75049
+ type: XLSX_RELATION_TYPE.metadata,
75050
+ target: "metadata.xml",
75051
+ });
74703
75052
  return files;
74704
75053
  }
74705
75054
  /**
@@ -75616,6 +75965,6 @@ const chartHelpers = { ...CHART_HELPERS, ...CHART_RUNTIME_HELPERS };
75616
75965
  export { AbstractCellClipboardHandler, AbstractChart, AbstractFigureClipboardHandler, CellErrorType, CommandResult, CorePlugin, CoreViewPlugin, DispatchResult, EvaluationError, Model, PivotRuntimeDefinition, Registry, Revision, SPREADSHEET_DIMENSIONS, Spreadsheet, SpreadsheetPivotTable, UIPlugin, __info__, addFunction, addRenderingLayer, astToFormula, chartHelpers, 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 };
75617
75966
 
75618
75967
 
75619
- __info__.version = "18.2.1";
75620
- __info__.date = "2025-02-25T06:03:13.262Z";
75621
- __info__.hash = "3b4b5c9";
75968
+ __info__.version = "18.2.3";
75969
+ __info__.date = "2025-03-12T15:32:36.274Z";
75970
+ __info__.hash = "81b0e08";