@odoo/o-spreadsheet 18.2.1 → 18.2.2

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.2.1
6
- * @date 2025-02-25T06:03:13.262Z
7
- * @hash 3b4b5c9
5
+ * @version 18.2.2
6
+ * @date 2025-03-07T10:41:04.411Z
7
+ * @hash f567932
8
8
  */
9
9
 
10
10
  import { useEnv, useSubEnv, onWillUnmount, useComponent, status, Component, useRef, onMounted, useEffect, App, blockDom, useState, onPatched, onWillPatch, onWillUpdateProps, useExternalListener, onWillStart, xml, useChildSubEnv, markRaw, toRaw } from '@odoo/owl';
@@ -6100,8 +6100,9 @@ function spreadRange(getters, dataSets) {
6100
6100
  if (zone.bottom !== zone.top && zone.left != zone.right) {
6101
6101
  if (zone.right) {
6102
6102
  for (let j = zone.left; j <= zone.right; ++j) {
6103
+ const datasetOptions = j === zone.left ? dataSet : { yAxisId: dataSet.yAxisId };
6103
6104
  postProcessedRanges.push({
6104
- ...dataSet,
6105
+ ...datasetOptions,
6105
6106
  dataRange: `${sheetPrefix}${zoneToXc({
6106
6107
  left: j,
6107
6108
  right: j,
@@ -6113,8 +6114,9 @@ function spreadRange(getters, dataSets) {
6113
6114
  }
6114
6115
  else {
6115
6116
  for (let j = zone.top; j <= zone.bottom; ++j) {
6117
+ const datasetOptions = j === zone.top ? dataSet : { yAxisId: dataSet.yAxisId };
6116
6118
  postProcessedRanges.push({
6117
- ...dataSet,
6119
+ ...datasetOptions,
6118
6120
  dataRange: `${sheetPrefix}${zoneToXc({
6119
6121
  left: zone.left,
6120
6122
  right: zone.right,
@@ -9532,150 +9534,6 @@ class ComposerFocusStore extends SpreadsheetStore {
9532
9534
  }
9533
9535
  }
9534
9536
 
9535
- /**
9536
- * This file is largely inspired by owl 1.
9537
- * `css` tag has been removed from owl 2 without workaround to manage css.
9538
- * So, the solution was to import the behavior of owl 1 directly in our
9539
- * codebase, with one difference: the css is added to the sheet as soon as the
9540
- * css tag is executed. In owl 1, the css was added as soon as a Component was
9541
- * created for the first time.
9542
- */
9543
- const STYLESHEETS = {};
9544
- let nextId = 0;
9545
- /**
9546
- * CSS tag helper for defining inline stylesheets. With this, one can simply define
9547
- * an inline stylesheet with just the following code:
9548
- * ```js
9549
- * css`.component-a { color: red; }`;
9550
- * ```
9551
- */
9552
- function css(strings, ...args) {
9553
- const name = `__sheet__${nextId++}`;
9554
- const value = String.raw(strings, ...args);
9555
- registerSheet(name, value);
9556
- activateSheet(name);
9557
- return name;
9558
- }
9559
- function processSheet(str) {
9560
- const tokens = str.split(/(\{|\}|;)/).map((s) => s.trim());
9561
- const selectorStack = [];
9562
- const parts = [];
9563
- let rules = [];
9564
- function generateSelector(stackIndex, parentSelector) {
9565
- const parts = [];
9566
- for (const selector of selectorStack[stackIndex]) {
9567
- let part = (parentSelector && parentSelector + " " + selector) || selector;
9568
- if (part.includes("&")) {
9569
- part = selector.replace(/&/g, parentSelector || "");
9570
- }
9571
- if (stackIndex < selectorStack.length - 1) {
9572
- part = generateSelector(stackIndex + 1, part);
9573
- }
9574
- parts.push(part);
9575
- }
9576
- return parts.join(", ");
9577
- }
9578
- function generateRules() {
9579
- if (rules.length) {
9580
- parts.push(generateSelector(0) + " {");
9581
- parts.push(...rules);
9582
- parts.push("}");
9583
- rules = [];
9584
- }
9585
- }
9586
- while (tokens.length) {
9587
- let token = tokens.shift();
9588
- if (token === "}") {
9589
- generateRules();
9590
- selectorStack.pop();
9591
- }
9592
- else {
9593
- if (tokens[0] === "{") {
9594
- generateRules();
9595
- selectorStack.push(token.split(/\s*,\s*/));
9596
- tokens.shift();
9597
- }
9598
- if (tokens[0] === ";") {
9599
- rules.push(" " + token + ";");
9600
- }
9601
- }
9602
- }
9603
- return parts.join("\n");
9604
- }
9605
- function registerSheet(id, css) {
9606
- const sheet = document.createElement("style");
9607
- sheet.textContent = processSheet(css);
9608
- STYLESHEETS[id] = sheet;
9609
- }
9610
- function activateSheet(id) {
9611
- const sheet = STYLESHEETS[id];
9612
- sheet.setAttribute("component", id);
9613
- document.head.appendChild(sheet);
9614
- }
9615
- function getTextDecoration({ strikethrough, underline, }) {
9616
- if (!strikethrough && !underline) {
9617
- return "none";
9618
- }
9619
- return `${strikethrough ? "line-through" : ""} ${underline ? "underline" : ""}`;
9620
- }
9621
- /**
9622
- * Convert the cell style to CSS properties.
9623
- */
9624
- function cellStyleToCss(style) {
9625
- const attributes = cellTextStyleToCss(style);
9626
- if (!style)
9627
- return attributes;
9628
- if (style.fillColor) {
9629
- attributes["background"] = style.fillColor;
9630
- }
9631
- return attributes;
9632
- }
9633
- /**
9634
- * Convert the cell text style to CSS properties.
9635
- */
9636
- function cellTextStyleToCss(style) {
9637
- const attributes = {};
9638
- if (!style)
9639
- return attributes;
9640
- if (style.bold) {
9641
- attributes["font-weight"] = "bold";
9642
- }
9643
- if (style.italic) {
9644
- attributes["font-style"] = "italic";
9645
- }
9646
- if (style.strikethrough || style.underline) {
9647
- let decoration = style.strikethrough ? "line-through" : "";
9648
- decoration = style.underline ? decoration + " underline" : decoration;
9649
- attributes["text-decoration"] = decoration;
9650
- }
9651
- if (style.textColor) {
9652
- attributes["color"] = style.textColor;
9653
- }
9654
- return attributes;
9655
- }
9656
- /**
9657
- * Transform CSS properties into a CSS string.
9658
- */
9659
- function cssPropertiesToCss(attributes) {
9660
- let styleStr = "";
9661
- for (const attName in attributes) {
9662
- if (!attributes[attName]) {
9663
- continue;
9664
- }
9665
- styleStr += `${attName}:${attributes[attName]}; `;
9666
- }
9667
- return styleStr;
9668
- }
9669
- function getElementMargins(el) {
9670
- const style = window.getComputedStyle(el);
9671
- return {
9672
- top: parseInt(style.marginTop, 10) || 0,
9673
- bottom: parseInt(style.marginBottom, 10) || 0,
9674
- left: parseInt(style.marginLeft, 10) || 0,
9675
- right: parseInt(style.marginRight, 10) || 0,
9676
- };
9677
- }
9678
-
9679
9537
  const TREND_LINE_XAXIS_ID = "x1";
9680
9538
  /**
9681
9539
  * This file contains helpers that are common to different charts (mainly
@@ -10220,79 +10078,341 @@ function getNextNonEmptyBar(bars, startIndex) {
10220
10078
  return bars.find((bar, i) => i > startIndex && bar.height !== 0);
10221
10079
  }
10222
10080
 
10223
- window.Chart?.register(waterfallLinesPlugin);
10224
- window.Chart?.register(chartShowValuesPlugin);
10225
- css /* scss */ `
10226
- .o-spreadsheet {
10227
- .o-chart-custom-tooltip {
10228
- font-size: 12px;
10229
- background-color: #fff;
10230
- z-index: ${ComponentsImportance.FigureTooltip};
10081
+ const GAUGE_PADDING_SIDE = 30;
10082
+ const GAUGE_PADDING_TOP = 10;
10083
+ const GAUGE_PADDING_BOTTOM = 20;
10084
+ const GAUGE_LABELS_FONT_SIZE = 12;
10085
+ const GAUGE_DEFAULT_VALUE_FONT_SIZE = 80;
10086
+ const GAUGE_BACKGROUND_COLOR = "#F3F2F1";
10087
+ const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
10088
+ const GAUGE_TITLE_SECTION_HEIGHT = 25;
10089
+ function drawGaugeChart(canvas, runtime) {
10090
+ const canvasBoundingRect = canvas.getBoundingClientRect();
10091
+ canvas.width = canvasBoundingRect.width;
10092
+ canvas.height = canvasBoundingRect.height;
10093
+ const ctx = canvas.getContext("2d");
10094
+ const config = getGaugeRenderingConfig(canvasBoundingRect, runtime, ctx);
10095
+ drawBackground(ctx, config);
10096
+ drawGauge(ctx, config);
10097
+ drawInflectionValues(ctx, config);
10098
+ drawLabels(ctx, config);
10099
+ drawTitle(ctx, config);
10100
+ }
10101
+ function drawGauge(ctx, config) {
10102
+ ctx.save();
10103
+ const gauge = config.gauge;
10104
+ const arcCenterX = gauge.rect.x + gauge.rect.width / 2;
10105
+ const arcCenterY = gauge.rect.y + gauge.rect.height;
10106
+ const arcRadius = gauge.rect.height - gauge.arcWidth / 2;
10107
+ if (arcRadius < 0) {
10108
+ return;
10231
10109
  }
10232
- }
10233
- `;
10234
- class ChartJsComponent extends Component {
10235
- static template = "o-spreadsheet-ChartJsComponent";
10236
- static props = {
10237
- figure: Object,
10110
+ const gaugeAngle = gauge.percentage === 1 ? 0 : Math.PI * (1 + gauge.percentage);
10111
+ // Gauge background
10112
+ ctx.strokeStyle = GAUGE_BACKGROUND_COLOR;
10113
+ ctx.beginPath();
10114
+ ctx.lineWidth = gauge.arcWidth;
10115
+ ctx.arc(arcCenterX, arcCenterY, arcRadius, gaugeAngle, 0);
10116
+ ctx.stroke();
10117
+ // Gauge value
10118
+ ctx.strokeStyle = gauge.color;
10119
+ ctx.beginPath();
10120
+ ctx.arc(arcCenterX, arcCenterY, arcRadius, Math.PI, gaugeAngle);
10121
+ ctx.stroke();
10122
+ ctx.restore();
10123
+ }
10124
+ function drawBackground(ctx, config) {
10125
+ ctx.save();
10126
+ ctx.fillStyle = config.backgroundColor;
10127
+ ctx.fillRect(0, 0, config.width, config.height);
10128
+ ctx.restore();
10129
+ }
10130
+ function drawLabels(ctx, config) {
10131
+ for (const label of [config.minLabel, config.maxLabel, config.gaugeValue]) {
10132
+ ctx.save();
10133
+ ctx.textAlign = "center";
10134
+ ctx.fillStyle = label.color;
10135
+ ctx.font = `${label.fontSize}px ${DEFAULT_FONT}`;
10136
+ ctx.fillText(label.label, label.textPosition.x, label.textPosition.y);
10137
+ ctx.restore();
10138
+ }
10139
+ }
10140
+ function drawInflectionValues(ctx, config) {
10141
+ const { x: rectX, y: rectY, width, height } = config.gauge.rect;
10142
+ for (const inflectionValue of config.inflectionValues) {
10143
+ ctx.save();
10144
+ ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment
10145
+ ctx.rotate(Math.PI / 2 - inflectionValue.rotation);
10146
+ ctx.lineWidth = 2;
10147
+ ctx.strokeStyle = chartMutedFontColor(config.backgroundColor) + "aa";
10148
+ ctx.beginPath();
10149
+ ctx.moveTo(0, -(height - config.gauge.arcWidth));
10150
+ ctx.lineTo(0, -height - 3);
10151
+ ctx.stroke();
10152
+ ctx.textAlign = "center";
10153
+ ctx.font = `${inflectionValue.fontSize}px ${DEFAULT_FONT}`;
10154
+ ctx.fillStyle = inflectionValue.color;
10155
+ const textY = -height - GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN - inflectionValue.offset;
10156
+ ctx.fillText(inflectionValue.label, 0, textY);
10157
+ ctx.restore();
10158
+ }
10159
+ }
10160
+ function drawTitle(ctx, config) {
10161
+ ctx.save();
10162
+ const title = config.title;
10163
+ ctx.font = getDefaultContextFont(title.fontSize, title.bold, title.italic);
10164
+ ctx.textBaseline = "middle";
10165
+ ctx.fillStyle = title.color;
10166
+ ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
10167
+ ctx.restore();
10168
+ }
10169
+ function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
10170
+ const maxValue = runtime.maxValue;
10171
+ const minValue = runtime.minValue;
10172
+ const gaugeValue = runtime.gaugeValue;
10173
+ const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
10174
+ const gaugeArcWidth = gaugeRect.width / 6;
10175
+ const gaugePercentage = gaugeValue
10176
+ ? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
10177
+ : 0;
10178
+ const gaugeValuePosition = {
10179
+ x: boundingRect.width / 2,
10180
+ y: gaugeRect.y + gaugeRect.height - gaugeRect.height / 12,
10238
10181
  };
10239
- canvas = useRef("graphContainer");
10240
- chart;
10241
- currentRuntime;
10242
- get background() {
10243
- return this.chartRuntime.background;
10182
+ let gaugeValueFontSize = GAUGE_DEFAULT_VALUE_FONT_SIZE;
10183
+ // Scale down the font size if the gaugeRect is too small
10184
+ if (gaugeRect.height < 300) {
10185
+ gaugeValueFontSize = gaugeValueFontSize * (gaugeRect.height / 300);
10244
10186
  }
10245
- get canvasStyle() {
10246
- return `background-color: ${this.background}`;
10187
+ // Scale down the font size if the text is too long
10188
+ const maxTextWidth = gaugeRect.width / 2;
10189
+ const gaugeLabel = gaugeValue?.label || "-";
10190
+ if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
10191
+ gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
10247
10192
  }
10248
- get chartRuntime() {
10249
- const runtime = this.env.model.getters.getChartRuntime(this.props.figure.id);
10250
- if (!("chartJsConfig" in runtime)) {
10251
- throw new Error("Unsupported chart runtime");
10252
- }
10253
- return runtime;
10193
+ const minLabelPosition = {
10194
+ x: gaugeRect.x + gaugeArcWidth / 2,
10195
+ y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
10196
+ };
10197
+ const maxLabelPosition = {
10198
+ x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
10199
+ y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
10200
+ };
10201
+ const textColor = chartMutedFontColor(runtime.background);
10202
+ const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);
10203
+ let x = 0, titleWidth = 0, titleHeight = 0;
10204
+ if (runtime.title.text) {
10205
+ ({ width: titleWidth, height: titleHeight } = computeTextDimension(ctx, runtime.title.text, { fontSize: CHART_TITLE_FONT_SIZE, ...runtime.title }, "px"));
10254
10206
  }
10255
- setup() {
10256
- onMounted(() => {
10257
- const runtime = this.chartRuntime;
10258
- this.currentRuntime = runtime;
10259
- // Note: chartJS modify the runtime in place, so it's important to give it a copy
10260
- this.createChart(deepCopy(runtime.chartJsConfig));
10261
- });
10262
- onWillUnmount(() => this.chart?.destroy());
10263
- useEffect(() => {
10264
- const runtime = this.chartRuntime;
10265
- if (runtime !== this.currentRuntime) {
10266
- if (runtime.chartJsConfig.type !== this.currentRuntime.chartJsConfig.type) {
10267
- this.chart?.destroy();
10268
- this.createChart(deepCopy(runtime.chartJsConfig));
10269
- }
10270
- else {
10271
- this.updateChartJs(deepCopy(runtime));
10272
- }
10273
- this.currentRuntime = runtime;
10274
- }
10207
+ switch (runtime.title.align) {
10208
+ case "right":
10209
+ x = boundingRect.width - titleWidth - CHART_PADDING$1;
10210
+ break;
10211
+ case "center":
10212
+ x = (boundingRect.width - titleWidth) / 2;
10213
+ break;
10214
+ case "left":
10215
+ default:
10216
+ x = CHART_PADDING$1;
10217
+ break;
10218
+ }
10219
+ return {
10220
+ width: boundingRect.width,
10221
+ height: boundingRect.height,
10222
+ title: {
10223
+ label: runtime.title.text ?? "",
10224
+ fontSize: runtime.title.fontSize ?? CHART_TITLE_FONT_SIZE,
10225
+ textPosition: {
10226
+ x,
10227
+ y: CHART_PADDING_TOP + titleHeight / 2,
10228
+ },
10229
+ color: runtime.title.color ?? textColor,
10230
+ bold: runtime.title.bold,
10231
+ italic: runtime.title.italic,
10232
+ },
10233
+ backgroundColor: runtime.background,
10234
+ gauge: {
10235
+ rect: gaugeRect,
10236
+ arcWidth: gaugeArcWidth,
10237
+ percentage: clip(gaugePercentage, 0, 1),
10238
+ color: getGaugeColor(runtime),
10239
+ },
10240
+ inflectionValues,
10241
+ gaugeValue: {
10242
+ label: gaugeLabel,
10243
+ textPosition: gaugeValuePosition,
10244
+ fontSize: gaugeValueFontSize,
10245
+ color: textColor,
10246
+ },
10247
+ minLabel: {
10248
+ label: runtime.minValue.label,
10249
+ textPosition: minLabelPosition,
10250
+ fontSize: GAUGE_LABELS_FONT_SIZE,
10251
+ color: textColor,
10252
+ },
10253
+ maxLabel: {
10254
+ label: runtime.maxValue.label,
10255
+ textPosition: maxLabelPosition,
10256
+ fontSize: GAUGE_LABELS_FONT_SIZE,
10257
+ color: textColor,
10258
+ },
10259
+ };
10260
+ }
10261
+ /**
10262
+ * Get the rectangle in which the gauge will be drawn, based on the bounding rectangle of the canvas and leaving
10263
+ * space for the title and labels.
10264
+ */
10265
+ function getGaugeRect(boundingRect, title) {
10266
+ const titleHeight = title ? GAUGE_TITLE_SECTION_HEIGHT : 0;
10267
+ const drawHeight = boundingRect.height - GAUGE_PADDING_BOTTOM - titleHeight - GAUGE_PADDING_TOP;
10268
+ const drawWidth = boundingRect.width - GAUGE_PADDING_SIDE * 2;
10269
+ let gaugeWidth;
10270
+ let gaugeHeight;
10271
+ if (drawWidth > 2 * drawHeight) {
10272
+ gaugeWidth = 2 * drawHeight;
10273
+ gaugeHeight = drawHeight;
10274
+ }
10275
+ else {
10276
+ gaugeWidth = drawWidth;
10277
+ gaugeHeight = drawWidth / 2;
10278
+ }
10279
+ const gaugeX = GAUGE_PADDING_SIDE + (drawWidth - gaugeWidth) / 2;
10280
+ const gaugeY = titleHeight + GAUGE_PADDING_TOP + (drawHeight - gaugeHeight) / 2;
10281
+ return {
10282
+ x: gaugeX,
10283
+ y: gaugeY,
10284
+ width: gaugeWidth,
10285
+ height: gaugeHeight,
10286
+ };
10287
+ }
10288
+ /**
10289
+ * 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).
10290
+ *
10291
+ * Also compute an offset for the text so that it doesn't overlap with other text.
10292
+ */
10293
+ function getInflectionValues(runtime, gaugeRect, textColor, ctx) {
10294
+ const maxValue = runtime.maxValue;
10295
+ const minValue = runtime.minValue;
10296
+ const gaugeCircleCenter = {
10297
+ x: gaugeRect.x + gaugeRect.width / 2,
10298
+ y: gaugeRect.y + gaugeRect.height,
10299
+ };
10300
+ const textStyle = { fontSize: GAUGE_LABELS_FONT_SIZE };
10301
+ const inflectionValues = [];
10302
+ const inflectionValuesTextRects = [];
10303
+ for (const inflectionValue of runtime.inflectionValues) {
10304
+ const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
10305
+ const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
10306
+ const angle = Math.PI - Math.PI * percentage;
10307
+ const textRect = getRectangleTangentToCircle(angle, // angle between X axis and the point where the rectangle is tangent to the circle
10308
+ gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, // radius of the gauge circle + margin below text
10309
+ gaugeCircleCenter.x, // center of the gauge circle
10310
+ gaugeCircleCenter.y, // center of the gauge circle
10311
+ labelWidth + 2, // width of the text + some margin
10312
+ GAUGE_LABELS_FONT_SIZE // height of the text
10313
+ );
10314
+ let offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect))
10315
+ ? GAUGE_LABELS_FONT_SIZE
10316
+ : 0;
10317
+ inflectionValuesTextRects.push(textRect);
10318
+ inflectionValues.push({
10319
+ rotation: angle,
10320
+ label: inflectionValue.label,
10321
+ fontSize: GAUGE_LABELS_FONT_SIZE,
10322
+ color: textColor,
10323
+ offset,
10275
10324
  });
10276
10325
  }
10277
- createChart(chartData) {
10278
- const canvas = this.canvas.el;
10279
- const ctx = canvas.getContext("2d");
10280
- this.chart = new window.Chart(ctx, chartData);
10326
+ return inflectionValues;
10327
+ }
10328
+ function getGaugeColor(runtime) {
10329
+ const gaugeValue = runtime.gaugeValue?.value;
10330
+ if (gaugeValue === undefined) {
10331
+ return GAUGE_BACKGROUND_COLOR;
10281
10332
  }
10282
- updateChartJs(chartRuntime) {
10283
- const chartData = chartRuntime.chartJsConfig;
10284
- if (chartData.data && chartData.data.datasets) {
10285
- this.chart.data = chartData.data;
10286
- if (chartData.options?.plugins?.title) {
10287
- this.chart.config.options.plugins.title = chartData.options.plugins.title;
10288
- }
10333
+ for (let i = 0; i < runtime.inflectionValues.length; i++) {
10334
+ const inflectionValue = runtime.inflectionValues[i];
10335
+ if (inflectionValue.operator === "<" && gaugeValue < inflectionValue.value) {
10336
+ return runtime.colors[i];
10289
10337
  }
10290
- else {
10291
- this.chart.data.datasets = [];
10338
+ else if (inflectionValue.operator === "<=" && gaugeValue <= inflectionValue.value) {
10339
+ return runtime.colors[i];
10292
10340
  }
10293
- this.chart.config.options = chartData.options;
10294
- this.chart.update();
10295
10341
  }
10342
+ return runtime.colors.at(-1);
10343
+ }
10344
+ function getSegmentsOfRectangle(rectangle) {
10345
+ return [
10346
+ { start: rectangle.topLeft, end: rectangle.topRight },
10347
+ { start: rectangle.topRight, end: rectangle.bottomRight },
10348
+ { start: rectangle.bottomRight, end: rectangle.bottomLeft },
10349
+ { start: rectangle.bottomLeft, end: rectangle.topLeft },
10350
+ ];
10351
+ }
10352
+ /**
10353
+ * Check if two segment intersect. The case where the segments are colinear (both segments on the same line)
10354
+ * is not handled.
10355
+ */
10356
+ function doSegmentIntersect(segment1, segment2) {
10357
+ const A = segment1.start;
10358
+ const B = segment1.end;
10359
+ const C = segment2.start;
10360
+ const D = segment2.end;
10361
+ /**
10362
+ * Line segment intersection algorithm
10363
+ * https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
10364
+ */
10365
+ function ccw(a, b, c) {
10366
+ return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
10367
+ }
10368
+ return ccw(A, C, D) !== ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D);
10369
+ }
10370
+ function doRectanglesIntersect(rect1, rect2) {
10371
+ const segments1 = getSegmentsOfRectangle(rect1);
10372
+ const segments2 = getSegmentsOfRectangle(rect2);
10373
+ for (const segment1 of segments1) {
10374
+ for (const segment2 of segments2) {
10375
+ if (doSegmentIntersect(segment1, segment2)) {
10376
+ return true;
10377
+ }
10378
+ }
10379
+ }
10380
+ return false;
10381
+ }
10382
+ /**
10383
+ * Get the rectangle that is tangent to a circle at a given angle.
10384
+ *
10385
+ * @param angle angle between X axis and the point where the rectangle is tangent to the circle
10386
+ */
10387
+ function getRectangleTangentToCircle(angle, radius, circleCenterX, circleCenterY, rectWidth, rectHeight) {
10388
+ const cos = Math.cos(angle);
10389
+ const sin = Math.sin(angle);
10390
+ // x, y are the distance from the center of the circle to the point where the rectangle is tangent to the circle
10391
+ const x = cos * radius;
10392
+ const y = sin * radius;
10393
+ // x2, y2 are the distance from the point the rectangle is tangent to the circle to the bottom left corner of the rectangle
10394
+ const x2 = sin * (rectWidth / 2); // cos(angle + 90°) = sin(angle)
10395
+ const y2 = cos * (rectWidth / 2);
10396
+ const bottomRight = {
10397
+ x: x + x2 + circleCenterX,
10398
+ y: circleCenterY - (y - y2),
10399
+ };
10400
+ const bottomLeft = {
10401
+ x: x - x2 + circleCenterX,
10402
+ y: circleCenterY - (y + y2),
10403
+ };
10404
+ // Same as above but for the top corners of the rectangle (radius + rectangle height instead of radius)
10405
+ const xp = cos * (radius + rectHeight);
10406
+ const yp = sin * (radius + rectHeight);
10407
+ const topLeft = {
10408
+ x: xp - x2 + circleCenterX,
10409
+ y: circleCenterY - (yp + y2),
10410
+ };
10411
+ const topRight = {
10412
+ x: xp + x2 + circleCenterX,
10413
+ y: circleCenterY - (yp - y2),
10414
+ };
10415
+ return { bottomLeft, bottomRight, topRight, topLeft };
10296
10416
  }
10297
10417
 
10298
10418
  /**
@@ -10874,6 +10994,299 @@ class ScorecardChartConfigBuilder {
10874
10994
  }
10875
10995
  }
10876
10996
 
10997
+ const CHART_COMMON_OPTIONS = {
10998
+ // https://www.chartjs.org/docs/latest/general/responsive.html
10999
+ responsive: true, // will resize when its container is resized
11000
+ maintainAspectRatio: false, // doesn't maintain the aspect ratio (width/height =2 by default) so the user has the choice of the exact layout
11001
+ elements: {
11002
+ line: {
11003
+ fill: false, // do not fill the area under line charts
11004
+ },
11005
+ point: {
11006
+ hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
11007
+ },
11008
+ },
11009
+ animation: false,
11010
+ };
11011
+ function chartToImage(runtime, figure, type) {
11012
+ // wrap the canvas in a div with a fixed size because chart.js would
11013
+ // fill the whole page otherwise
11014
+ const div = document.createElement("div");
11015
+ div.style.width = `${figure.width}px`;
11016
+ div.style.height = `${figure.height}px`;
11017
+ const canvas = document.createElement("canvas");
11018
+ div.append(canvas);
11019
+ canvas.setAttribute("width", figure.width.toString());
11020
+ canvas.setAttribute("height", figure.height.toString());
11021
+ // we have to add the canvas to the DOM otherwise it won't be rendered
11022
+ document.body.append(div);
11023
+ if ("chartJsConfig" in runtime) {
11024
+ const config = deepCopy(runtime.chartJsConfig);
11025
+ config.plugins = [backgroundColorChartJSPlugin];
11026
+ const Chart = getChartJSConstructor();
11027
+ const chart = new Chart(canvas, config);
11028
+ const imgContent = chart.toBase64Image();
11029
+ chart.destroy();
11030
+ div.remove();
11031
+ return imgContent;
11032
+ }
11033
+ else if (type === "scorecard") {
11034
+ const design = getScorecardConfiguration(figure, runtime);
11035
+ drawScoreChart(design, canvas);
11036
+ const imgContent = canvas.toDataURL();
11037
+ div.remove();
11038
+ return imgContent;
11039
+ }
11040
+ else if (type === "gauge") {
11041
+ drawGaugeChart(canvas, runtime);
11042
+ const imgContent = canvas.toDataURL();
11043
+ div.remove();
11044
+ return imgContent;
11045
+ }
11046
+ return undefined;
11047
+ }
11048
+ /**
11049
+ * Custom chart.js plugin to set the background color of the canvas
11050
+ * https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md
11051
+ */
11052
+ const backgroundColorChartJSPlugin = {
11053
+ id: "customCanvasBackgroundColor",
11054
+ beforeDraw: (chart) => {
11055
+ const { ctx } = chart;
11056
+ ctx.save();
11057
+ ctx.globalCompositeOperation = "destination-over";
11058
+ ctx.fillStyle = "#ffffff";
11059
+ ctx.fillRect(0, 0, chart.width, chart.height);
11060
+ ctx.restore();
11061
+ },
11062
+ };
11063
+ /** Return window.Chart, making sure all our extensions are loaded in ChartJS */
11064
+ function getChartJSConstructor() {
11065
+ if (window.Chart && !window.Chart?.registry.plugins.get("chartShowValuesPlugin")) {
11066
+ window.Chart.register(chartShowValuesPlugin);
11067
+ window.Chart.register(waterfallLinesPlugin);
11068
+ }
11069
+ return window.Chart;
11070
+ }
11071
+
11072
+ /**
11073
+ * This file is largely inspired by owl 1.
11074
+ * `css` tag has been removed from owl 2 without workaround to manage css.
11075
+ * So, the solution was to import the behavior of owl 1 directly in our
11076
+ * codebase, with one difference: the css is added to the sheet as soon as the
11077
+ * css tag is executed. In owl 1, the css was added as soon as a Component was
11078
+ * created for the first time.
11079
+ */
11080
+ const STYLESHEETS = {};
11081
+ let nextId = 0;
11082
+ /**
11083
+ * CSS tag helper for defining inline stylesheets. With this, one can simply define
11084
+ * an inline stylesheet with just the following code:
11085
+ * ```js
11086
+ * css`.component-a { color: red; }`;
11087
+ * ```
11088
+ */
11089
+ function css(strings, ...args) {
11090
+ const name = `__sheet__${nextId++}`;
11091
+ const value = String.raw(strings, ...args);
11092
+ registerSheet(name, value);
11093
+ activateSheet(name);
11094
+ return name;
11095
+ }
11096
+ function processSheet(str) {
11097
+ const tokens = str.split(/(\{|\}|;)/).map((s) => s.trim());
11098
+ const selectorStack = [];
11099
+ const parts = [];
11100
+ let rules = [];
11101
+ function generateSelector(stackIndex, parentSelector) {
11102
+ const parts = [];
11103
+ for (const selector of selectorStack[stackIndex]) {
11104
+ let part = (parentSelector && parentSelector + " " + selector) || selector;
11105
+ if (part.includes("&")) {
11106
+ part = selector.replace(/&/g, parentSelector || "");
11107
+ }
11108
+ if (stackIndex < selectorStack.length - 1) {
11109
+ part = generateSelector(stackIndex + 1, part);
11110
+ }
11111
+ parts.push(part);
11112
+ }
11113
+ return parts.join(", ");
11114
+ }
11115
+ function generateRules() {
11116
+ if (rules.length) {
11117
+ parts.push(generateSelector(0) + " {");
11118
+ parts.push(...rules);
11119
+ parts.push("}");
11120
+ rules = [];
11121
+ }
11122
+ }
11123
+ while (tokens.length) {
11124
+ let token = tokens.shift();
11125
+ if (token === "}") {
11126
+ generateRules();
11127
+ selectorStack.pop();
11128
+ }
11129
+ else {
11130
+ if (tokens[0] === "{") {
11131
+ generateRules();
11132
+ selectorStack.push(token.split(/\s*,\s*/));
11133
+ tokens.shift();
11134
+ }
11135
+ if (tokens[0] === ";") {
11136
+ rules.push(" " + token + ";");
11137
+ }
11138
+ }
11139
+ }
11140
+ return parts.join("\n");
11141
+ }
11142
+ function registerSheet(id, css) {
11143
+ const sheet = document.createElement("style");
11144
+ sheet.textContent = processSheet(css);
11145
+ STYLESHEETS[id] = sheet;
11146
+ }
11147
+ function activateSheet(id) {
11148
+ const sheet = STYLESHEETS[id];
11149
+ sheet.setAttribute("component", id);
11150
+ document.head.appendChild(sheet);
11151
+ }
11152
+ function getTextDecoration({ strikethrough, underline, }) {
11153
+ if (!strikethrough && !underline) {
11154
+ return "none";
11155
+ }
11156
+ return `${strikethrough ? "line-through" : ""} ${underline ? "underline" : ""}`;
11157
+ }
11158
+ /**
11159
+ * Convert the cell style to CSS properties.
11160
+ */
11161
+ function cellStyleToCss(style) {
11162
+ const attributes = cellTextStyleToCss(style);
11163
+ if (!style)
11164
+ return attributes;
11165
+ if (style.fillColor) {
11166
+ attributes["background"] = style.fillColor;
11167
+ }
11168
+ return attributes;
11169
+ }
11170
+ /**
11171
+ * Convert the cell text style to CSS properties.
11172
+ */
11173
+ function cellTextStyleToCss(style) {
11174
+ const attributes = {};
11175
+ if (!style)
11176
+ return attributes;
11177
+ if (style.bold) {
11178
+ attributes["font-weight"] = "bold";
11179
+ }
11180
+ if (style.italic) {
11181
+ attributes["font-style"] = "italic";
11182
+ }
11183
+ if (style.strikethrough || style.underline) {
11184
+ let decoration = style.strikethrough ? "line-through" : "";
11185
+ decoration = style.underline ? decoration + " underline" : decoration;
11186
+ attributes["text-decoration"] = decoration;
11187
+ }
11188
+ if (style.textColor) {
11189
+ attributes["color"] = style.textColor;
11190
+ }
11191
+ return attributes;
11192
+ }
11193
+ /**
11194
+ * Transform CSS properties into a CSS string.
11195
+ */
11196
+ function cssPropertiesToCss(attributes) {
11197
+ let styleStr = "";
11198
+ for (const attName in attributes) {
11199
+ if (!attributes[attName]) {
11200
+ continue;
11201
+ }
11202
+ styleStr += `${attName}:${attributes[attName]}; `;
11203
+ }
11204
+ return styleStr;
11205
+ }
11206
+ function getElementMargins(el) {
11207
+ const style = window.getComputedStyle(el);
11208
+ return {
11209
+ top: parseInt(style.marginTop, 10) || 0,
11210
+ bottom: parseInt(style.marginBottom, 10) || 0,
11211
+ left: parseInt(style.marginLeft, 10) || 0,
11212
+ right: parseInt(style.marginRight, 10) || 0,
11213
+ };
11214
+ }
11215
+
11216
+ css /* scss */ `
11217
+ .o-spreadsheet {
11218
+ .o-chart-custom-tooltip {
11219
+ font-size: 12px;
11220
+ background-color: #fff;
11221
+ z-index: ${ComponentsImportance.FigureTooltip};
11222
+ }
11223
+ }
11224
+ `;
11225
+ class ChartJsComponent extends Component {
11226
+ static template = "o-spreadsheet-ChartJsComponent";
11227
+ static props = {
11228
+ figure: Object,
11229
+ };
11230
+ canvas = useRef("graphContainer");
11231
+ chart;
11232
+ currentRuntime;
11233
+ get background() {
11234
+ return this.chartRuntime.background;
11235
+ }
11236
+ get canvasStyle() {
11237
+ return `background-color: ${this.background}`;
11238
+ }
11239
+ get chartRuntime() {
11240
+ const runtime = this.env.model.getters.getChartRuntime(this.props.figure.id);
11241
+ if (!("chartJsConfig" in runtime)) {
11242
+ throw new Error("Unsupported chart runtime");
11243
+ }
11244
+ return runtime;
11245
+ }
11246
+ setup() {
11247
+ onMounted(() => {
11248
+ const runtime = this.chartRuntime;
11249
+ this.currentRuntime = runtime;
11250
+ // Note: chartJS modify the runtime in place, so it's important to give it a copy
11251
+ this.createChart(deepCopy(runtime.chartJsConfig));
11252
+ });
11253
+ onWillUnmount(() => this.chart?.destroy());
11254
+ useEffect(() => {
11255
+ const runtime = this.chartRuntime;
11256
+ if (runtime !== this.currentRuntime) {
11257
+ if (runtime.chartJsConfig.type !== this.currentRuntime.chartJsConfig.type) {
11258
+ this.chart?.destroy();
11259
+ this.createChart(deepCopy(runtime.chartJsConfig));
11260
+ }
11261
+ else {
11262
+ this.updateChartJs(deepCopy(runtime));
11263
+ }
11264
+ this.currentRuntime = runtime;
11265
+ }
11266
+ });
11267
+ }
11268
+ createChart(chartData) {
11269
+ const canvas = this.canvas.el;
11270
+ const ctx = canvas.getContext("2d");
11271
+ const Chart = getChartJSConstructor();
11272
+ this.chart = new Chart(ctx, chartData);
11273
+ }
11274
+ updateChartJs(chartRuntime) {
11275
+ const chartData = chartRuntime.chartJsConfig;
11276
+ if (chartData.data && chartData.data.datasets) {
11277
+ this.chart.data = chartData.data;
11278
+ if (chartData.options?.plugins?.title) {
11279
+ this.chart.config.options.plugins.title = chartData.options.plugins.title;
11280
+ }
11281
+ }
11282
+ else {
11283
+ this.chart.data.datasets = [];
11284
+ }
11285
+ this.chart.config.options = chartData.options;
11286
+ this.chart.update();
11287
+ }
11288
+ }
11289
+
10877
11290
  class ScorecardChart extends Component {
10878
11291
  static template = "o-spreadsheet-ScorecardChart";
10879
11292
  static props = {
@@ -22326,7 +22739,7 @@ autofillRulesRegistry
22326
22739
  condition: (cell) => !cell.isFormula &&
22327
22740
  evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.text &&
22328
22741
  alphaNumericValueRegExp.test(cell.content),
22329
- generateRule: (cell, cells) => {
22742
+ generateRule: (cell, cells, direction) => {
22330
22743
  const numberPostfix = parseInt(cell.content.match(numberPostfixRegExp)[0]);
22331
22744
  const prefix = cell.content.match(stringPrefixRegExp)[0];
22332
22745
  const numberPostfixLength = cell.content.length - prefix.length;
@@ -22334,7 +22747,10 @@ autofillRulesRegistry
22334
22747
  alphaNumericValueRegExp.test(evaluatedCell.value)) // get consecutive alphanumeric cells, no matter what the prefix is
22335
22748
  .filter((cell) => prefix === (cell.value ?? "").toString().match(stringPrefixRegExp)[0])
22336
22749
  .map((cell) => parseInt((cell.value ?? "").toString().match(numberPostfixRegExp)[0]));
22337
- const increment = calculateIncrementBasedOnGroup(group);
22750
+ let increment = calculateIncrementBasedOnGroup(group);
22751
+ if (["up", "left"].includes(direction) && group.length === 1) {
22752
+ increment = -increment;
22753
+ }
22338
22754
  return {
22339
22755
  type: "ALPHANUMERIC_INCREMENT_MODIFIER",
22340
22756
  prefix,
@@ -22397,10 +22813,13 @@ autofillRulesRegistry
22397
22813
  .add("increment_number", {
22398
22814
  condition: (cell) => !cell.isFormula &&
22399
22815
  evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.number,
22400
- generateRule: (cell, cells) => {
22816
+ generateRule: (cell, cells, direction) => {
22401
22817
  const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number &&
22402
22818
  !isDateTimeFormat(evaluatedCell.format || "")).map((cell) => Number(cell.value));
22403
- const increment = calculateIncrementBasedOnGroup(group);
22819
+ let increment = calculateIncrementBasedOnGroup(group);
22820
+ if (["up", "left"].includes(direction) && group.length === 1) {
22821
+ increment = -increment;
22822
+ }
22404
22823
  const evaluation = evaluateLiteral(cell, { locale: DEFAULT_LOCALE });
22405
22824
  return {
22406
22825
  type: "INCREMENT_MODIFIER",
@@ -22444,343 +22863,6 @@ function getDateIntervals(dates) {
22444
22863
 
22445
22864
  const cellPopoverRegistry = new Registry();
22446
22865
 
22447
- const GAUGE_PADDING_SIDE = 30;
22448
- const GAUGE_PADDING_TOP = 10;
22449
- const GAUGE_PADDING_BOTTOM = 20;
22450
- const GAUGE_LABELS_FONT_SIZE = 12;
22451
- const GAUGE_DEFAULT_VALUE_FONT_SIZE = 80;
22452
- const GAUGE_BACKGROUND_COLOR = "#F3F2F1";
22453
- const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
22454
- const GAUGE_TITLE_SECTION_HEIGHT = 25;
22455
- function drawGaugeChart(canvas, runtime) {
22456
- const canvasBoundingRect = canvas.getBoundingClientRect();
22457
- canvas.width = canvasBoundingRect.width;
22458
- canvas.height = canvasBoundingRect.height;
22459
- const ctx = canvas.getContext("2d");
22460
- const config = getGaugeRenderingConfig(canvasBoundingRect, runtime, ctx);
22461
- drawBackground(ctx, config);
22462
- drawGauge(ctx, config);
22463
- drawInflectionValues(ctx, config);
22464
- drawLabels(ctx, config);
22465
- drawTitle(ctx, config);
22466
- }
22467
- function drawGauge(ctx, config) {
22468
- ctx.save();
22469
- const gauge = config.gauge;
22470
- const arcCenterX = gauge.rect.x + gauge.rect.width / 2;
22471
- const arcCenterY = gauge.rect.y + gauge.rect.height;
22472
- const arcRadius = gauge.rect.height - gauge.arcWidth / 2;
22473
- if (arcRadius < 0) {
22474
- return;
22475
- }
22476
- const gaugeAngle = gauge.percentage === 1 ? 0 : Math.PI * (1 + gauge.percentage);
22477
- // Gauge background
22478
- ctx.strokeStyle = GAUGE_BACKGROUND_COLOR;
22479
- ctx.beginPath();
22480
- ctx.lineWidth = gauge.arcWidth;
22481
- ctx.arc(arcCenterX, arcCenterY, arcRadius, gaugeAngle, 0);
22482
- ctx.stroke();
22483
- // Gauge value
22484
- ctx.strokeStyle = gauge.color;
22485
- ctx.beginPath();
22486
- ctx.arc(arcCenterX, arcCenterY, arcRadius, Math.PI, gaugeAngle);
22487
- ctx.stroke();
22488
- ctx.restore();
22489
- }
22490
- function drawBackground(ctx, config) {
22491
- ctx.save();
22492
- ctx.fillStyle = config.backgroundColor;
22493
- ctx.fillRect(0, 0, config.width, config.height);
22494
- ctx.restore();
22495
- }
22496
- function drawLabels(ctx, config) {
22497
- for (const label of [config.minLabel, config.maxLabel, config.gaugeValue]) {
22498
- ctx.save();
22499
- ctx.textAlign = "center";
22500
- ctx.fillStyle = label.color;
22501
- ctx.font = `${label.fontSize}px ${DEFAULT_FONT}`;
22502
- ctx.fillText(label.label, label.textPosition.x, label.textPosition.y);
22503
- ctx.restore();
22504
- }
22505
- }
22506
- function drawInflectionValues(ctx, config) {
22507
- const { x: rectX, y: rectY, width, height } = config.gauge.rect;
22508
- for (const inflectionValue of config.inflectionValues) {
22509
- ctx.save();
22510
- ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment
22511
- ctx.rotate(Math.PI / 2 - inflectionValue.rotation);
22512
- ctx.lineWidth = 2;
22513
- ctx.strokeStyle = chartMutedFontColor(config.backgroundColor) + "aa";
22514
- ctx.beginPath();
22515
- ctx.moveTo(0, -(height - config.gauge.arcWidth));
22516
- ctx.lineTo(0, -height - 3);
22517
- ctx.stroke();
22518
- ctx.textAlign = "center";
22519
- ctx.font = `${inflectionValue.fontSize}px ${DEFAULT_FONT}`;
22520
- ctx.fillStyle = inflectionValue.color;
22521
- const textY = -height - GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN - inflectionValue.offset;
22522
- ctx.fillText(inflectionValue.label, 0, textY);
22523
- ctx.restore();
22524
- }
22525
- }
22526
- function drawTitle(ctx, config) {
22527
- ctx.save();
22528
- const title = config.title;
22529
- ctx.font = getDefaultContextFont(title.fontSize, title.bold, title.italic);
22530
- ctx.textBaseline = "middle";
22531
- ctx.fillStyle = title.color;
22532
- ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
22533
- ctx.restore();
22534
- }
22535
- function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
22536
- const maxValue = runtime.maxValue;
22537
- const minValue = runtime.minValue;
22538
- const gaugeValue = runtime.gaugeValue;
22539
- const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
22540
- const gaugeArcWidth = gaugeRect.width / 6;
22541
- const gaugePercentage = gaugeValue
22542
- ? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
22543
- : 0;
22544
- const gaugeValuePosition = {
22545
- x: boundingRect.width / 2,
22546
- y: gaugeRect.y + gaugeRect.height - gaugeRect.height / 12,
22547
- };
22548
- let gaugeValueFontSize = GAUGE_DEFAULT_VALUE_FONT_SIZE;
22549
- // Scale down the font size if the gaugeRect is too small
22550
- if (gaugeRect.height < 300) {
22551
- gaugeValueFontSize = gaugeValueFontSize * (gaugeRect.height / 300);
22552
- }
22553
- // Scale down the font size if the text is too long
22554
- const maxTextWidth = gaugeRect.width / 2;
22555
- const gaugeLabel = gaugeValue?.label || "-";
22556
- if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
22557
- gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
22558
- }
22559
- const minLabelPosition = {
22560
- x: gaugeRect.x + gaugeArcWidth / 2,
22561
- y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
22562
- };
22563
- const maxLabelPosition = {
22564
- x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
22565
- y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
22566
- };
22567
- const textColor = chartMutedFontColor(runtime.background);
22568
- const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);
22569
- let x = 0, titleWidth = 0, titleHeight = 0;
22570
- if (runtime.title.text) {
22571
- ({ width: titleWidth, height: titleHeight } = computeTextDimension(ctx, runtime.title.text, { fontSize: CHART_TITLE_FONT_SIZE, ...runtime.title }, "px"));
22572
- }
22573
- switch (runtime.title.align) {
22574
- case "right":
22575
- x = boundingRect.width - titleWidth - CHART_PADDING$1;
22576
- break;
22577
- case "center":
22578
- x = (boundingRect.width - titleWidth) / 2;
22579
- break;
22580
- case "left":
22581
- default:
22582
- x = CHART_PADDING$1;
22583
- break;
22584
- }
22585
- return {
22586
- width: boundingRect.width,
22587
- height: boundingRect.height,
22588
- title: {
22589
- label: runtime.title.text ?? "",
22590
- fontSize: runtime.title.fontSize ?? CHART_TITLE_FONT_SIZE,
22591
- textPosition: {
22592
- x,
22593
- y: CHART_PADDING_TOP + titleHeight / 2,
22594
- },
22595
- color: runtime.title.color ?? textColor,
22596
- bold: runtime.title.bold,
22597
- italic: runtime.title.italic,
22598
- },
22599
- backgroundColor: runtime.background,
22600
- gauge: {
22601
- rect: gaugeRect,
22602
- arcWidth: gaugeArcWidth,
22603
- percentage: clip(gaugePercentage, 0, 1),
22604
- color: getGaugeColor(runtime),
22605
- },
22606
- inflectionValues,
22607
- gaugeValue: {
22608
- label: gaugeLabel,
22609
- textPosition: gaugeValuePosition,
22610
- fontSize: gaugeValueFontSize,
22611
- color: textColor,
22612
- },
22613
- minLabel: {
22614
- label: runtime.minValue.label,
22615
- textPosition: minLabelPosition,
22616
- fontSize: GAUGE_LABELS_FONT_SIZE,
22617
- color: textColor,
22618
- },
22619
- maxLabel: {
22620
- label: runtime.maxValue.label,
22621
- textPosition: maxLabelPosition,
22622
- fontSize: GAUGE_LABELS_FONT_SIZE,
22623
- color: textColor,
22624
- },
22625
- };
22626
- }
22627
- /**
22628
- * Get the rectangle in which the gauge will be drawn, based on the bounding rectangle of the canvas and leaving
22629
- * space for the title and labels.
22630
- */
22631
- function getGaugeRect(boundingRect, title) {
22632
- const titleHeight = title ? GAUGE_TITLE_SECTION_HEIGHT : 0;
22633
- const drawHeight = boundingRect.height - GAUGE_PADDING_BOTTOM - titleHeight - GAUGE_PADDING_TOP;
22634
- const drawWidth = boundingRect.width - GAUGE_PADDING_SIDE * 2;
22635
- let gaugeWidth;
22636
- let gaugeHeight;
22637
- if (drawWidth > 2 * drawHeight) {
22638
- gaugeWidth = 2 * drawHeight;
22639
- gaugeHeight = drawHeight;
22640
- }
22641
- else {
22642
- gaugeWidth = drawWidth;
22643
- gaugeHeight = drawWidth / 2;
22644
- }
22645
- const gaugeX = GAUGE_PADDING_SIDE + (drawWidth - gaugeWidth) / 2;
22646
- const gaugeY = titleHeight + GAUGE_PADDING_TOP + (drawHeight - gaugeHeight) / 2;
22647
- return {
22648
- x: gaugeX,
22649
- y: gaugeY,
22650
- width: gaugeWidth,
22651
- height: gaugeHeight,
22652
- };
22653
- }
22654
- /**
22655
- * 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).
22656
- *
22657
- * Also compute an offset for the text so that it doesn't overlap with other text.
22658
- */
22659
- function getInflectionValues(runtime, gaugeRect, textColor, ctx) {
22660
- const maxValue = runtime.maxValue;
22661
- const minValue = runtime.minValue;
22662
- const gaugeCircleCenter = {
22663
- x: gaugeRect.x + gaugeRect.width / 2,
22664
- y: gaugeRect.y + gaugeRect.height,
22665
- };
22666
- const textStyle = { fontSize: GAUGE_LABELS_FONT_SIZE };
22667
- const inflectionValues = [];
22668
- const inflectionValuesTextRects = [];
22669
- for (const inflectionValue of runtime.inflectionValues) {
22670
- const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
22671
- const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
22672
- const angle = Math.PI - Math.PI * percentage;
22673
- const textRect = getRectangleTangentToCircle(angle, // angle between X axis and the point where the rectangle is tangent to the circle
22674
- gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, // radius of the gauge circle + margin below text
22675
- gaugeCircleCenter.x, // center of the gauge circle
22676
- gaugeCircleCenter.y, // center of the gauge circle
22677
- labelWidth + 2, // width of the text + some margin
22678
- GAUGE_LABELS_FONT_SIZE // height of the text
22679
- );
22680
- let offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect))
22681
- ? GAUGE_LABELS_FONT_SIZE
22682
- : 0;
22683
- inflectionValuesTextRects.push(textRect);
22684
- inflectionValues.push({
22685
- rotation: angle,
22686
- label: inflectionValue.label,
22687
- fontSize: GAUGE_LABELS_FONT_SIZE,
22688
- color: textColor,
22689
- offset,
22690
- });
22691
- }
22692
- return inflectionValues;
22693
- }
22694
- function getGaugeColor(runtime) {
22695
- const gaugeValue = runtime.gaugeValue?.value;
22696
- if (gaugeValue === undefined) {
22697
- return GAUGE_BACKGROUND_COLOR;
22698
- }
22699
- for (let i = 0; i < runtime.inflectionValues.length; i++) {
22700
- const inflectionValue = runtime.inflectionValues[i];
22701
- if (inflectionValue.operator === "<" && gaugeValue < inflectionValue.value) {
22702
- return runtime.colors[i];
22703
- }
22704
- else if (inflectionValue.operator === "<=" && gaugeValue <= inflectionValue.value) {
22705
- return runtime.colors[i];
22706
- }
22707
- }
22708
- return runtime.colors.at(-1);
22709
- }
22710
- function getSegmentsOfRectangle(rectangle) {
22711
- return [
22712
- { start: rectangle.topLeft, end: rectangle.topRight },
22713
- { start: rectangle.topRight, end: rectangle.bottomRight },
22714
- { start: rectangle.bottomRight, end: rectangle.bottomLeft },
22715
- { start: rectangle.bottomLeft, end: rectangle.topLeft },
22716
- ];
22717
- }
22718
- /**
22719
- * Check if two segment intersect. The case where the segments are colinear (both segments on the same line)
22720
- * is not handled.
22721
- */
22722
- function doSegmentIntersect(segment1, segment2) {
22723
- const A = segment1.start;
22724
- const B = segment1.end;
22725
- const C = segment2.start;
22726
- const D = segment2.end;
22727
- /**
22728
- * Line segment intersection algorithm
22729
- * https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
22730
- */
22731
- function ccw(a, b, c) {
22732
- return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
22733
- }
22734
- return ccw(A, C, D) !== ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D);
22735
- }
22736
- function doRectanglesIntersect(rect1, rect2) {
22737
- const segments1 = getSegmentsOfRectangle(rect1);
22738
- const segments2 = getSegmentsOfRectangle(rect2);
22739
- for (const segment1 of segments1) {
22740
- for (const segment2 of segments2) {
22741
- if (doSegmentIntersect(segment1, segment2)) {
22742
- return true;
22743
- }
22744
- }
22745
- }
22746
- return false;
22747
- }
22748
- /**
22749
- * Get the rectangle that is tangent to a circle at a given angle.
22750
- *
22751
- * @param angle angle between X axis and the point where the rectangle is tangent to the circle
22752
- */
22753
- function getRectangleTangentToCircle(angle, radius, circleCenterX, circleCenterY, rectWidth, rectHeight) {
22754
- const cos = Math.cos(angle);
22755
- const sin = Math.sin(angle);
22756
- // x, y are the distance from the center of the circle to the point where the rectangle is tangent to the circle
22757
- const x = cos * radius;
22758
- const y = sin * radius;
22759
- // x2, y2 are the distance from the point the rectangle is tangent to the circle to the bottom left corner of the rectangle
22760
- const x2 = sin * (rectWidth / 2); // cos(angle + 90°) = sin(angle)
22761
- const y2 = cos * (rectWidth / 2);
22762
- const bottomRight = {
22763
- x: x + x2 + circleCenterX,
22764
- y: circleCenterY - (y - y2),
22765
- };
22766
- const bottomLeft = {
22767
- x: x - x2 + circleCenterX,
22768
- y: circleCenterY - (y + y2),
22769
- };
22770
- // Same as above but for the top corners of the rectangle (radius + rectangle height instead of radius)
22771
- const xp = cos * (radius + rectHeight);
22772
- const yp = sin * (radius + rectHeight);
22773
- const topLeft = {
22774
- x: xp - x2 + circleCenterX,
22775
- y: circleCenterY - (yp + y2),
22776
- };
22777
- const topRight = {
22778
- x: xp + x2 + circleCenterX,
22779
- y: circleCenterY - (yp - y2),
22780
- };
22781
- return { bottomLeft, bottomRight, topRight, topLeft };
22782
- }
22783
-
22784
22866
  class GaugeChartComponent extends Component {
22785
22867
  static template = "o-spreadsheet-GaugeChartComponent";
22786
22868
  canvas = useRef("chartContainer");
@@ -22813,72 +22895,6 @@ function toXlsxHexColor(color) {
22813
22895
  return color;
22814
22896
  }
22815
22897
 
22816
- const CHART_COMMON_OPTIONS = {
22817
- // https://www.chartjs.org/docs/latest/general/responsive.html
22818
- responsive: true, // will resize when its container is resized
22819
- maintainAspectRatio: false, // doesn't maintain the aspect ratio (width/height =2 by default) so the user has the choice of the exact layout
22820
- elements: {
22821
- line: {
22822
- fill: false, // do not fill the area under line charts
22823
- },
22824
- point: {
22825
- hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
22826
- },
22827
- },
22828
- animation: false,
22829
- };
22830
- function chartToImage(runtime, figure, type) {
22831
- // wrap the canvas in a div with a fixed size because chart.js would
22832
- // fill the whole page otherwise
22833
- const div = document.createElement("div");
22834
- div.style.width = `${figure.width}px`;
22835
- div.style.height = `${figure.height}px`;
22836
- const canvas = document.createElement("canvas");
22837
- div.append(canvas);
22838
- canvas.setAttribute("width", figure.width.toString());
22839
- canvas.setAttribute("height", figure.height.toString());
22840
- // we have to add the canvas to the DOM otherwise it won't be rendered
22841
- document.body.append(div);
22842
- if ("chartJsConfig" in runtime) {
22843
- const config = deepCopy(runtime.chartJsConfig);
22844
- config.plugins = [backgroundColorChartJSPlugin];
22845
- const chart = new window.Chart(canvas, config);
22846
- const imgContent = chart.toBase64Image();
22847
- chart.destroy();
22848
- div.remove();
22849
- return imgContent;
22850
- }
22851
- else if (type === "scorecard") {
22852
- const design = getScorecardConfiguration(figure, runtime);
22853
- drawScoreChart(design, canvas);
22854
- const imgContent = canvas.toDataURL();
22855
- div.remove();
22856
- return imgContent;
22857
- }
22858
- else if (type === "gauge") {
22859
- drawGaugeChart(canvas, runtime);
22860
- const imgContent = canvas.toDataURL();
22861
- div.remove();
22862
- return imgContent;
22863
- }
22864
- return undefined;
22865
- }
22866
- /**
22867
- * Custom chart.js plugin to set the background color of the canvas
22868
- * https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md
22869
- */
22870
- const backgroundColorChartJSPlugin = {
22871
- id: "customCanvasBackgroundColor",
22872
- beforeDraw: (chart) => {
22873
- const { ctx } = chart;
22874
- ctx.save();
22875
- ctx.globalCompositeOperation = "destination-over";
22876
- ctx.fillStyle = "#ffffff";
22877
- ctx.fillRect(0, 0, chart.width, chart.height);
22878
- ctx.restore();
22879
- },
22880
- };
22881
-
22882
22898
  /**
22883
22899
  * Represent a raw XML string
22884
22900
  */
@@ -22944,6 +22960,7 @@ const CONTENT_TYPES = {
22944
22960
  macroEnabledTemplateWorkbook: "application/vnd.ms-excel.template.macroEnabled.main+xml",
22945
22961
  excelAddInWorkbook: "application/vnd.ms-excel.addin.macroEnabled.main+xml",
22946
22962
  sheet: "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml",
22963
+ metadata: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheetMetadata+xml",
22947
22964
  sharedStrings: "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml",
22948
22965
  styles: "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml",
22949
22966
  drawing: "application/vnd.openxmlformats-officedocument.drawing+xml",
@@ -22956,6 +22973,7 @@ const CONTENT_TYPES = {
22956
22973
  const XLSX_RELATION_TYPE = {
22957
22974
  document: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument",
22958
22975
  sheet: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet",
22976
+ metadata: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sheetMetadata",
22959
22977
  sharedStrings: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings",
22960
22978
  styles: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles",
22961
22979
  drawing: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing",
@@ -22965,6 +22983,7 @@ const XLSX_RELATION_TYPE = {
22965
22983
  hyperlink: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
22966
22984
  image: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
22967
22985
  };
22986
+ const ARRAY_FORMULA_URI = "bdbb8cdc-fa1e-496e-a857-3c3f30c029c3";
22968
22987
  const RELATIONSHIP_NSR = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
22969
22988
  const HEIGHT_FACTOR = 0.75; // 100px => 75 u
22970
22989
  /**
@@ -25386,29 +25405,34 @@ function convertPivotTableConfig(pivotTable) {
25386
25405
  * In all the sheets, replace the table-only references in the formula cells with standard references.
25387
25406
  */
25388
25407
  function convertTableFormulaReferences(convertedSheets, xlsxSheets) {
25389
- for (let sheet of convertedSheets) {
25390
- const tables = xlsxSheets.find((s) => s.sheetName === sheet.name).tables;
25408
+ for (let tableSheet of convertedSheets) {
25409
+ const tables = xlsxSheets.find((s) => s.sheetName === tableSheet.name).tables;
25391
25410
  for (let table of tables) {
25392
25411
  const tabRef = table.name + "[";
25393
- for (let position of positions(toZone(table.ref))) {
25394
- const xc = toXC(position.col, position.row);
25395
- let cellContent = sheet.cells[xc];
25396
- if (cellContent?.startsWith("=")) {
25397
- let refIndex;
25398
- while ((refIndex = cellContent.indexOf(tabRef)) !== -1) {
25399
- let reference = cellContent.slice(refIndex + tabRef.length);
25400
- // Expression can either be tableName[colName] or tableName[[#This Row], [colName]]
25401
- let endIndex = reference.indexOf("]");
25402
- if (reference.startsWith(`[`)) {
25403
- endIndex = reference.indexOf("]", endIndex + 1);
25404
- endIndex = reference.indexOf("]", endIndex + 1);
25412
+ for (let sheet of convertedSheets) {
25413
+ for (let xc in sheet.cells) {
25414
+ const cell = sheet.cells[xc];
25415
+ let cellContent = sheet.cells[xc];
25416
+ if (cell && cellContent && cellContent.startsWith("=")) {
25417
+ let refIndex;
25418
+ while ((refIndex = cellContent.indexOf(tabRef)) !== -1) {
25419
+ let endIndex = refIndex + tabRef.length;
25420
+ let openBrackets = 1;
25421
+ while (openBrackets > 0 && endIndex < cellContent.length) {
25422
+ if (cellContent[endIndex] === "[") {
25423
+ openBrackets++;
25424
+ }
25425
+ else if (cellContent[endIndex] === "]") {
25426
+ openBrackets--;
25427
+ }
25428
+ endIndex++;
25429
+ }
25430
+ let reference = cellContent.slice(refIndex + tabRef.length, endIndex - 1);
25431
+ const sheetPrefix = tableSheet.id === sheet.id ? "" : tableSheet.name + "!";
25432
+ const convertedRef = convertTableReference(sheetPrefix, reference, table, xc);
25433
+ cellContent =
25434
+ cellContent.slice(0, refIndex) + convertedRef + cellContent.slice(endIndex);
25405
25435
  }
25406
- reference = reference.slice(0, endIndex);
25407
- const convertedRef = convertTableReference(reference, table, xc);
25408
- cellContent =
25409
- cellContent.slice(0, refIndex) +
25410
- convertedRef +
25411
- cellContent.slice(tabRef.length + refIndex + endIndex + 1);
25412
25436
  }
25413
25437
  sheet.cells[xc] = cellContent;
25414
25438
  }
@@ -25417,11 +25441,17 @@ function convertTableFormulaReferences(convertedSheets, xlsxSheets) {
25417
25441
  }
25418
25442
  }
25419
25443
  /**
25420
- * Convert table-specific references in formulas into standard references.
25444
+ * Convert table-specific references in formulas into standard references. A table reference is composed of columns names,
25445
+ * and of keywords determining the rows of the table to reference.
25421
25446
  *
25422
25447
  * A reference in a table can have the form (only the part between brackets should be given to this function):
25423
25448
  * - tableName[colName] : reference to the whole column "colName"
25449
+ * - tableName[#keyword] : reference to the whatever row the keyword refers to
25424
25450
  * - tableName[[#keyword], [colName]] : reference to some of the element(s) of the column colName
25451
+ * - tableName[[#keyword], [colName]:[col2Name]] : reference to some of the element(s) of the columns colName to col2Name
25452
+ * - tableName[[#keyword1], [#keyword2], [colName]] : reference to all the rows referenced by the keywords in the column colName
25453
+ * - tableName[[#keyword1], [colName], [#keyword2]]: the keywords and colName can be in any order
25454
+ *
25425
25455
  *
25426
25456
  * The available keywords are :
25427
25457
  * - #All : all the column (including totals)
@@ -25429,58 +25459,109 @@ function convertTableFormulaReferences(convertedSheets, xlsxSheets) {
25429
25459
  * - #Headers : only the header of the column
25430
25460
  * - #Totals : only the totals of the column
25431
25461
  * - #This Row : only the element in the same row as the cell
25462
+ *
25463
+ * Note that the only valid combination of multiple keywords are #Data + #Totals and #Headers + #Data.
25432
25464
  */
25433
- function convertTableReference(expr, table, cellXc) {
25434
- const refElements = expr.split(",");
25465
+ function convertTableReference(sheetPrefix, expr, table, cellXc) {
25466
+ // TODO: Ideally we'd want to make a real tokenizer, this simple approach won't work if for example the column name
25467
+ // contain # or , characters. But that's probably an edge case that we can ignore for now.
25468
+ const parts = expr.split(",").map((part) => part.trim());
25435
25469
  const tableZone = toZone(table.ref);
25436
- const refZone = { ...tableZone };
25437
- let isReferencedZoneValid = true;
25438
- // Single column reference
25439
- if (refElements.length === 1) {
25440
- const colRelativeIndex = table.cols.findIndex((col) => col.name === refElements[0]);
25441
- refZone.left = refZone.right = colRelativeIndex + tableZone.left;
25442
- if (table.headerRowCount) {
25443
- refZone.top += table.headerRowCount;
25444
- }
25445
- if (table.totalsRowCount) {
25446
- refZone.bottom -= 1;
25470
+ const colIndexes = [];
25471
+ const rowIndexes = [];
25472
+ const foundKeywords = [];
25473
+ for (const part of parts) {
25474
+ if (removeBrackets(part).startsWith("#")) {
25475
+ const keyWord = removeBrackets(part);
25476
+ foundKeywords.push(keyWord);
25477
+ switch (keyWord) {
25478
+ case "#All":
25479
+ rowIndexes.push(tableZone.top, tableZone.bottom);
25480
+ break;
25481
+ case "#Data":
25482
+ const top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
25483
+ const bottom = table.totalsRowCount
25484
+ ? tableZone.bottom - table.totalsRowCount
25485
+ : tableZone.bottom;
25486
+ rowIndexes.push(top, bottom);
25487
+ break;
25488
+ case "#This Row":
25489
+ rowIndexes.push(toCartesian(cellXc).row);
25490
+ break;
25491
+ case "#Headers":
25492
+ if (!table.headerRowCount) {
25493
+ return CellErrorType.InvalidReference;
25494
+ }
25495
+ rowIndexes.push(tableZone.top);
25496
+ break;
25497
+ case "#Totals":
25498
+ if (!table.totalsRowCount) {
25499
+ return CellErrorType.InvalidReference;
25500
+ }
25501
+ rowIndexes.push(tableZone.bottom);
25502
+ break;
25503
+ }
25447
25504
  }
25448
- }
25449
- // Other references
25450
- else {
25451
- switch (refElements[0].slice(1, refElements[0].length - 1)) {
25452
- case "#All":
25453
- refZone.top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
25454
- refZone.bottom = tableZone.bottom;
25455
- break;
25456
- case "#Data":
25457
- refZone.top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
25458
- refZone.bottom = table.totalsRowCount ? tableZone.bottom + 1 : tableZone.bottom;
25459
- break;
25460
- case "#This Row":
25461
- refZone.top = refZone.bottom = toCartesian(cellXc).row;
25462
- break;
25463
- case "#Headers":
25464
- refZone.top = refZone.bottom = tableZone.top;
25465
- if (!table.headerRowCount) {
25466
- isReferencedZoneValid = false;
25467
- }
25468
- break;
25469
- case "#Totals":
25470
- refZone.top = refZone.bottom = tableZone.bottom;
25471
- if (!table.totalsRowCount) {
25472
- isReferencedZoneValid = false;
25505
+ else {
25506
+ const columns = part
25507
+ .split(":")
25508
+ .map((part) => part.trim())
25509
+ .map(removeBrackets);
25510
+ if (colIndexes.length) {
25511
+ return CellErrorType.InvalidReference;
25512
+ }
25513
+ const colRelativeIndex = table.cols.findIndex((col) => col.name === columns[0]);
25514
+ if (colRelativeIndex === -1) {
25515
+ return CellErrorType.InvalidReference;
25516
+ }
25517
+ colIndexes.push(colRelativeIndex + tableZone.left);
25518
+ if (columns[1]) {
25519
+ const colRelativeIndex2 = table.cols.findIndex((col) => col.name === columns[1]);
25520
+ if (colRelativeIndex2 === -1) {
25521
+ return CellErrorType.InvalidReference;
25473
25522
  }
25474
- break;
25523
+ colIndexes.push(colRelativeIndex2 + tableZone.left);
25524
+ }
25475
25525
  }
25476
- const colRef = refElements[1].slice(1, refElements[1].length - 1);
25477
- const colRelativeIndex = table.cols.findIndex((col) => col.name === colRef);
25478
- refZone.left = refZone.right = colRelativeIndex + tableZone.left;
25479
25526
  }
25480
- if (!isReferencedZoneValid) {
25527
+ if (!areKeywordsCompatible(foundKeywords)) {
25481
25528
  return CellErrorType.InvalidReference;
25482
25529
  }
25483
- return refZone.top !== refZone.bottom ? zoneToXc(refZone) : toXC(refZone.left, refZone.top);
25530
+ if (rowIndexes.length === 0) {
25531
+ const top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
25532
+ const bottom = table.totalsRowCount
25533
+ ? tableZone.bottom - table.totalsRowCount
25534
+ : tableZone.bottom;
25535
+ rowIndexes.push(top, bottom);
25536
+ }
25537
+ if (colIndexes.length === 0) {
25538
+ colIndexes.push(tableZone.left, tableZone.right);
25539
+ }
25540
+ const refZone = {
25541
+ top: Math.min(...rowIndexes),
25542
+ left: Math.min(...colIndexes),
25543
+ bottom: Math.max(...rowIndexes),
25544
+ right: Math.max(...colIndexes),
25545
+ };
25546
+ return sheetPrefix + zoneToXc(refZone);
25547
+ }
25548
+ function removeBrackets(str) {
25549
+ return str.startsWith("[") && str.endsWith("]") ? str.slice(1, str.length - 1) : str;
25550
+ }
25551
+ function areKeywordsCompatible(keywords) {
25552
+ if (keywords.length < 2) {
25553
+ return true;
25554
+ }
25555
+ else if (keywords.length > 2) {
25556
+ return false;
25557
+ }
25558
+ else if (keywords.includes("#Data") && keywords.includes("#Totals")) {
25559
+ return true;
25560
+ }
25561
+ else if (keywords.includes("#Headers") && keywords.includes("#Data")) {
25562
+ return true;
25563
+ }
25564
+ return false;
25484
25565
  }
25485
25566
 
25486
25567
  // -------------------------------------
@@ -28648,11 +28729,12 @@ function canBeLinearChart(definition, dataSets, labelRange, getters) {
28648
28729
  }
28649
28730
  let missingTimeAdapterAlreadyWarned = false;
28650
28731
  function isLuxonTimeAdapterInstalled() {
28651
- if (!window.Chart) {
28732
+ const Chart = getChartJSConstructor();
28733
+ if (!Chart) {
28652
28734
  return false;
28653
28735
  }
28654
28736
  // @ts-ignore
28655
- const adapter = new window.Chart._adapters._date({});
28737
+ const adapter = new Chart._adapters._date({});
28656
28738
  const isInstalled = adapter._id === "luxon";
28657
28739
  if (!isInstalled && !missingTimeAdapterAlreadyWarned) {
28658
28740
  missingTimeAdapterAlreadyWarned = true;
@@ -32506,10 +32588,6 @@ class Popover extends Component {
32506
32588
  this.currentDisplayValue = newDisplay;
32507
32589
  if (!anchor)
32508
32590
  return;
32509
- el.style.top = "";
32510
- el.style.left = "";
32511
- el.style["max-height"] = "";
32512
- el.style["max-width"] = "";
32513
32591
  const propsMaxSize = { width: this.props.maxWidth, height: this.props.maxHeight };
32514
32592
  let elDims = {
32515
32593
  width: el.getBoundingClientRect().width,
@@ -34061,6 +34139,7 @@ var CHART_HELPERS = /*#__PURE__*/Object.freeze({
34061
34139
  duplicateLabelRangeInDuplicatedSheet: duplicateLabelRangeInDuplicatedSheet,
34062
34140
  formatChartDatasetValue: formatChartDatasetValue,
34063
34141
  formatTickValue: formatTickValue,
34142
+ getChartJSConstructor: getChartJSConstructor,
34064
34143
  getChartPositionAtCenterOfViewport: getChartPositionAtCenterOfViewport,
34065
34144
  getDefinedAxis: getDefinedAxis,
34066
34145
  getPieColors: getPieColors,
@@ -37861,6 +37940,11 @@ class SelectionInputStore extends SpreadsheetStore {
37861
37940
  }
37862
37941
  updateColors(colors) {
37863
37942
  this.colors = colors;
37943
+ const colorGenerator = new ColorGenerator(this.ranges.length, this.colors);
37944
+ this.ranges = this.ranges.map((range) => ({
37945
+ ...range,
37946
+ color: colorGenerator.next(),
37947
+ }));
37864
37948
  }
37865
37949
  confirm() {
37866
37950
  for (const range of this.selectionInputs) {
@@ -37895,12 +37979,11 @@ class SelectionInputStore extends SpreadsheetStore {
37895
37979
  * e.g. ["A1", "Sheet2!B3", "E12"]
37896
37980
  */
37897
37981
  get selectionInputs() {
37898
- const generator = new ColorGenerator(this.ranges.length, this.colors);
37899
37982
  return this.ranges.map((input, index) => Object.assign({}, input, {
37900
37983
  color: this.hasMainFocus &&
37901
37984
  this.focusedRangeIndex !== null &&
37902
37985
  this.getters.isRangeValid(input.xc)
37903
- ? generator.next()
37986
+ ? input.color
37904
37987
  : null,
37905
37988
  isFocused: this.hasMainFocus && this.focusedRangeIndex === index,
37906
37989
  isValidRange: input.xc === "" || this.getters.isRangeValid(input.xc),
@@ -38170,10 +38253,10 @@ class SelectionInput extends Component {
38170
38253
  if (originalIndex === finalIndex) {
38171
38254
  return;
38172
38255
  }
38173
- const draggedItems = [...draggableIds];
38174
- draggedItems.splice(originalIndex, 1);
38175
- draggedItems.splice(finalIndex, 0, rangeId);
38176
- this.props.onSelectionReordered?.(this.store.selectionInputs.map((range) => draggedItems.indexOf(range.id)));
38256
+ const indexes = range(0, draggableIds.length);
38257
+ indexes.splice(originalIndex, 1);
38258
+ indexes.splice(finalIndex, 0, originalIndex);
38259
+ this.props.onSelectionReordered?.(indexes);
38177
38260
  this.props.onSelectionConfirmed?.();
38178
38261
  this.store.confirm();
38179
38262
  },
@@ -38451,6 +38534,9 @@ class GenericChartConfigPanel extends Component {
38451
38534
  this.state.datasetDispatchResult = this.props.updateChart(this.props.figureId, {
38452
38535
  dataSets: this.dataSets,
38453
38536
  });
38537
+ if (this.state.datasetDispatchResult.isSuccessful) {
38538
+ this.dataSets = this.env.model.getters.getChartDefinition(this.props.figureId).dataSets;
38539
+ }
38454
38540
  }
38455
38541
  getDataSeriesRanges() {
38456
38542
  return this.dataSets;
@@ -39867,8 +39953,16 @@ class ContentEditableHelper {
39867
39953
  }
39868
39954
  let startNode = this.findChildAtCharacterIndex(start);
39869
39955
  let endNode = this.findChildAtCharacterIndex(end);
39870
- range.setStart(startNode.node, startNode.offset);
39871
- range.setEnd(endNode.node, endNode.offset);
39956
+ // setEnd (setStart) will result in a collapsed range if the end point is before the start point
39957
+ // https://developer.mozilla.org/en-US/docs/Web/API/Range/setEnd
39958
+ if (start <= end) {
39959
+ range.setStart(startNode.node, startNode.offset);
39960
+ range.setEnd(endNode.node, endNode.offset);
39961
+ }
39962
+ else {
39963
+ range.setStart(endNode.node, endNode.offset);
39964
+ range.setEnd(startNode.node, startNode.offset);
39965
+ }
39872
39966
  }
39873
39967
  }
39874
39968
  /**
@@ -40170,8 +40264,7 @@ css /* scss */ `
40170
40264
  }
40171
40265
 
40172
40266
  .o-composer-assistant {
40173
- position: absolute;
40174
- margin: 1px 4px;
40267
+ margin-top: 1px;
40175
40268
 
40176
40269
  .o-semi-bold {
40177
40270
  /* FIXME: to remove in favor of Bootstrap
@@ -40222,10 +40315,11 @@ class Composer extends Component {
40222
40315
  });
40223
40316
  compositionActive = false;
40224
40317
  spreadsheetRect = useSpreadsheetRect();
40225
- get assistantStyle() {
40318
+ get assistantStyleProperties() {
40226
40319
  const composerRect = this.composerRef.el.getBoundingClientRect();
40227
40320
  const assistantStyle = {};
40228
- assistantStyle["min-width"] = `${this.props.rect?.width || ASSISTANT_WIDTH}px`;
40321
+ const minWidth = Math.min(this.props.rect?.width || Infinity, ASSISTANT_WIDTH);
40322
+ assistantStyle["min-width"] = `${minWidth}px`;
40229
40323
  const proposals = this.autoCompleteState.provider?.proposals;
40230
40324
  const proposalsHaveDescription = proposals?.some((proposal) => proposal.description);
40231
40325
  if (this.functionDescriptionState.showDescription || proposalsHaveDescription) {
@@ -40249,13 +40343,29 @@ class Composer extends Component {
40249
40343
  }
40250
40344
  }
40251
40345
  else {
40252
- assistantStyle["max-height"] = `${this.spreadsheetRect.height - composerRect.bottom}px`;
40346
+ assistantStyle["max-height"] = `${this.spreadsheetRect.height - composerRect.bottom - 1}px`; // -1: margin
40253
40347
  if (composerRect.left + ASSISTANT_WIDTH + SCROLLBAR_WIDTH + CLOSE_ICON_RADIUS >
40254
40348
  this.spreadsheetRect.width) {
40255
40349
  assistantStyle.right = `${CLOSE_ICON_RADIUS}px`;
40256
40350
  }
40257
40351
  }
40258
- return cssPropertiesToCss(assistantStyle);
40352
+ return assistantStyle;
40353
+ }
40354
+ get assistantStyle() {
40355
+ const allProperties = this.assistantStyleProperties;
40356
+ return cssPropertiesToCss({
40357
+ "max-height": allProperties["max-height"],
40358
+ width: allProperties["width"],
40359
+ "min-width": allProperties["min-width"],
40360
+ });
40361
+ }
40362
+ get assistantContainerStyle() {
40363
+ const allProperties = this.assistantStyleProperties;
40364
+ return cssPropertiesToCss({
40365
+ top: allProperties["top"],
40366
+ right: allProperties["right"],
40367
+ transform: allProperties["transform"],
40368
+ });
40259
40369
  }
40260
40370
  // we can't allow input events to be triggered while we remove and add back the content of the composer in processContent
40261
40371
  shouldProcessInputEvents = false;
@@ -46729,9 +46839,7 @@ class PivotSidePanelStore extends SpreadsheetStore {
46729
46839
  pivot: this.draft,
46730
46840
  });
46731
46841
  this.draft = null;
46732
- if (!this.alreadyNotified &&
46733
- !this.isDynamicPivotInViewport() &&
46734
- this.isStaticPivotInViewport()) {
46842
+ if (!this.alreadyNotified && this.isUpdatedPivotVisibleInViewportOnlyAsStaticPivot()) {
46735
46843
  const formulaId = this.getters.getPivotFormulaId(this.pivotId);
46736
46844
  const pivotExample = `=PIVOT(${formulaId})`;
46737
46845
  this.alreadyNotified = true;
@@ -46787,26 +46895,33 @@ class PivotSidePanelStore extends SpreadsheetStore {
46787
46895
  this.applyUpdate();
46788
46896
  }
46789
46897
  }
46790
- isDynamicPivotInViewport() {
46791
- for (const position of this.getters.getVisibleCellPositions()) {
46792
- const isDynamicPivot = this.getters.isSpillPivotFormula(position);
46793
- if (isDynamicPivot) {
46794
- return true;
46795
- }
46796
- }
46797
- return false;
46798
- }
46799
- isStaticPivotInViewport() {
46898
+ /**
46899
+ * @returns true if the updated pivot is visible in the viewport only as a
46900
+ * static pivot and not as a dynamic pivot
46901
+ */
46902
+ isUpdatedPivotVisibleInViewportOnlyAsStaticPivot() {
46903
+ let staticPivotCount = 0;
46904
+ const updatedPivotFormulaId = this.getters.getPivotFormulaId(this.pivotId);
46800
46905
  for (const position of this.getters.getVisibleCellPositions()) {
46801
46906
  const cell = this.getters.getCell(position);
46802
46907
  if (cell?.isFormula) {
46803
46908
  const pivotFunction = getFirstPivotFunction(cell.compiledFormula.tokens);
46804
- if (pivotFunction && pivotFunction.functionName !== "PIVOT") {
46805
- return true;
46909
+ const pivotFormulaId = pivotFunction?.args[0]?.value;
46910
+ if (pivotFunction && updatedPivotFormulaId === pivotFormulaId.toString()) {
46911
+ if (pivotFunction.functionName === "PIVOT") {
46912
+ // if we have at least one dynamic pivot visible inserted the viewport
46913
+ // we return false
46914
+ return false;
46915
+ }
46916
+ else {
46917
+ staticPivotCount++;
46918
+ }
46806
46919
  }
46807
46920
  }
46808
46921
  }
46809
- return false;
46922
+ // we return true if there are only static pivots visible inserted the viewport,
46923
+ // otherwise false
46924
+ return staticPivotCount > 0;
46810
46925
  }
46811
46926
  addDefaultDateTimeGranularity(fields, definition) {
46812
46927
  const { columns, rows } = definition;
@@ -60660,6 +60775,7 @@ class EvaluationPlugin extends CoreViewPlugin {
60660
60775
  exportForExcel(data) {
60661
60776
  for (const sheet of data.sheets) {
60662
60777
  sheet.cellValues = {};
60778
+ sheet.formulaSpillRanges = {};
60663
60779
  }
60664
60780
  for (const position of this.evaluator.getEvaluatedPositions()) {
60665
60781
  const evaluatedCell = this.evaluator.getEvaluatedCell(position);
@@ -60671,8 +60787,9 @@ class EvaluationPlugin extends CoreViewPlugin {
60671
60787
  const exportedSheetData = data.sheets.find((sheet) => sheet.id === position.sheetId);
60672
60788
  const formulaCell = this.getCorrespondingFormulaCell(position);
60673
60789
  if (formulaCell) {
60790
+ const cell = this.getters.getCell(position);
60674
60791
  isExported = isExportableToExcel(formulaCell.compiledFormula.tokens);
60675
- isFormula = isExported;
60792
+ isFormula = isExported && cell?.content === formulaCell.content;
60676
60793
  // If the cell contains a non-exported formula and that is evaluates to
60677
60794
  // nothing* ,we don't export it.
60678
60795
  // * non-falsy value are relevant and so are 0 and FALSE, which only leaves
@@ -60695,7 +60812,11 @@ class EvaluationPlugin extends CoreViewPlugin {
60695
60812
  content = !isExported ? newContent : exportedCellData;
60696
60813
  }
60697
60814
  exportedSheetData.cells[xc] = content;
60698
- exportedSheetData.cellValues[xc] = value;
60815
+ exportedSheetData.cellValues[xc] = evaluatedCell.type !== "error" ? value : undefined;
60816
+ const spillZone = this.getSpreadZone(position);
60817
+ if (spillZone) {
60818
+ exportedSheetData.formulaSpillRanges[xc] = this.getters.getRangeString(this.getters.getRangeFromZone(position.sheetId, spillZone), position.sheetId);
60819
+ }
60699
60820
  }
60700
60821
  }
60701
60822
  /**
@@ -62923,7 +63044,7 @@ class AutofillPlugin extends UIPlugin {
62923
63044
  getRule(cell, cells) {
62924
63045
  const rules = autofillRulesRegistry.getAll().sort((a, b) => a.sequence - b.sequence);
62925
63046
  const rule = rules.find((rule) => rule.condition(cell, cells));
62926
- return rule && rule.generateRule(cell, cells);
63047
+ return rule && this.direction && rule.generateRule(cell, cells, this.direction);
62927
63048
  }
62928
63049
  /**
62929
63050
  * Create the generator to be able to autofill the next cells.
@@ -73405,7 +73526,7 @@ function numberRef(reference) {
73405
73526
  `;
73406
73527
  }
73407
73528
 
73408
- function addFormula(formula, value) {
73529
+ function addFormula(formula, value, formulaSpillRange) {
73409
73530
  if (!formula) {
73410
73531
  return { attrs: [], node: escapeXml `` };
73411
73532
  }
@@ -73413,10 +73534,17 @@ function addFormula(formula, value) {
73413
73534
  if (type === undefined) {
73414
73535
  return { attrs: [], node: escapeXml `` };
73415
73536
  }
73416
- const attrs = [["t", type]];
73537
+ const attrs = [
73538
+ ["cm", "1"],
73539
+ ["t", type],
73540
+ ];
73417
73541
  const XlsxFormula = adaptFormulaToExcel(formula);
73418
73542
  const exportedValue = adaptFormulaValueToExcel(value);
73419
- const node = escapeXml /*xml*/ `<f>${XlsxFormula}</f><v>${exportedValue}</v>`;
73543
+ // We treat all formulas as array formulas (a simple formula
73544
+ // is an array formula that spills on only one cell) to avoid
73545
+ // trying to detect spilling sub-formulas which is not a trivial task.
73546
+ let node;
73547
+ node = escapeXml /*xml*/ `<f t="array" ref="${formulaSpillRange}">${XlsxFormula}</f><v>${exportedValue}</v>`;
73420
73548
  return { attrs, node };
73421
73549
  }
73422
73550
  function addContent(content, sharedStrings, forceString = false) {
@@ -74406,7 +74534,7 @@ function addRows(construct, data, sheet) {
74406
74534
  let cellNode = escapeXml ``;
74407
74535
  // Either formula or static value inside the cell
74408
74536
  if (content?.startsWith("=") && value !== undefined) {
74409
- const res = addFormula(content, value);
74537
+ const res = addFormula(content, value, sheet.formulaSpillRanges[xc] ?? xc);
74410
74538
  if (!res) {
74411
74539
  continue;
74412
74540
  }
@@ -74692,6 +74820,30 @@ function createWorksheets(data, construct) {
74692
74820
  `;
74693
74821
  files.push(createXMLFile(parseXML(sheetXml), `xl/worksheets/sheet${sheetIndex}.xml`, "sheet"));
74694
74822
  }
74823
+ const sheetMetadataXml = escapeXml /*xml*/ `
74824
+ <metadata xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:xda="http://schemas.microsoft.com/office/spreadsheetml/2017/dynamicarray">
74825
+ <metadataTypes count="1">
74826
+ <metadataType name="XLDAPR" minSupportedVersion="120000" copy="1" pasteAll="1"
74827
+ pasteValues="1" merge="1" splitFirst="1" rowColShift="1" clearFormats="1"
74828
+ clearComments="1" assign="1" coerce="1" cellMeta="1" />
74829
+ </metadataTypes>
74830
+ <futureMetadata name="XLDAPR" count="1">
74831
+ <bk>
74832
+ <extLst>
74833
+ <ext uri="{${ARRAY_FORMULA_URI}}">
74834
+ <xda:dynamicArrayProperties fDynamic="1" fCollapsed="0" />
74835
+ </ext>
74836
+ </extLst>
74837
+ </bk>
74838
+ </futureMetadata>
74839
+ <cellMetadata count="1">
74840
+ <bk>
74841
+ <rc t="1" v="0" />
74842
+ </bk>
74843
+ </cellMetadata>
74844
+ </metadata>
74845
+ `;
74846
+ files.push(createXMLFile(parseXML(sheetMetadataXml), "xl/metadata.xml", "metadata"));
74695
74847
  addRelsToFile(construct.relsFiles, "xl/_rels/workbook.xml.rels", {
74696
74848
  type: XLSX_RELATION_TYPE.sharedStrings,
74697
74849
  target: "sharedStrings.xml",
@@ -74700,6 +74852,10 @@ function createWorksheets(data, construct) {
74700
74852
  type: XLSX_RELATION_TYPE.styles,
74701
74853
  target: "styles.xml",
74702
74854
  });
74855
+ addRelsToFile(construct.relsFiles, "xl/_rels/workbook.xml.rels", {
74856
+ type: XLSX_RELATION_TYPE.metadata,
74857
+ target: "metadata.xml",
74858
+ });
74703
74859
  return files;
74704
74860
  }
74705
74861
  /**
@@ -75616,6 +75772,6 @@ const chartHelpers = { ...CHART_HELPERS, ...CHART_RUNTIME_HELPERS };
75616
75772
  export { AbstractCellClipboardHandler, AbstractChart, AbstractFigureClipboardHandler, CellErrorType, CommandResult, CorePlugin, CoreViewPlugin, 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 };
75617
75773
 
75618
75774
 
75619
- __info__.version = "18.2.1";
75620
- __info__.date = "2025-02-25T06:03:13.262Z";
75621
- __info__.hash = "3b4b5c9";
75775
+ __info__.version = "18.2.2";
75776
+ __info__.date = "2025-03-07T10:41:04.411Z";
75777
+ __info__.hash = "f567932";