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