@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
  (function (exports, owl) {
@@ -6092,8 +6092,9 @@
6092
6092
  if (zone.bottom !== zone.top && zone.left != zone.right) {
6093
6093
  if (zone.right) {
6094
6094
  for (let j = zone.left; j <= zone.right; ++j) {
6095
+ const datasetOptions = j === zone.left ? dataSet : { yAxisId: dataSet.yAxisId };
6095
6096
  postProcessedRanges.push({
6096
- ...dataSet,
6097
+ ...datasetOptions,
6097
6098
  dataRange: `${sheetPrefix}${zoneToXc({
6098
6099
  left: j,
6099
6100
  right: j,
@@ -6105,8 +6106,9 @@
6105
6106
  }
6106
6107
  else {
6107
6108
  for (let j = zone.top; j <= zone.bottom; ++j) {
6109
+ const datasetOptions = j === zone.top ? dataSet : { yAxisId: dataSet.yAxisId };
6108
6110
  postProcessedRanges.push({
6109
- ...dataSet,
6111
+ ...datasetOptions,
6110
6112
  dataRange: `${sheetPrefix}${zoneToXc({
6111
6113
  left: zone.left,
6112
6114
  right: zone.right,
@@ -10057,70 +10059,341 @@ stores.inject(MyMetaStore, storeInstance);
10057
10059
  return bars.find((bar, i) => i > startIndex && bar.height !== 0);
10058
10060
  }
10059
10061
 
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,
10062
+ const GAUGE_PADDING_SIDE = 30;
10063
+ const GAUGE_PADDING_TOP = 10;
10064
+ const GAUGE_PADDING_BOTTOM = 20;
10065
+ const GAUGE_LABELS_FONT_SIZE = 12;
10066
+ const GAUGE_DEFAULT_VALUE_FONT_SIZE = 80;
10067
+ const GAUGE_BACKGROUND_COLOR = "#F3F2F1";
10068
+ const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
10069
+ const GAUGE_TITLE_SECTION_HEIGHT = 25;
10070
+ function drawGaugeChart(canvas, runtime) {
10071
+ const canvasBoundingRect = canvas.getBoundingClientRect();
10072
+ canvas.width = canvasBoundingRect.width;
10073
+ canvas.height = canvasBoundingRect.height;
10074
+ const ctx = canvas.getContext("2d");
10075
+ const config = getGaugeRenderingConfig(canvasBoundingRect, runtime, ctx);
10076
+ drawBackground(ctx, config);
10077
+ drawGauge(ctx, config);
10078
+ drawInflectionValues(ctx, config);
10079
+ drawLabels(ctx, config);
10080
+ drawTitle(ctx, config);
10081
+ }
10082
+ function drawGauge(ctx, config) {
10083
+ ctx.save();
10084
+ const gauge = config.gauge;
10085
+ const arcCenterX = gauge.rect.x + gauge.rect.width / 2;
10086
+ const arcCenterY = gauge.rect.y + gauge.rect.height;
10087
+ const arcRadius = gauge.rect.height - gauge.arcWidth / 2;
10088
+ if (arcRadius < 0) {
10089
+ return;
10090
+ }
10091
+ const gaugeAngle = gauge.percentage === 1 ? 0 : Math.PI * (1 + gauge.percentage);
10092
+ // Gauge background
10093
+ ctx.strokeStyle = GAUGE_BACKGROUND_COLOR;
10094
+ ctx.beginPath();
10095
+ ctx.lineWidth = gauge.arcWidth;
10096
+ ctx.arc(arcCenterX, arcCenterY, arcRadius, gaugeAngle, 0);
10097
+ ctx.stroke();
10098
+ // Gauge value
10099
+ ctx.strokeStyle = gauge.color;
10100
+ ctx.beginPath();
10101
+ ctx.arc(arcCenterX, arcCenterY, arcRadius, Math.PI, gaugeAngle);
10102
+ ctx.stroke();
10103
+ ctx.restore();
10104
+ }
10105
+ function drawBackground(ctx, config) {
10106
+ ctx.save();
10107
+ ctx.fillStyle = config.backgroundColor;
10108
+ ctx.fillRect(0, 0, config.width, config.height);
10109
+ ctx.restore();
10110
+ }
10111
+ function drawLabels(ctx, config) {
10112
+ for (const label of [config.minLabel, config.maxLabel, config.gaugeValue]) {
10113
+ ctx.save();
10114
+ ctx.textAlign = "center";
10115
+ ctx.fillStyle = label.color;
10116
+ ctx.font = `${label.fontSize}px ${DEFAULT_FONT}`;
10117
+ ctx.fillText(label.label, label.textPosition.x, label.textPosition.y);
10118
+ ctx.restore();
10119
+ }
10120
+ }
10121
+ function drawInflectionValues(ctx, config) {
10122
+ const { x: rectX, y: rectY, width, height } = config.gauge.rect;
10123
+ for (const inflectionValue of config.inflectionValues) {
10124
+ ctx.save();
10125
+ ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment
10126
+ ctx.rotate(Math.PI / 2 - inflectionValue.rotation);
10127
+ ctx.lineWidth = 2;
10128
+ ctx.strokeStyle = chartMutedFontColor(config.backgroundColor) + "aa";
10129
+ ctx.beginPath();
10130
+ ctx.moveTo(0, -(height - config.gauge.arcWidth));
10131
+ ctx.lineTo(0, -height - 3);
10132
+ ctx.stroke();
10133
+ ctx.textAlign = "center";
10134
+ ctx.font = `${inflectionValue.fontSize}px ${DEFAULT_FONT}`;
10135
+ ctx.fillStyle = inflectionValue.color;
10136
+ const textY = -height - GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN - inflectionValue.offset;
10137
+ ctx.fillText(inflectionValue.label, 0, textY);
10138
+ ctx.restore();
10139
+ }
10140
+ }
10141
+ function drawTitle(ctx, config) {
10142
+ ctx.save();
10143
+ const title = config.title;
10144
+ ctx.font = getDefaultContextFont(title.fontSize, title.bold, title.italic);
10145
+ ctx.textBaseline = "middle";
10146
+ ctx.fillStyle = title.color;
10147
+ ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
10148
+ ctx.restore();
10149
+ }
10150
+ function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
10151
+ const maxValue = runtime.maxValue;
10152
+ const minValue = runtime.minValue;
10153
+ const gaugeValue = runtime.gaugeValue;
10154
+ const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
10155
+ const gaugeArcWidth = gaugeRect.width / 6;
10156
+ const gaugePercentage = gaugeValue
10157
+ ? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
10158
+ : 0;
10159
+ const gaugeValuePosition = {
10160
+ x: boundingRect.width / 2,
10161
+ y: gaugeRect.y + gaugeRect.height - gaugeRect.height / 12,
10066
10162
  };
10067
- canvas = owl.useRef("graphContainer");
10068
- chart;
10069
- currentRuntime;
10070
- get background() {
10071
- return this.chartRuntime.background;
10163
+ let gaugeValueFontSize = GAUGE_DEFAULT_VALUE_FONT_SIZE;
10164
+ // Scale down the font size if the gaugeRect is too small
10165
+ if (gaugeRect.height < 300) {
10166
+ gaugeValueFontSize = gaugeValueFontSize * (gaugeRect.height / 300);
10072
10167
  }
10073
- get canvasStyle() {
10074
- return `background-color: ${this.background}`;
10168
+ // Scale down the font size if the text is too long
10169
+ const maxTextWidth = gaugeRect.width / 2;
10170
+ const gaugeLabel = gaugeValue?.label || "-";
10171
+ if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
10172
+ gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
10075
10173
  }
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;
10174
+ const minLabelPosition = {
10175
+ x: gaugeRect.x + gaugeArcWidth / 2,
10176
+ y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
10177
+ };
10178
+ const maxLabelPosition = {
10179
+ x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
10180
+ y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
10181
+ };
10182
+ const textColor = chartMutedFontColor(runtime.background);
10183
+ const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);
10184
+ let x = 0, titleWidth = 0, titleHeight = 0;
10185
+ if (runtime.title.text) {
10186
+ ({ width: titleWidth, height: titleHeight } = computeTextDimension(ctx, runtime.title.text, { fontSize: CHART_TITLE_FONT_SIZE, ...runtime.title }, "px"));
10082
10187
  }
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
- }
10188
+ switch (runtime.title.align) {
10189
+ case "right":
10190
+ x = boundingRect.width - titleWidth - CHART_PADDING$1;
10191
+ break;
10192
+ case "center":
10193
+ x = (boundingRect.width - titleWidth) / 2;
10194
+ break;
10195
+ case "left":
10196
+ default:
10197
+ x = CHART_PADDING$1;
10198
+ break;
10199
+ }
10200
+ return {
10201
+ width: boundingRect.width,
10202
+ height: boundingRect.height,
10203
+ title: {
10204
+ label: runtime.title.text ?? "",
10205
+ fontSize: runtime.title.fontSize ?? CHART_TITLE_FONT_SIZE,
10206
+ textPosition: {
10207
+ x,
10208
+ y: CHART_PADDING_TOP + titleHeight / 2,
10209
+ },
10210
+ color: runtime.title.color ?? textColor,
10211
+ bold: runtime.title.bold,
10212
+ italic: runtime.title.italic,
10213
+ },
10214
+ backgroundColor: runtime.background,
10215
+ gauge: {
10216
+ rect: gaugeRect,
10217
+ arcWidth: gaugeArcWidth,
10218
+ percentage: clip(gaugePercentage, 0, 1),
10219
+ color: getGaugeColor(runtime),
10220
+ },
10221
+ inflectionValues,
10222
+ gaugeValue: {
10223
+ label: gaugeLabel,
10224
+ textPosition: gaugeValuePosition,
10225
+ fontSize: gaugeValueFontSize,
10226
+ color: textColor,
10227
+ },
10228
+ minLabel: {
10229
+ label: runtime.minValue.label,
10230
+ textPosition: minLabelPosition,
10231
+ fontSize: GAUGE_LABELS_FONT_SIZE,
10232
+ color: textColor,
10233
+ },
10234
+ maxLabel: {
10235
+ label: runtime.maxValue.label,
10236
+ textPosition: maxLabelPosition,
10237
+ fontSize: GAUGE_LABELS_FONT_SIZE,
10238
+ color: textColor,
10239
+ },
10240
+ };
10241
+ }
10242
+ /**
10243
+ * Get the rectangle in which the gauge will be drawn, based on the bounding rectangle of the canvas and leaving
10244
+ * space for the title and labels.
10245
+ */
10246
+ function getGaugeRect(boundingRect, title) {
10247
+ const titleHeight = title ? GAUGE_TITLE_SECTION_HEIGHT : 0;
10248
+ const drawHeight = boundingRect.height - GAUGE_PADDING_BOTTOM - titleHeight - GAUGE_PADDING_TOP;
10249
+ const drawWidth = boundingRect.width - GAUGE_PADDING_SIDE * 2;
10250
+ let gaugeWidth;
10251
+ let gaugeHeight;
10252
+ if (drawWidth > 2 * drawHeight) {
10253
+ gaugeWidth = 2 * drawHeight;
10254
+ gaugeHeight = drawHeight;
10255
+ }
10256
+ else {
10257
+ gaugeWidth = drawWidth;
10258
+ gaugeHeight = drawWidth / 2;
10259
+ }
10260
+ const gaugeX = GAUGE_PADDING_SIDE + (drawWidth - gaugeWidth) / 2;
10261
+ const gaugeY = titleHeight + GAUGE_PADDING_TOP + (drawHeight - gaugeHeight) / 2;
10262
+ return {
10263
+ x: gaugeX,
10264
+ y: gaugeY,
10265
+ width: gaugeWidth,
10266
+ height: gaugeHeight,
10267
+ };
10268
+ }
10269
+ /**
10270
+ * 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).
10271
+ *
10272
+ * Also compute an offset for the text so that it doesn't overlap with other text.
10273
+ */
10274
+ function getInflectionValues(runtime, gaugeRect, textColor, ctx) {
10275
+ const maxValue = runtime.maxValue;
10276
+ const minValue = runtime.minValue;
10277
+ const gaugeCircleCenter = {
10278
+ x: gaugeRect.x + gaugeRect.width / 2,
10279
+ y: gaugeRect.y + gaugeRect.height,
10280
+ };
10281
+ const textStyle = { fontSize: GAUGE_LABELS_FONT_SIZE };
10282
+ const inflectionValues = [];
10283
+ const inflectionValuesTextRects = [];
10284
+ for (const inflectionValue of runtime.inflectionValues) {
10285
+ const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
10286
+ const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
10287
+ const angle = Math.PI - Math.PI * percentage;
10288
+ const textRect = getRectangleTangentToCircle(angle, // angle between X axis and the point where the rectangle is tangent to the circle
10289
+ gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, // radius of the gauge circle + margin below text
10290
+ gaugeCircleCenter.x, // center of the gauge circle
10291
+ gaugeCircleCenter.y, // center of the gauge circle
10292
+ labelWidth + 2, // width of the text + some margin
10293
+ GAUGE_LABELS_FONT_SIZE // height of the text
10294
+ );
10295
+ let offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect))
10296
+ ? GAUGE_LABELS_FONT_SIZE
10297
+ : 0;
10298
+ inflectionValuesTextRects.push(textRect);
10299
+ inflectionValues.push({
10300
+ rotation: angle,
10301
+ label: inflectionValue.label,
10302
+ fontSize: GAUGE_LABELS_FONT_SIZE,
10303
+ color: textColor,
10304
+ offset,
10103
10305
  });
10104
10306
  }
10105
- createChart(chartData) {
10106
- const canvas = this.canvas.el;
10107
- const ctx = canvas.getContext("2d");
10108
- this.chart = new window.Chart(ctx, chartData);
10307
+ return inflectionValues;
10308
+ }
10309
+ function getGaugeColor(runtime) {
10310
+ const gaugeValue = runtime.gaugeValue?.value;
10311
+ if (gaugeValue === undefined) {
10312
+ return GAUGE_BACKGROUND_COLOR;
10109
10313
  }
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
- }
10314
+ for (let i = 0; i < runtime.inflectionValues.length; i++) {
10315
+ const inflectionValue = runtime.inflectionValues[i];
10316
+ if (inflectionValue.operator === "<" && gaugeValue < inflectionValue.value) {
10317
+ return runtime.colors[i];
10117
10318
  }
10118
- else {
10119
- this.chart.data.datasets = [];
10319
+ else if (inflectionValue.operator === "<=" && gaugeValue <= inflectionValue.value) {
10320
+ return runtime.colors[i];
10120
10321
  }
10121
- this.chart.config.options = chartData.options;
10122
- this.chart.update();
10123
10322
  }
10323
+ return runtime.colors.at(-1);
10324
+ }
10325
+ function getSegmentsOfRectangle(rectangle) {
10326
+ return [
10327
+ { start: rectangle.topLeft, end: rectangle.topRight },
10328
+ { start: rectangle.topRight, end: rectangle.bottomRight },
10329
+ { start: rectangle.bottomRight, end: rectangle.bottomLeft },
10330
+ { start: rectangle.bottomLeft, end: rectangle.topLeft },
10331
+ ];
10332
+ }
10333
+ /**
10334
+ * Check if two segment intersect. The case where the segments are colinear (both segments on the same line)
10335
+ * is not handled.
10336
+ */
10337
+ function doSegmentIntersect(segment1, segment2) {
10338
+ const A = segment1.start;
10339
+ const B = segment1.end;
10340
+ const C = segment2.start;
10341
+ const D = segment2.end;
10342
+ /**
10343
+ * Line segment intersection algorithm
10344
+ * https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
10345
+ */
10346
+ function ccw(a, b, c) {
10347
+ return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
10348
+ }
10349
+ return ccw(A, C, D) !== ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D);
10350
+ }
10351
+ function doRectanglesIntersect(rect1, rect2) {
10352
+ const segments1 = getSegmentsOfRectangle(rect1);
10353
+ const segments2 = getSegmentsOfRectangle(rect2);
10354
+ for (const segment1 of segments1) {
10355
+ for (const segment2 of segments2) {
10356
+ if (doSegmentIntersect(segment1, segment2)) {
10357
+ return true;
10358
+ }
10359
+ }
10360
+ }
10361
+ return false;
10362
+ }
10363
+ /**
10364
+ * Get the rectangle that is tangent to a circle at a given angle.
10365
+ *
10366
+ * @param angle angle between X axis and the point where the rectangle is tangent to the circle
10367
+ */
10368
+ function getRectangleTangentToCircle(angle, radius, circleCenterX, circleCenterY, rectWidth, rectHeight) {
10369
+ const cos = Math.cos(angle);
10370
+ const sin = Math.sin(angle);
10371
+ // x, y are the distance from the center of the circle to the point where the rectangle is tangent to the circle
10372
+ const x = cos * radius;
10373
+ const y = sin * radius;
10374
+ // x2, y2 are the distance from the point the rectangle is tangent to the circle to the bottom left corner of the rectangle
10375
+ const x2 = sin * (rectWidth / 2); // cos(angle + 90°) = sin(angle)
10376
+ const y2 = cos * (rectWidth / 2);
10377
+ const bottomRight = {
10378
+ x: x + x2 + circleCenterX,
10379
+ y: circleCenterY - (y - y2),
10380
+ };
10381
+ const bottomLeft = {
10382
+ x: x - x2 + circleCenterX,
10383
+ y: circleCenterY - (y + y2),
10384
+ };
10385
+ // Same as above but for the top corners of the rectangle (radius + rectangle height instead of radius)
10386
+ const xp = cos * (radius + rectHeight);
10387
+ const yp = sin * (radius + rectHeight);
10388
+ const topLeft = {
10389
+ x: xp - x2 + circleCenterX,
10390
+ y: circleCenterY - (yp + y2),
10391
+ };
10392
+ const topRight = {
10393
+ x: xp + x2 + circleCenterX,
10394
+ y: circleCenterY - (yp - y2),
10395
+ };
10396
+ return { bottomLeft, bottomRight, topRight, topLeft };
10124
10397
  }
10125
10398
 
10126
10399
  /**
@@ -10702,6 +10975,155 @@ stores.inject(MyMetaStore, storeInstance);
10702
10975
  }
10703
10976
  }
10704
10977
 
10978
+ const CHART_COMMON_OPTIONS = {
10979
+ // https://www.chartjs.org/docs/latest/general/responsive.html
10980
+ responsive: true, // will resize when its container is resized
10981
+ maintainAspectRatio: false, // doesn't maintain the aspect ratio (width/height =2 by default) so the user has the choice of the exact layout
10982
+ elements: {
10983
+ line: {
10984
+ fill: false, // do not fill the area under line charts
10985
+ },
10986
+ point: {
10987
+ hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
10988
+ },
10989
+ },
10990
+ animation: false,
10991
+ };
10992
+ function truncateLabel(label) {
10993
+ if (!label) {
10994
+ return "";
10995
+ }
10996
+ if (label.length > MAX_CHAR_LABEL) {
10997
+ return label.substring(0, MAX_CHAR_LABEL) + "…";
10998
+ }
10999
+ return label;
11000
+ }
11001
+ function chartToImage(runtime, figure, type) {
11002
+ // wrap the canvas in a div with a fixed size because chart.js would
11003
+ // fill the whole page otherwise
11004
+ const div = document.createElement("div");
11005
+ div.style.width = `${figure.width}px`;
11006
+ div.style.height = `${figure.height}px`;
11007
+ const canvas = document.createElement("canvas");
11008
+ div.append(canvas);
11009
+ canvas.setAttribute("width", figure.width.toString());
11010
+ canvas.setAttribute("height", figure.height.toString());
11011
+ // we have to add the canvas to the DOM otherwise it won't be rendered
11012
+ document.body.append(div);
11013
+ if ("chartJsConfig" in runtime) {
11014
+ const config = deepCopy(runtime.chartJsConfig);
11015
+ config.plugins = [backgroundColorChartJSPlugin];
11016
+ const Chart = getChartJSConstructor();
11017
+ const chart = new Chart(canvas, config);
11018
+ const imgContent = chart.toBase64Image();
11019
+ chart.destroy();
11020
+ div.remove();
11021
+ return imgContent;
11022
+ }
11023
+ else if (type === "scorecard") {
11024
+ const design = getScorecardConfiguration(figure, runtime);
11025
+ drawScoreChart(design, canvas);
11026
+ const imgContent = canvas.toDataURL();
11027
+ div.remove();
11028
+ return imgContent;
11029
+ }
11030
+ else if (type === "gauge") {
11031
+ drawGaugeChart(canvas, runtime);
11032
+ const imgContent = canvas.toDataURL();
11033
+ div.remove();
11034
+ return imgContent;
11035
+ }
11036
+ return undefined;
11037
+ }
11038
+ /**
11039
+ * Custom chart.js plugin to set the background color of the canvas
11040
+ * https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md
11041
+ */
11042
+ const backgroundColorChartJSPlugin = {
11043
+ id: "customCanvasBackgroundColor",
11044
+ beforeDraw: (chart) => {
11045
+ const { ctx } = chart;
11046
+ ctx.save();
11047
+ ctx.globalCompositeOperation = "destination-over";
11048
+ ctx.fillStyle = "#ffffff";
11049
+ ctx.fillRect(0, 0, chart.width, chart.height);
11050
+ ctx.restore();
11051
+ },
11052
+ };
11053
+ /** Return window.Chart, making sure all our extensions are loaded in ChartJS */
11054
+ function getChartJSConstructor() {
11055
+ if (window.Chart && !window.Chart?.registry.plugins.get("chartShowValuesPlugin")) {
11056
+ window.Chart.register(chartShowValuesPlugin);
11057
+ window.Chart.register(waterfallLinesPlugin);
11058
+ }
11059
+ return window.Chart;
11060
+ }
11061
+
11062
+ class ChartJsComponent extends owl.Component {
11063
+ static template = "o-spreadsheet-ChartJsComponent";
11064
+ static props = {
11065
+ figure: Object,
11066
+ };
11067
+ canvas = owl.useRef("graphContainer");
11068
+ chart;
11069
+ currentRuntime;
11070
+ get background() {
11071
+ return this.chartRuntime.background;
11072
+ }
11073
+ get canvasStyle() {
11074
+ return `background-color: ${this.background}`;
11075
+ }
11076
+ get chartRuntime() {
11077
+ const runtime = this.env.model.getters.getChartRuntime(this.props.figure.id);
11078
+ if (!("chartJsConfig" in runtime)) {
11079
+ throw new Error("Unsupported chart runtime");
11080
+ }
11081
+ return runtime;
11082
+ }
11083
+ setup() {
11084
+ owl.onMounted(() => {
11085
+ const runtime = this.chartRuntime;
11086
+ this.currentRuntime = runtime;
11087
+ // Note: chartJS modify the runtime in place, so it's important to give it a copy
11088
+ this.createChart(deepCopy(runtime.chartJsConfig));
11089
+ });
11090
+ owl.onWillUnmount(() => this.chart?.destroy());
11091
+ owl.useEffect(() => {
11092
+ const runtime = this.chartRuntime;
11093
+ if (runtime !== this.currentRuntime) {
11094
+ if (runtime.chartJsConfig.type !== this.currentRuntime.chartJsConfig.type) {
11095
+ this.chart?.destroy();
11096
+ this.createChart(deepCopy(runtime.chartJsConfig));
11097
+ }
11098
+ else {
11099
+ this.updateChartJs(deepCopy(runtime));
11100
+ }
11101
+ this.currentRuntime = runtime;
11102
+ }
11103
+ });
11104
+ }
11105
+ createChart(chartData) {
11106
+ const canvas = this.canvas.el;
11107
+ const ctx = canvas.getContext("2d");
11108
+ const Chart = getChartJSConstructor();
11109
+ this.chart = new Chart(ctx, chartData);
11110
+ }
11111
+ updateChartJs(chartRuntime) {
11112
+ const chartData = chartRuntime.chartJsConfig;
11113
+ if (chartData.data && chartData.data.datasets) {
11114
+ this.chart.data = chartData.data;
11115
+ if (chartData.options?.plugins?.title) {
11116
+ this.chart.config.options.plugins.title = chartData.options.plugins.title;
11117
+ }
11118
+ }
11119
+ else {
11120
+ this.chart.data.datasets = [];
11121
+ }
11122
+ this.chart.config.options = chartData.options;
11123
+ this.chart.update();
11124
+ }
11125
+ }
11126
+
10705
11127
  class ScorecardChart extends owl.Component {
10706
11128
  static template = "o-spreadsheet-ScorecardChart";
10707
11129
  static props = {
@@ -22154,7 +22576,7 @@ stores.inject(MyMetaStore, storeInstance);
22154
22576
  condition: (cell) => !cell.isFormula &&
22155
22577
  evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.text &&
22156
22578
  alphaNumericValueRegExp.test(cell.content),
22157
- generateRule: (cell, cells) => {
22579
+ generateRule: (cell, cells, direction) => {
22158
22580
  const numberPostfix = parseInt(cell.content.match(numberPostfixRegExp)[0]);
22159
22581
  const prefix = cell.content.match(stringPrefixRegExp)[0];
22160
22582
  const numberPostfixLength = cell.content.length - prefix.length;
@@ -22162,7 +22584,10 @@ stores.inject(MyMetaStore, storeInstance);
22162
22584
  alphaNumericValueRegExp.test(evaluatedCell.value)) // get consecutive alphanumeric cells, no matter what the prefix is
22163
22585
  .filter((cell) => prefix === (cell.value ?? "").toString().match(stringPrefixRegExp)[0])
22164
22586
  .map((cell) => parseInt((cell.value ?? "").toString().match(numberPostfixRegExp)[0]));
22165
- const increment = calculateIncrementBasedOnGroup(group);
22587
+ let increment = calculateIncrementBasedOnGroup(group);
22588
+ if (["up", "left"].includes(direction) && group.length === 1) {
22589
+ increment = -increment;
22590
+ }
22166
22591
  return {
22167
22592
  type: "ALPHANUMERIC_INCREMENT_MODIFIER",
22168
22593
  prefix,
@@ -22225,10 +22650,13 @@ stores.inject(MyMetaStore, storeInstance);
22225
22650
  .add("increment_number", {
22226
22651
  condition: (cell) => !cell.isFormula &&
22227
22652
  evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.number,
22228
- generateRule: (cell, cells) => {
22653
+ generateRule: (cell, cells, direction) => {
22229
22654
  const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number &&
22230
22655
  !isDateTimeFormat(evaluatedCell.format || "")).map((cell) => Number(cell.value));
22231
- const increment = calculateIncrementBasedOnGroup(group);
22656
+ let increment = calculateIncrementBasedOnGroup(group);
22657
+ if (["up", "left"].includes(direction) && group.length === 1) {
22658
+ increment = -increment;
22659
+ }
22232
22660
  const evaluation = evaluateLiteral(cell, { locale: DEFAULT_LOCALE });
22233
22661
  return {
22234
22662
  type: "INCREMENT_MODIFIER",
@@ -22272,343 +22700,6 @@ stores.inject(MyMetaStore, storeInstance);
22272
22700
 
22273
22701
  const cellPopoverRegistry = new Registry();
22274
22702
 
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
22703
  class GaugeChartComponent extends owl.Component {
22613
22704
  static template = "o-spreadsheet-GaugeChartComponent";
22614
22705
  canvas = owl.useRef("chartContainer");
@@ -22641,81 +22732,6 @@ stores.inject(MyMetaStore, storeInstance);
22641
22732
  return color;
22642
22733
  }
22643
22734
 
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
22735
  /**
22720
22736
  * Represent a raw XML string
22721
22737
  */
@@ -22777,6 +22793,7 @@ stores.inject(MyMetaStore, storeInstance);
22777
22793
  const CONTENT_TYPES = {
22778
22794
  workbook: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml",
22779
22795
  sheet: "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml",
22796
+ metadata: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheetMetadata+xml",
22780
22797
  sharedStrings: "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml",
22781
22798
  styles: "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml",
22782
22799
  drawing: "application/vnd.openxmlformats-officedocument.drawing+xml",
@@ -22789,6 +22806,7 @@ stores.inject(MyMetaStore, storeInstance);
22789
22806
  const XLSX_RELATION_TYPE = {
22790
22807
  document: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument",
22791
22808
  sheet: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet",
22809
+ metadata: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sheetMetadata",
22792
22810
  sharedStrings: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings",
22793
22811
  styles: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles",
22794
22812
  drawing: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing",
@@ -22798,6 +22816,7 @@ stores.inject(MyMetaStore, storeInstance);
22798
22816
  hyperlink: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
22799
22817
  image: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
22800
22818
  };
22819
+ const ARRAY_FORMULA_URI = "bdbb8cdc-fa1e-496e-a857-3c3f30c029c3";
22801
22820
  const RELATIONSHIP_NSR = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
22802
22821
  const HEIGHT_FACTOR = 0.75; // 100px => 75 u
22803
22822
  /**
@@ -25363,29 +25382,34 @@ stores.inject(MyMetaStore, storeInstance);
25363
25382
  * In all the sheets, replace the table-only references in the formula cells with standard references.
25364
25383
  */
25365
25384
  function convertTableFormulaReferences(convertedSheets, xlsxSheets) {
25366
- for (let sheet of convertedSheets) {
25367
- const tables = xlsxSheets.find((s) => s.sheetName === sheet.name).tables;
25385
+ for (let tableSheet of convertedSheets) {
25386
+ const tables = xlsxSheets.find((s) => s.sheetName === tableSheet.name).tables;
25368
25387
  for (let table of tables) {
25369
25388
  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);
25389
+ for (let sheet of convertedSheets) {
25390
+ for (let xc in sheet.cells) {
25391
+ const cell = sheet.cells[xc];
25392
+ let cellContent = sheet.cells[xc];
25393
+ if (cell && cellContent && cellContent.startsWith("=")) {
25394
+ let refIndex;
25395
+ while ((refIndex = cellContent.indexOf(tabRef)) !== -1) {
25396
+ let endIndex = refIndex + tabRef.length;
25397
+ let openBrackets = 1;
25398
+ while (openBrackets > 0 && endIndex < cellContent.length) {
25399
+ if (cellContent[endIndex] === "[") {
25400
+ openBrackets++;
25401
+ }
25402
+ else if (cellContent[endIndex] === "]") {
25403
+ openBrackets--;
25404
+ }
25405
+ endIndex++;
25406
+ }
25407
+ let reference = cellContent.slice(refIndex + tabRef.length, endIndex - 1);
25408
+ const sheetPrefix = tableSheet.id === sheet.id ? "" : tableSheet.name + "!";
25409
+ const convertedRef = convertTableReference(sheetPrefix, reference, table, xc);
25410
+ cellContent =
25411
+ cellContent.slice(0, refIndex) + convertedRef + cellContent.slice(endIndex);
25382
25412
  }
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
25413
  }
25390
25414
  sheet.cells[xc] = cellContent;
25391
25415
  }
@@ -25394,11 +25418,17 @@ stores.inject(MyMetaStore, storeInstance);
25394
25418
  }
25395
25419
  }
25396
25420
  /**
25397
- * Convert table-specific references in formulas into standard references.
25421
+ * Convert table-specific references in formulas into standard references. A table reference is composed of columns names,
25422
+ * and of keywords determining the rows of the table to reference.
25398
25423
  *
25399
25424
  * A reference in a table can have the form (only the part between brackets should be given to this function):
25400
25425
  * - tableName[colName] : reference to the whole column "colName"
25426
+ * - tableName[#keyword] : reference to the whatever row the keyword refers to
25401
25427
  * - tableName[[#keyword], [colName]] : reference to some of the element(s) of the column colName
25428
+ * - tableName[[#keyword], [colName]:[col2Name]] : reference to some of the element(s) of the columns colName to col2Name
25429
+ * - tableName[[#keyword1], [#keyword2], [colName]] : reference to all the rows referenced by the keywords in the column colName
25430
+ * - tableName[[#keyword1], [colName], [#keyword2]]: the keywords and colName can be in any order
25431
+ *
25402
25432
  *
25403
25433
  * The available keywords are :
25404
25434
  * - #All : all the column (including totals)
@@ -25406,58 +25436,109 @@ stores.inject(MyMetaStore, storeInstance);
25406
25436
  * - #Headers : only the header of the column
25407
25437
  * - #Totals : only the totals of the column
25408
25438
  * - #This Row : only the element in the same row as the cell
25439
+ *
25440
+ * Note that the only valid combination of multiple keywords are #Data + #Totals and #Headers + #Data.
25409
25441
  */
25410
- function convertTableReference(expr, table, cellXc) {
25411
- const refElements = expr.split(",");
25442
+ function convertTableReference(sheetPrefix, expr, table, cellXc) {
25443
+ // TODO: Ideally we'd want to make a real tokenizer, this simple approach won't work if for example the column name
25444
+ // contain # or , characters. But that's probably an edge case that we can ignore for now.
25445
+ const parts = expr.split(",").map((part) => part.trim());
25412
25446
  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;
25447
+ const colIndexes = [];
25448
+ const rowIndexes = [];
25449
+ const foundKeywords = [];
25450
+ for (const part of parts) {
25451
+ if (removeBrackets(part).startsWith("#")) {
25452
+ const keyWord = removeBrackets(part);
25453
+ foundKeywords.push(keyWord);
25454
+ switch (keyWord) {
25455
+ case "#All":
25456
+ rowIndexes.push(tableZone.top, tableZone.bottom);
25457
+ break;
25458
+ case "#Data":
25459
+ const top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
25460
+ const bottom = table.totalsRowCount
25461
+ ? tableZone.bottom - table.totalsRowCount
25462
+ : tableZone.bottom;
25463
+ rowIndexes.push(top, bottom);
25464
+ break;
25465
+ case "#This Row":
25466
+ rowIndexes.push(toCartesian(cellXc).row);
25467
+ break;
25468
+ case "#Headers":
25469
+ if (!table.headerRowCount) {
25470
+ return CellErrorType.InvalidReference;
25471
+ }
25472
+ rowIndexes.push(tableZone.top);
25473
+ break;
25474
+ case "#Totals":
25475
+ if (!table.totalsRowCount) {
25476
+ return CellErrorType.InvalidReference;
25477
+ }
25478
+ rowIndexes.push(tableZone.bottom);
25479
+ break;
25480
+ }
25424
25481
  }
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;
25482
+ else {
25483
+ const columns = part
25484
+ .split(":")
25485
+ .map((part) => part.trim())
25486
+ .map(removeBrackets);
25487
+ if (colIndexes.length) {
25488
+ return CellErrorType.InvalidReference;
25489
+ }
25490
+ const colRelativeIndex = table.cols.findIndex((col) => col.name === columns[0]);
25491
+ if (colRelativeIndex === -1) {
25492
+ return CellErrorType.InvalidReference;
25493
+ }
25494
+ colIndexes.push(colRelativeIndex + tableZone.left);
25495
+ if (columns[1]) {
25496
+ const colRelativeIndex2 = table.cols.findIndex((col) => col.name === columns[1]);
25497
+ if (colRelativeIndex2 === -1) {
25498
+ return CellErrorType.InvalidReference;
25450
25499
  }
25451
- break;
25500
+ colIndexes.push(colRelativeIndex2 + tableZone.left);
25501
+ }
25452
25502
  }
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
25503
  }
25457
- if (!isReferencedZoneValid) {
25504
+ if (!areKeywordsCompatible(foundKeywords)) {
25458
25505
  return CellErrorType.InvalidReference;
25459
25506
  }
25460
- return refZone.top !== refZone.bottom ? zoneToXc(refZone) : toXC(refZone.left, refZone.top);
25507
+ if (rowIndexes.length === 0) {
25508
+ const top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
25509
+ const bottom = table.totalsRowCount
25510
+ ? tableZone.bottom - table.totalsRowCount
25511
+ : tableZone.bottom;
25512
+ rowIndexes.push(top, bottom);
25513
+ }
25514
+ if (colIndexes.length === 0) {
25515
+ colIndexes.push(tableZone.left, tableZone.right);
25516
+ }
25517
+ const refZone = {
25518
+ top: Math.min(...rowIndexes),
25519
+ left: Math.min(...colIndexes),
25520
+ bottom: Math.max(...rowIndexes),
25521
+ right: Math.max(...colIndexes),
25522
+ };
25523
+ return sheetPrefix + zoneToXc(refZone);
25524
+ }
25525
+ function removeBrackets(str) {
25526
+ return str.startsWith("[") && str.endsWith("]") ? str.slice(1, str.length - 1) : str;
25527
+ }
25528
+ function areKeywordsCompatible(keywords) {
25529
+ if (keywords.length < 2) {
25530
+ return true;
25531
+ }
25532
+ else if (keywords.length > 2) {
25533
+ return false;
25534
+ }
25535
+ else if (keywords.includes("#Data") && keywords.includes("#Totals")) {
25536
+ return true;
25537
+ }
25538
+ else if (keywords.includes("#Headers") && keywords.includes("#Data")) {
25539
+ return true;
25540
+ }
25541
+ return false;
25461
25542
  }
25462
25543
 
25463
25544
  // -------------------------------------
@@ -28625,11 +28706,12 @@ stores.inject(MyMetaStore, storeInstance);
28625
28706
  }
28626
28707
  let missingTimeAdapterAlreadyWarned = false;
28627
28708
  function isLuxonTimeAdapterInstalled() {
28628
- if (!window.Chart) {
28709
+ const Chart = getChartJSConstructor();
28710
+ if (!Chart) {
28629
28711
  return false;
28630
28712
  }
28631
28713
  // @ts-ignore
28632
- const adapter = new window.Chart._adapters._date({});
28714
+ const adapter = new Chart._adapters._date({});
28633
28715
  const isInstalled = adapter._id === "luxon";
28634
28716
  if (!isInstalled && !missingTimeAdapterAlreadyWarned) {
28635
28717
  missingTimeAdapterAlreadyWarned = true;
@@ -32326,10 +32408,6 @@ stores.inject(MyMetaStore, storeInstance);
32326
32408
  this.currentDisplayValue = newDisplay;
32327
32409
  if (!anchor)
32328
32410
  return;
32329
- el.style.top = "";
32330
- el.style.left = "";
32331
- el.style["max-height"] = "";
32332
- el.style["max-width"] = "";
32333
32411
  const propsMaxSize = { width: this.props.maxWidth, height: this.props.maxHeight };
32334
32412
  let elDims = {
32335
32413
  width: el.getBoundingClientRect().width,
@@ -33867,6 +33945,7 @@ stores.inject(MyMetaStore, storeInstance);
33867
33945
  drawScoreChart: drawScoreChart,
33868
33946
  formatChartDatasetValue: formatChartDatasetValue,
33869
33947
  formatTickValue: formatTickValue,
33948
+ getChartJSConstructor: getChartJSConstructor,
33870
33949
  getChartPositionAtCenterOfViewport: getChartPositionAtCenterOfViewport,
33871
33950
  getDefinedAxis: getDefinedAxis,
33872
33951
  getPieColors: getPieColors,
@@ -37750,6 +37829,9 @@ stores.inject(MyMetaStore, storeInstance);
37750
37829
  this.state.datasetDispatchResult = this.props.updateChart(this.props.figureId, {
37751
37830
  dataSets: this.dataSeriesRanges,
37752
37831
  });
37832
+ if (this.state.datasetDispatchResult.isSuccessful) {
37833
+ this.dataSeriesRanges = this.env.model.getters.getChartDefinition(this.props.figureId).dataSets;
37834
+ }
37753
37835
  }
37754
37836
  getDataSeriesRanges() {
37755
37837
  return this.dataSeriesRanges;
@@ -40389,8 +40471,7 @@ stores.inject(MyMetaStore, storeInstance);
40389
40471
  }
40390
40472
 
40391
40473
  .o-composer-assistant {
40392
- position: absolute;
40393
- margin: 1px 4px;
40474
+ margin-top: 1px;
40394
40475
 
40395
40476
  .o-semi-bold {
40396
40477
  /* FIXME: to remove in favor of Bootstrap
@@ -40441,10 +40522,11 @@ stores.inject(MyMetaStore, storeInstance);
40441
40522
  });
40442
40523
  compositionActive = false;
40443
40524
  spreadsheetRect = useSpreadsheetRect();
40444
- get assistantStyle() {
40525
+ get assistantStyleProperties() {
40445
40526
  const composerRect = this.composerRef.el.getBoundingClientRect();
40446
40527
  const assistantStyle = {};
40447
- assistantStyle["min-width"] = `${this.props.rect?.width || ASSISTANT_WIDTH}px`;
40528
+ const minWidth = Math.min(this.props.rect?.width || Infinity, ASSISTANT_WIDTH);
40529
+ assistantStyle["min-width"] = `${minWidth}px`;
40448
40530
  const proposals = this.autoCompleteState.provider?.proposals;
40449
40531
  const proposalsHaveDescription = proposals?.some((proposal) => proposal.description);
40450
40532
  if (this.functionDescriptionState.showDescription || proposalsHaveDescription) {
@@ -40468,13 +40550,29 @@ stores.inject(MyMetaStore, storeInstance);
40468
40550
  }
40469
40551
  }
40470
40552
  else {
40471
- assistantStyle["max-height"] = `${this.spreadsheetRect.height - composerRect.bottom}px`;
40553
+ assistantStyle["max-height"] = `${this.spreadsheetRect.height - composerRect.bottom - 1}px`; // -1: margin
40472
40554
  if (composerRect.left + ASSISTANT_WIDTH + SCROLLBAR_WIDTH + CLOSE_ICON_RADIUS >
40473
40555
  this.spreadsheetRect.width) {
40474
40556
  assistantStyle.right = `${CLOSE_ICON_RADIUS}px`;
40475
40557
  }
40476
40558
  }
40477
- return cssPropertiesToCss(assistantStyle);
40559
+ return assistantStyle;
40560
+ }
40561
+ get assistantStyle() {
40562
+ const allProperties = this.assistantStyleProperties;
40563
+ return cssPropertiesToCss({
40564
+ "max-height": allProperties["max-height"],
40565
+ width: allProperties["width"],
40566
+ "min-width": allProperties["min-width"],
40567
+ });
40568
+ }
40569
+ get assistantContainerStyle() {
40570
+ const allProperties = this.assistantStyleProperties;
40571
+ return cssPropertiesToCss({
40572
+ top: allProperties["top"],
40573
+ right: allProperties["right"],
40574
+ transform: allProperties["transform"],
40575
+ });
40478
40576
  }
40479
40577
  // we can't allow input events to be triggered while we remove and add back the content of the composer in processContent
40480
40578
  shouldProcessInputEvents = false;
@@ -46411,9 +46509,7 @@ stores.inject(MyMetaStore, storeInstance);
46411
46509
  pivot: this.draft,
46412
46510
  });
46413
46511
  this.draft = null;
46414
- if (!this.alreadyNotified &&
46415
- !this.isDynamicPivotInViewport() &&
46416
- this.isStaticPivotInViewport()) {
46512
+ if (!this.alreadyNotified && this.isUpdatedPivotVisibleInViewportOnlyAsStaticPivot()) {
46417
46513
  const formulaId = this.getters.getPivotFormulaId(this.pivotId);
46418
46514
  const pivotExample = `=PIVOT(${formulaId})`;
46419
46515
  this.alreadyNotified = true;
@@ -46469,29 +46565,33 @@ stores.inject(MyMetaStore, storeInstance);
46469
46565
  this.applyUpdate();
46470
46566
  }
46471
46567
  }
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() {
46568
+ /**
46569
+ * @returns true if the updated pivot is visible in the viewport only as a
46570
+ * static pivot and not as a dynamic pivot
46571
+ */
46572
+ isUpdatedPivotVisibleInViewportOnlyAsStaticPivot() {
46573
+ let staticPivotCount = 0;
46574
+ const updatedPivotFormulaId = this.getters.getPivotFormulaId(this.pivotId);
46485
46575
  for (const position of this.getters.getVisibleCellPositions()) {
46486
46576
  const cell = this.getters.getCell(position);
46487
46577
  if (cell?.isFormula) {
46488
46578
  const pivotFunction = getFirstPivotFunction(cell.compiledFormula.tokens);
46489
- if (pivotFunction && pivotFunction.functionName !== "PIVOT") {
46490
- return true;
46579
+ const pivotFormulaId = pivotFunction?.args[0]?.value;
46580
+ if (pivotFunction && updatedPivotFormulaId === pivotFormulaId.toString()) {
46581
+ if (pivotFunction.functionName === "PIVOT") {
46582
+ // if we have at least one dynamic pivot visible inserted the viewport
46583
+ // we return false
46584
+ return false;
46585
+ }
46586
+ else {
46587
+ staticPivotCount++;
46588
+ }
46491
46589
  }
46492
46590
  }
46493
46591
  }
46494
- return false;
46592
+ // we return true if there are only static pivots visible inserted the viewport,
46593
+ // otherwise false
46594
+ return staticPivotCount > 0;
46495
46595
  }
46496
46596
  addDefaultDateTimeGranularity(fields, definition) {
46497
46597
  const { columns, rows } = definition;
@@ -60331,6 +60431,7 @@ stores.inject(MyMetaStore, storeInstance);
60331
60431
  exportForExcel(data) {
60332
60432
  for (const sheet of data.sheets) {
60333
60433
  sheet.cellValues = {};
60434
+ sheet.formulaSpillRanges = {};
60334
60435
  }
60335
60436
  for (const position of this.evaluator.getEvaluatedPositions()) {
60336
60437
  const evaluatedCell = this.evaluator.getEvaluatedCell(position);
@@ -60342,8 +60443,9 @@ stores.inject(MyMetaStore, storeInstance);
60342
60443
  const exportedSheetData = data.sheets.find((sheet) => sheet.id === position.sheetId);
60343
60444
  const formulaCell = this.getCorrespondingFormulaCell(position);
60344
60445
  if (formulaCell) {
60446
+ const cell = this.getters.getCell(position);
60345
60447
  isExported = isExportableToExcel(formulaCell.compiledFormula.tokens);
60346
- isFormula = isExported;
60448
+ isFormula = isExported && cell?.content === formulaCell.content;
60347
60449
  // If the cell contains a non-exported formula and that is evaluates to
60348
60450
  // nothing* ,we don't export it.
60349
60451
  // * non-falsy value are relevant and so are 0 and FALSE, which only leaves
@@ -60366,7 +60468,11 @@ stores.inject(MyMetaStore, storeInstance);
60366
60468
  content = !isExported ? newContent : exportedCellData;
60367
60469
  }
60368
60470
  exportedSheetData.cells[xc] = content;
60369
- exportedSheetData.cellValues[xc] = value;
60471
+ exportedSheetData.cellValues[xc] = evaluatedCell.type !== "error" ? value : undefined;
60472
+ const spillZone = this.getSpreadZone(position);
60473
+ if (spillZone) {
60474
+ exportedSheetData.formulaSpillRanges[xc] = this.getters.getRangeString(this.getters.getRangeFromZone(position.sheetId, spillZone), position.sheetId);
60475
+ }
60370
60476
  }
60371
60477
  }
60372
60478
  /**
@@ -62573,7 +62679,7 @@ stores.inject(MyMetaStore, storeInstance);
62573
62679
  getRule(cell, cells) {
62574
62680
  const rules = autofillRulesRegistry.getAll().sort((a, b) => a.sequence - b.sequence);
62575
62681
  const rule = rules.find((rule) => rule.condition(cell, cells));
62576
- return rule && rule.generateRule(cell, cells);
62682
+ return rule && this.direction && rule.generateRule(cell, cells, this.direction);
62577
62683
  }
62578
62684
  /**
62579
62685
  * Create the generator to be able to autofill the next cells.
@@ -73103,7 +73209,7 @@ stores.inject(MyMetaStore, storeInstance);
73103
73209
  `;
73104
73210
  }
73105
73211
 
73106
- function addFormula(formula, value) {
73212
+ function addFormula(formula, value, formulaSpillRange) {
73107
73213
  if (!formula) {
73108
73214
  return { attrs: [], node: escapeXml `` };
73109
73215
  }
@@ -73111,10 +73217,17 @@ stores.inject(MyMetaStore, storeInstance);
73111
73217
  if (type === undefined) {
73112
73218
  return { attrs: [], node: escapeXml `` };
73113
73219
  }
73114
- const attrs = [["t", type]];
73220
+ const attrs = [
73221
+ ["cm", "1"],
73222
+ ["t", type],
73223
+ ];
73115
73224
  const XlsxFormula = adaptFormulaToExcel(formula);
73116
73225
  const exportedValue = adaptFormulaValueToExcel(value);
73117
- const node = escapeXml /*xml*/ `<f>${XlsxFormula}</f><v>${exportedValue}</v>`;
73226
+ // We treat all formulas as array formulas (a simple formula
73227
+ // is an array formula that spills on only one cell) to avoid
73228
+ // trying to detect spilling sub-formulas which is not a trivial task.
73229
+ let node;
73230
+ node = escapeXml /*xml*/ `<f t="array" ref="${formulaSpillRange}">${XlsxFormula}</f><v>${exportedValue}</v>`;
73118
73231
  return { attrs, node };
73119
73232
  }
73120
73233
  function addContent(content, sharedStrings, forceString = false) {
@@ -74104,7 +74217,7 @@ stores.inject(MyMetaStore, storeInstance);
74104
74217
  let cellNode = escapeXml ``;
74105
74218
  // Either formula or static value inside the cell
74106
74219
  if (content?.startsWith("=") && value !== undefined) {
74107
- const res = addFormula(content, value);
74220
+ const res = addFormula(content, value, sheet.formulaSpillRanges[xc] ?? xc);
74108
74221
  if (!res) {
74109
74222
  continue;
74110
74223
  }
@@ -74390,6 +74503,30 @@ stores.inject(MyMetaStore, storeInstance);
74390
74503
  `;
74391
74504
  files.push(createXMLFile(parseXML(sheetXml), `xl/worksheets/sheet${sheetIndex}.xml`, "sheet"));
74392
74505
  }
74506
+ const sheetMetadataXml = escapeXml /*xml*/ `
74507
+ <metadata xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:xda="http://schemas.microsoft.com/office/spreadsheetml/2017/dynamicarray">
74508
+ <metadataTypes count="1">
74509
+ <metadataType name="XLDAPR" minSupportedVersion="120000" copy="1" pasteAll="1"
74510
+ pasteValues="1" merge="1" splitFirst="1" rowColShift="1" clearFormats="1"
74511
+ clearComments="1" assign="1" coerce="1" cellMeta="1" />
74512
+ </metadataTypes>
74513
+ <futureMetadata name="XLDAPR" count="1">
74514
+ <bk>
74515
+ <extLst>
74516
+ <ext uri="{${ARRAY_FORMULA_URI}}">
74517
+ <xda:dynamicArrayProperties fDynamic="1" fCollapsed="0" />
74518
+ </ext>
74519
+ </extLst>
74520
+ </bk>
74521
+ </futureMetadata>
74522
+ <cellMetadata count="1">
74523
+ <bk>
74524
+ <rc t="1" v="0" />
74525
+ </bk>
74526
+ </cellMetadata>
74527
+ </metadata>
74528
+ `;
74529
+ files.push(createXMLFile(parseXML(sheetMetadataXml), "xl/metadata.xml", "metadata"));
74393
74530
  addRelsToFile(construct.relsFiles, "xl/_rels/workbook.xml.rels", {
74394
74531
  type: XLSX_RELATION_TYPE.sharedStrings,
74395
74532
  target: "sharedStrings.xml",
@@ -74398,6 +74535,10 @@ stores.inject(MyMetaStore, storeInstance);
74398
74535
  type: XLSX_RELATION_TYPE.styles,
74399
74536
  target: "styles.xml",
74400
74537
  });
74538
+ addRelsToFile(construct.relsFiles, "xl/_rels/workbook.xml.rels", {
74539
+ type: XLSX_RELATION_TYPE.metadata,
74540
+ target: "metadata.xml",
74541
+ });
74401
74542
  return files;
74402
74543
  }
74403
74544
  /**
@@ -75326,9 +75467,9 @@ stores.inject(MyMetaStore, storeInstance);
75326
75467
  exports.tokenize = tokenize;
75327
75468
 
75328
75469
 
75329
- __info__.version = "18.1.9";
75330
- __info__.date = "2025-02-25T05:59:45.472Z";
75331
- __info__.hash = "6789c1c";
75470
+ __info__.version = "18.1.10";
75471
+ __info__.date = "2025-03-07T10:34:41.861Z";
75472
+ __info__.hash = "31e4526";
75332
75473
 
75333
75474
 
75334
75475
  })(this.o_spreadsheet = this.o_spreadsheet || {}, owl);