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