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