@odoo/o-spreadsheet 18.1.9 → 18.1.10

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.10
6
+ * @date 2025-03-07T10:34:41.861Z
7
+ * @hash 31e4526
8
8
  */
9
9
 
10
10
  'use strict';
@@ -6093,8 +6093,9 @@ function spreadRange(getters, dataSets) {
6093
6093
  if (zone.bottom !== zone.top && zone.left != zone.right) {
6094
6094
  if (zone.right) {
6095
6095
  for (let j = zone.left; j <= zone.right; ++j) {
6096
+ const datasetOptions = j === zone.left ? dataSet : { yAxisId: dataSet.yAxisId };
6096
6097
  postProcessedRanges.push({
6097
- ...dataSet,
6098
+ ...datasetOptions,
6098
6099
  dataRange: `${sheetPrefix}${zoneToXc({
6099
6100
  left: j,
6100
6101
  right: j,
@@ -6106,8 +6107,9 @@ function spreadRange(getters, dataSets) {
6106
6107
  }
6107
6108
  else {
6108
6109
  for (let j = zone.top; j <= zone.bottom; ++j) {
6110
+ const datasetOptions = j === zone.top ? dataSet : { yAxisId: dataSet.yAxisId };
6109
6111
  postProcessedRanges.push({
6110
- ...dataSet,
6112
+ ...datasetOptions,
6111
6113
  dataRange: `${sheetPrefix}${zoneToXc({
6112
6114
  left: zone.left,
6113
6115
  right: zone.right,
@@ -10058,70 +10060,341 @@ function getNextNonEmptyBar(bars, startIndex) {
10058
10060
  return bars.find((bar, i) => i > startIndex && bar.height !== 0);
10059
10061
  }
10060
10062
 
10061
- window.Chart?.register(waterfallLinesPlugin);
10062
- window.Chart?.register(chartShowValuesPlugin);
10063
- class ChartJsComponent extends owl.Component {
10064
- static template = "o-spreadsheet-ChartJsComponent";
10065
- static props = {
10066
- figure: Object,
10063
+ const GAUGE_PADDING_SIDE = 30;
10064
+ const GAUGE_PADDING_TOP = 10;
10065
+ const GAUGE_PADDING_BOTTOM = 20;
10066
+ const GAUGE_LABELS_FONT_SIZE = 12;
10067
+ const GAUGE_DEFAULT_VALUE_FONT_SIZE = 80;
10068
+ const GAUGE_BACKGROUND_COLOR = "#F3F2F1";
10069
+ const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
10070
+ const GAUGE_TITLE_SECTION_HEIGHT = 25;
10071
+ function drawGaugeChart(canvas, runtime) {
10072
+ const canvasBoundingRect = canvas.getBoundingClientRect();
10073
+ canvas.width = canvasBoundingRect.width;
10074
+ canvas.height = canvasBoundingRect.height;
10075
+ const ctx = canvas.getContext("2d");
10076
+ const config = getGaugeRenderingConfig(canvasBoundingRect, runtime, ctx);
10077
+ drawBackground(ctx, config);
10078
+ drawGauge(ctx, config);
10079
+ drawInflectionValues(ctx, config);
10080
+ drawLabels(ctx, config);
10081
+ drawTitle(ctx, config);
10082
+ }
10083
+ function drawGauge(ctx, config) {
10084
+ ctx.save();
10085
+ const gauge = config.gauge;
10086
+ const arcCenterX = gauge.rect.x + gauge.rect.width / 2;
10087
+ const arcCenterY = gauge.rect.y + gauge.rect.height;
10088
+ const arcRadius = gauge.rect.height - gauge.arcWidth / 2;
10089
+ if (arcRadius < 0) {
10090
+ return;
10091
+ }
10092
+ const gaugeAngle = gauge.percentage === 1 ? 0 : Math.PI * (1 + gauge.percentage);
10093
+ // Gauge background
10094
+ ctx.strokeStyle = GAUGE_BACKGROUND_COLOR;
10095
+ ctx.beginPath();
10096
+ ctx.lineWidth = gauge.arcWidth;
10097
+ ctx.arc(arcCenterX, arcCenterY, arcRadius, gaugeAngle, 0);
10098
+ ctx.stroke();
10099
+ // Gauge value
10100
+ ctx.strokeStyle = gauge.color;
10101
+ ctx.beginPath();
10102
+ ctx.arc(arcCenterX, arcCenterY, arcRadius, Math.PI, gaugeAngle);
10103
+ ctx.stroke();
10104
+ ctx.restore();
10105
+ }
10106
+ function drawBackground(ctx, config) {
10107
+ ctx.save();
10108
+ ctx.fillStyle = config.backgroundColor;
10109
+ ctx.fillRect(0, 0, config.width, config.height);
10110
+ ctx.restore();
10111
+ }
10112
+ function drawLabels(ctx, config) {
10113
+ for (const label of [config.minLabel, config.maxLabel, config.gaugeValue]) {
10114
+ ctx.save();
10115
+ ctx.textAlign = "center";
10116
+ ctx.fillStyle = label.color;
10117
+ ctx.font = `${label.fontSize}px ${DEFAULT_FONT}`;
10118
+ ctx.fillText(label.label, label.textPosition.x, label.textPosition.y);
10119
+ ctx.restore();
10120
+ }
10121
+ }
10122
+ function drawInflectionValues(ctx, config) {
10123
+ const { x: rectX, y: rectY, width, height } = config.gauge.rect;
10124
+ for (const inflectionValue of config.inflectionValues) {
10125
+ ctx.save();
10126
+ ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment
10127
+ ctx.rotate(Math.PI / 2 - inflectionValue.rotation);
10128
+ ctx.lineWidth = 2;
10129
+ ctx.strokeStyle = chartMutedFontColor(config.backgroundColor) + "aa";
10130
+ ctx.beginPath();
10131
+ ctx.moveTo(0, -(height - config.gauge.arcWidth));
10132
+ ctx.lineTo(0, -height - 3);
10133
+ ctx.stroke();
10134
+ ctx.textAlign = "center";
10135
+ ctx.font = `${inflectionValue.fontSize}px ${DEFAULT_FONT}`;
10136
+ ctx.fillStyle = inflectionValue.color;
10137
+ const textY = -height - GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN - inflectionValue.offset;
10138
+ ctx.fillText(inflectionValue.label, 0, textY);
10139
+ ctx.restore();
10140
+ }
10141
+ }
10142
+ function drawTitle(ctx, config) {
10143
+ ctx.save();
10144
+ const title = config.title;
10145
+ ctx.font = getDefaultContextFont(title.fontSize, title.bold, title.italic);
10146
+ ctx.textBaseline = "middle";
10147
+ ctx.fillStyle = title.color;
10148
+ ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
10149
+ ctx.restore();
10150
+ }
10151
+ function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
10152
+ const maxValue = runtime.maxValue;
10153
+ const minValue = runtime.minValue;
10154
+ const gaugeValue = runtime.gaugeValue;
10155
+ const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
10156
+ const gaugeArcWidth = gaugeRect.width / 6;
10157
+ const gaugePercentage = gaugeValue
10158
+ ? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
10159
+ : 0;
10160
+ const gaugeValuePosition = {
10161
+ x: boundingRect.width / 2,
10162
+ y: gaugeRect.y + gaugeRect.height - gaugeRect.height / 12,
10067
10163
  };
10068
- canvas = owl.useRef("graphContainer");
10069
- chart;
10070
- currentRuntime;
10071
- get background() {
10072
- return this.chartRuntime.background;
10164
+ let gaugeValueFontSize = GAUGE_DEFAULT_VALUE_FONT_SIZE;
10165
+ // Scale down the font size if the gaugeRect is too small
10166
+ if (gaugeRect.height < 300) {
10167
+ gaugeValueFontSize = gaugeValueFontSize * (gaugeRect.height / 300);
10073
10168
  }
10074
- get canvasStyle() {
10075
- return `background-color: ${this.background}`;
10169
+ // Scale down the font size if the text is too long
10170
+ const maxTextWidth = gaugeRect.width / 2;
10171
+ const gaugeLabel = gaugeValue?.label || "-";
10172
+ if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
10173
+ gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
10076
10174
  }
10077
- get chartRuntime() {
10078
- const runtime = this.env.model.getters.getChartRuntime(this.props.figure.id);
10079
- if (!("chartJsConfig" in runtime)) {
10080
- throw new Error("Unsupported chart runtime");
10081
- }
10082
- return runtime;
10175
+ const minLabelPosition = {
10176
+ x: gaugeRect.x + gaugeArcWidth / 2,
10177
+ y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
10178
+ };
10179
+ const maxLabelPosition = {
10180
+ x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
10181
+ y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
10182
+ };
10183
+ const textColor = chartMutedFontColor(runtime.background);
10184
+ const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);
10185
+ let x = 0, titleWidth = 0, titleHeight = 0;
10186
+ if (runtime.title.text) {
10187
+ ({ width: titleWidth, height: titleHeight } = computeTextDimension(ctx, runtime.title.text, { fontSize: CHART_TITLE_FONT_SIZE, ...runtime.title }, "px"));
10083
10188
  }
10084
- setup() {
10085
- owl.onMounted(() => {
10086
- const runtime = this.chartRuntime;
10087
- this.currentRuntime = runtime;
10088
- // Note: chartJS modify the runtime in place, so it's important to give it a copy
10089
- this.createChart(deepCopy(runtime.chartJsConfig));
10090
- });
10091
- owl.onWillUnmount(() => this.chart?.destroy());
10092
- owl.useEffect(() => {
10093
- const runtime = this.chartRuntime;
10094
- if (runtime !== this.currentRuntime) {
10095
- if (runtime.chartJsConfig.type !== this.currentRuntime.chartJsConfig.type) {
10096
- this.chart?.destroy();
10097
- this.createChart(deepCopy(runtime.chartJsConfig));
10098
- }
10099
- else {
10100
- this.updateChartJs(deepCopy(runtime));
10101
- }
10102
- this.currentRuntime = runtime;
10103
- }
10189
+ switch (runtime.title.align) {
10190
+ case "right":
10191
+ x = boundingRect.width - titleWidth - CHART_PADDING$1;
10192
+ break;
10193
+ case "center":
10194
+ x = (boundingRect.width - titleWidth) / 2;
10195
+ break;
10196
+ case "left":
10197
+ default:
10198
+ x = CHART_PADDING$1;
10199
+ break;
10200
+ }
10201
+ return {
10202
+ width: boundingRect.width,
10203
+ height: boundingRect.height,
10204
+ title: {
10205
+ label: runtime.title.text ?? "",
10206
+ fontSize: runtime.title.fontSize ?? CHART_TITLE_FONT_SIZE,
10207
+ textPosition: {
10208
+ x,
10209
+ y: CHART_PADDING_TOP + titleHeight / 2,
10210
+ },
10211
+ color: runtime.title.color ?? textColor,
10212
+ bold: runtime.title.bold,
10213
+ italic: runtime.title.italic,
10214
+ },
10215
+ backgroundColor: runtime.background,
10216
+ gauge: {
10217
+ rect: gaugeRect,
10218
+ arcWidth: gaugeArcWidth,
10219
+ percentage: clip(gaugePercentage, 0, 1),
10220
+ color: getGaugeColor(runtime),
10221
+ },
10222
+ inflectionValues,
10223
+ gaugeValue: {
10224
+ label: gaugeLabel,
10225
+ textPosition: gaugeValuePosition,
10226
+ fontSize: gaugeValueFontSize,
10227
+ color: textColor,
10228
+ },
10229
+ minLabel: {
10230
+ label: runtime.minValue.label,
10231
+ textPosition: minLabelPosition,
10232
+ fontSize: GAUGE_LABELS_FONT_SIZE,
10233
+ color: textColor,
10234
+ },
10235
+ maxLabel: {
10236
+ label: runtime.maxValue.label,
10237
+ textPosition: maxLabelPosition,
10238
+ fontSize: GAUGE_LABELS_FONT_SIZE,
10239
+ color: textColor,
10240
+ },
10241
+ };
10242
+ }
10243
+ /**
10244
+ * Get the rectangle in which the gauge will be drawn, based on the bounding rectangle of the canvas and leaving
10245
+ * space for the title and labels.
10246
+ */
10247
+ function getGaugeRect(boundingRect, title) {
10248
+ const titleHeight = title ? GAUGE_TITLE_SECTION_HEIGHT : 0;
10249
+ const drawHeight = boundingRect.height - GAUGE_PADDING_BOTTOM - titleHeight - GAUGE_PADDING_TOP;
10250
+ const drawWidth = boundingRect.width - GAUGE_PADDING_SIDE * 2;
10251
+ let gaugeWidth;
10252
+ let gaugeHeight;
10253
+ if (drawWidth > 2 * drawHeight) {
10254
+ gaugeWidth = 2 * drawHeight;
10255
+ gaugeHeight = drawHeight;
10256
+ }
10257
+ else {
10258
+ gaugeWidth = drawWidth;
10259
+ gaugeHeight = drawWidth / 2;
10260
+ }
10261
+ const gaugeX = GAUGE_PADDING_SIDE + (drawWidth - gaugeWidth) / 2;
10262
+ const gaugeY = titleHeight + GAUGE_PADDING_TOP + (drawHeight - gaugeHeight) / 2;
10263
+ return {
10264
+ x: gaugeX,
10265
+ y: gaugeY,
10266
+ width: gaugeWidth,
10267
+ height: gaugeHeight,
10268
+ };
10269
+ }
10270
+ /**
10271
+ * 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).
10272
+ *
10273
+ * Also compute an offset for the text so that it doesn't overlap with other text.
10274
+ */
10275
+ function getInflectionValues(runtime, gaugeRect, textColor, ctx) {
10276
+ const maxValue = runtime.maxValue;
10277
+ const minValue = runtime.minValue;
10278
+ const gaugeCircleCenter = {
10279
+ x: gaugeRect.x + gaugeRect.width / 2,
10280
+ y: gaugeRect.y + gaugeRect.height,
10281
+ };
10282
+ const textStyle = { fontSize: GAUGE_LABELS_FONT_SIZE };
10283
+ const inflectionValues = [];
10284
+ const inflectionValuesTextRects = [];
10285
+ for (const inflectionValue of runtime.inflectionValues) {
10286
+ const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
10287
+ const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
10288
+ const angle = Math.PI - Math.PI * percentage;
10289
+ const textRect = getRectangleTangentToCircle(angle, // angle between X axis and the point where the rectangle is tangent to the circle
10290
+ gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, // radius of the gauge circle + margin below text
10291
+ gaugeCircleCenter.x, // center of the gauge circle
10292
+ gaugeCircleCenter.y, // center of the gauge circle
10293
+ labelWidth + 2, // width of the text + some margin
10294
+ GAUGE_LABELS_FONT_SIZE // height of the text
10295
+ );
10296
+ let offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect))
10297
+ ? GAUGE_LABELS_FONT_SIZE
10298
+ : 0;
10299
+ inflectionValuesTextRects.push(textRect);
10300
+ inflectionValues.push({
10301
+ rotation: angle,
10302
+ label: inflectionValue.label,
10303
+ fontSize: GAUGE_LABELS_FONT_SIZE,
10304
+ color: textColor,
10305
+ offset,
10104
10306
  });
10105
10307
  }
10106
- createChart(chartData) {
10107
- const canvas = this.canvas.el;
10108
- const ctx = canvas.getContext("2d");
10109
- this.chart = new window.Chart(ctx, chartData);
10308
+ return inflectionValues;
10309
+ }
10310
+ function getGaugeColor(runtime) {
10311
+ const gaugeValue = runtime.gaugeValue?.value;
10312
+ if (gaugeValue === undefined) {
10313
+ return GAUGE_BACKGROUND_COLOR;
10110
10314
  }
10111
- updateChartJs(chartRuntime) {
10112
- const chartData = chartRuntime.chartJsConfig;
10113
- if (chartData.data && chartData.data.datasets) {
10114
- this.chart.data = chartData.data;
10115
- if (chartData.options?.plugins?.title) {
10116
- this.chart.config.options.plugins.title = chartData.options.plugins.title;
10117
- }
10315
+ for (let i = 0; i < runtime.inflectionValues.length; i++) {
10316
+ const inflectionValue = runtime.inflectionValues[i];
10317
+ if (inflectionValue.operator === "<" && gaugeValue < inflectionValue.value) {
10318
+ return runtime.colors[i];
10118
10319
  }
10119
- else {
10120
- this.chart.data.datasets = [];
10320
+ else if (inflectionValue.operator === "<=" && gaugeValue <= inflectionValue.value) {
10321
+ return runtime.colors[i];
10121
10322
  }
10122
- this.chart.config.options = chartData.options;
10123
- this.chart.update();
10124
10323
  }
10324
+ return runtime.colors.at(-1);
10325
+ }
10326
+ function getSegmentsOfRectangle(rectangle) {
10327
+ return [
10328
+ { start: rectangle.topLeft, end: rectangle.topRight },
10329
+ { start: rectangle.topRight, end: rectangle.bottomRight },
10330
+ { start: rectangle.bottomRight, end: rectangle.bottomLeft },
10331
+ { start: rectangle.bottomLeft, end: rectangle.topLeft },
10332
+ ];
10333
+ }
10334
+ /**
10335
+ * Check if two segment intersect. The case where the segments are colinear (both segments on the same line)
10336
+ * is not handled.
10337
+ */
10338
+ function doSegmentIntersect(segment1, segment2) {
10339
+ const A = segment1.start;
10340
+ const B = segment1.end;
10341
+ const C = segment2.start;
10342
+ const D = segment2.end;
10343
+ /**
10344
+ * Line segment intersection algorithm
10345
+ * https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
10346
+ */
10347
+ function ccw(a, b, c) {
10348
+ return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
10349
+ }
10350
+ return ccw(A, C, D) !== ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D);
10351
+ }
10352
+ function doRectanglesIntersect(rect1, rect2) {
10353
+ const segments1 = getSegmentsOfRectangle(rect1);
10354
+ const segments2 = getSegmentsOfRectangle(rect2);
10355
+ for (const segment1 of segments1) {
10356
+ for (const segment2 of segments2) {
10357
+ if (doSegmentIntersect(segment1, segment2)) {
10358
+ return true;
10359
+ }
10360
+ }
10361
+ }
10362
+ return false;
10363
+ }
10364
+ /**
10365
+ * Get the rectangle that is tangent to a circle at a given angle.
10366
+ *
10367
+ * @param angle angle between X axis and the point where the rectangle is tangent to the circle
10368
+ */
10369
+ function getRectangleTangentToCircle(angle, radius, circleCenterX, circleCenterY, rectWidth, rectHeight) {
10370
+ const cos = Math.cos(angle);
10371
+ const sin = Math.sin(angle);
10372
+ // x, y are the distance from the center of the circle to the point where the rectangle is tangent to the circle
10373
+ const x = cos * radius;
10374
+ const y = sin * radius;
10375
+ // x2, y2 are the distance from the point the rectangle is tangent to the circle to the bottom left corner of the rectangle
10376
+ const x2 = sin * (rectWidth / 2); // cos(angle + 90°) = sin(angle)
10377
+ const y2 = cos * (rectWidth / 2);
10378
+ const bottomRight = {
10379
+ x: x + x2 + circleCenterX,
10380
+ y: circleCenterY - (y - y2),
10381
+ };
10382
+ const bottomLeft = {
10383
+ x: x - x2 + circleCenterX,
10384
+ y: circleCenterY - (y + y2),
10385
+ };
10386
+ // Same as above but for the top corners of the rectangle (radius + rectangle height instead of radius)
10387
+ const xp = cos * (radius + rectHeight);
10388
+ const yp = sin * (radius + rectHeight);
10389
+ const topLeft = {
10390
+ x: xp - x2 + circleCenterX,
10391
+ y: circleCenterY - (yp + y2),
10392
+ };
10393
+ const topRight = {
10394
+ x: xp + x2 + circleCenterX,
10395
+ y: circleCenterY - (yp - y2),
10396
+ };
10397
+ return { bottomLeft, bottomRight, topRight, topLeft };
10125
10398
  }
10126
10399
 
10127
10400
  /**
@@ -10703,6 +10976,155 @@ class ScorecardChartConfigBuilder {
10703
10976
  }
10704
10977
  }
10705
10978
 
10979
+ const CHART_COMMON_OPTIONS = {
10980
+ // https://www.chartjs.org/docs/latest/general/responsive.html
10981
+ responsive: true, // will resize when its container is resized
10982
+ maintainAspectRatio: false, // doesn't maintain the aspect ratio (width/height =2 by default) so the user has the choice of the exact layout
10983
+ elements: {
10984
+ line: {
10985
+ fill: false, // do not fill the area under line charts
10986
+ },
10987
+ point: {
10988
+ hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
10989
+ },
10990
+ },
10991
+ animation: false,
10992
+ };
10993
+ function truncateLabel(label) {
10994
+ if (!label) {
10995
+ return "";
10996
+ }
10997
+ if (label.length > MAX_CHAR_LABEL) {
10998
+ return label.substring(0, MAX_CHAR_LABEL) + "…";
10999
+ }
11000
+ return label;
11001
+ }
11002
+ function chartToImage(runtime, figure, type) {
11003
+ // wrap the canvas in a div with a fixed size because chart.js would
11004
+ // fill the whole page otherwise
11005
+ const div = document.createElement("div");
11006
+ div.style.width = `${figure.width}px`;
11007
+ div.style.height = `${figure.height}px`;
11008
+ const canvas = document.createElement("canvas");
11009
+ div.append(canvas);
11010
+ canvas.setAttribute("width", figure.width.toString());
11011
+ canvas.setAttribute("height", figure.height.toString());
11012
+ // we have to add the canvas to the DOM otherwise it won't be rendered
11013
+ document.body.append(div);
11014
+ if ("chartJsConfig" in runtime) {
11015
+ const config = deepCopy(runtime.chartJsConfig);
11016
+ config.plugins = [backgroundColorChartJSPlugin];
11017
+ const Chart = getChartJSConstructor();
11018
+ const chart = new Chart(canvas, config);
11019
+ const imgContent = chart.toBase64Image();
11020
+ chart.destroy();
11021
+ div.remove();
11022
+ return imgContent;
11023
+ }
11024
+ else if (type === "scorecard") {
11025
+ const design = getScorecardConfiguration(figure, runtime);
11026
+ drawScoreChart(design, canvas);
11027
+ const imgContent = canvas.toDataURL();
11028
+ div.remove();
11029
+ return imgContent;
11030
+ }
11031
+ else if (type === "gauge") {
11032
+ drawGaugeChart(canvas, runtime);
11033
+ const imgContent = canvas.toDataURL();
11034
+ div.remove();
11035
+ return imgContent;
11036
+ }
11037
+ return undefined;
11038
+ }
11039
+ /**
11040
+ * Custom chart.js plugin to set the background color of the canvas
11041
+ * https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md
11042
+ */
11043
+ const backgroundColorChartJSPlugin = {
11044
+ id: "customCanvasBackgroundColor",
11045
+ beforeDraw: (chart) => {
11046
+ const { ctx } = chart;
11047
+ ctx.save();
11048
+ ctx.globalCompositeOperation = "destination-over";
11049
+ ctx.fillStyle = "#ffffff";
11050
+ ctx.fillRect(0, 0, chart.width, chart.height);
11051
+ ctx.restore();
11052
+ },
11053
+ };
11054
+ /** Return window.Chart, making sure all our extensions are loaded in ChartJS */
11055
+ function getChartJSConstructor() {
11056
+ if (window.Chart && !window.Chart?.registry.plugins.get("chartShowValuesPlugin")) {
11057
+ window.Chart.register(chartShowValuesPlugin);
11058
+ window.Chart.register(waterfallLinesPlugin);
11059
+ }
11060
+ return window.Chart;
11061
+ }
11062
+
11063
+ class ChartJsComponent extends owl.Component {
11064
+ static template = "o-spreadsheet-ChartJsComponent";
11065
+ static props = {
11066
+ figure: Object,
11067
+ };
11068
+ canvas = owl.useRef("graphContainer");
11069
+ chart;
11070
+ currentRuntime;
11071
+ get background() {
11072
+ return this.chartRuntime.background;
11073
+ }
11074
+ get canvasStyle() {
11075
+ return `background-color: ${this.background}`;
11076
+ }
11077
+ get chartRuntime() {
11078
+ const runtime = this.env.model.getters.getChartRuntime(this.props.figure.id);
11079
+ if (!("chartJsConfig" in runtime)) {
11080
+ throw new Error("Unsupported chart runtime");
11081
+ }
11082
+ return runtime;
11083
+ }
11084
+ setup() {
11085
+ owl.onMounted(() => {
11086
+ const runtime = this.chartRuntime;
11087
+ this.currentRuntime = runtime;
11088
+ // Note: chartJS modify the runtime in place, so it's important to give it a copy
11089
+ this.createChart(deepCopy(runtime.chartJsConfig));
11090
+ });
11091
+ owl.onWillUnmount(() => this.chart?.destroy());
11092
+ owl.useEffect(() => {
11093
+ const runtime = this.chartRuntime;
11094
+ if (runtime !== this.currentRuntime) {
11095
+ if (runtime.chartJsConfig.type !== this.currentRuntime.chartJsConfig.type) {
11096
+ this.chart?.destroy();
11097
+ this.createChart(deepCopy(runtime.chartJsConfig));
11098
+ }
11099
+ else {
11100
+ this.updateChartJs(deepCopy(runtime));
11101
+ }
11102
+ this.currentRuntime = runtime;
11103
+ }
11104
+ });
11105
+ }
11106
+ createChart(chartData) {
11107
+ const canvas = this.canvas.el;
11108
+ const ctx = canvas.getContext("2d");
11109
+ const Chart = getChartJSConstructor();
11110
+ this.chart = new Chart(ctx, chartData);
11111
+ }
11112
+ updateChartJs(chartRuntime) {
11113
+ const chartData = chartRuntime.chartJsConfig;
11114
+ if (chartData.data && chartData.data.datasets) {
11115
+ this.chart.data = chartData.data;
11116
+ if (chartData.options?.plugins?.title) {
11117
+ this.chart.config.options.plugins.title = chartData.options.plugins.title;
11118
+ }
11119
+ }
11120
+ else {
11121
+ this.chart.data.datasets = [];
11122
+ }
11123
+ this.chart.config.options = chartData.options;
11124
+ this.chart.update();
11125
+ }
11126
+ }
11127
+
10706
11128
  class ScorecardChart extends owl.Component {
10707
11129
  static template = "o-spreadsheet-ScorecardChart";
10708
11130
  static props = {
@@ -22155,7 +22577,7 @@ autofillRulesRegistry
22155
22577
  condition: (cell) => !cell.isFormula &&
22156
22578
  evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.text &&
22157
22579
  alphaNumericValueRegExp.test(cell.content),
22158
- generateRule: (cell, cells) => {
22580
+ generateRule: (cell, cells, direction) => {
22159
22581
  const numberPostfix = parseInt(cell.content.match(numberPostfixRegExp)[0]);
22160
22582
  const prefix = cell.content.match(stringPrefixRegExp)[0];
22161
22583
  const numberPostfixLength = cell.content.length - prefix.length;
@@ -22163,7 +22585,10 @@ autofillRulesRegistry
22163
22585
  alphaNumericValueRegExp.test(evaluatedCell.value)) // get consecutive alphanumeric cells, no matter what the prefix is
22164
22586
  .filter((cell) => prefix === (cell.value ?? "").toString().match(stringPrefixRegExp)[0])
22165
22587
  .map((cell) => parseInt((cell.value ?? "").toString().match(numberPostfixRegExp)[0]));
22166
- const increment = calculateIncrementBasedOnGroup(group);
22588
+ let increment = calculateIncrementBasedOnGroup(group);
22589
+ if (["up", "left"].includes(direction) && group.length === 1) {
22590
+ increment = -increment;
22591
+ }
22167
22592
  return {
22168
22593
  type: "ALPHANUMERIC_INCREMENT_MODIFIER",
22169
22594
  prefix,
@@ -22226,10 +22651,13 @@ autofillRulesRegistry
22226
22651
  .add("increment_number", {
22227
22652
  condition: (cell) => !cell.isFormula &&
22228
22653
  evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.number,
22229
- generateRule: (cell, cells) => {
22654
+ generateRule: (cell, cells, direction) => {
22230
22655
  const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number &&
22231
22656
  !isDateTimeFormat(evaluatedCell.format || "")).map((cell) => Number(cell.value));
22232
- const increment = calculateIncrementBasedOnGroup(group);
22657
+ let increment = calculateIncrementBasedOnGroup(group);
22658
+ if (["up", "left"].includes(direction) && group.length === 1) {
22659
+ increment = -increment;
22660
+ }
22233
22661
  const evaluation = evaluateLiteral(cell, { locale: DEFAULT_LOCALE });
22234
22662
  return {
22235
22663
  type: "INCREMENT_MODIFIER",
@@ -22273,343 +22701,6 @@ function getDateIntervals(dates) {
22273
22701
 
22274
22702
  const cellPopoverRegistry = new Registry();
22275
22703
 
22276
- const GAUGE_PADDING_SIDE = 30;
22277
- const GAUGE_PADDING_TOP = 10;
22278
- const GAUGE_PADDING_BOTTOM = 20;
22279
- const GAUGE_LABELS_FONT_SIZE = 12;
22280
- const GAUGE_DEFAULT_VALUE_FONT_SIZE = 80;
22281
- const GAUGE_BACKGROUND_COLOR = "#F3F2F1";
22282
- const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
22283
- const GAUGE_TITLE_SECTION_HEIGHT = 25;
22284
- function drawGaugeChart(canvas, runtime) {
22285
- const canvasBoundingRect = canvas.getBoundingClientRect();
22286
- canvas.width = canvasBoundingRect.width;
22287
- canvas.height = canvasBoundingRect.height;
22288
- const ctx = canvas.getContext("2d");
22289
- const config = getGaugeRenderingConfig(canvasBoundingRect, runtime, ctx);
22290
- drawBackground(ctx, config);
22291
- drawGauge(ctx, config);
22292
- drawInflectionValues(ctx, config);
22293
- drawLabels(ctx, config);
22294
- drawTitle(ctx, config);
22295
- }
22296
- function drawGauge(ctx, config) {
22297
- ctx.save();
22298
- const gauge = config.gauge;
22299
- const arcCenterX = gauge.rect.x + gauge.rect.width / 2;
22300
- const arcCenterY = gauge.rect.y + gauge.rect.height;
22301
- const arcRadius = gauge.rect.height - gauge.arcWidth / 2;
22302
- if (arcRadius < 0) {
22303
- return;
22304
- }
22305
- const gaugeAngle = gauge.percentage === 1 ? 0 : Math.PI * (1 + gauge.percentage);
22306
- // Gauge background
22307
- ctx.strokeStyle = GAUGE_BACKGROUND_COLOR;
22308
- ctx.beginPath();
22309
- ctx.lineWidth = gauge.arcWidth;
22310
- ctx.arc(arcCenterX, arcCenterY, arcRadius, gaugeAngle, 0);
22311
- ctx.stroke();
22312
- // Gauge value
22313
- ctx.strokeStyle = gauge.color;
22314
- ctx.beginPath();
22315
- ctx.arc(arcCenterX, arcCenterY, arcRadius, Math.PI, gaugeAngle);
22316
- ctx.stroke();
22317
- ctx.restore();
22318
- }
22319
- function drawBackground(ctx, config) {
22320
- ctx.save();
22321
- ctx.fillStyle = config.backgroundColor;
22322
- ctx.fillRect(0, 0, config.width, config.height);
22323
- ctx.restore();
22324
- }
22325
- function drawLabels(ctx, config) {
22326
- for (const label of [config.minLabel, config.maxLabel, config.gaugeValue]) {
22327
- ctx.save();
22328
- ctx.textAlign = "center";
22329
- ctx.fillStyle = label.color;
22330
- ctx.font = `${label.fontSize}px ${DEFAULT_FONT}`;
22331
- ctx.fillText(label.label, label.textPosition.x, label.textPosition.y);
22332
- ctx.restore();
22333
- }
22334
- }
22335
- function drawInflectionValues(ctx, config) {
22336
- const { x: rectX, y: rectY, width, height } = config.gauge.rect;
22337
- for (const inflectionValue of config.inflectionValues) {
22338
- ctx.save();
22339
- ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment
22340
- ctx.rotate(Math.PI / 2 - inflectionValue.rotation);
22341
- ctx.lineWidth = 2;
22342
- ctx.strokeStyle = chartMutedFontColor(config.backgroundColor) + "aa";
22343
- ctx.beginPath();
22344
- ctx.moveTo(0, -(height - config.gauge.arcWidth));
22345
- ctx.lineTo(0, -height - 3);
22346
- ctx.stroke();
22347
- ctx.textAlign = "center";
22348
- ctx.font = `${inflectionValue.fontSize}px ${DEFAULT_FONT}`;
22349
- ctx.fillStyle = inflectionValue.color;
22350
- const textY = -height - GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN - inflectionValue.offset;
22351
- ctx.fillText(inflectionValue.label, 0, textY);
22352
- ctx.restore();
22353
- }
22354
- }
22355
- function drawTitle(ctx, config) {
22356
- ctx.save();
22357
- const title = config.title;
22358
- ctx.font = getDefaultContextFont(title.fontSize, title.bold, title.italic);
22359
- ctx.textBaseline = "middle";
22360
- ctx.fillStyle = title.color;
22361
- ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
22362
- ctx.restore();
22363
- }
22364
- function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
22365
- const maxValue = runtime.maxValue;
22366
- const minValue = runtime.minValue;
22367
- const gaugeValue = runtime.gaugeValue;
22368
- const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
22369
- const gaugeArcWidth = gaugeRect.width / 6;
22370
- const gaugePercentage = gaugeValue
22371
- ? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
22372
- : 0;
22373
- const gaugeValuePosition = {
22374
- x: boundingRect.width / 2,
22375
- y: gaugeRect.y + gaugeRect.height - gaugeRect.height / 12,
22376
- };
22377
- let gaugeValueFontSize = GAUGE_DEFAULT_VALUE_FONT_SIZE;
22378
- // Scale down the font size if the gaugeRect is too small
22379
- if (gaugeRect.height < 300) {
22380
- gaugeValueFontSize = gaugeValueFontSize * (gaugeRect.height / 300);
22381
- }
22382
- // Scale down the font size if the text is too long
22383
- const maxTextWidth = gaugeRect.width / 2;
22384
- const gaugeLabel = gaugeValue?.label || "-";
22385
- if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
22386
- gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
22387
- }
22388
- const minLabelPosition = {
22389
- x: gaugeRect.x + gaugeArcWidth / 2,
22390
- y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
22391
- };
22392
- const maxLabelPosition = {
22393
- x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
22394
- y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
22395
- };
22396
- const textColor = chartMutedFontColor(runtime.background);
22397
- const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);
22398
- let x = 0, titleWidth = 0, titleHeight = 0;
22399
- if (runtime.title.text) {
22400
- ({ width: titleWidth, height: titleHeight } = computeTextDimension(ctx, runtime.title.text, { fontSize: CHART_TITLE_FONT_SIZE, ...runtime.title }, "px"));
22401
- }
22402
- switch (runtime.title.align) {
22403
- case "right":
22404
- x = boundingRect.width - titleWidth - CHART_PADDING$1;
22405
- break;
22406
- case "center":
22407
- x = (boundingRect.width - titleWidth) / 2;
22408
- break;
22409
- case "left":
22410
- default:
22411
- x = CHART_PADDING$1;
22412
- break;
22413
- }
22414
- return {
22415
- width: boundingRect.width,
22416
- height: boundingRect.height,
22417
- title: {
22418
- label: runtime.title.text ?? "",
22419
- fontSize: runtime.title.fontSize ?? CHART_TITLE_FONT_SIZE,
22420
- textPosition: {
22421
- x,
22422
- y: CHART_PADDING_TOP + titleHeight / 2,
22423
- },
22424
- color: runtime.title.color ?? textColor,
22425
- bold: runtime.title.bold,
22426
- italic: runtime.title.italic,
22427
- },
22428
- backgroundColor: runtime.background,
22429
- gauge: {
22430
- rect: gaugeRect,
22431
- arcWidth: gaugeArcWidth,
22432
- percentage: clip(gaugePercentage, 0, 1),
22433
- color: getGaugeColor(runtime),
22434
- },
22435
- inflectionValues,
22436
- gaugeValue: {
22437
- label: gaugeLabel,
22438
- textPosition: gaugeValuePosition,
22439
- fontSize: gaugeValueFontSize,
22440
- color: textColor,
22441
- },
22442
- minLabel: {
22443
- label: runtime.minValue.label,
22444
- textPosition: minLabelPosition,
22445
- fontSize: GAUGE_LABELS_FONT_SIZE,
22446
- color: textColor,
22447
- },
22448
- maxLabel: {
22449
- label: runtime.maxValue.label,
22450
- textPosition: maxLabelPosition,
22451
- fontSize: GAUGE_LABELS_FONT_SIZE,
22452
- color: textColor,
22453
- },
22454
- };
22455
- }
22456
- /**
22457
- * Get the rectangle in which the gauge will be drawn, based on the bounding rectangle of the canvas and leaving
22458
- * space for the title and labels.
22459
- */
22460
- function getGaugeRect(boundingRect, title) {
22461
- const titleHeight = title ? GAUGE_TITLE_SECTION_HEIGHT : 0;
22462
- const drawHeight = boundingRect.height - GAUGE_PADDING_BOTTOM - titleHeight - GAUGE_PADDING_TOP;
22463
- const drawWidth = boundingRect.width - GAUGE_PADDING_SIDE * 2;
22464
- let gaugeWidth;
22465
- let gaugeHeight;
22466
- if (drawWidth > 2 * drawHeight) {
22467
- gaugeWidth = 2 * drawHeight;
22468
- gaugeHeight = drawHeight;
22469
- }
22470
- else {
22471
- gaugeWidth = drawWidth;
22472
- gaugeHeight = drawWidth / 2;
22473
- }
22474
- const gaugeX = GAUGE_PADDING_SIDE + (drawWidth - gaugeWidth) / 2;
22475
- const gaugeY = titleHeight + GAUGE_PADDING_TOP + (drawHeight - gaugeHeight) / 2;
22476
- return {
22477
- x: gaugeX,
22478
- y: gaugeY,
22479
- width: gaugeWidth,
22480
- height: gaugeHeight,
22481
- };
22482
- }
22483
- /**
22484
- * Get the infliction values of the gauge, and where to draw them (the angle from the center of the gauge at which they are drawn).
22485
- *
22486
- * Also compute an offset for the text so that it doesn't overlap with other text.
22487
- */
22488
- function getInflectionValues(runtime, gaugeRect, textColor, ctx) {
22489
- const maxValue = runtime.maxValue;
22490
- const minValue = runtime.minValue;
22491
- const gaugeCircleCenter = {
22492
- x: gaugeRect.x + gaugeRect.width / 2,
22493
- y: gaugeRect.y + gaugeRect.height,
22494
- };
22495
- const textStyle = { fontSize: GAUGE_LABELS_FONT_SIZE };
22496
- const inflectionValues = [];
22497
- const inflectionValuesTextRects = [];
22498
- for (const inflectionValue of runtime.inflectionValues) {
22499
- const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
22500
- const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
22501
- const angle = Math.PI - Math.PI * percentage;
22502
- const textRect = getRectangleTangentToCircle(angle, // angle between X axis and the point where the rectangle is tangent to the circle
22503
- gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, // radius of the gauge circle + margin below text
22504
- gaugeCircleCenter.x, // center of the gauge circle
22505
- gaugeCircleCenter.y, // center of the gauge circle
22506
- labelWidth + 2, // width of the text + some margin
22507
- GAUGE_LABELS_FONT_SIZE // height of the text
22508
- );
22509
- let offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect))
22510
- ? GAUGE_LABELS_FONT_SIZE
22511
- : 0;
22512
- inflectionValuesTextRects.push(textRect);
22513
- inflectionValues.push({
22514
- rotation: angle,
22515
- label: inflectionValue.label,
22516
- fontSize: GAUGE_LABELS_FONT_SIZE,
22517
- color: textColor,
22518
- offset,
22519
- });
22520
- }
22521
- return inflectionValues;
22522
- }
22523
- function getGaugeColor(runtime) {
22524
- const gaugeValue = runtime.gaugeValue?.value;
22525
- if (gaugeValue === undefined) {
22526
- return GAUGE_BACKGROUND_COLOR;
22527
- }
22528
- for (let i = 0; i < runtime.inflectionValues.length; i++) {
22529
- const inflectionValue = runtime.inflectionValues[i];
22530
- if (inflectionValue.operator === "<" && gaugeValue < inflectionValue.value) {
22531
- return runtime.colors[i];
22532
- }
22533
- else if (inflectionValue.operator === "<=" && gaugeValue <= inflectionValue.value) {
22534
- return runtime.colors[i];
22535
- }
22536
- }
22537
- return runtime.colors.at(-1);
22538
- }
22539
- function getSegmentsOfRectangle(rectangle) {
22540
- return [
22541
- { start: rectangle.topLeft, end: rectangle.topRight },
22542
- { start: rectangle.topRight, end: rectangle.bottomRight },
22543
- { start: rectangle.bottomRight, end: rectangle.bottomLeft },
22544
- { start: rectangle.bottomLeft, end: rectangle.topLeft },
22545
- ];
22546
- }
22547
- /**
22548
- * Check if two segment intersect. The case where the segments are colinear (both segments on the same line)
22549
- * is not handled.
22550
- */
22551
- function doSegmentIntersect(segment1, segment2) {
22552
- const A = segment1.start;
22553
- const B = segment1.end;
22554
- const C = segment2.start;
22555
- const D = segment2.end;
22556
- /**
22557
- * Line segment intersection algorithm
22558
- * https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
22559
- */
22560
- function ccw(a, b, c) {
22561
- return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
22562
- }
22563
- return ccw(A, C, D) !== ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D);
22564
- }
22565
- function doRectanglesIntersect(rect1, rect2) {
22566
- const segments1 = getSegmentsOfRectangle(rect1);
22567
- const segments2 = getSegmentsOfRectangle(rect2);
22568
- for (const segment1 of segments1) {
22569
- for (const segment2 of segments2) {
22570
- if (doSegmentIntersect(segment1, segment2)) {
22571
- return true;
22572
- }
22573
- }
22574
- }
22575
- return false;
22576
- }
22577
- /**
22578
- * Get the rectangle that is tangent to a circle at a given angle.
22579
- *
22580
- * @param angle angle between X axis and the point where the rectangle is tangent to the circle
22581
- */
22582
- function getRectangleTangentToCircle(angle, radius, circleCenterX, circleCenterY, rectWidth, rectHeight) {
22583
- const cos = Math.cos(angle);
22584
- const sin = Math.sin(angle);
22585
- // x, y are the distance from the center of the circle to the point where the rectangle is tangent to the circle
22586
- const x = cos * radius;
22587
- const y = sin * radius;
22588
- // x2, y2 are the distance from the point the rectangle is tangent to the circle to the bottom left corner of the rectangle
22589
- const x2 = sin * (rectWidth / 2); // cos(angle + 90°) = sin(angle)
22590
- const y2 = cos * (rectWidth / 2);
22591
- const bottomRight = {
22592
- x: x + x2 + circleCenterX,
22593
- y: circleCenterY - (y - y2),
22594
- };
22595
- const bottomLeft = {
22596
- x: x - x2 + circleCenterX,
22597
- y: circleCenterY - (y + y2),
22598
- };
22599
- // Same as above but for the top corners of the rectangle (radius + rectangle height instead of radius)
22600
- const xp = cos * (radius + rectHeight);
22601
- const yp = sin * (radius + rectHeight);
22602
- const topLeft = {
22603
- x: xp - x2 + circleCenterX,
22604
- y: circleCenterY - (yp + y2),
22605
- };
22606
- const topRight = {
22607
- x: xp + x2 + circleCenterX,
22608
- y: circleCenterY - (yp - y2),
22609
- };
22610
- return { bottomLeft, bottomRight, topRight, topLeft };
22611
- }
22612
-
22613
22704
  class GaugeChartComponent extends owl.Component {
22614
22705
  static template = "o-spreadsheet-GaugeChartComponent";
22615
22706
  canvas = owl.useRef("chartContainer");
@@ -22642,81 +22733,6 @@ function toXlsxHexColor(color) {
22642
22733
  return color;
22643
22734
  }
22644
22735
 
22645
- const CHART_COMMON_OPTIONS = {
22646
- // https://www.chartjs.org/docs/latest/general/responsive.html
22647
- responsive: true, // will resize when its container is resized
22648
- maintainAspectRatio: false, // doesn't maintain the aspect ratio (width/height =2 by default) so the user has the choice of the exact layout
22649
- elements: {
22650
- line: {
22651
- fill: false, // do not fill the area under line charts
22652
- },
22653
- point: {
22654
- hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
22655
- },
22656
- },
22657
- animation: false,
22658
- };
22659
- function truncateLabel(label) {
22660
- if (!label) {
22661
- return "";
22662
- }
22663
- if (label.length > MAX_CHAR_LABEL) {
22664
- return label.substring(0, MAX_CHAR_LABEL) + "…";
22665
- }
22666
- return label;
22667
- }
22668
- function chartToImage(runtime, figure, type) {
22669
- // wrap the canvas in a div with a fixed size because chart.js would
22670
- // fill the whole page otherwise
22671
- const div = document.createElement("div");
22672
- div.style.width = `${figure.width}px`;
22673
- div.style.height = `${figure.height}px`;
22674
- const canvas = document.createElement("canvas");
22675
- div.append(canvas);
22676
- canvas.setAttribute("width", figure.width.toString());
22677
- canvas.setAttribute("height", figure.height.toString());
22678
- // we have to add the canvas to the DOM otherwise it won't be rendered
22679
- document.body.append(div);
22680
- if ("chartJsConfig" in runtime) {
22681
- const config = deepCopy(runtime.chartJsConfig);
22682
- config.plugins = [backgroundColorChartJSPlugin];
22683
- const chart = new window.Chart(canvas, config);
22684
- const imgContent = chart.toBase64Image();
22685
- chart.destroy();
22686
- div.remove();
22687
- return imgContent;
22688
- }
22689
- else if (type === "scorecard") {
22690
- const design = getScorecardConfiguration(figure, runtime);
22691
- drawScoreChart(design, canvas);
22692
- const imgContent = canvas.toDataURL();
22693
- div.remove();
22694
- return imgContent;
22695
- }
22696
- else if (type === "gauge") {
22697
- drawGaugeChart(canvas, runtime);
22698
- const imgContent = canvas.toDataURL();
22699
- div.remove();
22700
- return imgContent;
22701
- }
22702
- return undefined;
22703
- }
22704
- /**
22705
- * Custom chart.js plugin to set the background color of the canvas
22706
- * https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md
22707
- */
22708
- const backgroundColorChartJSPlugin = {
22709
- id: "customCanvasBackgroundColor",
22710
- beforeDraw: (chart) => {
22711
- const { ctx } = chart;
22712
- ctx.save();
22713
- ctx.globalCompositeOperation = "destination-over";
22714
- ctx.fillStyle = "#ffffff";
22715
- ctx.fillRect(0, 0, chart.width, chart.height);
22716
- ctx.restore();
22717
- },
22718
- };
22719
-
22720
22736
  /**
22721
22737
  * Represent a raw XML string
22722
22738
  */
@@ -22778,6 +22794,7 @@ const DRAWING_NS_C = "http://schemas.openxmlformats.org/drawingml/2006/chart";
22778
22794
  const CONTENT_TYPES = {
22779
22795
  workbook: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml",
22780
22796
  sheet: "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml",
22797
+ metadata: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheetMetadata+xml",
22781
22798
  sharedStrings: "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml",
22782
22799
  styles: "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml",
22783
22800
  drawing: "application/vnd.openxmlformats-officedocument.drawing+xml",
@@ -22790,6 +22807,7 @@ const CONTENT_TYPES = {
22790
22807
  const XLSX_RELATION_TYPE = {
22791
22808
  document: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument",
22792
22809
  sheet: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet",
22810
+ metadata: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sheetMetadata",
22793
22811
  sharedStrings: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings",
22794
22812
  styles: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles",
22795
22813
  drawing: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing",
@@ -22799,6 +22817,7 @@ const XLSX_RELATION_TYPE = {
22799
22817
  hyperlink: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
22800
22818
  image: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
22801
22819
  };
22820
+ const ARRAY_FORMULA_URI = "bdbb8cdc-fa1e-496e-a857-3c3f30c029c3";
22802
22821
  const RELATIONSHIP_NSR = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
22803
22822
  const HEIGHT_FACTOR = 0.75; // 100px => 75 u
22804
22823
  /**
@@ -25364,29 +25383,34 @@ function convertPivotTableConfig(pivotTable) {
25364
25383
  * In all the sheets, replace the table-only references in the formula cells with standard references.
25365
25384
  */
25366
25385
  function convertTableFormulaReferences(convertedSheets, xlsxSheets) {
25367
- for (let sheet of convertedSheets) {
25368
- const tables = xlsxSheets.find((s) => s.sheetName === sheet.name).tables;
25386
+ for (let tableSheet of convertedSheets) {
25387
+ const tables = xlsxSheets.find((s) => s.sheetName === tableSheet.name).tables;
25369
25388
  for (let table of tables) {
25370
25389
  const tabRef = table.name + "[";
25371
- for (let position of positions(toZone(table.ref))) {
25372
- const xc = toXC(position.col, position.row);
25373
- let cellContent = sheet.cells[xc];
25374
- if (cellContent?.startsWith("=")) {
25375
- let refIndex;
25376
- while ((refIndex = cellContent.indexOf(tabRef)) !== -1) {
25377
- let reference = cellContent.slice(refIndex + tabRef.length);
25378
- // Expression can either be tableName[colName] or tableName[[#This Row], [colName]]
25379
- let endIndex = reference.indexOf("]");
25380
- if (reference.startsWith(`[`)) {
25381
- endIndex = reference.indexOf("]", endIndex + 1);
25382
- endIndex = reference.indexOf("]", endIndex + 1);
25390
+ for (let sheet of convertedSheets) {
25391
+ for (let xc in sheet.cells) {
25392
+ const cell = sheet.cells[xc];
25393
+ let cellContent = sheet.cells[xc];
25394
+ if (cell && cellContent && cellContent.startsWith("=")) {
25395
+ let refIndex;
25396
+ while ((refIndex = cellContent.indexOf(tabRef)) !== -1) {
25397
+ let endIndex = refIndex + tabRef.length;
25398
+ let openBrackets = 1;
25399
+ while (openBrackets > 0 && endIndex < cellContent.length) {
25400
+ if (cellContent[endIndex] === "[") {
25401
+ openBrackets++;
25402
+ }
25403
+ else if (cellContent[endIndex] === "]") {
25404
+ openBrackets--;
25405
+ }
25406
+ endIndex++;
25407
+ }
25408
+ let reference = cellContent.slice(refIndex + tabRef.length, endIndex - 1);
25409
+ const sheetPrefix = tableSheet.id === sheet.id ? "" : tableSheet.name + "!";
25410
+ const convertedRef = convertTableReference(sheetPrefix, reference, table, xc);
25411
+ cellContent =
25412
+ cellContent.slice(0, refIndex) + convertedRef + cellContent.slice(endIndex);
25383
25413
  }
25384
- reference = reference.slice(0, endIndex);
25385
- const convertedRef = convertTableReference(reference, table, xc);
25386
- cellContent =
25387
- cellContent.slice(0, refIndex) +
25388
- convertedRef +
25389
- cellContent.slice(tabRef.length + refIndex + endIndex + 1);
25390
25414
  }
25391
25415
  sheet.cells[xc] = cellContent;
25392
25416
  }
@@ -25395,11 +25419,17 @@ function convertTableFormulaReferences(convertedSheets, xlsxSheets) {
25395
25419
  }
25396
25420
  }
25397
25421
  /**
25398
- * Convert table-specific references in formulas into standard references.
25422
+ * Convert table-specific references in formulas into standard references. A table reference is composed of columns names,
25423
+ * and of keywords determining the rows of the table to reference.
25399
25424
  *
25400
25425
  * A reference in a table can have the form (only the part between brackets should be given to this function):
25401
25426
  * - tableName[colName] : reference to the whole column "colName"
25427
+ * - tableName[#keyword] : reference to the whatever row the keyword refers to
25402
25428
  * - tableName[[#keyword], [colName]] : reference to some of the element(s) of the column colName
25429
+ * - tableName[[#keyword], [colName]:[col2Name]] : reference to some of the element(s) of the columns colName to col2Name
25430
+ * - tableName[[#keyword1], [#keyword2], [colName]] : reference to all the rows referenced by the keywords in the column colName
25431
+ * - tableName[[#keyword1], [colName], [#keyword2]]: the keywords and colName can be in any order
25432
+ *
25403
25433
  *
25404
25434
  * The available keywords are :
25405
25435
  * - #All : all the column (including totals)
@@ -25407,58 +25437,109 @@ function convertTableFormulaReferences(convertedSheets, xlsxSheets) {
25407
25437
  * - #Headers : only the header of the column
25408
25438
  * - #Totals : only the totals of the column
25409
25439
  * - #This Row : only the element in the same row as the cell
25440
+ *
25441
+ * Note that the only valid combination of multiple keywords are #Data + #Totals and #Headers + #Data.
25410
25442
  */
25411
- function convertTableReference(expr, table, cellXc) {
25412
- const refElements = expr.split(",");
25443
+ function convertTableReference(sheetPrefix, expr, table, cellXc) {
25444
+ // TODO: Ideally we'd want to make a real tokenizer, this simple approach won't work if for example the column name
25445
+ // contain # or , characters. But that's probably an edge case that we can ignore for now.
25446
+ const parts = expr.split(",").map((part) => part.trim());
25413
25447
  const tableZone = toZone(table.ref);
25414
- const refZone = { ...tableZone };
25415
- let isReferencedZoneValid = true;
25416
- // Single column reference
25417
- if (refElements.length === 1) {
25418
- const colRelativeIndex = table.cols.findIndex((col) => col.name === refElements[0]);
25419
- refZone.left = refZone.right = colRelativeIndex + tableZone.left;
25420
- if (table.headerRowCount) {
25421
- refZone.top += table.headerRowCount;
25422
- }
25423
- if (table.totalsRowCount) {
25424
- refZone.bottom -= 1;
25448
+ const colIndexes = [];
25449
+ const rowIndexes = [];
25450
+ const foundKeywords = [];
25451
+ for (const part of parts) {
25452
+ if (removeBrackets(part).startsWith("#")) {
25453
+ const keyWord = removeBrackets(part);
25454
+ foundKeywords.push(keyWord);
25455
+ switch (keyWord) {
25456
+ case "#All":
25457
+ rowIndexes.push(tableZone.top, tableZone.bottom);
25458
+ break;
25459
+ case "#Data":
25460
+ const top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
25461
+ const bottom = table.totalsRowCount
25462
+ ? tableZone.bottom - table.totalsRowCount
25463
+ : tableZone.bottom;
25464
+ rowIndexes.push(top, bottom);
25465
+ break;
25466
+ case "#This Row":
25467
+ rowIndexes.push(toCartesian(cellXc).row);
25468
+ break;
25469
+ case "#Headers":
25470
+ if (!table.headerRowCount) {
25471
+ return CellErrorType.InvalidReference;
25472
+ }
25473
+ rowIndexes.push(tableZone.top);
25474
+ break;
25475
+ case "#Totals":
25476
+ if (!table.totalsRowCount) {
25477
+ return CellErrorType.InvalidReference;
25478
+ }
25479
+ rowIndexes.push(tableZone.bottom);
25480
+ break;
25481
+ }
25425
25482
  }
25426
- }
25427
- // Other references
25428
- else {
25429
- switch (refElements[0].slice(1, refElements[0].length - 1)) {
25430
- case "#All":
25431
- refZone.top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
25432
- refZone.bottom = tableZone.bottom;
25433
- break;
25434
- case "#Data":
25435
- refZone.top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
25436
- refZone.bottom = table.totalsRowCount ? tableZone.bottom + 1 : tableZone.bottom;
25437
- break;
25438
- case "#This Row":
25439
- refZone.top = refZone.bottom = toCartesian(cellXc).row;
25440
- break;
25441
- case "#Headers":
25442
- refZone.top = refZone.bottom = tableZone.top;
25443
- if (!table.headerRowCount) {
25444
- isReferencedZoneValid = false;
25445
- }
25446
- break;
25447
- case "#Totals":
25448
- refZone.top = refZone.bottom = tableZone.bottom;
25449
- if (!table.totalsRowCount) {
25450
- isReferencedZoneValid = false;
25483
+ else {
25484
+ const columns = part
25485
+ .split(":")
25486
+ .map((part) => part.trim())
25487
+ .map(removeBrackets);
25488
+ if (colIndexes.length) {
25489
+ return CellErrorType.InvalidReference;
25490
+ }
25491
+ const colRelativeIndex = table.cols.findIndex((col) => col.name === columns[0]);
25492
+ if (colRelativeIndex === -1) {
25493
+ return CellErrorType.InvalidReference;
25494
+ }
25495
+ colIndexes.push(colRelativeIndex + tableZone.left);
25496
+ if (columns[1]) {
25497
+ const colRelativeIndex2 = table.cols.findIndex((col) => col.name === columns[1]);
25498
+ if (colRelativeIndex2 === -1) {
25499
+ return CellErrorType.InvalidReference;
25451
25500
  }
25452
- break;
25501
+ colIndexes.push(colRelativeIndex2 + tableZone.left);
25502
+ }
25453
25503
  }
25454
- const colRef = refElements[1].slice(1, refElements[1].length - 1);
25455
- const colRelativeIndex = table.cols.findIndex((col) => col.name === colRef);
25456
- refZone.left = refZone.right = colRelativeIndex + tableZone.left;
25457
25504
  }
25458
- if (!isReferencedZoneValid) {
25505
+ if (!areKeywordsCompatible(foundKeywords)) {
25459
25506
  return CellErrorType.InvalidReference;
25460
25507
  }
25461
- return refZone.top !== refZone.bottom ? zoneToXc(refZone) : toXC(refZone.left, refZone.top);
25508
+ if (rowIndexes.length === 0) {
25509
+ const top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
25510
+ const bottom = table.totalsRowCount
25511
+ ? tableZone.bottom - table.totalsRowCount
25512
+ : tableZone.bottom;
25513
+ rowIndexes.push(top, bottom);
25514
+ }
25515
+ if (colIndexes.length === 0) {
25516
+ colIndexes.push(tableZone.left, tableZone.right);
25517
+ }
25518
+ const refZone = {
25519
+ top: Math.min(...rowIndexes),
25520
+ left: Math.min(...colIndexes),
25521
+ bottom: Math.max(...rowIndexes),
25522
+ right: Math.max(...colIndexes),
25523
+ };
25524
+ return sheetPrefix + zoneToXc(refZone);
25525
+ }
25526
+ function removeBrackets(str) {
25527
+ return str.startsWith("[") && str.endsWith("]") ? str.slice(1, str.length - 1) : str;
25528
+ }
25529
+ function areKeywordsCompatible(keywords) {
25530
+ if (keywords.length < 2) {
25531
+ return true;
25532
+ }
25533
+ else if (keywords.length > 2) {
25534
+ return false;
25535
+ }
25536
+ else if (keywords.includes("#Data") && keywords.includes("#Totals")) {
25537
+ return true;
25538
+ }
25539
+ else if (keywords.includes("#Headers") && keywords.includes("#Data")) {
25540
+ return true;
25541
+ }
25542
+ return false;
25462
25543
  }
25463
25544
 
25464
25545
  // -------------------------------------
@@ -28626,11 +28707,12 @@ function canBeLinearChart(definition, dataSets, labelRange, getters) {
28626
28707
  }
28627
28708
  let missingTimeAdapterAlreadyWarned = false;
28628
28709
  function isLuxonTimeAdapterInstalled() {
28629
- if (!window.Chart) {
28710
+ const Chart = getChartJSConstructor();
28711
+ if (!Chart) {
28630
28712
  return false;
28631
28713
  }
28632
28714
  // @ts-ignore
28633
- const adapter = new window.Chart._adapters._date({});
28715
+ const adapter = new Chart._adapters._date({});
28634
28716
  const isInstalled = adapter._id === "luxon";
28635
28717
  if (!isInstalled && !missingTimeAdapterAlreadyWarned) {
28636
28718
  missingTimeAdapterAlreadyWarned = true;
@@ -32327,10 +32409,6 @@ class Popover extends owl.Component {
32327
32409
  this.currentDisplayValue = newDisplay;
32328
32410
  if (!anchor)
32329
32411
  return;
32330
- el.style.top = "";
32331
- el.style.left = "";
32332
- el.style["max-height"] = "";
32333
- el.style["max-width"] = "";
32334
32412
  const propsMaxSize = { width: this.props.maxWidth, height: this.props.maxHeight };
32335
32413
  let elDims = {
32336
32414
  width: el.getBoundingClientRect().width,
@@ -33868,6 +33946,7 @@ var CHART_HELPERS = /*#__PURE__*/Object.freeze({
33868
33946
  drawScoreChart: drawScoreChart,
33869
33947
  formatChartDatasetValue: formatChartDatasetValue,
33870
33948
  formatTickValue: formatTickValue,
33949
+ getChartJSConstructor: getChartJSConstructor,
33871
33950
  getChartPositionAtCenterOfViewport: getChartPositionAtCenterOfViewport,
33872
33951
  getDefinedAxis: getDefinedAxis,
33873
33952
  getPieColors: getPieColors,
@@ -37751,6 +37830,9 @@ class GenericChartConfigPanel extends owl.Component {
37751
37830
  this.state.datasetDispatchResult = this.props.updateChart(this.props.figureId, {
37752
37831
  dataSets: this.dataSeriesRanges,
37753
37832
  });
37833
+ if (this.state.datasetDispatchResult.isSuccessful) {
37834
+ this.dataSeriesRanges = this.env.model.getters.getChartDefinition(this.props.figureId).dataSets;
37835
+ }
37754
37836
  }
37755
37837
  getDataSeriesRanges() {
37756
37838
  return this.dataSeriesRanges;
@@ -40390,8 +40472,7 @@ css /* scss */ `
40390
40472
  }
40391
40473
 
40392
40474
  .o-composer-assistant {
40393
- position: absolute;
40394
- margin: 1px 4px;
40475
+ margin-top: 1px;
40395
40476
 
40396
40477
  .o-semi-bold {
40397
40478
  /* FIXME: to remove in favor of Bootstrap
@@ -40442,10 +40523,11 @@ class Composer extends owl.Component {
40442
40523
  });
40443
40524
  compositionActive = false;
40444
40525
  spreadsheetRect = useSpreadsheetRect();
40445
- get assistantStyle() {
40526
+ get assistantStyleProperties() {
40446
40527
  const composerRect = this.composerRef.el.getBoundingClientRect();
40447
40528
  const assistantStyle = {};
40448
- assistantStyle["min-width"] = `${this.props.rect?.width || ASSISTANT_WIDTH}px`;
40529
+ const minWidth = Math.min(this.props.rect?.width || Infinity, ASSISTANT_WIDTH);
40530
+ assistantStyle["min-width"] = `${minWidth}px`;
40449
40531
  const proposals = this.autoCompleteState.provider?.proposals;
40450
40532
  const proposalsHaveDescription = proposals?.some((proposal) => proposal.description);
40451
40533
  if (this.functionDescriptionState.showDescription || proposalsHaveDescription) {
@@ -40469,13 +40551,29 @@ class Composer extends owl.Component {
40469
40551
  }
40470
40552
  }
40471
40553
  else {
40472
- assistantStyle["max-height"] = `${this.spreadsheetRect.height - composerRect.bottom}px`;
40554
+ assistantStyle["max-height"] = `${this.spreadsheetRect.height - composerRect.bottom - 1}px`; // -1: margin
40473
40555
  if (composerRect.left + ASSISTANT_WIDTH + SCROLLBAR_WIDTH + CLOSE_ICON_RADIUS >
40474
40556
  this.spreadsheetRect.width) {
40475
40557
  assistantStyle.right = `${CLOSE_ICON_RADIUS}px`;
40476
40558
  }
40477
40559
  }
40478
- return cssPropertiesToCss(assistantStyle);
40560
+ return assistantStyle;
40561
+ }
40562
+ get assistantStyle() {
40563
+ const allProperties = this.assistantStyleProperties;
40564
+ return cssPropertiesToCss({
40565
+ "max-height": allProperties["max-height"],
40566
+ width: allProperties["width"],
40567
+ "min-width": allProperties["min-width"],
40568
+ });
40569
+ }
40570
+ get assistantContainerStyle() {
40571
+ const allProperties = this.assistantStyleProperties;
40572
+ return cssPropertiesToCss({
40573
+ top: allProperties["top"],
40574
+ right: allProperties["right"],
40575
+ transform: allProperties["transform"],
40576
+ });
40479
40577
  }
40480
40578
  // we can't allow input events to be triggered while we remove and add back the content of the composer in processContent
40481
40579
  shouldProcessInputEvents = false;
@@ -46412,9 +46510,7 @@ class PivotSidePanelStore extends SpreadsheetStore {
46412
46510
  pivot: this.draft,
46413
46511
  });
46414
46512
  this.draft = null;
46415
- if (!this.alreadyNotified &&
46416
- !this.isDynamicPivotInViewport() &&
46417
- this.isStaticPivotInViewport()) {
46513
+ if (!this.alreadyNotified && this.isUpdatedPivotVisibleInViewportOnlyAsStaticPivot()) {
46418
46514
  const formulaId = this.getters.getPivotFormulaId(this.pivotId);
46419
46515
  const pivotExample = `=PIVOT(${formulaId})`;
46420
46516
  this.alreadyNotified = true;
@@ -46470,29 +46566,33 @@ class PivotSidePanelStore extends SpreadsheetStore {
46470
46566
  this.applyUpdate();
46471
46567
  }
46472
46568
  }
46473
- isDynamicPivotInViewport() {
46474
- const sheetId = this.getters.getActiveSheetId();
46475
- for (const col of this.getters.getSheetViewVisibleCols()) {
46476
- for (const row of this.getters.getSheetViewVisibleRows()) {
46477
- const isDynamicPivot = this.getters.isSpillPivotFormula({ sheetId, col, row });
46478
- if (isDynamicPivot) {
46479
- return true;
46480
- }
46481
- }
46482
- }
46483
- return false;
46484
- }
46485
- isStaticPivotInViewport() {
46569
+ /**
46570
+ * @returns true if the updated pivot is visible in the viewport only as a
46571
+ * static pivot and not as a dynamic pivot
46572
+ */
46573
+ isUpdatedPivotVisibleInViewportOnlyAsStaticPivot() {
46574
+ let staticPivotCount = 0;
46575
+ const updatedPivotFormulaId = this.getters.getPivotFormulaId(this.pivotId);
46486
46576
  for (const position of this.getters.getVisibleCellPositions()) {
46487
46577
  const cell = this.getters.getCell(position);
46488
46578
  if (cell?.isFormula) {
46489
46579
  const pivotFunction = getFirstPivotFunction(cell.compiledFormula.tokens);
46490
- if (pivotFunction && pivotFunction.functionName !== "PIVOT") {
46491
- return true;
46580
+ const pivotFormulaId = pivotFunction?.args[0]?.value;
46581
+ if (pivotFunction && updatedPivotFormulaId === pivotFormulaId.toString()) {
46582
+ if (pivotFunction.functionName === "PIVOT") {
46583
+ // if we have at least one dynamic pivot visible inserted the viewport
46584
+ // we return false
46585
+ return false;
46586
+ }
46587
+ else {
46588
+ staticPivotCount++;
46589
+ }
46492
46590
  }
46493
46591
  }
46494
46592
  }
46495
- return false;
46593
+ // we return true if there are only static pivots visible inserted the viewport,
46594
+ // otherwise false
46595
+ return staticPivotCount > 0;
46496
46596
  }
46497
46597
  addDefaultDateTimeGranularity(fields, definition) {
46498
46598
  const { columns, rows } = definition;
@@ -60332,6 +60432,7 @@ class EvaluationPlugin extends UIPlugin {
60332
60432
  exportForExcel(data) {
60333
60433
  for (const sheet of data.sheets) {
60334
60434
  sheet.cellValues = {};
60435
+ sheet.formulaSpillRanges = {};
60335
60436
  }
60336
60437
  for (const position of this.evaluator.getEvaluatedPositions()) {
60337
60438
  const evaluatedCell = this.evaluator.getEvaluatedCell(position);
@@ -60343,8 +60444,9 @@ class EvaluationPlugin extends UIPlugin {
60343
60444
  const exportedSheetData = data.sheets.find((sheet) => sheet.id === position.sheetId);
60344
60445
  const formulaCell = this.getCorrespondingFormulaCell(position);
60345
60446
  if (formulaCell) {
60447
+ const cell = this.getters.getCell(position);
60346
60448
  isExported = isExportableToExcel(formulaCell.compiledFormula.tokens);
60347
- isFormula = isExported;
60449
+ isFormula = isExported && cell?.content === formulaCell.content;
60348
60450
  // If the cell contains a non-exported formula and that is evaluates to
60349
60451
  // nothing* ,we don't export it.
60350
60452
  // * non-falsy value are relevant and so are 0 and FALSE, which only leaves
@@ -60367,7 +60469,11 @@ class EvaluationPlugin extends UIPlugin {
60367
60469
  content = !isExported ? newContent : exportedCellData;
60368
60470
  }
60369
60471
  exportedSheetData.cells[xc] = content;
60370
- exportedSheetData.cellValues[xc] = value;
60472
+ exportedSheetData.cellValues[xc] = evaluatedCell.type !== "error" ? value : undefined;
60473
+ const spillZone = this.getSpreadZone(position);
60474
+ if (spillZone) {
60475
+ exportedSheetData.formulaSpillRanges[xc] = this.getters.getRangeString(this.getters.getRangeFromZone(position.sheetId, spillZone), position.sheetId);
60476
+ }
60371
60477
  }
60372
60478
  }
60373
60479
  /**
@@ -62574,7 +62680,7 @@ class AutofillPlugin extends UIPlugin {
62574
62680
  getRule(cell, cells) {
62575
62681
  const rules = autofillRulesRegistry.getAll().sort((a, b) => a.sequence - b.sequence);
62576
62682
  const rule = rules.find((rule) => rule.condition(cell, cells));
62577
- return rule && rule.generateRule(cell, cells);
62683
+ return rule && this.direction && rule.generateRule(cell, cells, this.direction);
62578
62684
  }
62579
62685
  /**
62580
62686
  * Create the generator to be able to autofill the next cells.
@@ -73104,7 +73210,7 @@ function numberRef(reference) {
73104
73210
  `;
73105
73211
  }
73106
73212
 
73107
- function addFormula(formula, value) {
73213
+ function addFormula(formula, value, formulaSpillRange) {
73108
73214
  if (!formula) {
73109
73215
  return { attrs: [], node: escapeXml `` };
73110
73216
  }
@@ -73112,10 +73218,17 @@ function addFormula(formula, value) {
73112
73218
  if (type === undefined) {
73113
73219
  return { attrs: [], node: escapeXml `` };
73114
73220
  }
73115
- const attrs = [["t", type]];
73221
+ const attrs = [
73222
+ ["cm", "1"],
73223
+ ["t", type],
73224
+ ];
73116
73225
  const XlsxFormula = adaptFormulaToExcel(formula);
73117
73226
  const exportedValue = adaptFormulaValueToExcel(value);
73118
- const node = escapeXml /*xml*/ `<f>${XlsxFormula}</f><v>${exportedValue}</v>`;
73227
+ // We treat all formulas as array formulas (a simple formula
73228
+ // is an array formula that spills on only one cell) to avoid
73229
+ // trying to detect spilling sub-formulas which is not a trivial task.
73230
+ let node;
73231
+ node = escapeXml /*xml*/ `<f t="array" ref="${formulaSpillRange}">${XlsxFormula}</f><v>${exportedValue}</v>`;
73119
73232
  return { attrs, node };
73120
73233
  }
73121
73234
  function addContent(content, sharedStrings, forceString = false) {
@@ -74105,7 +74218,7 @@ function addRows(construct, data, sheet) {
74105
74218
  let cellNode = escapeXml ``;
74106
74219
  // Either formula or static value inside the cell
74107
74220
  if (content?.startsWith("=") && value !== undefined) {
74108
- const res = addFormula(content, value);
74221
+ const res = addFormula(content, value, sheet.formulaSpillRanges[xc] ?? xc);
74109
74222
  if (!res) {
74110
74223
  continue;
74111
74224
  }
@@ -74391,6 +74504,30 @@ function createWorksheets(data, construct) {
74391
74504
  `;
74392
74505
  files.push(createXMLFile(parseXML(sheetXml), `xl/worksheets/sheet${sheetIndex}.xml`, "sheet"));
74393
74506
  }
74507
+ const sheetMetadataXml = escapeXml /*xml*/ `
74508
+ <metadata xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:xda="http://schemas.microsoft.com/office/spreadsheetml/2017/dynamicarray">
74509
+ <metadataTypes count="1">
74510
+ <metadataType name="XLDAPR" minSupportedVersion="120000" copy="1" pasteAll="1"
74511
+ pasteValues="1" merge="1" splitFirst="1" rowColShift="1" clearFormats="1"
74512
+ clearComments="1" assign="1" coerce="1" cellMeta="1" />
74513
+ </metadataTypes>
74514
+ <futureMetadata name="XLDAPR" count="1">
74515
+ <bk>
74516
+ <extLst>
74517
+ <ext uri="{${ARRAY_FORMULA_URI}}">
74518
+ <xda:dynamicArrayProperties fDynamic="1" fCollapsed="0" />
74519
+ </ext>
74520
+ </extLst>
74521
+ </bk>
74522
+ </futureMetadata>
74523
+ <cellMetadata count="1">
74524
+ <bk>
74525
+ <rc t="1" v="0" />
74526
+ </bk>
74527
+ </cellMetadata>
74528
+ </metadata>
74529
+ `;
74530
+ files.push(createXMLFile(parseXML(sheetMetadataXml), "xl/metadata.xml", "metadata"));
74394
74531
  addRelsToFile(construct.relsFiles, "xl/_rels/workbook.xml.rels", {
74395
74532
  type: XLSX_RELATION_TYPE.sharedStrings,
74396
74533
  target: "sharedStrings.xml",
@@ -74399,6 +74536,10 @@ function createWorksheets(data, construct) {
74399
74536
  type: XLSX_RELATION_TYPE.styles,
74400
74537
  target: "styles.xml",
74401
74538
  });
74539
+ addRelsToFile(construct.relsFiles, "xl/_rels/workbook.xml.rels", {
74540
+ type: XLSX_RELATION_TYPE.metadata,
74541
+ target: "metadata.xml",
74542
+ });
74402
74543
  return files;
74403
74544
  }
74404
74545
  /**
@@ -75327,6 +75468,6 @@ exports.tokenColors = tokenColors;
75327
75468
  exports.tokenize = tokenize;
75328
75469
 
75329
75470
 
75330
- __info__.version = "18.1.9";
75331
- __info__.date = "2025-02-25T05:59:45.472Z";
75332
- __info__.hash = "6789c1c";
75471
+ __info__.version = "18.1.10";
75472
+ __info__.date = "2025-03-07T10:34:41.861Z";
75473
+ __info__.hash = "31e4526";