@odoo/o-spreadsheet 18.1.9 → 18.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,9 +2,9 @@
2
2
  /**
3
3
  * This file is generated by o-spreadsheet build tools. Do not edit it.
4
4
  * @see https://github.com/odoo/o-spreadsheet
5
- * @version 18.1.9
6
- * @date 2025-02-25T05:59:45.472Z
7
- * @hash 6789c1c
5
+ * @version 18.1.11
6
+ * @date 2025-03-12T15:31:44.276Z
7
+ * @hash 7de2363
8
8
  */
9
9
 
10
10
  'use strict';
@@ -424,7 +424,6 @@ function escapeRegExp(str) {
424
424
  * Sparse arrays remain sparse.
425
425
  */
426
426
  function deepCopy(obj) {
427
- const result = Array.isArray(obj) ? [] : {};
428
427
  switch (typeof obj) {
429
428
  case "object": {
430
429
  if (obj === null) {
@@ -436,8 +435,18 @@ function deepCopy(obj) {
436
435
  else if (!(isPlainObject(obj) || obj instanceof Array)) {
437
436
  throw new Error("Unsupported type: only objects and arrays are supported");
438
437
  }
439
- for (const key in obj) {
440
- result[key] = deepCopy(obj[key]);
438
+ const result = Array.isArray(obj) ? new Array(obj.length) : {};
439
+ if (Array.isArray(obj)) {
440
+ for (let i = 0, len = obj.length; i < len; i++) {
441
+ if (i in obj) {
442
+ result[i] = deepCopy(obj[i]);
443
+ }
444
+ }
445
+ }
446
+ else {
447
+ for (const key in obj) {
448
+ result[key] = deepCopy(obj[key]);
449
+ }
441
450
  }
442
451
  return result;
443
452
  }
@@ -2690,21 +2699,30 @@ function mergeContiguousZones(zones) {
2690
2699
  return mergedZones;
2691
2700
  }
2692
2701
 
2702
+ const globalReverseLookup$1 = new WeakMap();
2703
+ const globalIdCounter = new WeakMap();
2693
2704
  /**
2694
2705
  * Get the id of the given item (its key in the given dictionary).
2695
2706
  * If the given item does not exist in the dictionary, it creates one with a new id.
2696
2707
  */
2697
2708
  function getItemId(item, itemsDic) {
2698
- for (const key in itemsDic) {
2699
- if (deepEquals(itemsDic[key], item)) {
2700
- return parseInt(key, 10);
2701
- }
2709
+ if (!globalReverseLookup$1.has(itemsDic)) {
2710
+ globalReverseLookup$1.set(itemsDic, new Map());
2711
+ globalIdCounter.set(itemsDic, 0);
2712
+ }
2713
+ const reverseLookup = globalReverseLookup$1.get(itemsDic);
2714
+ const canonical = getCanonicalRepresentation(item);
2715
+ if (reverseLookup.has(canonical)) {
2716
+ const id = reverseLookup.get(canonical);
2717
+ itemsDic[id] = item;
2718
+ return id;
2702
2719
  }
2703
2720
  // Generate new Id if the item didn't exist in the dictionary
2704
- const ids = Object.keys(itemsDic);
2705
- const maxId = ids.length === 0 ? 0 : largeMax(ids.map((id) => parseInt(id, 10)));
2706
- itemsDic[maxId + 1] = item;
2707
- return maxId + 1;
2721
+ const newId = globalIdCounter.get(itemsDic) + 1;
2722
+ reverseLookup.set(canonical, newId);
2723
+ globalIdCounter.set(itemsDic, newId);
2724
+ itemsDic[newId] = item;
2725
+ return newId;
2708
2726
  }
2709
2727
  function groupItemIdsByZones(positionsByItemId) {
2710
2728
  const result = {};
@@ -2728,6 +2746,33 @@ function* iterateItemIdsPositions(sheetId, itemIdsByZones) {
2728
2746
  }
2729
2747
  }
2730
2748
  }
2749
+ function getCanonicalRepresentation(item) {
2750
+ if (item === null)
2751
+ return "null";
2752
+ if (item === undefined)
2753
+ return "undefined";
2754
+ if (typeof item !== "object")
2755
+ return String(item);
2756
+ if (Array.isArray(item)) {
2757
+ const len = item.length;
2758
+ let result = "[";
2759
+ for (let i = 0; i < len; i++) {
2760
+ if (i > 0)
2761
+ result += ",";
2762
+ result += getCanonicalRepresentation(item[i]);
2763
+ }
2764
+ return result + "]";
2765
+ }
2766
+ const keys = Object.keys(item).sort();
2767
+ let repr = "{";
2768
+ for (const key of keys) {
2769
+ if (item[key] !== undefined) {
2770
+ repr += `"${key}":${getCanonicalRepresentation(item[key])},`;
2771
+ }
2772
+ }
2773
+ repr += "}";
2774
+ return repr;
2775
+ }
2731
2776
 
2732
2777
  // -----------------------------------------------------------------------------
2733
2778
  // Date Type
@@ -6093,8 +6138,9 @@ function spreadRange(getters, dataSets) {
6093
6138
  if (zone.bottom !== zone.top && zone.left != zone.right) {
6094
6139
  if (zone.right) {
6095
6140
  for (let j = zone.left; j <= zone.right; ++j) {
6141
+ const datasetOptions = j === zone.left ? dataSet : { yAxisId: dataSet.yAxisId };
6096
6142
  postProcessedRanges.push({
6097
- ...dataSet,
6143
+ ...datasetOptions,
6098
6144
  dataRange: `${sheetPrefix}${zoneToXc({
6099
6145
  left: j,
6100
6146
  right: j,
@@ -6106,8 +6152,9 @@ function spreadRange(getters, dataSets) {
6106
6152
  }
6107
6153
  else {
6108
6154
  for (let j = zone.top; j <= zone.bottom; ++j) {
6155
+ const datasetOptions = j === zone.top ? dataSet : { yAxisId: dataSet.yAxisId };
6109
6156
  postProcessedRanges.push({
6110
- ...dataSet,
6157
+ ...datasetOptions,
6111
6158
  dataRange: `${sheetPrefix}${zoneToXc({
6112
6159
  left: zone.left,
6113
6160
  right: zone.right,
@@ -8279,7 +8326,8 @@ function isSortedColumnValid(sortedColumn, pivot) {
8279
8326
  const possibleValues = pivot
8280
8327
  .getPossibleFieldValues(columns[i])
8281
8328
  .map((v) => v.value);
8282
- if (!possibleValues.includes(sortedColumn.domain[i].value)) {
8329
+ if (!possibleValues.includes(sortedColumn.domain[i].value) &&
8330
+ !(sortedColumn.domain[i].value === null && possibleValues.includes(""))) {
8283
8331
  return false;
8284
8332
  }
8285
8333
  }
@@ -10058,70 +10106,341 @@ function getNextNonEmptyBar(bars, startIndex) {
10058
10106
  return bars.find((bar, i) => i > startIndex && bar.height !== 0);
10059
10107
  }
10060
10108
 
10061
- window.Chart?.register(waterfallLinesPlugin);
10062
- window.Chart?.register(chartShowValuesPlugin);
10063
- class ChartJsComponent extends owl.Component {
10064
- static template = "o-spreadsheet-ChartJsComponent";
10065
- static props = {
10066
- figure: Object,
10109
+ const GAUGE_PADDING_SIDE = 30;
10110
+ const GAUGE_PADDING_TOP = 10;
10111
+ const GAUGE_PADDING_BOTTOM = 20;
10112
+ const GAUGE_LABELS_FONT_SIZE = 12;
10113
+ const GAUGE_DEFAULT_VALUE_FONT_SIZE = 80;
10114
+ const GAUGE_BACKGROUND_COLOR = "#F3F2F1";
10115
+ const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
10116
+ const GAUGE_TITLE_SECTION_HEIGHT = 25;
10117
+ function drawGaugeChart(canvas, runtime) {
10118
+ const canvasBoundingRect = canvas.getBoundingClientRect();
10119
+ canvas.width = canvasBoundingRect.width;
10120
+ canvas.height = canvasBoundingRect.height;
10121
+ const ctx = canvas.getContext("2d");
10122
+ const config = getGaugeRenderingConfig(canvasBoundingRect, runtime, ctx);
10123
+ drawBackground(ctx, config);
10124
+ drawGauge(ctx, config);
10125
+ drawInflectionValues(ctx, config);
10126
+ drawLabels(ctx, config);
10127
+ drawTitle(ctx, config);
10128
+ }
10129
+ function drawGauge(ctx, config) {
10130
+ ctx.save();
10131
+ const gauge = config.gauge;
10132
+ const arcCenterX = gauge.rect.x + gauge.rect.width / 2;
10133
+ const arcCenterY = gauge.rect.y + gauge.rect.height;
10134
+ const arcRadius = gauge.rect.height - gauge.arcWidth / 2;
10135
+ if (arcRadius < 0) {
10136
+ return;
10137
+ }
10138
+ const gaugeAngle = gauge.percentage === 1 ? 0 : Math.PI * (1 + gauge.percentage);
10139
+ // Gauge background
10140
+ ctx.strokeStyle = GAUGE_BACKGROUND_COLOR;
10141
+ ctx.beginPath();
10142
+ ctx.lineWidth = gauge.arcWidth;
10143
+ ctx.arc(arcCenterX, arcCenterY, arcRadius, gaugeAngle, 0);
10144
+ ctx.stroke();
10145
+ // Gauge value
10146
+ ctx.strokeStyle = gauge.color;
10147
+ ctx.beginPath();
10148
+ ctx.arc(arcCenterX, arcCenterY, arcRadius, Math.PI, gaugeAngle);
10149
+ ctx.stroke();
10150
+ ctx.restore();
10151
+ }
10152
+ function drawBackground(ctx, config) {
10153
+ ctx.save();
10154
+ ctx.fillStyle = config.backgroundColor;
10155
+ ctx.fillRect(0, 0, config.width, config.height);
10156
+ ctx.restore();
10157
+ }
10158
+ function drawLabels(ctx, config) {
10159
+ for (const label of [config.minLabel, config.maxLabel, config.gaugeValue]) {
10160
+ ctx.save();
10161
+ ctx.textAlign = "center";
10162
+ ctx.fillStyle = label.color;
10163
+ ctx.font = `${label.fontSize}px ${DEFAULT_FONT}`;
10164
+ ctx.fillText(label.label, label.textPosition.x, label.textPosition.y);
10165
+ ctx.restore();
10166
+ }
10167
+ }
10168
+ function drawInflectionValues(ctx, config) {
10169
+ const { x: rectX, y: rectY, width, height } = config.gauge.rect;
10170
+ for (const inflectionValue of config.inflectionValues) {
10171
+ ctx.save();
10172
+ ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment
10173
+ ctx.rotate(Math.PI / 2 - inflectionValue.rotation);
10174
+ ctx.lineWidth = 2;
10175
+ ctx.strokeStyle = chartMutedFontColor(config.backgroundColor) + "aa";
10176
+ ctx.beginPath();
10177
+ ctx.moveTo(0, -(height - config.gauge.arcWidth));
10178
+ ctx.lineTo(0, -height - 3);
10179
+ ctx.stroke();
10180
+ ctx.textAlign = "center";
10181
+ ctx.font = `${inflectionValue.fontSize}px ${DEFAULT_FONT}`;
10182
+ ctx.fillStyle = inflectionValue.color;
10183
+ const textY = -height - GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN - inflectionValue.offset;
10184
+ ctx.fillText(inflectionValue.label, 0, textY);
10185
+ ctx.restore();
10186
+ }
10187
+ }
10188
+ function drawTitle(ctx, config) {
10189
+ ctx.save();
10190
+ const title = config.title;
10191
+ ctx.font = getDefaultContextFont(title.fontSize, title.bold, title.italic);
10192
+ ctx.textBaseline = "middle";
10193
+ ctx.fillStyle = title.color;
10194
+ ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
10195
+ ctx.restore();
10196
+ }
10197
+ function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
10198
+ const maxValue = runtime.maxValue;
10199
+ const minValue = runtime.minValue;
10200
+ const gaugeValue = runtime.gaugeValue;
10201
+ const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
10202
+ const gaugeArcWidth = gaugeRect.width / 6;
10203
+ const gaugePercentage = gaugeValue
10204
+ ? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
10205
+ : 0;
10206
+ const gaugeValuePosition = {
10207
+ x: boundingRect.width / 2,
10208
+ y: gaugeRect.y + gaugeRect.height - gaugeRect.height / 12,
10067
10209
  };
10068
- canvas = owl.useRef("graphContainer");
10069
- chart;
10070
- currentRuntime;
10071
- get background() {
10072
- return this.chartRuntime.background;
10210
+ let gaugeValueFontSize = GAUGE_DEFAULT_VALUE_FONT_SIZE;
10211
+ // Scale down the font size if the gaugeRect is too small
10212
+ if (gaugeRect.height < 300) {
10213
+ gaugeValueFontSize = gaugeValueFontSize * (gaugeRect.height / 300);
10073
10214
  }
10074
- get canvasStyle() {
10075
- return `background-color: ${this.background}`;
10215
+ // Scale down the font size if the text is too long
10216
+ const maxTextWidth = gaugeRect.width / 2;
10217
+ const gaugeLabel = gaugeValue?.label || "-";
10218
+ if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
10219
+ gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
10076
10220
  }
10077
- get chartRuntime() {
10078
- const runtime = this.env.model.getters.getChartRuntime(this.props.figure.id);
10079
- if (!("chartJsConfig" in runtime)) {
10080
- throw new Error("Unsupported chart runtime");
10081
- }
10082
- return runtime;
10221
+ const minLabelPosition = {
10222
+ x: gaugeRect.x + gaugeArcWidth / 2,
10223
+ y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
10224
+ };
10225
+ const maxLabelPosition = {
10226
+ x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
10227
+ y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
10228
+ };
10229
+ const textColor = chartMutedFontColor(runtime.background);
10230
+ const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);
10231
+ let x = 0, titleWidth = 0, titleHeight = 0;
10232
+ if (runtime.title.text) {
10233
+ ({ width: titleWidth, height: titleHeight } = computeTextDimension(ctx, runtime.title.text, { fontSize: CHART_TITLE_FONT_SIZE, ...runtime.title }, "px"));
10083
10234
  }
10084
- setup() {
10085
- owl.onMounted(() => {
10086
- const runtime = this.chartRuntime;
10087
- this.currentRuntime = runtime;
10088
- // Note: chartJS modify the runtime in place, so it's important to give it a copy
10089
- this.createChart(deepCopy(runtime.chartJsConfig));
10090
- });
10091
- owl.onWillUnmount(() => this.chart?.destroy());
10092
- owl.useEffect(() => {
10093
- const runtime = this.chartRuntime;
10094
- if (runtime !== this.currentRuntime) {
10095
- if (runtime.chartJsConfig.type !== this.currentRuntime.chartJsConfig.type) {
10096
- this.chart?.destroy();
10097
- this.createChart(deepCopy(runtime.chartJsConfig));
10098
- }
10099
- else {
10100
- this.updateChartJs(deepCopy(runtime));
10101
- }
10102
- this.currentRuntime = runtime;
10103
- }
10235
+ switch (runtime.title.align) {
10236
+ case "right":
10237
+ x = boundingRect.width - titleWidth - CHART_PADDING$1;
10238
+ break;
10239
+ case "center":
10240
+ x = (boundingRect.width - titleWidth) / 2;
10241
+ break;
10242
+ case "left":
10243
+ default:
10244
+ x = CHART_PADDING$1;
10245
+ break;
10246
+ }
10247
+ return {
10248
+ width: boundingRect.width,
10249
+ height: boundingRect.height,
10250
+ title: {
10251
+ label: runtime.title.text ?? "",
10252
+ fontSize: runtime.title.fontSize ?? CHART_TITLE_FONT_SIZE,
10253
+ textPosition: {
10254
+ x,
10255
+ y: CHART_PADDING_TOP + titleHeight / 2,
10256
+ },
10257
+ color: runtime.title.color ?? textColor,
10258
+ bold: runtime.title.bold,
10259
+ italic: runtime.title.italic,
10260
+ },
10261
+ backgroundColor: runtime.background,
10262
+ gauge: {
10263
+ rect: gaugeRect,
10264
+ arcWidth: gaugeArcWidth,
10265
+ percentage: clip(gaugePercentage, 0, 1),
10266
+ color: getGaugeColor(runtime),
10267
+ },
10268
+ inflectionValues,
10269
+ gaugeValue: {
10270
+ label: gaugeLabel,
10271
+ textPosition: gaugeValuePosition,
10272
+ fontSize: gaugeValueFontSize,
10273
+ color: textColor,
10274
+ },
10275
+ minLabel: {
10276
+ label: runtime.minValue.label,
10277
+ textPosition: minLabelPosition,
10278
+ fontSize: GAUGE_LABELS_FONT_SIZE,
10279
+ color: textColor,
10280
+ },
10281
+ maxLabel: {
10282
+ label: runtime.maxValue.label,
10283
+ textPosition: maxLabelPosition,
10284
+ fontSize: GAUGE_LABELS_FONT_SIZE,
10285
+ color: textColor,
10286
+ },
10287
+ };
10288
+ }
10289
+ /**
10290
+ * Get the rectangle in which the gauge will be drawn, based on the bounding rectangle of the canvas and leaving
10291
+ * space for the title and labels.
10292
+ */
10293
+ function getGaugeRect(boundingRect, title) {
10294
+ const titleHeight = title ? GAUGE_TITLE_SECTION_HEIGHT : 0;
10295
+ const drawHeight = boundingRect.height - GAUGE_PADDING_BOTTOM - titleHeight - GAUGE_PADDING_TOP;
10296
+ const drawWidth = boundingRect.width - GAUGE_PADDING_SIDE * 2;
10297
+ let gaugeWidth;
10298
+ let gaugeHeight;
10299
+ if (drawWidth > 2 * drawHeight) {
10300
+ gaugeWidth = 2 * drawHeight;
10301
+ gaugeHeight = drawHeight;
10302
+ }
10303
+ else {
10304
+ gaugeWidth = drawWidth;
10305
+ gaugeHeight = drawWidth / 2;
10306
+ }
10307
+ const gaugeX = GAUGE_PADDING_SIDE + (drawWidth - gaugeWidth) / 2;
10308
+ const gaugeY = titleHeight + GAUGE_PADDING_TOP + (drawHeight - gaugeHeight) / 2;
10309
+ return {
10310
+ x: gaugeX,
10311
+ y: gaugeY,
10312
+ width: gaugeWidth,
10313
+ height: gaugeHeight,
10314
+ };
10315
+ }
10316
+ /**
10317
+ * 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).
10318
+ *
10319
+ * Also compute an offset for the text so that it doesn't overlap with other text.
10320
+ */
10321
+ function getInflectionValues(runtime, gaugeRect, textColor, ctx) {
10322
+ const maxValue = runtime.maxValue;
10323
+ const minValue = runtime.minValue;
10324
+ const gaugeCircleCenter = {
10325
+ x: gaugeRect.x + gaugeRect.width / 2,
10326
+ y: gaugeRect.y + gaugeRect.height,
10327
+ };
10328
+ const textStyle = { fontSize: GAUGE_LABELS_FONT_SIZE };
10329
+ const inflectionValues = [];
10330
+ const inflectionValuesTextRects = [];
10331
+ for (const inflectionValue of runtime.inflectionValues) {
10332
+ const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
10333
+ const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
10334
+ const angle = Math.PI - Math.PI * percentage;
10335
+ const textRect = getRectangleTangentToCircle(angle, // angle between X axis and the point where the rectangle is tangent to the circle
10336
+ gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, // radius of the gauge circle + margin below text
10337
+ gaugeCircleCenter.x, // center of the gauge circle
10338
+ gaugeCircleCenter.y, // center of the gauge circle
10339
+ labelWidth + 2, // width of the text + some margin
10340
+ GAUGE_LABELS_FONT_SIZE // height of the text
10341
+ );
10342
+ let offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect))
10343
+ ? GAUGE_LABELS_FONT_SIZE
10344
+ : 0;
10345
+ inflectionValuesTextRects.push(textRect);
10346
+ inflectionValues.push({
10347
+ rotation: angle,
10348
+ label: inflectionValue.label,
10349
+ fontSize: GAUGE_LABELS_FONT_SIZE,
10350
+ color: textColor,
10351
+ offset,
10104
10352
  });
10105
10353
  }
10106
- createChart(chartData) {
10107
- const canvas = this.canvas.el;
10108
- const ctx = canvas.getContext("2d");
10109
- this.chart = new window.Chart(ctx, chartData);
10354
+ return inflectionValues;
10355
+ }
10356
+ function getGaugeColor(runtime) {
10357
+ const gaugeValue = runtime.gaugeValue?.value;
10358
+ if (gaugeValue === undefined) {
10359
+ return GAUGE_BACKGROUND_COLOR;
10110
10360
  }
10111
- updateChartJs(chartRuntime) {
10112
- const chartData = chartRuntime.chartJsConfig;
10113
- if (chartData.data && chartData.data.datasets) {
10114
- this.chart.data = chartData.data;
10115
- if (chartData.options?.plugins?.title) {
10116
- this.chart.config.options.plugins.title = chartData.options.plugins.title;
10117
- }
10361
+ for (let i = 0; i < runtime.inflectionValues.length; i++) {
10362
+ const inflectionValue = runtime.inflectionValues[i];
10363
+ if (inflectionValue.operator === "<" && gaugeValue < inflectionValue.value) {
10364
+ return runtime.colors[i];
10118
10365
  }
10119
- else {
10120
- this.chart.data.datasets = [];
10366
+ else if (inflectionValue.operator === "<=" && gaugeValue <= inflectionValue.value) {
10367
+ return runtime.colors[i];
10121
10368
  }
10122
- this.chart.config.options = chartData.options;
10123
- this.chart.update();
10124
10369
  }
10370
+ return runtime.colors.at(-1);
10371
+ }
10372
+ function getSegmentsOfRectangle(rectangle) {
10373
+ return [
10374
+ { start: rectangle.topLeft, end: rectangle.topRight },
10375
+ { start: rectangle.topRight, end: rectangle.bottomRight },
10376
+ { start: rectangle.bottomRight, end: rectangle.bottomLeft },
10377
+ { start: rectangle.bottomLeft, end: rectangle.topLeft },
10378
+ ];
10379
+ }
10380
+ /**
10381
+ * Check if two segment intersect. The case where the segments are colinear (both segments on the same line)
10382
+ * is not handled.
10383
+ */
10384
+ function doSegmentIntersect(segment1, segment2) {
10385
+ const A = segment1.start;
10386
+ const B = segment1.end;
10387
+ const C = segment2.start;
10388
+ const D = segment2.end;
10389
+ /**
10390
+ * Line segment intersection algorithm
10391
+ * https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
10392
+ */
10393
+ function ccw(a, b, c) {
10394
+ return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
10395
+ }
10396
+ return ccw(A, C, D) !== ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D);
10397
+ }
10398
+ function doRectanglesIntersect(rect1, rect2) {
10399
+ const segments1 = getSegmentsOfRectangle(rect1);
10400
+ const segments2 = getSegmentsOfRectangle(rect2);
10401
+ for (const segment1 of segments1) {
10402
+ for (const segment2 of segments2) {
10403
+ if (doSegmentIntersect(segment1, segment2)) {
10404
+ return true;
10405
+ }
10406
+ }
10407
+ }
10408
+ return false;
10409
+ }
10410
+ /**
10411
+ * Get the rectangle that is tangent to a circle at a given angle.
10412
+ *
10413
+ * @param angle angle between X axis and the point where the rectangle is tangent to the circle
10414
+ */
10415
+ function getRectangleTangentToCircle(angle, radius, circleCenterX, circleCenterY, rectWidth, rectHeight) {
10416
+ const cos = Math.cos(angle);
10417
+ const sin = Math.sin(angle);
10418
+ // x, y are the distance from the center of the circle to the point where the rectangle is tangent to the circle
10419
+ const x = cos * radius;
10420
+ const y = sin * radius;
10421
+ // x2, y2 are the distance from the point the rectangle is tangent to the circle to the bottom left corner of the rectangle
10422
+ const x2 = sin * (rectWidth / 2); // cos(angle + 90°) = sin(angle)
10423
+ const y2 = cos * (rectWidth / 2);
10424
+ const bottomRight = {
10425
+ x: x + x2 + circleCenterX,
10426
+ y: circleCenterY - (y - y2),
10427
+ };
10428
+ const bottomLeft = {
10429
+ x: x - x2 + circleCenterX,
10430
+ y: circleCenterY - (y + y2),
10431
+ };
10432
+ // Same as above but for the top corners of the rectangle (radius + rectangle height instead of radius)
10433
+ const xp = cos * (radius + rectHeight);
10434
+ const yp = sin * (radius + rectHeight);
10435
+ const topLeft = {
10436
+ x: xp - x2 + circleCenterX,
10437
+ y: circleCenterY - (yp + y2),
10438
+ };
10439
+ const topRight = {
10440
+ x: xp + x2 + circleCenterX,
10441
+ y: circleCenterY - (yp - y2),
10442
+ };
10443
+ return { bottomLeft, bottomRight, topRight, topLeft };
10125
10444
  }
10126
10445
 
10127
10446
  /**
@@ -10703,6 +11022,155 @@ class ScorecardChartConfigBuilder {
10703
11022
  }
10704
11023
  }
10705
11024
 
11025
+ const CHART_COMMON_OPTIONS = {
11026
+ // https://www.chartjs.org/docs/latest/general/responsive.html
11027
+ responsive: true, // will resize when its container is resized
11028
+ maintainAspectRatio: false, // doesn't maintain the aspect ratio (width/height =2 by default) so the user has the choice of the exact layout
11029
+ elements: {
11030
+ line: {
11031
+ fill: false, // do not fill the area under line charts
11032
+ },
11033
+ point: {
11034
+ hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
11035
+ },
11036
+ },
11037
+ animation: false,
11038
+ };
11039
+ function truncateLabel(label) {
11040
+ if (!label) {
11041
+ return "";
11042
+ }
11043
+ if (label.length > MAX_CHAR_LABEL) {
11044
+ return label.substring(0, MAX_CHAR_LABEL) + "…";
11045
+ }
11046
+ return label;
11047
+ }
11048
+ function chartToImage(runtime, figure, type) {
11049
+ // wrap the canvas in a div with a fixed size because chart.js would
11050
+ // fill the whole page otherwise
11051
+ const div = document.createElement("div");
11052
+ div.style.width = `${figure.width}px`;
11053
+ div.style.height = `${figure.height}px`;
11054
+ const canvas = document.createElement("canvas");
11055
+ div.append(canvas);
11056
+ canvas.setAttribute("width", figure.width.toString());
11057
+ canvas.setAttribute("height", figure.height.toString());
11058
+ // we have to add the canvas to the DOM otherwise it won't be rendered
11059
+ document.body.append(div);
11060
+ if ("chartJsConfig" in runtime) {
11061
+ const config = deepCopy(runtime.chartJsConfig);
11062
+ config.plugins = [backgroundColorChartJSPlugin];
11063
+ const Chart = getChartJSConstructor();
11064
+ const chart = new Chart(canvas, config);
11065
+ const imgContent = chart.toBase64Image();
11066
+ chart.destroy();
11067
+ div.remove();
11068
+ return imgContent;
11069
+ }
11070
+ else if (type === "scorecard") {
11071
+ const design = getScorecardConfiguration(figure, runtime);
11072
+ drawScoreChart(design, canvas);
11073
+ const imgContent = canvas.toDataURL();
11074
+ div.remove();
11075
+ return imgContent;
11076
+ }
11077
+ else if (type === "gauge") {
11078
+ drawGaugeChart(canvas, runtime);
11079
+ const imgContent = canvas.toDataURL();
11080
+ div.remove();
11081
+ return imgContent;
11082
+ }
11083
+ return undefined;
11084
+ }
11085
+ /**
11086
+ * Custom chart.js plugin to set the background color of the canvas
11087
+ * https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md
11088
+ */
11089
+ const backgroundColorChartJSPlugin = {
11090
+ id: "customCanvasBackgroundColor",
11091
+ beforeDraw: (chart) => {
11092
+ const { ctx } = chart;
11093
+ ctx.save();
11094
+ ctx.globalCompositeOperation = "destination-over";
11095
+ ctx.fillStyle = "#ffffff";
11096
+ ctx.fillRect(0, 0, chart.width, chart.height);
11097
+ ctx.restore();
11098
+ },
11099
+ };
11100
+ /** Return window.Chart, making sure all our extensions are loaded in ChartJS */
11101
+ function getChartJSConstructor() {
11102
+ if (window.Chart && !window.Chart?.registry.plugins.get("chartShowValuesPlugin")) {
11103
+ window.Chart.register(chartShowValuesPlugin);
11104
+ window.Chart.register(waterfallLinesPlugin);
11105
+ }
11106
+ return window.Chart;
11107
+ }
11108
+
11109
+ class ChartJsComponent extends owl.Component {
11110
+ static template = "o-spreadsheet-ChartJsComponent";
11111
+ static props = {
11112
+ figure: Object,
11113
+ };
11114
+ canvas = owl.useRef("graphContainer");
11115
+ chart;
11116
+ currentRuntime;
11117
+ get background() {
11118
+ return this.chartRuntime.background;
11119
+ }
11120
+ get canvasStyle() {
11121
+ return `background-color: ${this.background}`;
11122
+ }
11123
+ get chartRuntime() {
11124
+ const runtime = this.env.model.getters.getChartRuntime(this.props.figure.id);
11125
+ if (!("chartJsConfig" in runtime)) {
11126
+ throw new Error("Unsupported chart runtime");
11127
+ }
11128
+ return runtime;
11129
+ }
11130
+ setup() {
11131
+ owl.onMounted(() => {
11132
+ const runtime = this.chartRuntime;
11133
+ this.currentRuntime = runtime;
11134
+ // Note: chartJS modify the runtime in place, so it's important to give it a copy
11135
+ this.createChart(deepCopy(runtime.chartJsConfig));
11136
+ });
11137
+ owl.onWillUnmount(() => this.chart?.destroy());
11138
+ owl.useEffect(() => {
11139
+ const runtime = this.chartRuntime;
11140
+ if (runtime !== this.currentRuntime) {
11141
+ if (runtime.chartJsConfig.type !== this.currentRuntime.chartJsConfig.type) {
11142
+ this.chart?.destroy();
11143
+ this.createChart(deepCopy(runtime.chartJsConfig));
11144
+ }
11145
+ else {
11146
+ this.updateChartJs(deepCopy(runtime));
11147
+ }
11148
+ this.currentRuntime = runtime;
11149
+ }
11150
+ });
11151
+ }
11152
+ createChart(chartData) {
11153
+ const canvas = this.canvas.el;
11154
+ const ctx = canvas.getContext("2d");
11155
+ const Chart = getChartJSConstructor();
11156
+ this.chart = new Chart(ctx, chartData);
11157
+ }
11158
+ updateChartJs(chartRuntime) {
11159
+ const chartData = chartRuntime.chartJsConfig;
11160
+ if (chartData.data && chartData.data.datasets) {
11161
+ this.chart.data = chartData.data;
11162
+ if (chartData.options?.plugins?.title) {
11163
+ this.chart.config.options.plugins.title = chartData.options.plugins.title;
11164
+ }
11165
+ }
11166
+ else {
11167
+ this.chart.data.datasets = [];
11168
+ }
11169
+ this.chart.config.options = chartData.options;
11170
+ this.chart.update();
11171
+ }
11172
+ }
11173
+
10706
11174
  class ScorecardChart extends owl.Component {
10707
11175
  static template = "o-spreadsheet-ScorecardChart";
10708
11176
  static props = {
@@ -10734,6 +11202,7 @@ class ScorecardChart extends owl.Component {
10734
11202
  const autoCompleteProviders = new Registry();
10735
11203
 
10736
11204
  autoCompleteProviders.add("dataValidation", {
11205
+ displayAllOnInitialContent: true,
10737
11206
  getProposals(tokenAtCursor, content) {
10738
11207
  if (content.startsWith("=")) {
10739
11208
  return [];
@@ -21030,6 +21499,15 @@ class AbstractComposerStore extends SpreadsheetStore {
21030
21499
  const exactMatch = proposals?.find((p) => p.text === tokenAtCursor.value);
21031
21500
  // remove tokens that are likely to be other parts of the formula that slipped in the token if it's a string
21032
21501
  const searchTerm = tokenAtCursor.value.replace(/[ ,\(\)]/g, "");
21502
+ if (this._currentContent === this.initialContent &&
21503
+ provider.displayAllOnInitialContent &&
21504
+ proposals?.length) {
21505
+ return {
21506
+ proposals,
21507
+ selectProposal: provider.selectProposal,
21508
+ autoSelectFirstProposal: provider.autoSelectFirstProposal ?? false,
21509
+ };
21510
+ }
21033
21511
  if (exactMatch && this._currentContent !== this.initialContent) {
21034
21512
  // this means the user has chosen a proposal
21035
21513
  return;
@@ -22155,7 +22633,7 @@ autofillRulesRegistry
22155
22633
  condition: (cell) => !cell.isFormula &&
22156
22634
  evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.text &&
22157
22635
  alphaNumericValueRegExp.test(cell.content),
22158
- generateRule: (cell, cells) => {
22636
+ generateRule: (cell, cells, direction) => {
22159
22637
  const numberPostfix = parseInt(cell.content.match(numberPostfixRegExp)[0]);
22160
22638
  const prefix = cell.content.match(stringPrefixRegExp)[0];
22161
22639
  const numberPostfixLength = cell.content.length - prefix.length;
@@ -22163,7 +22641,10 @@ autofillRulesRegistry
22163
22641
  alphaNumericValueRegExp.test(evaluatedCell.value)) // get consecutive alphanumeric cells, no matter what the prefix is
22164
22642
  .filter((cell) => prefix === (cell.value ?? "").toString().match(stringPrefixRegExp)[0])
22165
22643
  .map((cell) => parseInt((cell.value ?? "").toString().match(numberPostfixRegExp)[0]));
22166
- const increment = calculateIncrementBasedOnGroup(group);
22644
+ let increment = calculateIncrementBasedOnGroup(group);
22645
+ if (["up", "left"].includes(direction) && group.length === 1) {
22646
+ increment = -increment;
22647
+ }
22167
22648
  return {
22168
22649
  type: "ALPHANUMERIC_INCREMENT_MODIFIER",
22169
22650
  prefix,
@@ -22226,10 +22707,13 @@ autofillRulesRegistry
22226
22707
  .add("increment_number", {
22227
22708
  condition: (cell) => !cell.isFormula &&
22228
22709
  evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.number,
22229
- generateRule: (cell, cells) => {
22710
+ generateRule: (cell, cells, direction) => {
22230
22711
  const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number &&
22231
22712
  !isDateTimeFormat(evaluatedCell.format || "")).map((cell) => Number(cell.value));
22232
- const increment = calculateIncrementBasedOnGroup(group);
22713
+ let increment = calculateIncrementBasedOnGroup(group);
22714
+ if (["up", "left"].includes(direction) && group.length === 1) {
22715
+ increment = -increment;
22716
+ }
22233
22717
  const evaluation = evaluateLiteral(cell, { locale: DEFAULT_LOCALE });
22234
22718
  return {
22235
22719
  type: "INCREMENT_MODIFIER",
@@ -22273,343 +22757,6 @@ function getDateIntervals(dates) {
22273
22757
 
22274
22758
  const cellPopoverRegistry = new Registry();
22275
22759
 
22276
- const GAUGE_PADDING_SIDE = 30;
22277
- const GAUGE_PADDING_TOP = 10;
22278
- const GAUGE_PADDING_BOTTOM = 20;
22279
- const GAUGE_LABELS_FONT_SIZE = 12;
22280
- const GAUGE_DEFAULT_VALUE_FONT_SIZE = 80;
22281
- const GAUGE_BACKGROUND_COLOR = "#F3F2F1";
22282
- const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
22283
- const GAUGE_TITLE_SECTION_HEIGHT = 25;
22284
- function drawGaugeChart(canvas, runtime) {
22285
- const canvasBoundingRect = canvas.getBoundingClientRect();
22286
- canvas.width = canvasBoundingRect.width;
22287
- canvas.height = canvasBoundingRect.height;
22288
- const ctx = canvas.getContext("2d");
22289
- const config = getGaugeRenderingConfig(canvasBoundingRect, runtime, ctx);
22290
- drawBackground(ctx, config);
22291
- drawGauge(ctx, config);
22292
- drawInflectionValues(ctx, config);
22293
- drawLabels(ctx, config);
22294
- drawTitle(ctx, config);
22295
- }
22296
- function drawGauge(ctx, config) {
22297
- ctx.save();
22298
- const gauge = config.gauge;
22299
- const arcCenterX = gauge.rect.x + gauge.rect.width / 2;
22300
- const arcCenterY = gauge.rect.y + gauge.rect.height;
22301
- const arcRadius = gauge.rect.height - gauge.arcWidth / 2;
22302
- if (arcRadius < 0) {
22303
- return;
22304
- }
22305
- const gaugeAngle = gauge.percentage === 1 ? 0 : Math.PI * (1 + gauge.percentage);
22306
- // Gauge background
22307
- ctx.strokeStyle = GAUGE_BACKGROUND_COLOR;
22308
- ctx.beginPath();
22309
- ctx.lineWidth = gauge.arcWidth;
22310
- ctx.arc(arcCenterX, arcCenterY, arcRadius, gaugeAngle, 0);
22311
- ctx.stroke();
22312
- // Gauge value
22313
- ctx.strokeStyle = gauge.color;
22314
- ctx.beginPath();
22315
- ctx.arc(arcCenterX, arcCenterY, arcRadius, Math.PI, gaugeAngle);
22316
- ctx.stroke();
22317
- ctx.restore();
22318
- }
22319
- function drawBackground(ctx, config) {
22320
- ctx.save();
22321
- ctx.fillStyle = config.backgroundColor;
22322
- ctx.fillRect(0, 0, config.width, config.height);
22323
- ctx.restore();
22324
- }
22325
- function drawLabels(ctx, config) {
22326
- for (const label of [config.minLabel, config.maxLabel, config.gaugeValue]) {
22327
- ctx.save();
22328
- ctx.textAlign = "center";
22329
- ctx.fillStyle = label.color;
22330
- ctx.font = `${label.fontSize}px ${DEFAULT_FONT}`;
22331
- ctx.fillText(label.label, label.textPosition.x, label.textPosition.y);
22332
- ctx.restore();
22333
- }
22334
- }
22335
- function drawInflectionValues(ctx, config) {
22336
- const { x: rectX, y: rectY, width, height } = config.gauge.rect;
22337
- for (const inflectionValue of config.inflectionValues) {
22338
- ctx.save();
22339
- ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment
22340
- ctx.rotate(Math.PI / 2 - inflectionValue.rotation);
22341
- ctx.lineWidth = 2;
22342
- ctx.strokeStyle = chartMutedFontColor(config.backgroundColor) + "aa";
22343
- ctx.beginPath();
22344
- ctx.moveTo(0, -(height - config.gauge.arcWidth));
22345
- ctx.lineTo(0, -height - 3);
22346
- ctx.stroke();
22347
- ctx.textAlign = "center";
22348
- ctx.font = `${inflectionValue.fontSize}px ${DEFAULT_FONT}`;
22349
- ctx.fillStyle = inflectionValue.color;
22350
- const textY = -height - GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN - inflectionValue.offset;
22351
- ctx.fillText(inflectionValue.label, 0, textY);
22352
- ctx.restore();
22353
- }
22354
- }
22355
- function drawTitle(ctx, config) {
22356
- ctx.save();
22357
- const title = config.title;
22358
- ctx.font = getDefaultContextFont(title.fontSize, title.bold, title.italic);
22359
- ctx.textBaseline = "middle";
22360
- ctx.fillStyle = title.color;
22361
- ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
22362
- ctx.restore();
22363
- }
22364
- function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
22365
- const maxValue = runtime.maxValue;
22366
- const minValue = runtime.minValue;
22367
- const gaugeValue = runtime.gaugeValue;
22368
- const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
22369
- const gaugeArcWidth = gaugeRect.width / 6;
22370
- const gaugePercentage = gaugeValue
22371
- ? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
22372
- : 0;
22373
- const gaugeValuePosition = {
22374
- x: boundingRect.width / 2,
22375
- y: gaugeRect.y + gaugeRect.height - gaugeRect.height / 12,
22376
- };
22377
- let gaugeValueFontSize = GAUGE_DEFAULT_VALUE_FONT_SIZE;
22378
- // Scale down the font size if the gaugeRect is too small
22379
- if (gaugeRect.height < 300) {
22380
- gaugeValueFontSize = gaugeValueFontSize * (gaugeRect.height / 300);
22381
- }
22382
- // Scale down the font size if the text is too long
22383
- const maxTextWidth = gaugeRect.width / 2;
22384
- const gaugeLabel = gaugeValue?.label || "-";
22385
- if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
22386
- gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
22387
- }
22388
- const minLabelPosition = {
22389
- x: gaugeRect.x + gaugeArcWidth / 2,
22390
- y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
22391
- };
22392
- const maxLabelPosition = {
22393
- x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
22394
- y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
22395
- };
22396
- const textColor = chartMutedFontColor(runtime.background);
22397
- const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);
22398
- let x = 0, titleWidth = 0, titleHeight = 0;
22399
- if (runtime.title.text) {
22400
- ({ width: titleWidth, height: titleHeight } = computeTextDimension(ctx, runtime.title.text, { fontSize: CHART_TITLE_FONT_SIZE, ...runtime.title }, "px"));
22401
- }
22402
- switch (runtime.title.align) {
22403
- case "right":
22404
- x = boundingRect.width - titleWidth - CHART_PADDING$1;
22405
- break;
22406
- case "center":
22407
- x = (boundingRect.width - titleWidth) / 2;
22408
- break;
22409
- case "left":
22410
- default:
22411
- x = CHART_PADDING$1;
22412
- break;
22413
- }
22414
- return {
22415
- width: boundingRect.width,
22416
- height: boundingRect.height,
22417
- title: {
22418
- label: runtime.title.text ?? "",
22419
- fontSize: runtime.title.fontSize ?? CHART_TITLE_FONT_SIZE,
22420
- textPosition: {
22421
- x,
22422
- y: CHART_PADDING_TOP + titleHeight / 2,
22423
- },
22424
- color: runtime.title.color ?? textColor,
22425
- bold: runtime.title.bold,
22426
- italic: runtime.title.italic,
22427
- },
22428
- backgroundColor: runtime.background,
22429
- gauge: {
22430
- rect: gaugeRect,
22431
- arcWidth: gaugeArcWidth,
22432
- percentage: clip(gaugePercentage, 0, 1),
22433
- color: getGaugeColor(runtime),
22434
- },
22435
- inflectionValues,
22436
- gaugeValue: {
22437
- label: gaugeLabel,
22438
- textPosition: gaugeValuePosition,
22439
- fontSize: gaugeValueFontSize,
22440
- color: textColor,
22441
- },
22442
- minLabel: {
22443
- label: runtime.minValue.label,
22444
- textPosition: minLabelPosition,
22445
- fontSize: GAUGE_LABELS_FONT_SIZE,
22446
- color: textColor,
22447
- },
22448
- maxLabel: {
22449
- label: runtime.maxValue.label,
22450
- textPosition: maxLabelPosition,
22451
- fontSize: GAUGE_LABELS_FONT_SIZE,
22452
- color: textColor,
22453
- },
22454
- };
22455
- }
22456
- /**
22457
- * Get the rectangle in which the gauge will be drawn, based on the bounding rectangle of the canvas and leaving
22458
- * space for the title and labels.
22459
- */
22460
- function getGaugeRect(boundingRect, title) {
22461
- const titleHeight = title ? GAUGE_TITLE_SECTION_HEIGHT : 0;
22462
- const drawHeight = boundingRect.height - GAUGE_PADDING_BOTTOM - titleHeight - GAUGE_PADDING_TOP;
22463
- const drawWidth = boundingRect.width - GAUGE_PADDING_SIDE * 2;
22464
- let gaugeWidth;
22465
- let gaugeHeight;
22466
- if (drawWidth > 2 * drawHeight) {
22467
- gaugeWidth = 2 * drawHeight;
22468
- gaugeHeight = drawHeight;
22469
- }
22470
- else {
22471
- gaugeWidth = drawWidth;
22472
- gaugeHeight = drawWidth / 2;
22473
- }
22474
- const gaugeX = GAUGE_PADDING_SIDE + (drawWidth - gaugeWidth) / 2;
22475
- const gaugeY = titleHeight + GAUGE_PADDING_TOP + (drawHeight - gaugeHeight) / 2;
22476
- return {
22477
- x: gaugeX,
22478
- y: gaugeY,
22479
- width: gaugeWidth,
22480
- height: gaugeHeight,
22481
- };
22482
- }
22483
- /**
22484
- * 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).
22485
- *
22486
- * Also compute an offset for the text so that it doesn't overlap with other text.
22487
- */
22488
- function getInflectionValues(runtime, gaugeRect, textColor, ctx) {
22489
- const maxValue = runtime.maxValue;
22490
- const minValue = runtime.minValue;
22491
- const gaugeCircleCenter = {
22492
- x: gaugeRect.x + gaugeRect.width / 2,
22493
- y: gaugeRect.y + gaugeRect.height,
22494
- };
22495
- const textStyle = { fontSize: GAUGE_LABELS_FONT_SIZE };
22496
- const inflectionValues = [];
22497
- const inflectionValuesTextRects = [];
22498
- for (const inflectionValue of runtime.inflectionValues) {
22499
- const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
22500
- const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
22501
- const angle = Math.PI - Math.PI * percentage;
22502
- const textRect = getRectangleTangentToCircle(angle, // angle between X axis and the point where the rectangle is tangent to the circle
22503
- gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, // radius of the gauge circle + margin below text
22504
- gaugeCircleCenter.x, // center of the gauge circle
22505
- gaugeCircleCenter.y, // center of the gauge circle
22506
- labelWidth + 2, // width of the text + some margin
22507
- GAUGE_LABELS_FONT_SIZE // height of the text
22508
- );
22509
- let offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect))
22510
- ? GAUGE_LABELS_FONT_SIZE
22511
- : 0;
22512
- inflectionValuesTextRects.push(textRect);
22513
- inflectionValues.push({
22514
- rotation: angle,
22515
- label: inflectionValue.label,
22516
- fontSize: GAUGE_LABELS_FONT_SIZE,
22517
- color: textColor,
22518
- offset,
22519
- });
22520
- }
22521
- return inflectionValues;
22522
- }
22523
- function getGaugeColor(runtime) {
22524
- const gaugeValue = runtime.gaugeValue?.value;
22525
- if (gaugeValue === undefined) {
22526
- return GAUGE_BACKGROUND_COLOR;
22527
- }
22528
- for (let i = 0; i < runtime.inflectionValues.length; i++) {
22529
- const inflectionValue = runtime.inflectionValues[i];
22530
- if (inflectionValue.operator === "<" && gaugeValue < inflectionValue.value) {
22531
- return runtime.colors[i];
22532
- }
22533
- else if (inflectionValue.operator === "<=" && gaugeValue <= inflectionValue.value) {
22534
- return runtime.colors[i];
22535
- }
22536
- }
22537
- return runtime.colors.at(-1);
22538
- }
22539
- function getSegmentsOfRectangle(rectangle) {
22540
- return [
22541
- { start: rectangle.topLeft, end: rectangle.topRight },
22542
- { start: rectangle.topRight, end: rectangle.bottomRight },
22543
- { start: rectangle.bottomRight, end: rectangle.bottomLeft },
22544
- { start: rectangle.bottomLeft, end: rectangle.topLeft },
22545
- ];
22546
- }
22547
- /**
22548
- * Check if two segment intersect. The case where the segments are colinear (both segments on the same line)
22549
- * is not handled.
22550
- */
22551
- function doSegmentIntersect(segment1, segment2) {
22552
- const A = segment1.start;
22553
- const B = segment1.end;
22554
- const C = segment2.start;
22555
- const D = segment2.end;
22556
- /**
22557
- * Line segment intersection algorithm
22558
- * https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
22559
- */
22560
- function ccw(a, b, c) {
22561
- return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
22562
- }
22563
- return ccw(A, C, D) !== ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D);
22564
- }
22565
- function doRectanglesIntersect(rect1, rect2) {
22566
- const segments1 = getSegmentsOfRectangle(rect1);
22567
- const segments2 = getSegmentsOfRectangle(rect2);
22568
- for (const segment1 of segments1) {
22569
- for (const segment2 of segments2) {
22570
- if (doSegmentIntersect(segment1, segment2)) {
22571
- return true;
22572
- }
22573
- }
22574
- }
22575
- return false;
22576
- }
22577
- /**
22578
- * Get the rectangle that is tangent to a circle at a given angle.
22579
- *
22580
- * @param angle angle between X axis and the point where the rectangle is tangent to the circle
22581
- */
22582
- function getRectangleTangentToCircle(angle, radius, circleCenterX, circleCenterY, rectWidth, rectHeight) {
22583
- const cos = Math.cos(angle);
22584
- const sin = Math.sin(angle);
22585
- // x, y are the distance from the center of the circle to the point where the rectangle is tangent to the circle
22586
- const x = cos * radius;
22587
- const y = sin * radius;
22588
- // x2, y2 are the distance from the point the rectangle is tangent to the circle to the bottom left corner of the rectangle
22589
- const x2 = sin * (rectWidth / 2); // cos(angle + 90°) = sin(angle)
22590
- const y2 = cos * (rectWidth / 2);
22591
- const bottomRight = {
22592
- x: x + x2 + circleCenterX,
22593
- y: circleCenterY - (y - y2),
22594
- };
22595
- const bottomLeft = {
22596
- x: x - x2 + circleCenterX,
22597
- y: circleCenterY - (y + y2),
22598
- };
22599
- // Same as above but for the top corners of the rectangle (radius + rectangle height instead of radius)
22600
- const xp = cos * (radius + rectHeight);
22601
- const yp = sin * (radius + rectHeight);
22602
- const topLeft = {
22603
- x: xp - x2 + circleCenterX,
22604
- y: circleCenterY - (yp + y2),
22605
- };
22606
- const topRight = {
22607
- x: xp + x2 + circleCenterX,
22608
- y: circleCenterY - (yp - y2),
22609
- };
22610
- return { bottomLeft, bottomRight, topRight, topLeft };
22611
- }
22612
-
22613
22760
  class GaugeChartComponent extends owl.Component {
22614
22761
  static template = "o-spreadsheet-GaugeChartComponent";
22615
22762
  canvas = owl.useRef("chartContainer");
@@ -22642,81 +22789,6 @@ function toXlsxHexColor(color) {
22642
22789
  return color;
22643
22790
  }
22644
22791
 
22645
- const CHART_COMMON_OPTIONS = {
22646
- // https://www.chartjs.org/docs/latest/general/responsive.html
22647
- responsive: true, // will resize when its container is resized
22648
- maintainAspectRatio: false, // doesn't maintain the aspect ratio (width/height =2 by default) so the user has the choice of the exact layout
22649
- elements: {
22650
- line: {
22651
- fill: false, // do not fill the area under line charts
22652
- },
22653
- point: {
22654
- hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
22655
- },
22656
- },
22657
- animation: false,
22658
- };
22659
- function truncateLabel(label) {
22660
- if (!label) {
22661
- return "";
22662
- }
22663
- if (label.length > MAX_CHAR_LABEL) {
22664
- return label.substring(0, MAX_CHAR_LABEL) + "…";
22665
- }
22666
- return label;
22667
- }
22668
- function chartToImage(runtime, figure, type) {
22669
- // wrap the canvas in a div with a fixed size because chart.js would
22670
- // fill the whole page otherwise
22671
- const div = document.createElement("div");
22672
- div.style.width = `${figure.width}px`;
22673
- div.style.height = `${figure.height}px`;
22674
- const canvas = document.createElement("canvas");
22675
- div.append(canvas);
22676
- canvas.setAttribute("width", figure.width.toString());
22677
- canvas.setAttribute("height", figure.height.toString());
22678
- // we have to add the canvas to the DOM otherwise it won't be rendered
22679
- document.body.append(div);
22680
- if ("chartJsConfig" in runtime) {
22681
- const config = deepCopy(runtime.chartJsConfig);
22682
- config.plugins = [backgroundColorChartJSPlugin];
22683
- const chart = new window.Chart(canvas, config);
22684
- const imgContent = chart.toBase64Image();
22685
- chart.destroy();
22686
- div.remove();
22687
- return imgContent;
22688
- }
22689
- else if (type === "scorecard") {
22690
- const design = getScorecardConfiguration(figure, runtime);
22691
- drawScoreChart(design, canvas);
22692
- const imgContent = canvas.toDataURL();
22693
- div.remove();
22694
- return imgContent;
22695
- }
22696
- else if (type === "gauge") {
22697
- drawGaugeChart(canvas, runtime);
22698
- const imgContent = canvas.toDataURL();
22699
- div.remove();
22700
- return imgContent;
22701
- }
22702
- return undefined;
22703
- }
22704
- /**
22705
- * Custom chart.js plugin to set the background color of the canvas
22706
- * https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md
22707
- */
22708
- const backgroundColorChartJSPlugin = {
22709
- id: "customCanvasBackgroundColor",
22710
- beforeDraw: (chart) => {
22711
- const { ctx } = chart;
22712
- ctx.save();
22713
- ctx.globalCompositeOperation = "destination-over";
22714
- ctx.fillStyle = "#ffffff";
22715
- ctx.fillRect(0, 0, chart.width, chart.height);
22716
- ctx.restore();
22717
- },
22718
- };
22719
-
22720
22792
  /**
22721
22793
  * Represent a raw XML string
22722
22794
  */
@@ -22778,6 +22850,7 @@ const DRAWING_NS_C = "http://schemas.openxmlformats.org/drawingml/2006/chart";
22778
22850
  const CONTENT_TYPES = {
22779
22851
  workbook: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml",
22780
22852
  sheet: "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml",
22853
+ metadata: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheetMetadata+xml",
22781
22854
  sharedStrings: "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml",
22782
22855
  styles: "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml",
22783
22856
  drawing: "application/vnd.openxmlformats-officedocument.drawing+xml",
@@ -22790,6 +22863,7 @@ const CONTENT_TYPES = {
22790
22863
  const XLSX_RELATION_TYPE = {
22791
22864
  document: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument",
22792
22865
  sheet: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet",
22866
+ metadata: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sheetMetadata",
22793
22867
  sharedStrings: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings",
22794
22868
  styles: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles",
22795
22869
  drawing: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing",
@@ -22799,6 +22873,7 @@ const XLSX_RELATION_TYPE = {
22799
22873
  hyperlink: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
22800
22874
  image: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
22801
22875
  };
22876
+ const ARRAY_FORMULA_URI = "bdbb8cdc-fa1e-496e-a857-3c3f30c029c3";
22802
22877
  const RELATIONSHIP_NSR = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
22803
22878
  const HEIGHT_FACTOR = 0.75; // 100px => 75 u
22804
22879
  /**
@@ -24318,16 +24393,25 @@ function addRelsToFile(relsFiles, path, rel) {
24318
24393
  }
24319
24394
  return id;
24320
24395
  }
24396
+ const globalReverseLookup = new WeakMap();
24321
24397
  function pushElement(property, propertyList) {
24322
- let len = propertyList.length;
24323
- const operator = typeof property === "object" ? deepEquals : (a, b) => a === b;
24324
- for (let i = 0; i < len; i++) {
24325
- if (operator(property, propertyList[i])) {
24326
- return i;
24398
+ let reverseLookup = globalReverseLookup.get(propertyList);
24399
+ if (!reverseLookup) {
24400
+ reverseLookup = new Map();
24401
+ for (let i = 0; i < propertyList.length; i++) {
24402
+ const canonical = getCanonicalRepresentation(propertyList[i]);
24403
+ reverseLookup.set(canonical, i);
24327
24404
  }
24405
+ globalReverseLookup.set(propertyList, reverseLookup);
24328
24406
  }
24329
- propertyList[propertyList.length] = property;
24330
- return propertyList.length - 1;
24407
+ const canonical = getCanonicalRepresentation(property);
24408
+ if (reverseLookup.has(canonical)) {
24409
+ return reverseLookup.get(canonical);
24410
+ }
24411
+ const maxId = propertyList.length;
24412
+ propertyList.push(property);
24413
+ reverseLookup.set(canonical, maxId);
24414
+ return maxId;
24331
24415
  }
24332
24416
  const chartIds = [];
24333
24417
  /**
@@ -25364,29 +25448,34 @@ function convertPivotTableConfig(pivotTable) {
25364
25448
  * In all the sheets, replace the table-only references in the formula cells with standard references.
25365
25449
  */
25366
25450
  function convertTableFormulaReferences(convertedSheets, xlsxSheets) {
25367
- for (let sheet of convertedSheets) {
25368
- const tables = xlsxSheets.find((s) => s.sheetName === sheet.name).tables;
25451
+ for (let tableSheet of convertedSheets) {
25452
+ const tables = xlsxSheets.find((s) => s.sheetName === tableSheet.name).tables;
25369
25453
  for (let table of tables) {
25370
25454
  const tabRef = table.name + "[";
25371
- for (let position of positions(toZone(table.ref))) {
25372
- const xc = toXC(position.col, position.row);
25373
- let cellContent = sheet.cells[xc];
25374
- if (cellContent?.startsWith("=")) {
25375
- let refIndex;
25376
- while ((refIndex = cellContent.indexOf(tabRef)) !== -1) {
25377
- let reference = cellContent.slice(refIndex + tabRef.length);
25378
- // Expression can either be tableName[colName] or tableName[[#This Row], [colName]]
25379
- let endIndex = reference.indexOf("]");
25380
- if (reference.startsWith(`[`)) {
25381
- endIndex = reference.indexOf("]", endIndex + 1);
25382
- endIndex = reference.indexOf("]", endIndex + 1);
25455
+ for (let sheet of convertedSheets) {
25456
+ for (let xc in sheet.cells) {
25457
+ const cell = sheet.cells[xc];
25458
+ let cellContent = sheet.cells[xc];
25459
+ if (cell && cellContent && cellContent.startsWith("=")) {
25460
+ let refIndex;
25461
+ while ((refIndex = cellContent.indexOf(tabRef)) !== -1) {
25462
+ let endIndex = refIndex + tabRef.length;
25463
+ let openBrackets = 1;
25464
+ while (openBrackets > 0 && endIndex < cellContent.length) {
25465
+ if (cellContent[endIndex] === "[") {
25466
+ openBrackets++;
25467
+ }
25468
+ else if (cellContent[endIndex] === "]") {
25469
+ openBrackets--;
25470
+ }
25471
+ endIndex++;
25472
+ }
25473
+ let reference = cellContent.slice(refIndex + tabRef.length, endIndex - 1);
25474
+ const sheetPrefix = tableSheet.id === sheet.id ? "" : tableSheet.name + "!";
25475
+ const convertedRef = convertTableReference(sheetPrefix, reference, table, xc);
25476
+ cellContent =
25477
+ cellContent.slice(0, refIndex) + convertedRef + cellContent.slice(endIndex);
25383
25478
  }
25384
- reference = reference.slice(0, endIndex);
25385
- const convertedRef = convertTableReference(reference, table, xc);
25386
- cellContent =
25387
- cellContent.slice(0, refIndex) +
25388
- convertedRef +
25389
- cellContent.slice(tabRef.length + refIndex + endIndex + 1);
25390
25479
  }
25391
25480
  sheet.cells[xc] = cellContent;
25392
25481
  }
@@ -25395,11 +25484,17 @@ function convertTableFormulaReferences(convertedSheets, xlsxSheets) {
25395
25484
  }
25396
25485
  }
25397
25486
  /**
25398
- * Convert table-specific references in formulas into standard references.
25487
+ * Convert table-specific references in formulas into standard references. A table reference is composed of columns names,
25488
+ * and of keywords determining the rows of the table to reference.
25399
25489
  *
25400
25490
  * A reference in a table can have the form (only the part between brackets should be given to this function):
25401
25491
  * - tableName[colName] : reference to the whole column "colName"
25492
+ * - tableName[#keyword] : reference to the whatever row the keyword refers to
25402
25493
  * - tableName[[#keyword], [colName]] : reference to some of the element(s) of the column colName
25494
+ * - tableName[[#keyword], [colName]:[col2Name]] : reference to some of the element(s) of the columns colName to col2Name
25495
+ * - tableName[[#keyword1], [#keyword2], [colName]] : reference to all the rows referenced by the keywords in the column colName
25496
+ * - tableName[[#keyword1], [colName], [#keyword2]]: the keywords and colName can be in any order
25497
+ *
25403
25498
  *
25404
25499
  * The available keywords are :
25405
25500
  * - #All : all the column (including totals)
@@ -25407,58 +25502,109 @@ function convertTableFormulaReferences(convertedSheets, xlsxSheets) {
25407
25502
  * - #Headers : only the header of the column
25408
25503
  * - #Totals : only the totals of the column
25409
25504
  * - #This Row : only the element in the same row as the cell
25505
+ *
25506
+ * Note that the only valid combination of multiple keywords are #Data + #Totals and #Headers + #Data.
25410
25507
  */
25411
- function convertTableReference(expr, table, cellXc) {
25412
- const refElements = expr.split(",");
25508
+ function convertTableReference(sheetPrefix, expr, table, cellXc) {
25509
+ // TODO: Ideally we'd want to make a real tokenizer, this simple approach won't work if for example the column name
25510
+ // contain # or , characters. But that's probably an edge case that we can ignore for now.
25511
+ const parts = expr.split(",").map((part) => part.trim());
25413
25512
  const tableZone = toZone(table.ref);
25414
- const refZone = { ...tableZone };
25415
- let isReferencedZoneValid = true;
25416
- // Single column reference
25417
- if (refElements.length === 1) {
25418
- const colRelativeIndex = table.cols.findIndex((col) => col.name === refElements[0]);
25419
- refZone.left = refZone.right = colRelativeIndex + tableZone.left;
25420
- if (table.headerRowCount) {
25421
- refZone.top += table.headerRowCount;
25422
- }
25423
- if (table.totalsRowCount) {
25424
- refZone.bottom -= 1;
25513
+ const colIndexes = [];
25514
+ const rowIndexes = [];
25515
+ const foundKeywords = [];
25516
+ for (const part of parts) {
25517
+ if (removeBrackets(part).startsWith("#")) {
25518
+ const keyWord = removeBrackets(part);
25519
+ foundKeywords.push(keyWord);
25520
+ switch (keyWord) {
25521
+ case "#All":
25522
+ rowIndexes.push(tableZone.top, tableZone.bottom);
25523
+ break;
25524
+ case "#Data":
25525
+ const top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
25526
+ const bottom = table.totalsRowCount
25527
+ ? tableZone.bottom - table.totalsRowCount
25528
+ : tableZone.bottom;
25529
+ rowIndexes.push(top, bottom);
25530
+ break;
25531
+ case "#This Row":
25532
+ rowIndexes.push(toCartesian(cellXc).row);
25533
+ break;
25534
+ case "#Headers":
25535
+ if (!table.headerRowCount) {
25536
+ return CellErrorType.InvalidReference;
25537
+ }
25538
+ rowIndexes.push(tableZone.top);
25539
+ break;
25540
+ case "#Totals":
25541
+ if (!table.totalsRowCount) {
25542
+ return CellErrorType.InvalidReference;
25543
+ }
25544
+ rowIndexes.push(tableZone.bottom);
25545
+ break;
25546
+ }
25425
25547
  }
25426
- }
25427
- // Other references
25428
- else {
25429
- switch (refElements[0].slice(1, refElements[0].length - 1)) {
25430
- case "#All":
25431
- refZone.top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
25432
- refZone.bottom = tableZone.bottom;
25433
- break;
25434
- case "#Data":
25435
- refZone.top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
25436
- refZone.bottom = table.totalsRowCount ? tableZone.bottom + 1 : tableZone.bottom;
25437
- break;
25438
- case "#This Row":
25439
- refZone.top = refZone.bottom = toCartesian(cellXc).row;
25440
- break;
25441
- case "#Headers":
25442
- refZone.top = refZone.bottom = tableZone.top;
25443
- if (!table.headerRowCount) {
25444
- isReferencedZoneValid = false;
25445
- }
25446
- break;
25447
- case "#Totals":
25448
- refZone.top = refZone.bottom = tableZone.bottom;
25449
- if (!table.totalsRowCount) {
25450
- isReferencedZoneValid = false;
25548
+ else {
25549
+ const columns = part
25550
+ .split(":")
25551
+ .map((part) => part.trim())
25552
+ .map(removeBrackets);
25553
+ if (colIndexes.length) {
25554
+ return CellErrorType.InvalidReference;
25555
+ }
25556
+ const colRelativeIndex = table.cols.findIndex((col) => col.name === columns[0]);
25557
+ if (colRelativeIndex === -1) {
25558
+ return CellErrorType.InvalidReference;
25559
+ }
25560
+ colIndexes.push(colRelativeIndex + tableZone.left);
25561
+ if (columns[1]) {
25562
+ const colRelativeIndex2 = table.cols.findIndex((col) => col.name === columns[1]);
25563
+ if (colRelativeIndex2 === -1) {
25564
+ return CellErrorType.InvalidReference;
25451
25565
  }
25452
- break;
25566
+ colIndexes.push(colRelativeIndex2 + tableZone.left);
25567
+ }
25453
25568
  }
25454
- const colRef = refElements[1].slice(1, refElements[1].length - 1);
25455
- const colRelativeIndex = table.cols.findIndex((col) => col.name === colRef);
25456
- refZone.left = refZone.right = colRelativeIndex + tableZone.left;
25457
25569
  }
25458
- if (!isReferencedZoneValid) {
25570
+ if (!areKeywordsCompatible(foundKeywords)) {
25459
25571
  return CellErrorType.InvalidReference;
25460
25572
  }
25461
- return refZone.top !== refZone.bottom ? zoneToXc(refZone) : toXC(refZone.left, refZone.top);
25573
+ if (rowIndexes.length === 0) {
25574
+ const top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
25575
+ const bottom = table.totalsRowCount
25576
+ ? tableZone.bottom - table.totalsRowCount
25577
+ : tableZone.bottom;
25578
+ rowIndexes.push(top, bottom);
25579
+ }
25580
+ if (colIndexes.length === 0) {
25581
+ colIndexes.push(tableZone.left, tableZone.right);
25582
+ }
25583
+ const refZone = {
25584
+ top: Math.min(...rowIndexes),
25585
+ left: Math.min(...colIndexes),
25586
+ bottom: Math.max(...rowIndexes),
25587
+ right: Math.max(...colIndexes),
25588
+ };
25589
+ return sheetPrefix + zoneToXc(refZone);
25590
+ }
25591
+ function removeBrackets(str) {
25592
+ return str.startsWith("[") && str.endsWith("]") ? str.slice(1, str.length - 1) : str;
25593
+ }
25594
+ function areKeywordsCompatible(keywords) {
25595
+ if (keywords.length < 2) {
25596
+ return true;
25597
+ }
25598
+ else if (keywords.length > 2) {
25599
+ return false;
25600
+ }
25601
+ else if (keywords.includes("#Data") && keywords.includes("#Totals")) {
25602
+ return true;
25603
+ }
25604
+ else if (keywords.includes("#Headers") && keywords.includes("#Data")) {
25605
+ return true;
25606
+ }
25607
+ return false;
25462
25608
  }
25463
25609
 
25464
25610
  // -------------------------------------
@@ -26112,7 +26258,7 @@ class XlsxChartExtractor extends XlsxBaseExtractor {
26112
26258
  title: { text: chartTitle },
26113
26259
  type: CHART_TYPE_CONVERSION_MAP[chartType],
26114
26260
  dataSets: this.extractChartDatasets(this.querySelectorAll(rootChartElement, `c:${chartType}`), chartType),
26115
- labelRange: this.extractChildTextContent(rootChartElement, `c:ser ${chartType === "scatterChart" ? "c:numRef" : "c:cat"} c:f`),
26261
+ labelRange: this.extractLabelRange(chartType, rootChartElement),
26116
26262
  backgroundColor: this.extractChildAttr(rootChartElement, "c:chartSpace > c:spPr a:srgbClr", "val", {
26117
26263
  default: "ffffff",
26118
26264
  }).asString(),
@@ -26124,6 +26270,13 @@ class XlsxChartExtractor extends XlsxBaseExtractor {
26124
26270
  };
26125
26271
  })[0];
26126
26272
  }
26273
+ extractLabelRange(chartType, rootChartElement) {
26274
+ if (chartType === "scatterChart") {
26275
+ return (this.extractChildTextContent(rootChartElement, `c:ser c:strRef c:f`) ||
26276
+ this.extractChildTextContent(rootChartElement, `c:ser c:numRef c:f`));
26277
+ }
26278
+ return this.extractChildTextContent(rootChartElement, `c:ser c:cat c:f`);
26279
+ }
26127
26280
  extractComboChart(chartElement) {
26128
26281
  // Title can be separated into multiple xml elements (for styling and such), we only import the text
26129
26282
  const chartTitle = this.mapOnElements({ parent: chartElement, query: "c:title a:t" }, (textElement) => {
@@ -28626,11 +28779,12 @@ function canBeLinearChart(definition, dataSets, labelRange, getters) {
28626
28779
  }
28627
28780
  let missingTimeAdapterAlreadyWarned = false;
28628
28781
  function isLuxonTimeAdapterInstalled() {
28629
- if (!window.Chart) {
28782
+ const Chart = getChartJSConstructor();
28783
+ if (!Chart) {
28630
28784
  return false;
28631
28785
  }
28632
28786
  // @ts-ignore
28633
- const adapter = new window.Chart._adapters._date({});
28787
+ const adapter = new Chart._adapters._date({});
28634
28788
  const isInstalled = adapter._id === "luxon";
28635
28789
  if (!isInstalled && !missingTimeAdapterAlreadyWarned) {
28636
28790
  missingTimeAdapterAlreadyWarned = true;
@@ -32327,10 +32481,6 @@ class Popover extends owl.Component {
32327
32481
  this.currentDisplayValue = newDisplay;
32328
32482
  if (!anchor)
32329
32483
  return;
32330
- el.style.top = "";
32331
- el.style.left = "";
32332
- el.style["max-height"] = "";
32333
- el.style["max-width"] = "";
32334
32484
  const propsMaxSize = { width: this.props.maxWidth, height: this.props.maxHeight };
32335
32485
  let elDims = {
32336
32486
  width: el.getBoundingClientRect().width,
@@ -33868,6 +34018,7 @@ var CHART_HELPERS = /*#__PURE__*/Object.freeze({
33868
34018
  drawScoreChart: drawScoreChart,
33869
34019
  formatChartDatasetValue: formatChartDatasetValue,
33870
34020
  formatTickValue: formatTickValue,
34021
+ getChartJSConstructor: getChartJSConstructor,
33871
34022
  getChartPositionAtCenterOfViewport: getChartPositionAtCenterOfViewport,
33872
34023
  getDefinedAxis: getDefinedAxis,
33873
34024
  getPieColors: getPieColors,
@@ -36127,6 +36278,7 @@ const irregularityMap = {
36127
36278
  fingerprintStore.enable();
36128
36279
  }
36129
36280
  },
36281
+ isReadonlyAllowed: true,
36130
36282
  icon: "o-spreadsheet-Icon.IRREGULARITY_MAP",
36131
36283
  };
36132
36284
  const viewFormulas = {
@@ -37751,6 +37903,9 @@ class GenericChartConfigPanel extends owl.Component {
37751
37903
  this.state.datasetDispatchResult = this.props.updateChart(this.props.figureId, {
37752
37904
  dataSets: this.dataSeriesRanges,
37753
37905
  });
37906
+ if (this.state.datasetDispatchResult.isSuccessful) {
37907
+ this.dataSeriesRanges = this.env.model.getters.getChartDefinition(this.props.figureId).dataSets;
37908
+ }
37754
37909
  }
37755
37910
  getDataSeriesRanges() {
37756
37911
  return this.dataSeriesRanges;
@@ -40390,8 +40545,7 @@ css /* scss */ `
40390
40545
  }
40391
40546
 
40392
40547
  .o-composer-assistant {
40393
- position: absolute;
40394
- margin: 1px 4px;
40548
+ margin-top: 1px;
40395
40549
 
40396
40550
  .o-semi-bold {
40397
40551
  /* FIXME: to remove in favor of Bootstrap
@@ -40442,10 +40596,11 @@ class Composer extends owl.Component {
40442
40596
  });
40443
40597
  compositionActive = false;
40444
40598
  spreadsheetRect = useSpreadsheetRect();
40445
- get assistantStyle() {
40599
+ get assistantStyleProperties() {
40446
40600
  const composerRect = this.composerRef.el.getBoundingClientRect();
40447
40601
  const assistantStyle = {};
40448
- assistantStyle["min-width"] = `${this.props.rect?.width || ASSISTANT_WIDTH}px`;
40602
+ const minWidth = Math.min(this.props.rect?.width || Infinity, ASSISTANT_WIDTH);
40603
+ assistantStyle["min-width"] = `${minWidth}px`;
40449
40604
  const proposals = this.autoCompleteState.provider?.proposals;
40450
40605
  const proposalsHaveDescription = proposals?.some((proposal) => proposal.description);
40451
40606
  if (this.functionDescriptionState.showDescription || proposalsHaveDescription) {
@@ -40469,13 +40624,29 @@ class Composer extends owl.Component {
40469
40624
  }
40470
40625
  }
40471
40626
  else {
40472
- assistantStyle["max-height"] = `${this.spreadsheetRect.height - composerRect.bottom}px`;
40627
+ assistantStyle["max-height"] = `${this.spreadsheetRect.height - composerRect.bottom - 1}px`; // -1: margin
40473
40628
  if (composerRect.left + ASSISTANT_WIDTH + SCROLLBAR_WIDTH + CLOSE_ICON_RADIUS >
40474
40629
  this.spreadsheetRect.width) {
40475
40630
  assistantStyle.right = `${CLOSE_ICON_RADIUS}px`;
40476
40631
  }
40477
40632
  }
40478
- return cssPropertiesToCss(assistantStyle);
40633
+ return assistantStyle;
40634
+ }
40635
+ get assistantStyle() {
40636
+ const allProperties = this.assistantStyleProperties;
40637
+ return cssPropertiesToCss({
40638
+ "max-height": allProperties["max-height"],
40639
+ width: allProperties["width"],
40640
+ "min-width": allProperties["min-width"],
40641
+ });
40642
+ }
40643
+ get assistantContainerStyle() {
40644
+ const allProperties = this.assistantStyleProperties;
40645
+ return cssPropertiesToCss({
40646
+ top: allProperties["top"],
40647
+ right: allProperties["right"],
40648
+ transform: allProperties["transform"],
40649
+ });
40479
40650
  }
40480
40651
  // we can't allow input events to be triggered while we remove and add back the content of the composer in processContent
40481
40652
  shouldProcessInputEvents = false;
@@ -46412,9 +46583,7 @@ class PivotSidePanelStore extends SpreadsheetStore {
46412
46583
  pivot: this.draft,
46413
46584
  });
46414
46585
  this.draft = null;
46415
- if (!this.alreadyNotified &&
46416
- !this.isDynamicPivotInViewport() &&
46417
- this.isStaticPivotInViewport()) {
46586
+ if (!this.alreadyNotified && this.isUpdatedPivotVisibleInViewportOnlyAsStaticPivot()) {
46418
46587
  const formulaId = this.getters.getPivotFormulaId(this.pivotId);
46419
46588
  const pivotExample = `=PIVOT(${formulaId})`;
46420
46589
  this.alreadyNotified = true;
@@ -46470,29 +46639,33 @@ class PivotSidePanelStore extends SpreadsheetStore {
46470
46639
  this.applyUpdate();
46471
46640
  }
46472
46641
  }
46473
- isDynamicPivotInViewport() {
46474
- const sheetId = this.getters.getActiveSheetId();
46475
- for (const col of this.getters.getSheetViewVisibleCols()) {
46476
- for (const row of this.getters.getSheetViewVisibleRows()) {
46477
- const isDynamicPivot = this.getters.isSpillPivotFormula({ sheetId, col, row });
46478
- if (isDynamicPivot) {
46479
- return true;
46480
- }
46481
- }
46482
- }
46483
- return false;
46484
- }
46485
- isStaticPivotInViewport() {
46642
+ /**
46643
+ * @returns true if the updated pivot is visible in the viewport only as a
46644
+ * static pivot and not as a dynamic pivot
46645
+ */
46646
+ isUpdatedPivotVisibleInViewportOnlyAsStaticPivot() {
46647
+ let staticPivotCount = 0;
46648
+ const updatedPivotFormulaId = this.getters.getPivotFormulaId(this.pivotId);
46486
46649
  for (const position of this.getters.getVisibleCellPositions()) {
46487
46650
  const cell = this.getters.getCell(position);
46488
46651
  if (cell?.isFormula) {
46489
46652
  const pivotFunction = getFirstPivotFunction(cell.compiledFormula.tokens);
46490
- if (pivotFunction && pivotFunction.functionName !== "PIVOT") {
46491
- return true;
46653
+ const pivotFormulaId = pivotFunction?.args[0]?.value;
46654
+ if (pivotFunction && updatedPivotFormulaId === pivotFormulaId.toString()) {
46655
+ if (pivotFunction.functionName === "PIVOT") {
46656
+ // if we have at least one dynamic pivot visible inserted the viewport
46657
+ // we return false
46658
+ return false;
46659
+ }
46660
+ else {
46661
+ staticPivotCount++;
46662
+ }
46492
46663
  }
46493
46664
  }
46494
46665
  }
46495
- return false;
46666
+ // we return true if there are only static pivots visible inserted the viewport,
46667
+ // otherwise false
46668
+ return staticPivotCount > 0;
46496
46669
  }
46497
46670
  addDefaultDateTimeGranularity(fields, definition) {
46498
46671
  const { columns, rows } = definition;
@@ -50311,6 +50484,9 @@ class ColResizer extends AbstractResizer {
50311
50484
  this.MAX_SIZE_MARGIN = 90;
50312
50485
  this.MIN_ELEMENT_SIZE = MIN_COL_WIDTH;
50313
50486
  }
50487
+ get sheetId() {
50488
+ return this.env.model.getters.getActiveSheetId();
50489
+ }
50314
50490
  _getEvOffset(ev) {
50315
50491
  return ev.offsetX;
50316
50492
  }
@@ -50333,10 +50509,10 @@ class ColResizer extends AbstractResizer {
50333
50509
  return this.env.model.getters.getEdgeScrollCol(position, position, position);
50334
50510
  }
50335
50511
  _getDimensionsInViewport(index) {
50336
- return this.env.model.getters.getColDimensionsInViewport(this.env.model.getters.getActiveSheetId(), index);
50512
+ return this.env.model.getters.getColDimensionsInViewport(this.sheetId, index);
50337
50513
  }
50338
50514
  _getElementSize(index) {
50339
- return this.env.model.getters.getColSize(this.env.model.getters.getActiveSheetId(), index);
50515
+ return this.env.model.getters.getColSize(this.sheetId, index);
50340
50516
  }
50341
50517
  _getMaxSize() {
50342
50518
  return this.colResizerRef.el.clientWidth;
@@ -50347,7 +50523,7 @@ class ColResizer extends AbstractResizer {
50347
50523
  const cols = this.env.model.getters.getActiveCols();
50348
50524
  this.env.model.dispatch("RESIZE_COLUMNS_ROWS", {
50349
50525
  dimension: "COL",
50350
- sheetId: this.env.model.getters.getActiveSheetId(),
50526
+ sheetId: this.sheetId,
50351
50527
  elements: cols.has(index) ? [...cols] : [index],
50352
50528
  size,
50353
50529
  });
@@ -50360,7 +50536,7 @@ class ColResizer extends AbstractResizer {
50360
50536
  elements.push(colIndex);
50361
50537
  }
50362
50538
  const result = this.env.model.dispatch("MOVE_COLUMNS_ROWS", {
50363
- sheetId: this.env.model.getters.getActiveSheetId(),
50539
+ sheetId: this.sheetId,
50364
50540
  dimension: "COL",
50365
50541
  base: this.state.base,
50366
50542
  elements,
@@ -50379,7 +50555,7 @@ class ColResizer extends AbstractResizer {
50379
50555
  _fitElementSize(index) {
50380
50556
  const cols = this.env.model.getters.getActiveCols();
50381
50557
  this.env.model.dispatch("AUTORESIZE_COLUMNS", {
50382
- sheetId: this.env.model.getters.getActiveSheetId(),
50558
+ sheetId: this.sheetId,
50383
50559
  cols: cols.has(index) ? [...cols] : [index],
50384
50560
  });
50385
50561
  }
@@ -50390,7 +50566,7 @@ class ColResizer extends AbstractResizer {
50390
50566
  return this.env.model.getters.getActiveCols();
50391
50567
  }
50392
50568
  _getPreviousVisibleElement(index) {
50393
- const sheetId = this.env.model.getters.getActiveSheetId();
50569
+ const sheetId = this.sheetId;
50394
50570
  let row;
50395
50571
  for (row = index - 1; row >= 0; row--) {
50396
50572
  if (!this.env.model.getters.isColHidden(sheetId, row)) {
@@ -50401,7 +50577,7 @@ class ColResizer extends AbstractResizer {
50401
50577
  }
50402
50578
  unhide(hiddenElements) {
50403
50579
  this.env.model.dispatch("UNHIDE_COLUMNS_ROWS", {
50404
- sheetId: this.env.model.getters.getActiveSheetId(),
50580
+ sheetId: this.sheetId,
50405
50581
  elements: hiddenElements,
50406
50582
  dimension: "COL",
50407
50583
  });
@@ -50417,7 +50593,7 @@ css /* scss */ `
50417
50593
  left: 0;
50418
50594
  right: 0;
50419
50595
  width: ${HEADER_WIDTH}px;
50420
- height: 100%;
50596
+ height: calc(100% - ${HEADER_HEIGHT + SCROLLBAR_WIDTH}px);
50421
50597
  &.o-dragging {
50422
50598
  cursor: grabbing;
50423
50599
  }
@@ -50475,6 +50651,9 @@ class RowResizer extends AbstractResizer {
50475
50651
  this.MIN_ELEMENT_SIZE = MIN_ROW_HEIGHT;
50476
50652
  }
50477
50653
  rowResizerRef;
50654
+ get sheetId() {
50655
+ return this.env.model.getters.getActiveSheetId();
50656
+ }
50478
50657
  _getEvOffset(ev) {
50479
50658
  return ev.offsetY;
50480
50659
  }
@@ -50497,10 +50676,10 @@ class RowResizer extends AbstractResizer {
50497
50676
  return this.env.model.getters.getEdgeScrollRow(position, position, position);
50498
50677
  }
50499
50678
  _getDimensionsInViewport(index) {
50500
- return this.env.model.getters.getRowDimensionsInViewport(this.env.model.getters.getActiveSheetId(), index);
50679
+ return this.env.model.getters.getRowDimensionsInViewport(this.sheetId, index);
50501
50680
  }
50502
50681
  _getElementSize(index) {
50503
- return this.env.model.getters.getRowSize(this.env.model.getters.getActiveSheetId(), index);
50682
+ return this.env.model.getters.getRowSize(this.sheetId, index);
50504
50683
  }
50505
50684
  _getMaxSize() {
50506
50685
  return this.rowResizerRef.el.clientHeight;
@@ -50511,7 +50690,7 @@ class RowResizer extends AbstractResizer {
50511
50690
  const rows = this.env.model.getters.getActiveRows();
50512
50691
  this.env.model.dispatch("RESIZE_COLUMNS_ROWS", {
50513
50692
  dimension: "ROW",
50514
- sheetId: this.env.model.getters.getActiveSheetId(),
50693
+ sheetId: this.sheetId,
50515
50694
  elements: rows.has(index) ? [...rows] : [index],
50516
50695
  size,
50517
50696
  });
@@ -50524,7 +50703,7 @@ class RowResizer extends AbstractResizer {
50524
50703
  elements.push(rowIndex);
50525
50704
  }
50526
50705
  const result = this.env.model.dispatch("MOVE_COLUMNS_ROWS", {
50527
- sheetId: this.env.model.getters.getActiveSheetId(),
50706
+ sheetId: this.sheetId,
50528
50707
  dimension: "ROW",
50529
50708
  base: this.state.base,
50530
50709
  elements,
@@ -50543,7 +50722,7 @@ class RowResizer extends AbstractResizer {
50543
50722
  _fitElementSize(index) {
50544
50723
  const rows = this.env.model.getters.getActiveRows();
50545
50724
  this.env.model.dispatch("AUTORESIZE_ROWS", {
50546
- sheetId: this.env.model.getters.getActiveSheetId(),
50725
+ sheetId: this.sheetId,
50547
50726
  rows: rows.has(index) ? [...rows] : [index],
50548
50727
  });
50549
50728
  }
@@ -50554,7 +50733,7 @@ class RowResizer extends AbstractResizer {
50554
50733
  return this.env.model.getters.getActiveRows();
50555
50734
  }
50556
50735
  _getPreviousVisibleElement(index) {
50557
- const sheetId = this.env.model.getters.getActiveSheetId();
50736
+ const sheetId = this.sheetId;
50558
50737
  let row;
50559
50738
  for (row = index - 1; row >= 0; row--) {
50560
50739
  if (!this.env.model.getters.isRowHidden(sheetId, row)) {
@@ -50565,7 +50744,7 @@ class RowResizer extends AbstractResizer {
50565
50744
  }
50566
50745
  unhide(hiddenElements) {
50567
50746
  this.env.model.dispatch("UNHIDE_COLUMNS_ROWS", {
50568
- sheetId: this.env.model.getters.getActiveSheetId(),
50747
+ sheetId: this.sheetId,
50569
50748
  dimension: "ROW",
50570
50749
  elements: hiddenElements,
50571
50750
  });
@@ -60332,6 +60511,7 @@ class EvaluationPlugin extends UIPlugin {
60332
60511
  exportForExcel(data) {
60333
60512
  for (const sheet of data.sheets) {
60334
60513
  sheet.cellValues = {};
60514
+ sheet.formulaSpillRanges = {};
60335
60515
  }
60336
60516
  for (const position of this.evaluator.getEvaluatedPositions()) {
60337
60517
  const evaluatedCell = this.evaluator.getEvaluatedCell(position);
@@ -60343,8 +60523,9 @@ class EvaluationPlugin extends UIPlugin {
60343
60523
  const exportedSheetData = data.sheets.find((sheet) => sheet.id === position.sheetId);
60344
60524
  const formulaCell = this.getCorrespondingFormulaCell(position);
60345
60525
  if (formulaCell) {
60526
+ const cell = this.getters.getCell(position);
60346
60527
  isExported = isExportableToExcel(formulaCell.compiledFormula.tokens);
60347
- isFormula = isExported;
60528
+ isFormula = isExported && cell?.content === formulaCell.content;
60348
60529
  // If the cell contains a non-exported formula and that is evaluates to
60349
60530
  // nothing* ,we don't export it.
60350
60531
  // * non-falsy value are relevant and so are 0 and FALSE, which only leaves
@@ -60367,7 +60548,11 @@ class EvaluationPlugin extends UIPlugin {
60367
60548
  content = !isExported ? newContent : exportedCellData;
60368
60549
  }
60369
60550
  exportedSheetData.cells[xc] = content;
60370
- exportedSheetData.cellValues[xc] = value;
60551
+ exportedSheetData.cellValues[xc] = evaluatedCell.type !== "error" ? value : undefined;
60552
+ const spillZone = this.getSpreadZone(position);
60553
+ if (spillZone) {
60554
+ exportedSheetData.formulaSpillRanges[xc] = this.getters.getRangeString(this.getters.getRangeFromZone(position.sheetId, spillZone), position.sheetId);
60555
+ }
60371
60556
  }
60372
60557
  }
60373
60558
  /**
@@ -62574,7 +62759,7 @@ class AutofillPlugin extends UIPlugin {
62574
62759
  getRule(cell, cells) {
62575
62760
  const rules = autofillRulesRegistry.getAll().sort((a, b) => a.sequence - b.sequence);
62576
62761
  const rule = rules.find((rule) => rule.condition(cell, cells));
62577
- return rule && rule.generateRule(cell, cells);
62762
+ return rule && this.direction && rule.generateRule(cell, cells, this.direction);
62578
62763
  }
62579
62764
  /**
62580
62765
  * Create the generator to be able to autofill the next cells.
@@ -67798,7 +67983,8 @@ class SheetViewPlugin extends UIPlugin {
67798
67983
  ? this.getters.getSheetViewVisibleCols()
67799
67984
  : this.getters.getSheetViewVisibleRows();
67800
67985
  const startIndex = visibleHeaders.findIndex((header) => referenceHeaderIndex >= header);
67801
- const endIndex = visibleHeaders.findIndex((header) => targetHeaderIndex <= header);
67986
+ let endIndex = visibleHeaders.findIndex((header) => targetHeaderIndex <= header);
67987
+ endIndex = endIndex === -1 ? visibleHeaders.length : endIndex;
67802
67988
  const relevantIndexes = visibleHeaders.slice(startIndex, endIndex);
67803
67989
  let offset = 0;
67804
67990
  for (const i of relevantIndexes) {
@@ -73104,7 +73290,7 @@ function numberRef(reference) {
73104
73290
  `;
73105
73291
  }
73106
73292
 
73107
- function addFormula(formula, value) {
73293
+ function addFormula(formula, value, formulaSpillRange) {
73108
73294
  if (!formula) {
73109
73295
  return { attrs: [], node: escapeXml `` };
73110
73296
  }
@@ -73112,10 +73298,17 @@ function addFormula(formula, value) {
73112
73298
  if (type === undefined) {
73113
73299
  return { attrs: [], node: escapeXml `` };
73114
73300
  }
73115
- const attrs = [["t", type]];
73301
+ const attrs = [
73302
+ ["cm", "1"],
73303
+ ["t", type],
73304
+ ];
73116
73305
  const XlsxFormula = adaptFormulaToExcel(formula);
73117
73306
  const exportedValue = adaptFormulaValueToExcel(value);
73118
- const node = escapeXml /*xml*/ `<f>${XlsxFormula}</f><v>${exportedValue}</v>`;
73307
+ // We treat all formulas as array formulas (a simple formula
73308
+ // is an array formula that spills on only one cell) to avoid
73309
+ // trying to detect spilling sub-formulas which is not a trivial task.
73310
+ let node;
73311
+ node = escapeXml /*xml*/ `<f t="array" ref="${formulaSpillRange}">${XlsxFormula}</f><v>${exportedValue}</v>`;
73119
73312
  return { attrs, node };
73120
73313
  }
73121
73314
  function addContent(content, sharedStrings, forceString = false) {
@@ -73898,7 +74091,7 @@ function addStyles(styles) {
73898
74091
  }
73899
74092
  if (alignAttrs.length > 0) {
73900
74093
  attributes.push(["applyAlignment", "1"]); // for Libre Office
73901
- styleNodes.push(escapeXml /*xml*/ `<xf ${formatAttributes(attributes)}>${escapeXml /*xml*/ `<alignment ${formatAttributes(alignAttrs)} />`}</xf> `);
74094
+ styleNodes.push(escapeXml /*xml*/ `<xf ${formatAttributes(attributes)}><alignment ${formatAttributes(alignAttrs)} /></xf> `);
73902
74095
  }
73903
74096
  else {
73904
74097
  styleNodes.push(escapeXml /*xml*/ `<xf ${formatAttributes(attributes)} />`);
@@ -74066,6 +74259,9 @@ function addColumns(cols) {
74066
74259
  }
74067
74260
  function addRows(construct, data, sheet) {
74068
74261
  const rowNodes = [];
74262
+ const styles = new PositionMap(iterateItemIdsPositions(sheet.id, sheet.styles));
74263
+ const borders = new PositionMap(iterateItemIdsPositions(sheet.id, sheet.borders));
74264
+ const formats = new PositionMap(iterateItemIdsPositions(sheet.id, sheet.formats));
74069
74265
  for (let r = 0; r < sheet.rowNumber; r++) {
74070
74266
  const rowAttrs = [["r", r + 1]];
74071
74267
  const row = sheet.rows[r] || {};
@@ -74081,9 +74277,6 @@ function addRows(construct, data, sheet) {
74081
74277
  if (row.collapsed) {
74082
74278
  rowAttrs.push(["collapsed", 1]);
74083
74279
  }
74084
- const styles = new PositionMap(iterateItemIdsPositions(sheet.id, sheet.styles));
74085
- const borders = new PositionMap(iterateItemIdsPositions(sheet.id, sheet.borders));
74086
- const formats = new PositionMap(iterateItemIdsPositions(sheet.id, sheet.formats));
74087
74280
  const cellNodes = [];
74088
74281
  for (let c = 0; c < sheet.colNumber; c++) {
74089
74282
  const xc = toXC(c, r);
@@ -74105,7 +74298,7 @@ function addRows(construct, data, sheet) {
74105
74298
  let cellNode = escapeXml ``;
74106
74299
  // Either formula or static value inside the cell
74107
74300
  if (content?.startsWith("=") && value !== undefined) {
74108
- const res = addFormula(content, value);
74301
+ const res = addFormula(content, value, sheet.formulaSpillRanges[xc] ?? xc);
74109
74302
  if (!res) {
74110
74303
  continue;
74111
74304
  }
@@ -74391,6 +74584,30 @@ function createWorksheets(data, construct) {
74391
74584
  `;
74392
74585
  files.push(createXMLFile(parseXML(sheetXml), `xl/worksheets/sheet${sheetIndex}.xml`, "sheet"));
74393
74586
  }
74587
+ const sheetMetadataXml = escapeXml /*xml*/ `
74588
+ <metadata xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:xda="http://schemas.microsoft.com/office/spreadsheetml/2017/dynamicarray">
74589
+ <metadataTypes count="1">
74590
+ <metadataType name="XLDAPR" minSupportedVersion="120000" copy="1" pasteAll="1"
74591
+ pasteValues="1" merge="1" splitFirst="1" rowColShift="1" clearFormats="1"
74592
+ clearComments="1" assign="1" coerce="1" cellMeta="1" />
74593
+ </metadataTypes>
74594
+ <futureMetadata name="XLDAPR" count="1">
74595
+ <bk>
74596
+ <extLst>
74597
+ <ext uri="{${ARRAY_FORMULA_URI}}">
74598
+ <xda:dynamicArrayProperties fDynamic="1" fCollapsed="0" />
74599
+ </ext>
74600
+ </extLst>
74601
+ </bk>
74602
+ </futureMetadata>
74603
+ <cellMetadata count="1">
74604
+ <bk>
74605
+ <rc t="1" v="0" />
74606
+ </bk>
74607
+ </cellMetadata>
74608
+ </metadata>
74609
+ `;
74610
+ files.push(createXMLFile(parseXML(sheetMetadataXml), "xl/metadata.xml", "metadata"));
74394
74611
  addRelsToFile(construct.relsFiles, "xl/_rels/workbook.xml.rels", {
74395
74612
  type: XLSX_RELATION_TYPE.sharedStrings,
74396
74613
  target: "sharedStrings.xml",
@@ -74399,6 +74616,10 @@ function createWorksheets(data, construct) {
74399
74616
  type: XLSX_RELATION_TYPE.styles,
74400
74617
  target: "styles.xml",
74401
74618
  });
74619
+ addRelsToFile(construct.relsFiles, "xl/_rels/workbook.xml.rels", {
74620
+ type: XLSX_RELATION_TYPE.metadata,
74621
+ target: "metadata.xml",
74622
+ });
74402
74623
  return files;
74403
74624
  }
74404
74625
  /**
@@ -75327,6 +75548,6 @@ exports.tokenColors = tokenColors;
75327
75548
  exports.tokenize = tokenize;
75328
75549
 
75329
75550
 
75330
- __info__.version = "18.1.9";
75331
- __info__.date = "2025-02-25T05:59:45.472Z";
75332
- __info__.hash = "6789c1c";
75551
+ __info__.version = "18.1.11";
75552
+ __info__.date = "2025-03-12T15:31:44.276Z";
75553
+ __info__.hash = "7de2363";