@odoo/o-spreadsheet 18.2.1 → 18.2.3

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.3
6
+ * @date 2025-03-12T15:32:36.274Z
7
+ * @hash 81b0e08
8
8
  */
9
9
 
10
10
  (function (exports, owl) {
@@ -424,7 +424,6 @@
424
424
  * Sparse arrays remain sparse.
425
425
  */
426
426
  function deepCopy(obj) {
427
- const result = Array.isArray(obj) ? [] : {};
428
427
  switch (typeof obj) {
429
428
  case "object": {
430
429
  if (obj === null) {
@@ -436,8 +435,18 @@
436
435
  else if (!(isPlainObject(obj) || obj instanceof Array)) {
437
436
  throw new Error("Unsupported type: only objects and arrays are supported");
438
437
  }
439
- for (const key in obj) {
440
- result[key] = deepCopy(obj[key]);
438
+ const result = Array.isArray(obj) ? new Array(obj.length) : {};
439
+ if (Array.isArray(obj)) {
440
+ for (let i = 0, len = obj.length; i < len; i++) {
441
+ if (i in obj) {
442
+ result[i] = deepCopy(obj[i]);
443
+ }
444
+ }
445
+ }
446
+ else {
447
+ for (const key in obj) {
448
+ result[key] = deepCopy(obj[key]);
449
+ }
441
450
  }
442
451
  return result;
443
452
  }
@@ -2700,21 +2709,30 @@
2700
2709
  return mergedZones;
2701
2710
  }
2702
2711
 
2712
+ const globalReverseLookup$1 = new WeakMap();
2713
+ const globalIdCounter = new WeakMap();
2703
2714
  /**
2704
2715
  * Get the id of the given item (its key in the given dictionary).
2705
2716
  * If the given item does not exist in the dictionary, it creates one with a new id.
2706
2717
  */
2707
2718
  function getItemId(item, itemsDic) {
2708
- for (const key in itemsDic) {
2709
- if (deepEquals(itemsDic[key], item)) {
2710
- return parseInt(key, 10);
2711
- }
2719
+ if (!globalReverseLookup$1.has(itemsDic)) {
2720
+ globalReverseLookup$1.set(itemsDic, new Map());
2721
+ globalIdCounter.set(itemsDic, 0);
2722
+ }
2723
+ const reverseLookup = globalReverseLookup$1.get(itemsDic);
2724
+ const canonical = getCanonicalRepresentation(item);
2725
+ if (reverseLookup.has(canonical)) {
2726
+ const id = reverseLookup.get(canonical);
2727
+ itemsDic[id] = item;
2728
+ return id;
2712
2729
  }
2713
2730
  // Generate new Id if the item didn't exist in the dictionary
2714
- const ids = Object.keys(itemsDic);
2715
- const maxId = ids.length === 0 ? 0 : largeMax(ids.map((id) => parseInt(id, 10)));
2716
- itemsDic[maxId + 1] = item;
2717
- return maxId + 1;
2731
+ const newId = globalIdCounter.get(itemsDic) + 1;
2732
+ reverseLookup.set(canonical, newId);
2733
+ globalIdCounter.set(itemsDic, newId);
2734
+ itemsDic[newId] = item;
2735
+ return newId;
2718
2736
  }
2719
2737
  function groupItemIdsByZones(positionsByItemId) {
2720
2738
  const result = {};
@@ -2738,6 +2756,33 @@
2738
2756
  }
2739
2757
  }
2740
2758
  }
2759
+ function getCanonicalRepresentation(item) {
2760
+ if (item === null)
2761
+ return "null";
2762
+ if (item === undefined)
2763
+ return "undefined";
2764
+ if (typeof item !== "object")
2765
+ return String(item);
2766
+ if (Array.isArray(item)) {
2767
+ const len = item.length;
2768
+ let result = "[";
2769
+ for (let i = 0; i < len; i++) {
2770
+ if (i > 0)
2771
+ result += ",";
2772
+ result += getCanonicalRepresentation(item[i]);
2773
+ }
2774
+ return result + "]";
2775
+ }
2776
+ const keys = Object.keys(item).sort();
2777
+ let repr = "{";
2778
+ for (const key of keys) {
2779
+ if (item[key] !== undefined) {
2780
+ repr += `"${key}":${getCanonicalRepresentation(item[key])},`;
2781
+ }
2782
+ }
2783
+ repr += "}";
2784
+ return repr;
2785
+ }
2741
2786
 
2742
2787
  // -----------------------------------------------------------------------------
2743
2788
  // Date Type
@@ -6101,8 +6146,9 @@
6101
6146
  if (zone.bottom !== zone.top && zone.left != zone.right) {
6102
6147
  if (zone.right) {
6103
6148
  for (let j = zone.left; j <= zone.right; ++j) {
6149
+ const datasetOptions = j === zone.left ? dataSet : { yAxisId: dataSet.yAxisId };
6104
6150
  postProcessedRanges.push({
6105
- ...dataSet,
6151
+ ...datasetOptions,
6106
6152
  dataRange: `${sheetPrefix}${zoneToXc({
6107
6153
  left: j,
6108
6154
  right: j,
@@ -6114,8 +6160,9 @@
6114
6160
  }
6115
6161
  else {
6116
6162
  for (let j = zone.top; j <= zone.bottom; ++j) {
6163
+ const datasetOptions = j === zone.top ? dataSet : { yAxisId: dataSet.yAxisId };
6117
6164
  postProcessedRanges.push({
6118
- ...dataSet,
6165
+ ...datasetOptions,
6119
6166
  dataRange: `${sheetPrefix}${zoneToXc({
6120
6167
  left: zone.left,
6121
6168
  right: zone.right,
@@ -8288,7 +8335,8 @@
8288
8335
  const possibleValues = pivot
8289
8336
  .getPossibleFieldValues(columns[i])
8290
8337
  .map((v) => v.value);
8291
- if (!possibleValues.includes(sortedColumn.domain[i].value)) {
8338
+ if (!possibleValues.includes(sortedColumn.domain[i].value) &&
8339
+ !(sortedColumn.domain[i].value === null && possibleValues.includes(""))) {
8292
8340
  return false;
8293
8341
  }
8294
8342
  }
@@ -9533,150 +9581,6 @@ stores.inject(MyMetaStore, storeInstance);
9533
9581
  }
9534
9582
  }
9535
9583
 
9536
- /**
9537
- * This file is largely inspired by owl 1.
9538
- * `css` tag has been removed from owl 2 without workaround to manage css.
9539
- * So, the solution was to import the behavior of owl 1 directly in our
9540
- * codebase, with one difference: the css is added to the sheet as soon as the
9541
- * css tag is executed. In owl 1, the css was added as soon as a Component was
9542
- * created for the first time.
9543
- */
9544
- const STYLESHEETS = {};
9545
- let nextId = 0;
9546
- /**
9547
- * CSS tag helper for defining inline stylesheets. With this, one can simply define
9548
- * an inline stylesheet with just the following code:
9549
- * ```js
9550
- * css`.component-a { color: red; }`;
9551
- * ```
9552
- */
9553
- function css(strings, ...args) {
9554
- const name = `__sheet__${nextId++}`;
9555
- const value = String.raw(strings, ...args);
9556
- registerSheet(name, value);
9557
- activateSheet(name);
9558
- return name;
9559
- }
9560
- function processSheet(str) {
9561
- const tokens = str.split(/(\{|\}|;)/).map((s) => s.trim());
9562
- const selectorStack = [];
9563
- const parts = [];
9564
- let rules = [];
9565
- function generateSelector(stackIndex, parentSelector) {
9566
- const parts = [];
9567
- for (const selector of selectorStack[stackIndex]) {
9568
- let part = (parentSelector && parentSelector + " " + selector) || selector;
9569
- if (part.includes("&")) {
9570
- part = selector.replace(/&/g, parentSelector || "");
9571
- }
9572
- if (stackIndex < selectorStack.length - 1) {
9573
- part = generateSelector(stackIndex + 1, part);
9574
- }
9575
- parts.push(part);
9576
- }
9577
- return parts.join(", ");
9578
- }
9579
- function generateRules() {
9580
- if (rules.length) {
9581
- parts.push(generateSelector(0) + " {");
9582
- parts.push(...rules);
9583
- parts.push("}");
9584
- rules = [];
9585
- }
9586
- }
9587
- while (tokens.length) {
9588
- let token = tokens.shift();
9589
- if (token === "}") {
9590
- generateRules();
9591
- selectorStack.pop();
9592
- }
9593
- else {
9594
- if (tokens[0] === "{") {
9595
- generateRules();
9596
- selectorStack.push(token.split(/\s*,\s*/));
9597
- tokens.shift();
9598
- }
9599
- if (tokens[0] === ";") {
9600
- rules.push(" " + token + ";");
9601
- }
9602
- }
9603
- }
9604
- return parts.join("\n");
9605
- }
9606
- function registerSheet(id, css) {
9607
- const sheet = document.createElement("style");
9608
- sheet.textContent = processSheet(css);
9609
- STYLESHEETS[id] = sheet;
9610
- }
9611
- function activateSheet(id) {
9612
- const sheet = STYLESHEETS[id];
9613
- sheet.setAttribute("component", id);
9614
- document.head.appendChild(sheet);
9615
- }
9616
- function getTextDecoration({ strikethrough, underline, }) {
9617
- if (!strikethrough && !underline) {
9618
- return "none";
9619
- }
9620
- return `${strikethrough ? "line-through" : ""} ${underline ? "underline" : ""}`;
9621
- }
9622
- /**
9623
- * Convert the cell style to CSS properties.
9624
- */
9625
- function cellStyleToCss(style) {
9626
- const attributes = cellTextStyleToCss(style);
9627
- if (!style)
9628
- return attributes;
9629
- if (style.fillColor) {
9630
- attributes["background"] = style.fillColor;
9631
- }
9632
- return attributes;
9633
- }
9634
- /**
9635
- * Convert the cell text style to CSS properties.
9636
- */
9637
- function cellTextStyleToCss(style) {
9638
- const attributes = {};
9639
- if (!style)
9640
- return attributes;
9641
- if (style.bold) {
9642
- attributes["font-weight"] = "bold";
9643
- }
9644
- if (style.italic) {
9645
- attributes["font-style"] = "italic";
9646
- }
9647
- if (style.strikethrough || style.underline) {
9648
- let decoration = style.strikethrough ? "line-through" : "";
9649
- decoration = style.underline ? decoration + " underline" : decoration;
9650
- attributes["text-decoration"] = decoration;
9651
- }
9652
- if (style.textColor) {
9653
- attributes["color"] = style.textColor;
9654
- }
9655
- return attributes;
9656
- }
9657
- /**
9658
- * Transform CSS properties into a CSS string.
9659
- */
9660
- function cssPropertiesToCss(attributes) {
9661
- let styleStr = "";
9662
- for (const attName in attributes) {
9663
- if (!attributes[attName]) {
9664
- continue;
9665
- }
9666
- styleStr += `${attName}:${attributes[attName]}; `;
9667
- }
9668
- return styleStr;
9669
- }
9670
- function getElementMargins(el) {
9671
- const style = window.getComputedStyle(el);
9672
- return {
9673
- top: parseInt(style.marginTop, 10) || 0,
9674
- bottom: parseInt(style.marginBottom, 10) || 0,
9675
- left: parseInt(style.marginLeft, 10) || 0,
9676
- right: parseInt(style.marginRight, 10) || 0,
9677
- };
9678
- }
9679
-
9680
9584
  const TREND_LINE_XAXIS_ID = "x1";
9681
9585
  /**
9682
9586
  * This file contains helpers that are common to different charts (mainly
@@ -10221,79 +10125,341 @@ stores.inject(MyMetaStore, storeInstance);
10221
10125
  return bars.find((bar, i) => i > startIndex && bar.height !== 0);
10222
10126
  }
10223
10127
 
10224
- window.Chart?.register(waterfallLinesPlugin);
10225
- window.Chart?.register(chartShowValuesPlugin);
10226
- css /* scss */ `
10227
- .o-spreadsheet {
10228
- .o-chart-custom-tooltip {
10229
- font-size: 12px;
10230
- background-color: #fff;
10231
- z-index: ${ComponentsImportance.FigureTooltip};
10232
- }
10128
+ const GAUGE_PADDING_SIDE = 30;
10129
+ const GAUGE_PADDING_TOP = 10;
10130
+ const GAUGE_PADDING_BOTTOM = 20;
10131
+ const GAUGE_LABELS_FONT_SIZE = 12;
10132
+ const GAUGE_DEFAULT_VALUE_FONT_SIZE = 80;
10133
+ const GAUGE_BACKGROUND_COLOR = "#F3F2F1";
10134
+ const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
10135
+ const GAUGE_TITLE_SECTION_HEIGHT = 25;
10136
+ function drawGaugeChart(canvas, runtime) {
10137
+ const canvasBoundingRect = canvas.getBoundingClientRect();
10138
+ canvas.width = canvasBoundingRect.width;
10139
+ canvas.height = canvasBoundingRect.height;
10140
+ const ctx = canvas.getContext("2d");
10141
+ const config = getGaugeRenderingConfig(canvasBoundingRect, runtime, ctx);
10142
+ drawBackground(ctx, config);
10143
+ drawGauge(ctx, config);
10144
+ drawInflectionValues(ctx, config);
10145
+ drawLabels(ctx, config);
10146
+ drawTitle(ctx, config);
10233
10147
  }
10234
- `;
10235
- class ChartJsComponent extends owl.Component {
10236
- static template = "o-spreadsheet-ChartJsComponent";
10237
- static props = {
10238
- figure: Object,
10148
+ function drawGauge(ctx, config) {
10149
+ ctx.save();
10150
+ const gauge = config.gauge;
10151
+ const arcCenterX = gauge.rect.x + gauge.rect.width / 2;
10152
+ const arcCenterY = gauge.rect.y + gauge.rect.height;
10153
+ const arcRadius = gauge.rect.height - gauge.arcWidth / 2;
10154
+ if (arcRadius < 0) {
10155
+ return;
10156
+ }
10157
+ const gaugeAngle = gauge.percentage === 1 ? 0 : Math.PI * (1 + gauge.percentage);
10158
+ // Gauge background
10159
+ ctx.strokeStyle = GAUGE_BACKGROUND_COLOR;
10160
+ ctx.beginPath();
10161
+ ctx.lineWidth = gauge.arcWidth;
10162
+ ctx.arc(arcCenterX, arcCenterY, arcRadius, gaugeAngle, 0);
10163
+ ctx.stroke();
10164
+ // Gauge value
10165
+ ctx.strokeStyle = gauge.color;
10166
+ ctx.beginPath();
10167
+ ctx.arc(arcCenterX, arcCenterY, arcRadius, Math.PI, gaugeAngle);
10168
+ ctx.stroke();
10169
+ ctx.restore();
10170
+ }
10171
+ function drawBackground(ctx, config) {
10172
+ ctx.save();
10173
+ ctx.fillStyle = config.backgroundColor;
10174
+ ctx.fillRect(0, 0, config.width, config.height);
10175
+ ctx.restore();
10176
+ }
10177
+ function drawLabels(ctx, config) {
10178
+ for (const label of [config.minLabel, config.maxLabel, config.gaugeValue]) {
10179
+ ctx.save();
10180
+ ctx.textAlign = "center";
10181
+ ctx.fillStyle = label.color;
10182
+ ctx.font = `${label.fontSize}px ${DEFAULT_FONT}`;
10183
+ ctx.fillText(label.label, label.textPosition.x, label.textPosition.y);
10184
+ ctx.restore();
10185
+ }
10186
+ }
10187
+ function drawInflectionValues(ctx, config) {
10188
+ const { x: rectX, y: rectY, width, height } = config.gauge.rect;
10189
+ for (const inflectionValue of config.inflectionValues) {
10190
+ ctx.save();
10191
+ ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment
10192
+ ctx.rotate(Math.PI / 2 - inflectionValue.rotation);
10193
+ ctx.lineWidth = 2;
10194
+ ctx.strokeStyle = chartMutedFontColor(config.backgroundColor) + "aa";
10195
+ ctx.beginPath();
10196
+ ctx.moveTo(0, -(height - config.gauge.arcWidth));
10197
+ ctx.lineTo(0, -height - 3);
10198
+ ctx.stroke();
10199
+ ctx.textAlign = "center";
10200
+ ctx.font = `${inflectionValue.fontSize}px ${DEFAULT_FONT}`;
10201
+ ctx.fillStyle = inflectionValue.color;
10202
+ const textY = -height - GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN - inflectionValue.offset;
10203
+ ctx.fillText(inflectionValue.label, 0, textY);
10204
+ ctx.restore();
10205
+ }
10206
+ }
10207
+ function drawTitle(ctx, config) {
10208
+ ctx.save();
10209
+ const title = config.title;
10210
+ ctx.font = getDefaultContextFont(title.fontSize, title.bold, title.italic);
10211
+ ctx.textBaseline = "middle";
10212
+ ctx.fillStyle = title.color;
10213
+ ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
10214
+ ctx.restore();
10215
+ }
10216
+ function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
10217
+ const maxValue = runtime.maxValue;
10218
+ const minValue = runtime.minValue;
10219
+ const gaugeValue = runtime.gaugeValue;
10220
+ const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
10221
+ const gaugeArcWidth = gaugeRect.width / 6;
10222
+ const gaugePercentage = gaugeValue
10223
+ ? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
10224
+ : 0;
10225
+ const gaugeValuePosition = {
10226
+ x: boundingRect.width / 2,
10227
+ y: gaugeRect.y + gaugeRect.height - gaugeRect.height / 12,
10239
10228
  };
10240
- canvas = owl.useRef("graphContainer");
10241
- chart;
10242
- currentRuntime;
10243
- get background() {
10244
- return this.chartRuntime.background;
10229
+ let gaugeValueFontSize = GAUGE_DEFAULT_VALUE_FONT_SIZE;
10230
+ // Scale down the font size if the gaugeRect is too small
10231
+ if (gaugeRect.height < 300) {
10232
+ gaugeValueFontSize = gaugeValueFontSize * (gaugeRect.height / 300);
10245
10233
  }
10246
- get canvasStyle() {
10247
- return `background-color: ${this.background}`;
10234
+ // Scale down the font size if the text is too long
10235
+ const maxTextWidth = gaugeRect.width / 2;
10236
+ const gaugeLabel = gaugeValue?.label || "-";
10237
+ if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
10238
+ gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
10248
10239
  }
10249
- get chartRuntime() {
10250
- const runtime = this.env.model.getters.getChartRuntime(this.props.figure.id);
10251
- if (!("chartJsConfig" in runtime)) {
10252
- throw new Error("Unsupported chart runtime");
10253
- }
10254
- return runtime;
10240
+ const minLabelPosition = {
10241
+ x: gaugeRect.x + gaugeArcWidth / 2,
10242
+ y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
10243
+ };
10244
+ const maxLabelPosition = {
10245
+ x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
10246
+ y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
10247
+ };
10248
+ const textColor = chartMutedFontColor(runtime.background);
10249
+ const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);
10250
+ let x = 0, titleWidth = 0, titleHeight = 0;
10251
+ if (runtime.title.text) {
10252
+ ({ width: titleWidth, height: titleHeight } = computeTextDimension(ctx, runtime.title.text, { fontSize: CHART_TITLE_FONT_SIZE, ...runtime.title }, "px"));
10255
10253
  }
10256
- setup() {
10257
- owl.onMounted(() => {
10258
- const runtime = this.chartRuntime;
10259
- this.currentRuntime = runtime;
10260
- // Note: chartJS modify the runtime in place, so it's important to give it a copy
10261
- this.createChart(deepCopy(runtime.chartJsConfig));
10262
- });
10263
- owl.onWillUnmount(() => this.chart?.destroy());
10264
- owl.useEffect(() => {
10265
- const runtime = this.chartRuntime;
10266
- if (runtime !== this.currentRuntime) {
10267
- if (runtime.chartJsConfig.type !== this.currentRuntime.chartJsConfig.type) {
10268
- this.chart?.destroy();
10269
- this.createChart(deepCopy(runtime.chartJsConfig));
10270
- }
10271
- else {
10272
- this.updateChartJs(deepCopy(runtime));
10273
- }
10274
- this.currentRuntime = runtime;
10275
- }
10254
+ switch (runtime.title.align) {
10255
+ case "right":
10256
+ x = boundingRect.width - titleWidth - CHART_PADDING$1;
10257
+ break;
10258
+ case "center":
10259
+ x = (boundingRect.width - titleWidth) / 2;
10260
+ break;
10261
+ case "left":
10262
+ default:
10263
+ x = CHART_PADDING$1;
10264
+ break;
10265
+ }
10266
+ return {
10267
+ width: boundingRect.width,
10268
+ height: boundingRect.height,
10269
+ title: {
10270
+ label: runtime.title.text ?? "",
10271
+ fontSize: runtime.title.fontSize ?? CHART_TITLE_FONT_SIZE,
10272
+ textPosition: {
10273
+ x,
10274
+ y: CHART_PADDING_TOP + titleHeight / 2,
10275
+ },
10276
+ color: runtime.title.color ?? textColor,
10277
+ bold: runtime.title.bold,
10278
+ italic: runtime.title.italic,
10279
+ },
10280
+ backgroundColor: runtime.background,
10281
+ gauge: {
10282
+ rect: gaugeRect,
10283
+ arcWidth: gaugeArcWidth,
10284
+ percentage: clip(gaugePercentage, 0, 1),
10285
+ color: getGaugeColor(runtime),
10286
+ },
10287
+ inflectionValues,
10288
+ gaugeValue: {
10289
+ label: gaugeLabel,
10290
+ textPosition: gaugeValuePosition,
10291
+ fontSize: gaugeValueFontSize,
10292
+ color: textColor,
10293
+ },
10294
+ minLabel: {
10295
+ label: runtime.minValue.label,
10296
+ textPosition: minLabelPosition,
10297
+ fontSize: GAUGE_LABELS_FONT_SIZE,
10298
+ color: textColor,
10299
+ },
10300
+ maxLabel: {
10301
+ label: runtime.maxValue.label,
10302
+ textPosition: maxLabelPosition,
10303
+ fontSize: GAUGE_LABELS_FONT_SIZE,
10304
+ color: textColor,
10305
+ },
10306
+ };
10307
+ }
10308
+ /**
10309
+ * Get the rectangle in which the gauge will be drawn, based on the bounding rectangle of the canvas and leaving
10310
+ * space for the title and labels.
10311
+ */
10312
+ function getGaugeRect(boundingRect, title) {
10313
+ const titleHeight = title ? GAUGE_TITLE_SECTION_HEIGHT : 0;
10314
+ const drawHeight = boundingRect.height - GAUGE_PADDING_BOTTOM - titleHeight - GAUGE_PADDING_TOP;
10315
+ const drawWidth = boundingRect.width - GAUGE_PADDING_SIDE * 2;
10316
+ let gaugeWidth;
10317
+ let gaugeHeight;
10318
+ if (drawWidth > 2 * drawHeight) {
10319
+ gaugeWidth = 2 * drawHeight;
10320
+ gaugeHeight = drawHeight;
10321
+ }
10322
+ else {
10323
+ gaugeWidth = drawWidth;
10324
+ gaugeHeight = drawWidth / 2;
10325
+ }
10326
+ const gaugeX = GAUGE_PADDING_SIDE + (drawWidth - gaugeWidth) / 2;
10327
+ const gaugeY = titleHeight + GAUGE_PADDING_TOP + (drawHeight - gaugeHeight) / 2;
10328
+ return {
10329
+ x: gaugeX,
10330
+ y: gaugeY,
10331
+ width: gaugeWidth,
10332
+ height: gaugeHeight,
10333
+ };
10334
+ }
10335
+ /**
10336
+ * 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).
10337
+ *
10338
+ * Also compute an offset for the text so that it doesn't overlap with other text.
10339
+ */
10340
+ function getInflectionValues(runtime, gaugeRect, textColor, ctx) {
10341
+ const maxValue = runtime.maxValue;
10342
+ const minValue = runtime.minValue;
10343
+ const gaugeCircleCenter = {
10344
+ x: gaugeRect.x + gaugeRect.width / 2,
10345
+ y: gaugeRect.y + gaugeRect.height,
10346
+ };
10347
+ const textStyle = { fontSize: GAUGE_LABELS_FONT_SIZE };
10348
+ const inflectionValues = [];
10349
+ const inflectionValuesTextRects = [];
10350
+ for (const inflectionValue of runtime.inflectionValues) {
10351
+ const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
10352
+ const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
10353
+ const angle = Math.PI - Math.PI * percentage;
10354
+ const textRect = getRectangleTangentToCircle(angle, // angle between X axis and the point where the rectangle is tangent to the circle
10355
+ gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, // radius of the gauge circle + margin below text
10356
+ gaugeCircleCenter.x, // center of the gauge circle
10357
+ gaugeCircleCenter.y, // center of the gauge circle
10358
+ labelWidth + 2, // width of the text + some margin
10359
+ GAUGE_LABELS_FONT_SIZE // height of the text
10360
+ );
10361
+ let offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect))
10362
+ ? GAUGE_LABELS_FONT_SIZE
10363
+ : 0;
10364
+ inflectionValuesTextRects.push(textRect);
10365
+ inflectionValues.push({
10366
+ rotation: angle,
10367
+ label: inflectionValue.label,
10368
+ fontSize: GAUGE_LABELS_FONT_SIZE,
10369
+ color: textColor,
10370
+ offset,
10276
10371
  });
10277
10372
  }
10278
- createChart(chartData) {
10279
- const canvas = this.canvas.el;
10280
- const ctx = canvas.getContext("2d");
10281
- this.chart = new window.Chart(ctx, chartData);
10373
+ return inflectionValues;
10374
+ }
10375
+ function getGaugeColor(runtime) {
10376
+ const gaugeValue = runtime.gaugeValue?.value;
10377
+ if (gaugeValue === undefined) {
10378
+ return GAUGE_BACKGROUND_COLOR;
10282
10379
  }
10283
- updateChartJs(chartRuntime) {
10284
- const chartData = chartRuntime.chartJsConfig;
10285
- if (chartData.data && chartData.data.datasets) {
10286
- this.chart.data = chartData.data;
10287
- if (chartData.options?.plugins?.title) {
10288
- this.chart.config.options.plugins.title = chartData.options.plugins.title;
10289
- }
10380
+ for (let i = 0; i < runtime.inflectionValues.length; i++) {
10381
+ const inflectionValue = runtime.inflectionValues[i];
10382
+ if (inflectionValue.operator === "<" && gaugeValue < inflectionValue.value) {
10383
+ return runtime.colors[i];
10290
10384
  }
10291
- else {
10292
- this.chart.data.datasets = [];
10385
+ else if (inflectionValue.operator === "<=" && gaugeValue <= inflectionValue.value) {
10386
+ return runtime.colors[i];
10293
10387
  }
10294
- this.chart.config.options = chartData.options;
10295
- this.chart.update();
10296
10388
  }
10389
+ return runtime.colors.at(-1);
10390
+ }
10391
+ function getSegmentsOfRectangle(rectangle) {
10392
+ return [
10393
+ { start: rectangle.topLeft, end: rectangle.topRight },
10394
+ { start: rectangle.topRight, end: rectangle.bottomRight },
10395
+ { start: rectangle.bottomRight, end: rectangle.bottomLeft },
10396
+ { start: rectangle.bottomLeft, end: rectangle.topLeft },
10397
+ ];
10398
+ }
10399
+ /**
10400
+ * Check if two segment intersect. The case where the segments are colinear (both segments on the same line)
10401
+ * is not handled.
10402
+ */
10403
+ function doSegmentIntersect(segment1, segment2) {
10404
+ const A = segment1.start;
10405
+ const B = segment1.end;
10406
+ const C = segment2.start;
10407
+ const D = segment2.end;
10408
+ /**
10409
+ * Line segment intersection algorithm
10410
+ * https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
10411
+ */
10412
+ function ccw(a, b, c) {
10413
+ return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
10414
+ }
10415
+ return ccw(A, C, D) !== ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D);
10416
+ }
10417
+ function doRectanglesIntersect(rect1, rect2) {
10418
+ const segments1 = getSegmentsOfRectangle(rect1);
10419
+ const segments2 = getSegmentsOfRectangle(rect2);
10420
+ for (const segment1 of segments1) {
10421
+ for (const segment2 of segments2) {
10422
+ if (doSegmentIntersect(segment1, segment2)) {
10423
+ return true;
10424
+ }
10425
+ }
10426
+ }
10427
+ return false;
10428
+ }
10429
+ /**
10430
+ * Get the rectangle that is tangent to a circle at a given angle.
10431
+ *
10432
+ * @param angle angle between X axis and the point where the rectangle is tangent to the circle
10433
+ */
10434
+ function getRectangleTangentToCircle(angle, radius, circleCenterX, circleCenterY, rectWidth, rectHeight) {
10435
+ const cos = Math.cos(angle);
10436
+ const sin = Math.sin(angle);
10437
+ // x, y are the distance from the center of the circle to the point where the rectangle is tangent to the circle
10438
+ const x = cos * radius;
10439
+ const y = sin * radius;
10440
+ // x2, y2 are the distance from the point the rectangle is tangent to the circle to the bottom left corner of the rectangle
10441
+ const x2 = sin * (rectWidth / 2); // cos(angle + 90°) = sin(angle)
10442
+ const y2 = cos * (rectWidth / 2);
10443
+ const bottomRight = {
10444
+ x: x + x2 + circleCenterX,
10445
+ y: circleCenterY - (y - y2),
10446
+ };
10447
+ const bottomLeft = {
10448
+ x: x - x2 + circleCenterX,
10449
+ y: circleCenterY - (y + y2),
10450
+ };
10451
+ // Same as above but for the top corners of the rectangle (radius + rectangle height instead of radius)
10452
+ const xp = cos * (radius + rectHeight);
10453
+ const yp = sin * (radius + rectHeight);
10454
+ const topLeft = {
10455
+ x: xp - x2 + circleCenterX,
10456
+ y: circleCenterY - (yp + y2),
10457
+ };
10458
+ const topRight = {
10459
+ x: xp + x2 + circleCenterX,
10460
+ y: circleCenterY - (yp - y2),
10461
+ };
10462
+ return { bottomLeft, bottomRight, topRight, topLeft };
10297
10463
  }
10298
10464
 
10299
10465
  /**
@@ -10875,6 +11041,299 @@ stores.inject(MyMetaStore, storeInstance);
10875
11041
  }
10876
11042
  }
10877
11043
 
11044
+ const CHART_COMMON_OPTIONS = {
11045
+ // https://www.chartjs.org/docs/latest/general/responsive.html
11046
+ responsive: true, // will resize when its container is resized
11047
+ maintainAspectRatio: false, // doesn't maintain the aspect ratio (width/height =2 by default) so the user has the choice of the exact layout
11048
+ elements: {
11049
+ line: {
11050
+ fill: false, // do not fill the area under line charts
11051
+ },
11052
+ point: {
11053
+ hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
11054
+ },
11055
+ },
11056
+ animation: false,
11057
+ };
11058
+ function chartToImage(runtime, figure, type) {
11059
+ // wrap the canvas in a div with a fixed size because chart.js would
11060
+ // fill the whole page otherwise
11061
+ const div = document.createElement("div");
11062
+ div.style.width = `${figure.width}px`;
11063
+ div.style.height = `${figure.height}px`;
11064
+ const canvas = document.createElement("canvas");
11065
+ div.append(canvas);
11066
+ canvas.setAttribute("width", figure.width.toString());
11067
+ canvas.setAttribute("height", figure.height.toString());
11068
+ // we have to add the canvas to the DOM otherwise it won't be rendered
11069
+ document.body.append(div);
11070
+ if ("chartJsConfig" in runtime) {
11071
+ const config = deepCopy(runtime.chartJsConfig);
11072
+ config.plugins = [backgroundColorChartJSPlugin];
11073
+ const Chart = getChartJSConstructor();
11074
+ const chart = new Chart(canvas, config);
11075
+ const imgContent = chart.toBase64Image();
11076
+ chart.destroy();
11077
+ div.remove();
11078
+ return imgContent;
11079
+ }
11080
+ else if (type === "scorecard") {
11081
+ const design = getScorecardConfiguration(figure, runtime);
11082
+ drawScoreChart(design, canvas);
11083
+ const imgContent = canvas.toDataURL();
11084
+ div.remove();
11085
+ return imgContent;
11086
+ }
11087
+ else if (type === "gauge") {
11088
+ drawGaugeChart(canvas, runtime);
11089
+ const imgContent = canvas.toDataURL();
11090
+ div.remove();
11091
+ return imgContent;
11092
+ }
11093
+ return undefined;
11094
+ }
11095
+ /**
11096
+ * Custom chart.js plugin to set the background color of the canvas
11097
+ * https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md
11098
+ */
11099
+ const backgroundColorChartJSPlugin = {
11100
+ id: "customCanvasBackgroundColor",
11101
+ beforeDraw: (chart) => {
11102
+ const { ctx } = chart;
11103
+ ctx.save();
11104
+ ctx.globalCompositeOperation = "destination-over";
11105
+ ctx.fillStyle = "#ffffff";
11106
+ ctx.fillRect(0, 0, chart.width, chart.height);
11107
+ ctx.restore();
11108
+ },
11109
+ };
11110
+ /** Return window.Chart, making sure all our extensions are loaded in ChartJS */
11111
+ function getChartJSConstructor() {
11112
+ if (window.Chart && !window.Chart?.registry.plugins.get("chartShowValuesPlugin")) {
11113
+ window.Chart.register(chartShowValuesPlugin);
11114
+ window.Chart.register(waterfallLinesPlugin);
11115
+ }
11116
+ return window.Chart;
11117
+ }
11118
+
11119
+ /**
11120
+ * This file is largely inspired by owl 1.
11121
+ * `css` tag has been removed from owl 2 without workaround to manage css.
11122
+ * So, the solution was to import the behavior of owl 1 directly in our
11123
+ * codebase, with one difference: the css is added to the sheet as soon as the
11124
+ * css tag is executed. In owl 1, the css was added as soon as a Component was
11125
+ * created for the first time.
11126
+ */
11127
+ const STYLESHEETS = {};
11128
+ let nextId = 0;
11129
+ /**
11130
+ * CSS tag helper for defining inline stylesheets. With this, one can simply define
11131
+ * an inline stylesheet with just the following code:
11132
+ * ```js
11133
+ * css`.component-a { color: red; }`;
11134
+ * ```
11135
+ */
11136
+ function css(strings, ...args) {
11137
+ const name = `__sheet__${nextId++}`;
11138
+ const value = String.raw(strings, ...args);
11139
+ registerSheet(name, value);
11140
+ activateSheet(name);
11141
+ return name;
11142
+ }
11143
+ function processSheet(str) {
11144
+ const tokens = str.split(/(\{|\}|;)/).map((s) => s.trim());
11145
+ const selectorStack = [];
11146
+ const parts = [];
11147
+ let rules = [];
11148
+ function generateSelector(stackIndex, parentSelector) {
11149
+ const parts = [];
11150
+ for (const selector of selectorStack[stackIndex]) {
11151
+ let part = (parentSelector && parentSelector + " " + selector) || selector;
11152
+ if (part.includes("&")) {
11153
+ part = selector.replace(/&/g, parentSelector || "");
11154
+ }
11155
+ if (stackIndex < selectorStack.length - 1) {
11156
+ part = generateSelector(stackIndex + 1, part);
11157
+ }
11158
+ parts.push(part);
11159
+ }
11160
+ return parts.join(", ");
11161
+ }
11162
+ function generateRules() {
11163
+ if (rules.length) {
11164
+ parts.push(generateSelector(0) + " {");
11165
+ parts.push(...rules);
11166
+ parts.push("}");
11167
+ rules = [];
11168
+ }
11169
+ }
11170
+ while (tokens.length) {
11171
+ let token = tokens.shift();
11172
+ if (token === "}") {
11173
+ generateRules();
11174
+ selectorStack.pop();
11175
+ }
11176
+ else {
11177
+ if (tokens[0] === "{") {
11178
+ generateRules();
11179
+ selectorStack.push(token.split(/\s*,\s*/));
11180
+ tokens.shift();
11181
+ }
11182
+ if (tokens[0] === ";") {
11183
+ rules.push(" " + token + ";");
11184
+ }
11185
+ }
11186
+ }
11187
+ return parts.join("\n");
11188
+ }
11189
+ function registerSheet(id, css) {
11190
+ const sheet = document.createElement("style");
11191
+ sheet.textContent = processSheet(css);
11192
+ STYLESHEETS[id] = sheet;
11193
+ }
11194
+ function activateSheet(id) {
11195
+ const sheet = STYLESHEETS[id];
11196
+ sheet.setAttribute("component", id);
11197
+ document.head.appendChild(sheet);
11198
+ }
11199
+ function getTextDecoration({ strikethrough, underline, }) {
11200
+ if (!strikethrough && !underline) {
11201
+ return "none";
11202
+ }
11203
+ return `${strikethrough ? "line-through" : ""} ${underline ? "underline" : ""}`;
11204
+ }
11205
+ /**
11206
+ * Convert the cell style to CSS properties.
11207
+ */
11208
+ function cellStyleToCss(style) {
11209
+ const attributes = cellTextStyleToCss(style);
11210
+ if (!style)
11211
+ return attributes;
11212
+ if (style.fillColor) {
11213
+ attributes["background"] = style.fillColor;
11214
+ }
11215
+ return attributes;
11216
+ }
11217
+ /**
11218
+ * Convert the cell text style to CSS properties.
11219
+ */
11220
+ function cellTextStyleToCss(style) {
11221
+ const attributes = {};
11222
+ if (!style)
11223
+ return attributes;
11224
+ if (style.bold) {
11225
+ attributes["font-weight"] = "bold";
11226
+ }
11227
+ if (style.italic) {
11228
+ attributes["font-style"] = "italic";
11229
+ }
11230
+ if (style.strikethrough || style.underline) {
11231
+ let decoration = style.strikethrough ? "line-through" : "";
11232
+ decoration = style.underline ? decoration + " underline" : decoration;
11233
+ attributes["text-decoration"] = decoration;
11234
+ }
11235
+ if (style.textColor) {
11236
+ attributes["color"] = style.textColor;
11237
+ }
11238
+ return attributes;
11239
+ }
11240
+ /**
11241
+ * Transform CSS properties into a CSS string.
11242
+ */
11243
+ function cssPropertiesToCss(attributes) {
11244
+ let styleStr = "";
11245
+ for (const attName in attributes) {
11246
+ if (!attributes[attName]) {
11247
+ continue;
11248
+ }
11249
+ styleStr += `${attName}:${attributes[attName]}; `;
11250
+ }
11251
+ return styleStr;
11252
+ }
11253
+ function getElementMargins(el) {
11254
+ const style = window.getComputedStyle(el);
11255
+ return {
11256
+ top: parseInt(style.marginTop, 10) || 0,
11257
+ bottom: parseInt(style.marginBottom, 10) || 0,
11258
+ left: parseInt(style.marginLeft, 10) || 0,
11259
+ right: parseInt(style.marginRight, 10) || 0,
11260
+ };
11261
+ }
11262
+
11263
+ css /* scss */ `
11264
+ .o-spreadsheet {
11265
+ .o-chart-custom-tooltip {
11266
+ font-size: 12px;
11267
+ background-color: #fff;
11268
+ z-index: ${ComponentsImportance.FigureTooltip};
11269
+ }
11270
+ }
11271
+ `;
11272
+ class ChartJsComponent extends owl.Component {
11273
+ static template = "o-spreadsheet-ChartJsComponent";
11274
+ static props = {
11275
+ figure: Object,
11276
+ };
11277
+ canvas = owl.useRef("graphContainer");
11278
+ chart;
11279
+ currentRuntime;
11280
+ get background() {
11281
+ return this.chartRuntime.background;
11282
+ }
11283
+ get canvasStyle() {
11284
+ return `background-color: ${this.background}`;
11285
+ }
11286
+ get chartRuntime() {
11287
+ const runtime = this.env.model.getters.getChartRuntime(this.props.figure.id);
11288
+ if (!("chartJsConfig" in runtime)) {
11289
+ throw new Error("Unsupported chart runtime");
11290
+ }
11291
+ return runtime;
11292
+ }
11293
+ setup() {
11294
+ owl.onMounted(() => {
11295
+ const runtime = this.chartRuntime;
11296
+ this.currentRuntime = runtime;
11297
+ // Note: chartJS modify the runtime in place, so it's important to give it a copy
11298
+ this.createChart(deepCopy(runtime.chartJsConfig));
11299
+ });
11300
+ owl.onWillUnmount(() => this.chart?.destroy());
11301
+ owl.useEffect(() => {
11302
+ const runtime = this.chartRuntime;
11303
+ if (runtime !== this.currentRuntime) {
11304
+ if (runtime.chartJsConfig.type !== this.currentRuntime.chartJsConfig.type) {
11305
+ this.chart?.destroy();
11306
+ this.createChart(deepCopy(runtime.chartJsConfig));
11307
+ }
11308
+ else {
11309
+ this.updateChartJs(deepCopy(runtime));
11310
+ }
11311
+ this.currentRuntime = runtime;
11312
+ }
11313
+ });
11314
+ }
11315
+ createChart(chartData) {
11316
+ const canvas = this.canvas.el;
11317
+ const ctx = canvas.getContext("2d");
11318
+ const Chart = getChartJSConstructor();
11319
+ this.chart = new Chart(ctx, chartData);
11320
+ }
11321
+ updateChartJs(chartRuntime) {
11322
+ const chartData = chartRuntime.chartJsConfig;
11323
+ if (chartData.data && chartData.data.datasets) {
11324
+ this.chart.data = chartData.data;
11325
+ if (chartData.options?.plugins?.title) {
11326
+ this.chart.config.options.plugins.title = chartData.options.plugins.title;
11327
+ }
11328
+ }
11329
+ else {
11330
+ this.chart.data.datasets = [];
11331
+ }
11332
+ this.chart.config.options = chartData.options;
11333
+ this.chart.update();
11334
+ }
11335
+ }
11336
+
10878
11337
  class ScorecardChart extends owl.Component {
10879
11338
  static template = "o-spreadsheet-ScorecardChart";
10880
11339
  static props = {
@@ -10906,6 +11365,7 @@ stores.inject(MyMetaStore, storeInstance);
10906
11365
  const autoCompleteProviders = new Registry();
10907
11366
 
10908
11367
  autoCompleteProviders.add("dataValidation", {
11368
+ displayAllOnInitialContent: true,
10909
11369
  getProposals(tokenAtCursor, content) {
10910
11370
  if (content.startsWith("=")) {
10911
11371
  return [];
@@ -21202,6 +21662,15 @@ stores.inject(MyMetaStore, storeInstance);
21202
21662
  const exactMatch = proposals?.find((p) => p.text === tokenAtCursor.value);
21203
21663
  // remove tokens that are likely to be other parts of the formula that slipped in the token if it's a string
21204
21664
  const searchTerm = tokenAtCursor.value.replace(/[ ,\(\)]/g, "");
21665
+ if (this._currentContent === this.initialContent &&
21666
+ provider.displayAllOnInitialContent &&
21667
+ proposals?.length) {
21668
+ return {
21669
+ proposals,
21670
+ selectProposal: provider.selectProposal,
21671
+ autoSelectFirstProposal: provider.autoSelectFirstProposal ?? false,
21672
+ };
21673
+ }
21205
21674
  if (exactMatch && this._currentContent !== this.initialContent) {
21206
21675
  // this means the user has chosen a proposal
21207
21676
  return;
@@ -22202,586 +22671,255 @@ stores.inject(MyMetaStore, storeInstance);
22202
22671
  /**
22203
22672
  * Get the consecutive evaluated cells that can pass the filter function (e.g. certain type filter).
22204
22673
  * Return the one which contains the given cell
22205
- */
22206
- function getGroup(cell, cells, filter) {
22207
- let group = [];
22208
- let found = false;
22209
- for (let x of cells) {
22210
- if (x === cell) {
22211
- found = true;
22212
- }
22213
- const cellValue = x === undefined || x.isFormula
22214
- ? undefined
22215
- : evaluateLiteral(x, { locale: DEFAULT_LOCALE, format: x.format });
22216
- if (cellValue && filter(cellValue)) {
22217
- group.push(cellValue);
22218
- }
22219
- else {
22220
- if (found) {
22221
- return group;
22222
- }
22223
- group = [];
22224
- }
22225
- }
22226
- return group;
22227
- }
22228
- /**
22229
- * Get the average steps between numbers
22230
- */
22231
- function getAverageIncrement(group) {
22232
- const averages = [];
22233
- let last = group[0];
22234
- for (let i = 1; i < group.length; i++) {
22235
- const current = group[i];
22236
- averages.push(current - last);
22237
- last = current;
22238
- }
22239
- return averages.reduce((a, b) => a + b, 0) / averages.length;
22240
- }
22241
- /**
22242
- * Get the step for a group
22243
- */
22244
- function calculateIncrementBasedOnGroup(group) {
22245
- let increment = 1;
22246
- if (group.length >= 2) {
22247
- increment = getAverageIncrement(group) * group.length;
22248
- }
22249
- return increment;
22250
- }
22251
- /**
22252
- * Iterates on a list of date intervals.
22253
- * if every interval is the same, return the interval
22254
- * Otherwise return undefined
22255
- *
22256
- */
22257
- function getEqualInterval(intervals) {
22258
- if (intervals.length < 2) {
22259
- return intervals[0] || { years: 0, months: 0, days: 0 };
22260
- }
22261
- const equal = intervals.every((interval) => interval.years === intervals[0].years &&
22262
- interval.months === intervals[0].months &&
22263
- interval.days === intervals[0].days);
22264
- return equal ? intervals[0] : undefined;
22265
- }
22266
- /**
22267
- * Based on a group of dates, calculate the increment that should be applied
22268
- * to the next date.
22269
- *
22270
- * This will compute the date difference in calendar terms (years, months, days)
22271
- * In order to make abstraction of leap years and months with different number of days.
22272
- *
22273
- * In case the dates are not equidistant in calendar terms, no rule can be extrapolated
22274
- * In case of equidistant dates, we either have in that order:
22275
- * - exact date interval (e.g. +n year OR +n month OR +n day) in which case we increment by the same interval
22276
- * - exact day interval (e.g. +n days) in which case we increment by the same day interval
22277
- * - equidistant dates but not the same interval, in which case we return increment of the same interval
22278
- *
22279
- * */
22280
- function calculateDateIncrementBasedOnGroup(group) {
22281
- if (group.length < 2) {
22282
- return 1;
22283
- }
22284
- const jsDates = group.map((date) => toJsDate(date, DEFAULT_LOCALE));
22285
- const datesIntervals = getDateIntervals(jsDates);
22286
- const datesEquidistantInterval = getEqualInterval(datesIntervals);
22287
- if (datesEquidistantInterval === undefined) {
22288
- // dates are not equidistant in terms of years, months or days, thus no rule can be extrapolated
22289
- return undefined;
22290
- }
22291
- // The dates are apart by an exact interval of years, months or days
22292
- // but not a combination of them
22293
- const exactDateInterval = Object.values(datesEquidistantInterval).filter((value) => value !== 0).length === 1;
22294
- const isSameDay = Object.values(datesEquidistantInterval).every((el) => el === 0); // handles time values (strict decimals)
22295
- if (!exactDateInterval || isSameDay) {
22296
- const timeIntervals = jsDates
22297
- .map((date, index) => {
22298
- if (index === 0) {
22299
- return 0;
22300
- }
22301
- const previous = jsDates[index - 1];
22302
- return Math.floor(date.getTime()) - Math.floor(previous.getTime());
22303
- })
22304
- .slice(1);
22305
- const equidistantDates = timeIntervals.every((interval) => interval === timeIntervals[0]);
22306
- if (equidistantDates) {
22307
- return group.length * (group[1] - group[0]);
22308
- }
22309
- }
22310
- return {
22311
- years: datesEquidistantInterval.years * group.length,
22312
- months: datesEquidistantInterval.months * group.length,
22313
- days: datesEquidistantInterval.days * group.length,
22314
- };
22315
- }
22316
- autofillRulesRegistry
22317
- .add("simple_value_copy", {
22318
- condition: (cell, cells) => {
22319
- return (cells.length === 1 && !cell.isFormula && !(cell.format && isDateTimeFormat(cell.format)));
22320
- },
22321
- generateRule: () => {
22322
- return { type: "COPY_MODIFIER" };
22323
- },
22324
- sequence: 10,
22325
- })
22326
- .add("increment_alphanumeric_value", {
22327
- condition: (cell) => !cell.isFormula &&
22328
- evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.text &&
22329
- alphaNumericValueRegExp.test(cell.content),
22330
- generateRule: (cell, cells) => {
22331
- const numberPostfix = parseInt(cell.content.match(numberPostfixRegExp)[0]);
22332
- const prefix = cell.content.match(stringPrefixRegExp)[0];
22333
- const numberPostfixLength = cell.content.length - prefix.length;
22334
- const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.text &&
22335
- alphaNumericValueRegExp.test(evaluatedCell.value)) // get consecutive alphanumeric cells, no matter what the prefix is
22336
- .filter((cell) => prefix === (cell.value ?? "").toString().match(stringPrefixRegExp)[0])
22337
- .map((cell) => parseInt((cell.value ?? "").toString().match(numberPostfixRegExp)[0]));
22338
- const increment = calculateIncrementBasedOnGroup(group);
22339
- return {
22340
- type: "ALPHANUMERIC_INCREMENT_MODIFIER",
22341
- prefix,
22342
- current: numberPostfix,
22343
- increment,
22344
- numberPostfixLength,
22345
- };
22346
- },
22347
- sequence: 15,
22348
- })
22349
- .add("copy_text", {
22350
- condition: (cell) => !cell.isFormula &&
22351
- evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.text,
22352
- generateRule: () => {
22353
- return { type: "COPY_MODIFIER" };
22354
- },
22355
- sequence: 20,
22356
- })
22357
- .add("update_formula", {
22358
- condition: (cell) => cell.isFormula,
22359
- generateRule: (_, cells) => {
22360
- return { type: "FORMULA_MODIFIER", increment: cells.length, current: 0 };
22361
- },
22362
- sequence: 30,
22363
- })
22364
- .add("increment_dates", {
22365
- condition: (cell, cells) => {
22366
- return (!cell.isFormula &&
22367
- evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.number &&
22368
- !!cell.format &&
22369
- isDateTimeFormat(cell.format));
22370
- },
22371
- generateRule: (cell, cells) => {
22372
- const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number &&
22373
- !!evaluatedCell.format &&
22374
- isDateTimeFormat(evaluatedCell.format)).map((cell) => Number(cell.value));
22375
- const increment = calculateDateIncrementBasedOnGroup(group);
22376
- if (increment === undefined) {
22377
- return { type: "COPY_MODIFIER" };
22378
- }
22379
- /** requires to detect the current date (requires to be an integer value with the right format)
22380
- * detect if year or if month or if day then extrapolate increment required (+1 month, +1 year + 1 day)
22381
- */
22382
- const evaluation = evaluateLiteral(cell, { locale: DEFAULT_LOCALE });
22383
- if (typeof increment === "object") {
22384
- return {
22385
- type: "DATE_INCREMENT_MODIFIER",
22386
- increment,
22387
- current: evaluation.type === CellValueType.number ? evaluation.value : 0,
22388
- };
22389
- }
22390
- return {
22391
- type: "INCREMENT_MODIFIER",
22392
- increment,
22393
- current: evaluation.type === CellValueType.number ? evaluation.value : 0,
22394
- };
22395
- },
22396
- sequence: 25,
22397
- })
22398
- .add("increment_number", {
22399
- condition: (cell) => !cell.isFormula &&
22400
- evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.number,
22401
- generateRule: (cell, cells) => {
22402
- const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number &&
22403
- !isDateTimeFormat(evaluatedCell.format || "")).map((cell) => Number(cell.value));
22404
- const increment = calculateIncrementBasedOnGroup(group);
22405
- const evaluation = evaluateLiteral(cell, { locale: DEFAULT_LOCALE });
22406
- return {
22407
- type: "INCREMENT_MODIFIER",
22408
- increment,
22409
- current: evaluation.type === CellValueType.number ? evaluation.value : 0,
22410
- };
22411
- },
22412
- sequence: 40,
22413
- });
22414
- /**
22415
- * Returns the date intervals between consecutive dates of an array
22416
- * in the format of { years: number, months: number, days: number }
22417
- *
22418
- * The split is necessary to make abstraction of leap years and
22419
- * months with different number of days.
22420
- *
22421
- * @param dates
22422
- */
22423
- function getDateIntervals(dates) {
22424
- if (dates.length < 2) {
22425
- return [{ years: 0, months: 0, days: 0 }];
22426
- }
22427
- const res = dates.map((date, index) => {
22428
- if (index === 0) {
22429
- return { years: 0, months: 0, days: 0 };
22430
- }
22431
- const previous = DateTime.fromTimestamp(dates[index - 1].getTime());
22432
- const years = getTimeDifferenceInWholeYears(previous, date);
22433
- const months = getTimeDifferenceInWholeMonths(previous, date) % 12;
22434
- previous.setFullYear(previous.getFullYear() + years);
22435
- previous.setMonth(previous.getMonth() + months);
22436
- const days = getTimeDifferenceInWholeDays(previous, date);
22437
- return {
22438
- years,
22439
- months,
22440
- days,
22441
- };
22442
- });
22443
- return res.slice(1);
22444
- }
22445
-
22446
- const cellPopoverRegistry = new Registry();
22447
-
22448
- const GAUGE_PADDING_SIDE = 30;
22449
- const GAUGE_PADDING_TOP = 10;
22450
- const GAUGE_PADDING_BOTTOM = 20;
22451
- const GAUGE_LABELS_FONT_SIZE = 12;
22452
- const GAUGE_DEFAULT_VALUE_FONT_SIZE = 80;
22453
- const GAUGE_BACKGROUND_COLOR = "#F3F2F1";
22454
- const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
22455
- const GAUGE_TITLE_SECTION_HEIGHT = 25;
22456
- function drawGaugeChart(canvas, runtime) {
22457
- const canvasBoundingRect = canvas.getBoundingClientRect();
22458
- canvas.width = canvasBoundingRect.width;
22459
- canvas.height = canvasBoundingRect.height;
22460
- const ctx = canvas.getContext("2d");
22461
- const config = getGaugeRenderingConfig(canvasBoundingRect, runtime, ctx);
22462
- drawBackground(ctx, config);
22463
- drawGauge(ctx, config);
22464
- drawInflectionValues(ctx, config);
22465
- drawLabels(ctx, config);
22466
- drawTitle(ctx, config);
22467
- }
22468
- function drawGauge(ctx, config) {
22469
- ctx.save();
22470
- const gauge = config.gauge;
22471
- const arcCenterX = gauge.rect.x + gauge.rect.width / 2;
22472
- const arcCenterY = gauge.rect.y + gauge.rect.height;
22473
- const arcRadius = gauge.rect.height - gauge.arcWidth / 2;
22474
- if (arcRadius < 0) {
22475
- return;
22476
- }
22477
- const gaugeAngle = gauge.percentage === 1 ? 0 : Math.PI * (1 + gauge.percentage);
22478
- // Gauge background
22479
- ctx.strokeStyle = GAUGE_BACKGROUND_COLOR;
22480
- ctx.beginPath();
22481
- ctx.lineWidth = gauge.arcWidth;
22482
- ctx.arc(arcCenterX, arcCenterY, arcRadius, gaugeAngle, 0);
22483
- ctx.stroke();
22484
- // Gauge value
22485
- ctx.strokeStyle = gauge.color;
22486
- ctx.beginPath();
22487
- ctx.arc(arcCenterX, arcCenterY, arcRadius, Math.PI, gaugeAngle);
22488
- ctx.stroke();
22489
- ctx.restore();
22490
- }
22491
- function drawBackground(ctx, config) {
22492
- ctx.save();
22493
- ctx.fillStyle = config.backgroundColor;
22494
- ctx.fillRect(0, 0, config.width, config.height);
22495
- ctx.restore();
22496
- }
22497
- function drawLabels(ctx, config) {
22498
- for (const label of [config.minLabel, config.maxLabel, config.gaugeValue]) {
22499
- ctx.save();
22500
- ctx.textAlign = "center";
22501
- ctx.fillStyle = label.color;
22502
- ctx.font = `${label.fontSize}px ${DEFAULT_FONT}`;
22503
- ctx.fillText(label.label, label.textPosition.x, label.textPosition.y);
22504
- ctx.restore();
22505
- }
22506
- }
22507
- function drawInflectionValues(ctx, config) {
22508
- const { x: rectX, y: rectY, width, height } = config.gauge.rect;
22509
- for (const inflectionValue of config.inflectionValues) {
22510
- ctx.save();
22511
- ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment
22512
- ctx.rotate(Math.PI / 2 - inflectionValue.rotation);
22513
- ctx.lineWidth = 2;
22514
- ctx.strokeStyle = chartMutedFontColor(config.backgroundColor) + "aa";
22515
- ctx.beginPath();
22516
- ctx.moveTo(0, -(height - config.gauge.arcWidth));
22517
- ctx.lineTo(0, -height - 3);
22518
- ctx.stroke();
22519
- ctx.textAlign = "center";
22520
- ctx.font = `${inflectionValue.fontSize}px ${DEFAULT_FONT}`;
22521
- ctx.fillStyle = inflectionValue.color;
22522
- const textY = -height - GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN - inflectionValue.offset;
22523
- ctx.fillText(inflectionValue.label, 0, textY);
22524
- ctx.restore();
22525
- }
22526
- }
22527
- function drawTitle(ctx, config) {
22528
- ctx.save();
22529
- const title = config.title;
22530
- ctx.font = getDefaultContextFont(title.fontSize, title.bold, title.italic);
22531
- ctx.textBaseline = "middle";
22532
- ctx.fillStyle = title.color;
22533
- ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
22534
- ctx.restore();
22535
- }
22536
- function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
22537
- const maxValue = runtime.maxValue;
22538
- const minValue = runtime.minValue;
22539
- const gaugeValue = runtime.gaugeValue;
22540
- const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
22541
- const gaugeArcWidth = gaugeRect.width / 6;
22542
- const gaugePercentage = gaugeValue
22543
- ? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
22544
- : 0;
22545
- const gaugeValuePosition = {
22546
- x: boundingRect.width / 2,
22547
- y: gaugeRect.y + gaugeRect.height - gaugeRect.height / 12,
22548
- };
22549
- let gaugeValueFontSize = GAUGE_DEFAULT_VALUE_FONT_SIZE;
22550
- // Scale down the font size if the gaugeRect is too small
22551
- if (gaugeRect.height < 300) {
22552
- gaugeValueFontSize = gaugeValueFontSize * (gaugeRect.height / 300);
22553
- }
22554
- // Scale down the font size if the text is too long
22555
- const maxTextWidth = gaugeRect.width / 2;
22556
- const gaugeLabel = gaugeValue?.label || "-";
22557
- if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
22558
- gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
22559
- }
22560
- const minLabelPosition = {
22561
- x: gaugeRect.x + gaugeArcWidth / 2,
22562
- y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
22563
- };
22564
- const maxLabelPosition = {
22565
- x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
22566
- y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
22567
- };
22568
- const textColor = chartMutedFontColor(runtime.background);
22569
- const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);
22570
- let x = 0, titleWidth = 0, titleHeight = 0;
22571
- if (runtime.title.text) {
22572
- ({ width: titleWidth, height: titleHeight } = computeTextDimension(ctx, runtime.title.text, { fontSize: CHART_TITLE_FONT_SIZE, ...runtime.title }, "px"));
22573
- }
22574
- switch (runtime.title.align) {
22575
- case "right":
22576
- x = boundingRect.width - titleWidth - CHART_PADDING$1;
22577
- break;
22578
- case "center":
22579
- x = (boundingRect.width - titleWidth) / 2;
22580
- break;
22581
- case "left":
22582
- default:
22583
- x = CHART_PADDING$1;
22584
- break;
22585
- }
22586
- return {
22587
- width: boundingRect.width,
22588
- height: boundingRect.height,
22589
- title: {
22590
- label: runtime.title.text ?? "",
22591
- fontSize: runtime.title.fontSize ?? CHART_TITLE_FONT_SIZE,
22592
- textPosition: {
22593
- x,
22594
- y: CHART_PADDING_TOP + titleHeight / 2,
22595
- },
22596
- color: runtime.title.color ?? textColor,
22597
- bold: runtime.title.bold,
22598
- italic: runtime.title.italic,
22599
- },
22600
- backgroundColor: runtime.background,
22601
- gauge: {
22602
- rect: gaugeRect,
22603
- arcWidth: gaugeArcWidth,
22604
- percentage: clip(gaugePercentage, 0, 1),
22605
- color: getGaugeColor(runtime),
22606
- },
22607
- inflectionValues,
22608
- gaugeValue: {
22609
- label: gaugeLabel,
22610
- textPosition: gaugeValuePosition,
22611
- fontSize: gaugeValueFontSize,
22612
- color: textColor,
22613
- },
22614
- minLabel: {
22615
- label: runtime.minValue.label,
22616
- textPosition: minLabelPosition,
22617
- fontSize: GAUGE_LABELS_FONT_SIZE,
22618
- color: textColor,
22619
- },
22620
- maxLabel: {
22621
- label: runtime.maxValue.label,
22622
- textPosition: maxLabelPosition,
22623
- fontSize: GAUGE_LABELS_FONT_SIZE,
22624
- color: textColor,
22625
- },
22626
- };
22627
- }
22628
- /**
22629
- * Get the rectangle in which the gauge will be drawn, based on the bounding rectangle of the canvas and leaving
22630
- * space for the title and labels.
22631
- */
22632
- function getGaugeRect(boundingRect, title) {
22633
- const titleHeight = title ? GAUGE_TITLE_SECTION_HEIGHT : 0;
22634
- const drawHeight = boundingRect.height - GAUGE_PADDING_BOTTOM - titleHeight - GAUGE_PADDING_TOP;
22635
- const drawWidth = boundingRect.width - GAUGE_PADDING_SIDE * 2;
22636
- let gaugeWidth;
22637
- let gaugeHeight;
22638
- if (drawWidth > 2 * drawHeight) {
22639
- gaugeWidth = 2 * drawHeight;
22640
- gaugeHeight = drawHeight;
22641
- }
22642
- else {
22643
- gaugeWidth = drawWidth;
22644
- gaugeHeight = drawWidth / 2;
22645
- }
22646
- const gaugeX = GAUGE_PADDING_SIDE + (drawWidth - gaugeWidth) / 2;
22647
- const gaugeY = titleHeight + GAUGE_PADDING_TOP + (drawHeight - gaugeHeight) / 2;
22648
- return {
22649
- x: gaugeX,
22650
- y: gaugeY,
22651
- width: gaugeWidth,
22652
- height: gaugeHeight,
22653
- };
22654
- }
22655
- /**
22656
- * 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).
22657
- *
22658
- * Also compute an offset for the text so that it doesn't overlap with other text.
22659
- */
22660
- function getInflectionValues(runtime, gaugeRect, textColor, ctx) {
22661
- const maxValue = runtime.maxValue;
22662
- const minValue = runtime.minValue;
22663
- const gaugeCircleCenter = {
22664
- x: gaugeRect.x + gaugeRect.width / 2,
22665
- y: gaugeRect.y + gaugeRect.height,
22666
- };
22667
- const textStyle = { fontSize: GAUGE_LABELS_FONT_SIZE };
22668
- const inflectionValues = [];
22669
- const inflectionValuesTextRects = [];
22670
- for (const inflectionValue of runtime.inflectionValues) {
22671
- const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
22672
- const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
22673
- const angle = Math.PI - Math.PI * percentage;
22674
- const textRect = getRectangleTangentToCircle(angle, // angle between X axis and the point where the rectangle is tangent to the circle
22675
- gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, // radius of the gauge circle + margin below text
22676
- gaugeCircleCenter.x, // center of the gauge circle
22677
- gaugeCircleCenter.y, // center of the gauge circle
22678
- labelWidth + 2, // width of the text + some margin
22679
- GAUGE_LABELS_FONT_SIZE // height of the text
22680
- );
22681
- let offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect))
22682
- ? GAUGE_LABELS_FONT_SIZE
22683
- : 0;
22684
- inflectionValuesTextRects.push(textRect);
22685
- inflectionValues.push({
22686
- rotation: angle,
22687
- label: inflectionValue.label,
22688
- fontSize: GAUGE_LABELS_FONT_SIZE,
22689
- color: textColor,
22690
- offset,
22691
- });
22692
- }
22693
- return inflectionValues;
22694
- }
22695
- function getGaugeColor(runtime) {
22696
- const gaugeValue = runtime.gaugeValue?.value;
22697
- if (gaugeValue === undefined) {
22698
- return GAUGE_BACKGROUND_COLOR;
22699
- }
22700
- for (let i = 0; i < runtime.inflectionValues.length; i++) {
22701
- const inflectionValue = runtime.inflectionValues[i];
22702
- if (inflectionValue.operator === "<" && gaugeValue < inflectionValue.value) {
22703
- return runtime.colors[i];
22674
+ */
22675
+ function getGroup(cell, cells, filter) {
22676
+ let group = [];
22677
+ let found = false;
22678
+ for (let x of cells) {
22679
+ if (x === cell) {
22680
+ found = true;
22704
22681
  }
22705
- else if (inflectionValue.operator === "<=" && gaugeValue <= inflectionValue.value) {
22706
- return runtime.colors[i];
22682
+ const cellValue = x === undefined || x.isFormula
22683
+ ? undefined
22684
+ : evaluateLiteral(x, { locale: DEFAULT_LOCALE, format: x.format });
22685
+ if (cellValue && filter(cellValue)) {
22686
+ group.push(cellValue);
22687
+ }
22688
+ else {
22689
+ if (found) {
22690
+ return group;
22691
+ }
22692
+ group = [];
22707
22693
  }
22708
22694
  }
22709
- return runtime.colors.at(-1);
22695
+ return group;
22710
22696
  }
22711
- function getSegmentsOfRectangle(rectangle) {
22712
- return [
22713
- { start: rectangle.topLeft, end: rectangle.topRight },
22714
- { start: rectangle.topRight, end: rectangle.bottomRight },
22715
- { start: rectangle.bottomRight, end: rectangle.bottomLeft },
22716
- { start: rectangle.bottomLeft, end: rectangle.topLeft },
22717
- ];
22697
+ /**
22698
+ * Get the average steps between numbers
22699
+ */
22700
+ function getAverageIncrement(group) {
22701
+ const averages = [];
22702
+ let last = group[0];
22703
+ for (let i = 1; i < group.length; i++) {
22704
+ const current = group[i];
22705
+ averages.push(current - last);
22706
+ last = current;
22707
+ }
22708
+ return averages.reduce((a, b) => a + b, 0) / averages.length;
22718
22709
  }
22719
22710
  /**
22720
- * Check if two segment intersect. The case where the segments are colinear (both segments on the same line)
22721
- * is not handled.
22711
+ * Get the step for a group
22722
22712
  */
22723
- function doSegmentIntersect(segment1, segment2) {
22724
- const A = segment1.start;
22725
- const B = segment1.end;
22726
- const C = segment2.start;
22727
- const D = segment2.end;
22728
- /**
22729
- * Line segment intersection algorithm
22730
- * https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
22731
- */
22732
- function ccw(a, b, c) {
22733
- return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
22713
+ function calculateIncrementBasedOnGroup(group) {
22714
+ let increment = 1;
22715
+ if (group.length >= 2) {
22716
+ increment = getAverageIncrement(group) * group.length;
22734
22717
  }
22735
- return ccw(A, C, D) !== ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D);
22718
+ return increment;
22736
22719
  }
22737
- function doRectanglesIntersect(rect1, rect2) {
22738
- const segments1 = getSegmentsOfRectangle(rect1);
22739
- const segments2 = getSegmentsOfRectangle(rect2);
22740
- for (const segment1 of segments1) {
22741
- for (const segment2 of segments2) {
22742
- if (doSegmentIntersect(segment1, segment2)) {
22743
- return true;
22720
+ /**
22721
+ * Iterates on a list of date intervals.
22722
+ * if every interval is the same, return the interval
22723
+ * Otherwise return undefined
22724
+ *
22725
+ */
22726
+ function getEqualInterval(intervals) {
22727
+ if (intervals.length < 2) {
22728
+ return intervals[0] || { years: 0, months: 0, days: 0 };
22729
+ }
22730
+ const equal = intervals.every((interval) => interval.years === intervals[0].years &&
22731
+ interval.months === intervals[0].months &&
22732
+ interval.days === intervals[0].days);
22733
+ return equal ? intervals[0] : undefined;
22734
+ }
22735
+ /**
22736
+ * Based on a group of dates, calculate the increment that should be applied
22737
+ * to the next date.
22738
+ *
22739
+ * This will compute the date difference in calendar terms (years, months, days)
22740
+ * In order to make abstraction of leap years and months with different number of days.
22741
+ *
22742
+ * In case the dates are not equidistant in calendar terms, no rule can be extrapolated
22743
+ * In case of equidistant dates, we either have in that order:
22744
+ * - exact date interval (e.g. +n year OR +n month OR +n day) in which case we increment by the same interval
22745
+ * - exact day interval (e.g. +n days) in which case we increment by the same day interval
22746
+ * - equidistant dates but not the same interval, in which case we return increment of the same interval
22747
+ *
22748
+ * */
22749
+ function calculateDateIncrementBasedOnGroup(group) {
22750
+ if (group.length < 2) {
22751
+ return 1;
22752
+ }
22753
+ const jsDates = group.map((date) => toJsDate(date, DEFAULT_LOCALE));
22754
+ const datesIntervals = getDateIntervals(jsDates);
22755
+ const datesEquidistantInterval = getEqualInterval(datesIntervals);
22756
+ if (datesEquidistantInterval === undefined) {
22757
+ // dates are not equidistant in terms of years, months or days, thus no rule can be extrapolated
22758
+ return undefined;
22759
+ }
22760
+ // The dates are apart by an exact interval of years, months or days
22761
+ // but not a combination of them
22762
+ const exactDateInterval = Object.values(datesEquidistantInterval).filter((value) => value !== 0).length === 1;
22763
+ const isSameDay = Object.values(datesEquidistantInterval).every((el) => el === 0); // handles time values (strict decimals)
22764
+ if (!exactDateInterval || isSameDay) {
22765
+ const timeIntervals = jsDates
22766
+ .map((date, index) => {
22767
+ if (index === 0) {
22768
+ return 0;
22744
22769
  }
22770
+ const previous = jsDates[index - 1];
22771
+ return Math.floor(date.getTime()) - Math.floor(previous.getTime());
22772
+ })
22773
+ .slice(1);
22774
+ const equidistantDates = timeIntervals.every((interval) => interval === timeIntervals[0]);
22775
+ if (equidistantDates) {
22776
+ return group.length * (group[1] - group[0]);
22745
22777
  }
22746
22778
  }
22747
- return false;
22779
+ return {
22780
+ years: datesEquidistantInterval.years * group.length,
22781
+ months: datesEquidistantInterval.months * group.length,
22782
+ days: datesEquidistantInterval.days * group.length,
22783
+ };
22748
22784
  }
22785
+ autofillRulesRegistry
22786
+ .add("simple_value_copy", {
22787
+ condition: (cell, cells) => {
22788
+ return (cells.length === 1 && !cell.isFormula && !(cell.format && isDateTimeFormat(cell.format)));
22789
+ },
22790
+ generateRule: () => {
22791
+ return { type: "COPY_MODIFIER" };
22792
+ },
22793
+ sequence: 10,
22794
+ })
22795
+ .add("increment_alphanumeric_value", {
22796
+ condition: (cell) => !cell.isFormula &&
22797
+ evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.text &&
22798
+ alphaNumericValueRegExp.test(cell.content),
22799
+ generateRule: (cell, cells, direction) => {
22800
+ const numberPostfix = parseInt(cell.content.match(numberPostfixRegExp)[0]);
22801
+ const prefix = cell.content.match(stringPrefixRegExp)[0];
22802
+ const numberPostfixLength = cell.content.length - prefix.length;
22803
+ const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.text &&
22804
+ alphaNumericValueRegExp.test(evaluatedCell.value)) // get consecutive alphanumeric cells, no matter what the prefix is
22805
+ .filter((cell) => prefix === (cell.value ?? "").toString().match(stringPrefixRegExp)[0])
22806
+ .map((cell) => parseInt((cell.value ?? "").toString().match(numberPostfixRegExp)[0]));
22807
+ let increment = calculateIncrementBasedOnGroup(group);
22808
+ if (["up", "left"].includes(direction) && group.length === 1) {
22809
+ increment = -increment;
22810
+ }
22811
+ return {
22812
+ type: "ALPHANUMERIC_INCREMENT_MODIFIER",
22813
+ prefix,
22814
+ current: numberPostfix,
22815
+ increment,
22816
+ numberPostfixLength,
22817
+ };
22818
+ },
22819
+ sequence: 15,
22820
+ })
22821
+ .add("copy_text", {
22822
+ condition: (cell) => !cell.isFormula &&
22823
+ evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.text,
22824
+ generateRule: () => {
22825
+ return { type: "COPY_MODIFIER" };
22826
+ },
22827
+ sequence: 20,
22828
+ })
22829
+ .add("update_formula", {
22830
+ condition: (cell) => cell.isFormula,
22831
+ generateRule: (_, cells) => {
22832
+ return { type: "FORMULA_MODIFIER", increment: cells.length, current: 0 };
22833
+ },
22834
+ sequence: 30,
22835
+ })
22836
+ .add("increment_dates", {
22837
+ condition: (cell, cells) => {
22838
+ return (!cell.isFormula &&
22839
+ evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.number &&
22840
+ !!cell.format &&
22841
+ isDateTimeFormat(cell.format));
22842
+ },
22843
+ generateRule: (cell, cells) => {
22844
+ const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number &&
22845
+ !!evaluatedCell.format &&
22846
+ isDateTimeFormat(evaluatedCell.format)).map((cell) => Number(cell.value));
22847
+ const increment = calculateDateIncrementBasedOnGroup(group);
22848
+ if (increment === undefined) {
22849
+ return { type: "COPY_MODIFIER" };
22850
+ }
22851
+ /** requires to detect the current date (requires to be an integer value with the right format)
22852
+ * detect if year or if month or if day then extrapolate increment required (+1 month, +1 year + 1 day)
22853
+ */
22854
+ const evaluation = evaluateLiteral(cell, { locale: DEFAULT_LOCALE });
22855
+ if (typeof increment === "object") {
22856
+ return {
22857
+ type: "DATE_INCREMENT_MODIFIER",
22858
+ increment,
22859
+ current: evaluation.type === CellValueType.number ? evaluation.value : 0,
22860
+ };
22861
+ }
22862
+ return {
22863
+ type: "INCREMENT_MODIFIER",
22864
+ increment,
22865
+ current: evaluation.type === CellValueType.number ? evaluation.value : 0,
22866
+ };
22867
+ },
22868
+ sequence: 25,
22869
+ })
22870
+ .add("increment_number", {
22871
+ condition: (cell) => !cell.isFormula &&
22872
+ evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.number,
22873
+ generateRule: (cell, cells, direction) => {
22874
+ const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number &&
22875
+ !isDateTimeFormat(evaluatedCell.format || "")).map((cell) => Number(cell.value));
22876
+ let increment = calculateIncrementBasedOnGroup(group);
22877
+ if (["up", "left"].includes(direction) && group.length === 1) {
22878
+ increment = -increment;
22879
+ }
22880
+ const evaluation = evaluateLiteral(cell, { locale: DEFAULT_LOCALE });
22881
+ return {
22882
+ type: "INCREMENT_MODIFIER",
22883
+ increment,
22884
+ current: evaluation.type === CellValueType.number ? evaluation.value : 0,
22885
+ };
22886
+ },
22887
+ sequence: 40,
22888
+ });
22749
22889
  /**
22750
- * Get the rectangle that is tangent to a circle at a given angle.
22890
+ * Returns the date intervals between consecutive dates of an array
22891
+ * in the format of { years: number, months: number, days: number }
22751
22892
  *
22752
- * @param angle angle between X axis and the point where the rectangle is tangent to the circle
22893
+ * The split is necessary to make abstraction of leap years and
22894
+ * months with different number of days.
22895
+ *
22896
+ * @param dates
22753
22897
  */
22754
- function getRectangleTangentToCircle(angle, radius, circleCenterX, circleCenterY, rectWidth, rectHeight) {
22755
- const cos = Math.cos(angle);
22756
- const sin = Math.sin(angle);
22757
- // x, y are the distance from the center of the circle to the point where the rectangle is tangent to the circle
22758
- const x = cos * radius;
22759
- const y = sin * radius;
22760
- // x2, y2 are the distance from the point the rectangle is tangent to the circle to the bottom left corner of the rectangle
22761
- const x2 = sin * (rectWidth / 2); // cos(angle + 90°) = sin(angle)
22762
- const y2 = cos * (rectWidth / 2);
22763
- const bottomRight = {
22764
- x: x + x2 + circleCenterX,
22765
- y: circleCenterY - (y - y2),
22766
- };
22767
- const bottomLeft = {
22768
- x: x - x2 + circleCenterX,
22769
- y: circleCenterY - (y + y2),
22770
- };
22771
- // Same as above but for the top corners of the rectangle (radius + rectangle height instead of radius)
22772
- const xp = cos * (radius + rectHeight);
22773
- const yp = sin * (radius + rectHeight);
22774
- const topLeft = {
22775
- x: xp - x2 + circleCenterX,
22776
- y: circleCenterY - (yp + y2),
22777
- };
22778
- const topRight = {
22779
- x: xp + x2 + circleCenterX,
22780
- y: circleCenterY - (yp - y2),
22781
- };
22782
- return { bottomLeft, bottomRight, topRight, topLeft };
22898
+ function getDateIntervals(dates) {
22899
+ if (dates.length < 2) {
22900
+ return [{ years: 0, months: 0, days: 0 }];
22901
+ }
22902
+ const res = dates.map((date, index) => {
22903
+ if (index === 0) {
22904
+ return { years: 0, months: 0, days: 0 };
22905
+ }
22906
+ const previous = DateTime.fromTimestamp(dates[index - 1].getTime());
22907
+ const years = getTimeDifferenceInWholeYears(previous, date);
22908
+ const months = getTimeDifferenceInWholeMonths(previous, date) % 12;
22909
+ previous.setFullYear(previous.getFullYear() + years);
22910
+ previous.setMonth(previous.getMonth() + months);
22911
+ const days = getTimeDifferenceInWholeDays(previous, date);
22912
+ return {
22913
+ years,
22914
+ months,
22915
+ days,
22916
+ };
22917
+ });
22918
+ return res.slice(1);
22783
22919
  }
22784
22920
 
22921
+ const cellPopoverRegistry = new Registry();
22922
+
22785
22923
  class GaugeChartComponent extends owl.Component {
22786
22924
  static template = "o-spreadsheet-GaugeChartComponent";
22787
22925
  canvas = owl.useRef("chartContainer");
@@ -22814,72 +22952,6 @@ stores.inject(MyMetaStore, storeInstance);
22814
22952
  return color;
22815
22953
  }
22816
22954
 
22817
- const CHART_COMMON_OPTIONS = {
22818
- // https://www.chartjs.org/docs/latest/general/responsive.html
22819
- responsive: true, // will resize when its container is resized
22820
- maintainAspectRatio: false, // doesn't maintain the aspect ratio (width/height =2 by default) so the user has the choice of the exact layout
22821
- elements: {
22822
- line: {
22823
- fill: false, // do not fill the area under line charts
22824
- },
22825
- point: {
22826
- hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
22827
- },
22828
- },
22829
- animation: false,
22830
- };
22831
- function chartToImage(runtime, figure, type) {
22832
- // wrap the canvas in a div with a fixed size because chart.js would
22833
- // fill the whole page otherwise
22834
- const div = document.createElement("div");
22835
- div.style.width = `${figure.width}px`;
22836
- div.style.height = `${figure.height}px`;
22837
- const canvas = document.createElement("canvas");
22838
- div.append(canvas);
22839
- canvas.setAttribute("width", figure.width.toString());
22840
- canvas.setAttribute("height", figure.height.toString());
22841
- // we have to add the canvas to the DOM otherwise it won't be rendered
22842
- document.body.append(div);
22843
- if ("chartJsConfig" in runtime) {
22844
- const config = deepCopy(runtime.chartJsConfig);
22845
- config.plugins = [backgroundColorChartJSPlugin];
22846
- const chart = new window.Chart(canvas, config);
22847
- const imgContent = chart.toBase64Image();
22848
- chart.destroy();
22849
- div.remove();
22850
- return imgContent;
22851
- }
22852
- else if (type === "scorecard") {
22853
- const design = getScorecardConfiguration(figure, runtime);
22854
- drawScoreChart(design, canvas);
22855
- const imgContent = canvas.toDataURL();
22856
- div.remove();
22857
- return imgContent;
22858
- }
22859
- else if (type === "gauge") {
22860
- drawGaugeChart(canvas, runtime);
22861
- const imgContent = canvas.toDataURL();
22862
- div.remove();
22863
- return imgContent;
22864
- }
22865
- return undefined;
22866
- }
22867
- /**
22868
- * Custom chart.js plugin to set the background color of the canvas
22869
- * https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md
22870
- */
22871
- const backgroundColorChartJSPlugin = {
22872
- id: "customCanvasBackgroundColor",
22873
- beforeDraw: (chart) => {
22874
- const { ctx } = chart;
22875
- ctx.save();
22876
- ctx.globalCompositeOperation = "destination-over";
22877
- ctx.fillStyle = "#ffffff";
22878
- ctx.fillRect(0, 0, chart.width, chart.height);
22879
- ctx.restore();
22880
- },
22881
- };
22882
-
22883
22955
  /**
22884
22956
  * Represent a raw XML string
22885
22957
  */
@@ -22945,6 +23017,7 @@ stores.inject(MyMetaStore, storeInstance);
22945
23017
  macroEnabledTemplateWorkbook: "application/vnd.ms-excel.template.macroEnabled.main+xml",
22946
23018
  excelAddInWorkbook: "application/vnd.ms-excel.addin.macroEnabled.main+xml",
22947
23019
  sheet: "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml",
23020
+ metadata: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheetMetadata+xml",
22948
23021
  sharedStrings: "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml",
22949
23022
  styles: "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml",
22950
23023
  drawing: "application/vnd.openxmlformats-officedocument.drawing+xml",
@@ -22957,6 +23030,7 @@ stores.inject(MyMetaStore, storeInstance);
22957
23030
  const XLSX_RELATION_TYPE = {
22958
23031
  document: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument",
22959
23032
  sheet: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet",
23033
+ metadata: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sheetMetadata",
22960
23034
  sharedStrings: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings",
22961
23035
  styles: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles",
22962
23036
  drawing: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing",
@@ -22966,6 +23040,7 @@ stores.inject(MyMetaStore, storeInstance);
22966
23040
  hyperlink: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
22967
23041
  image: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
22968
23042
  };
23043
+ const ARRAY_FORMULA_URI = "bdbb8cdc-fa1e-496e-a857-3c3f30c029c3";
22969
23044
  const RELATIONSHIP_NSR = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
22970
23045
  const HEIGHT_FACTOR = 0.75; // 100px => 75 u
22971
23046
  /**
@@ -24341,16 +24416,25 @@ stores.inject(MyMetaStore, storeInstance);
24341
24416
  }
24342
24417
  return id;
24343
24418
  }
24419
+ const globalReverseLookup = new WeakMap();
24344
24420
  function pushElement(property, propertyList) {
24345
- let len = propertyList.length;
24346
- const operator = typeof property === "object" ? deepEquals : (a, b) => a === b;
24347
- for (let i = 0; i < len; i++) {
24348
- if (operator(property, propertyList[i])) {
24349
- return i;
24421
+ let reverseLookup = globalReverseLookup.get(propertyList);
24422
+ if (!reverseLookup) {
24423
+ reverseLookup = new Map();
24424
+ for (let i = 0; i < propertyList.length; i++) {
24425
+ const canonical = getCanonicalRepresentation(propertyList[i]);
24426
+ reverseLookup.set(canonical, i);
24350
24427
  }
24428
+ globalReverseLookup.set(propertyList, reverseLookup);
24429
+ }
24430
+ const canonical = getCanonicalRepresentation(property);
24431
+ if (reverseLookup.has(canonical)) {
24432
+ return reverseLookup.get(canonical);
24351
24433
  }
24352
- propertyList[propertyList.length] = property;
24353
- return propertyList.length - 1;
24434
+ const maxId = propertyList.length;
24435
+ propertyList.push(property);
24436
+ reverseLookup.set(canonical, maxId);
24437
+ return maxId;
24354
24438
  }
24355
24439
  const chartIds = [];
24356
24440
  /**
@@ -25387,29 +25471,34 @@ stores.inject(MyMetaStore, storeInstance);
25387
25471
  * In all the sheets, replace the table-only references in the formula cells with standard references.
25388
25472
  */
25389
25473
  function convertTableFormulaReferences(convertedSheets, xlsxSheets) {
25390
- for (let sheet of convertedSheets) {
25391
- const tables = xlsxSheets.find((s) => s.sheetName === sheet.name).tables;
25474
+ for (let tableSheet of convertedSheets) {
25475
+ const tables = xlsxSheets.find((s) => s.sheetName === tableSheet.name).tables;
25392
25476
  for (let table of tables) {
25393
25477
  const tabRef = table.name + "[";
25394
- for (let position of positions(toZone(table.ref))) {
25395
- const xc = toXC(position.col, position.row);
25396
- let cellContent = sheet.cells[xc];
25397
- if (cellContent?.startsWith("=")) {
25398
- let refIndex;
25399
- while ((refIndex = cellContent.indexOf(tabRef)) !== -1) {
25400
- let reference = cellContent.slice(refIndex + tabRef.length);
25401
- // Expression can either be tableName[colName] or tableName[[#This Row], [colName]]
25402
- let endIndex = reference.indexOf("]");
25403
- if (reference.startsWith(`[`)) {
25404
- endIndex = reference.indexOf("]", endIndex + 1);
25405
- endIndex = reference.indexOf("]", endIndex + 1);
25478
+ for (let sheet of convertedSheets) {
25479
+ for (let xc in sheet.cells) {
25480
+ const cell = sheet.cells[xc];
25481
+ let cellContent = sheet.cells[xc];
25482
+ if (cell && cellContent && cellContent.startsWith("=")) {
25483
+ let refIndex;
25484
+ while ((refIndex = cellContent.indexOf(tabRef)) !== -1) {
25485
+ let endIndex = refIndex + tabRef.length;
25486
+ let openBrackets = 1;
25487
+ while (openBrackets > 0 && endIndex < cellContent.length) {
25488
+ if (cellContent[endIndex] === "[") {
25489
+ openBrackets++;
25490
+ }
25491
+ else if (cellContent[endIndex] === "]") {
25492
+ openBrackets--;
25493
+ }
25494
+ endIndex++;
25495
+ }
25496
+ let reference = cellContent.slice(refIndex + tabRef.length, endIndex - 1);
25497
+ const sheetPrefix = tableSheet.id === sheet.id ? "" : tableSheet.name + "!";
25498
+ const convertedRef = convertTableReference(sheetPrefix, reference, table, xc);
25499
+ cellContent =
25500
+ cellContent.slice(0, refIndex) + convertedRef + cellContent.slice(endIndex);
25406
25501
  }
25407
- reference = reference.slice(0, endIndex);
25408
- const convertedRef = convertTableReference(reference, table, xc);
25409
- cellContent =
25410
- cellContent.slice(0, refIndex) +
25411
- convertedRef +
25412
- cellContent.slice(tabRef.length + refIndex + endIndex + 1);
25413
25502
  }
25414
25503
  sheet.cells[xc] = cellContent;
25415
25504
  }
@@ -25418,11 +25507,17 @@ stores.inject(MyMetaStore, storeInstance);
25418
25507
  }
25419
25508
  }
25420
25509
  /**
25421
- * Convert table-specific references in formulas into standard references.
25510
+ * Convert table-specific references in formulas into standard references. A table reference is composed of columns names,
25511
+ * and of keywords determining the rows of the table to reference.
25422
25512
  *
25423
25513
  * A reference in a table can have the form (only the part between brackets should be given to this function):
25424
25514
  * - tableName[colName] : reference to the whole column "colName"
25515
+ * - tableName[#keyword] : reference to the whatever row the keyword refers to
25425
25516
  * - tableName[[#keyword], [colName]] : reference to some of the element(s) of the column colName
25517
+ * - tableName[[#keyword], [colName]:[col2Name]] : reference to some of the element(s) of the columns colName to col2Name
25518
+ * - tableName[[#keyword1], [#keyword2], [colName]] : reference to all the rows referenced by the keywords in the column colName
25519
+ * - tableName[[#keyword1], [colName], [#keyword2]]: the keywords and colName can be in any order
25520
+ *
25426
25521
  *
25427
25522
  * The available keywords are :
25428
25523
  * - #All : all the column (including totals)
@@ -25430,58 +25525,109 @@ stores.inject(MyMetaStore, storeInstance);
25430
25525
  * - #Headers : only the header of the column
25431
25526
  * - #Totals : only the totals of the column
25432
25527
  * - #This Row : only the element in the same row as the cell
25528
+ *
25529
+ * Note that the only valid combination of multiple keywords are #Data + #Totals and #Headers + #Data.
25433
25530
  */
25434
- function convertTableReference(expr, table, cellXc) {
25435
- const refElements = expr.split(",");
25531
+ function convertTableReference(sheetPrefix, expr, table, cellXc) {
25532
+ // TODO: Ideally we'd want to make a real tokenizer, this simple approach won't work if for example the column name
25533
+ // contain # or , characters. But that's probably an edge case that we can ignore for now.
25534
+ const parts = expr.split(",").map((part) => part.trim());
25436
25535
  const tableZone = toZone(table.ref);
25437
- const refZone = { ...tableZone };
25438
- let isReferencedZoneValid = true;
25439
- // Single column reference
25440
- if (refElements.length === 1) {
25441
- const colRelativeIndex = table.cols.findIndex((col) => col.name === refElements[0]);
25442
- refZone.left = refZone.right = colRelativeIndex + tableZone.left;
25443
- if (table.headerRowCount) {
25444
- refZone.top += table.headerRowCount;
25445
- }
25446
- if (table.totalsRowCount) {
25447
- refZone.bottom -= 1;
25536
+ const colIndexes = [];
25537
+ const rowIndexes = [];
25538
+ const foundKeywords = [];
25539
+ for (const part of parts) {
25540
+ if (removeBrackets(part).startsWith("#")) {
25541
+ const keyWord = removeBrackets(part);
25542
+ foundKeywords.push(keyWord);
25543
+ switch (keyWord) {
25544
+ case "#All":
25545
+ rowIndexes.push(tableZone.top, tableZone.bottom);
25546
+ break;
25547
+ case "#Data":
25548
+ const top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
25549
+ const bottom = table.totalsRowCount
25550
+ ? tableZone.bottom - table.totalsRowCount
25551
+ : tableZone.bottom;
25552
+ rowIndexes.push(top, bottom);
25553
+ break;
25554
+ case "#This Row":
25555
+ rowIndexes.push(toCartesian(cellXc).row);
25556
+ break;
25557
+ case "#Headers":
25558
+ if (!table.headerRowCount) {
25559
+ return CellErrorType.InvalidReference;
25560
+ }
25561
+ rowIndexes.push(tableZone.top);
25562
+ break;
25563
+ case "#Totals":
25564
+ if (!table.totalsRowCount) {
25565
+ return CellErrorType.InvalidReference;
25566
+ }
25567
+ rowIndexes.push(tableZone.bottom);
25568
+ break;
25569
+ }
25448
25570
  }
25449
- }
25450
- // Other references
25451
- else {
25452
- switch (refElements[0].slice(1, refElements[0].length - 1)) {
25453
- case "#All":
25454
- refZone.top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
25455
- refZone.bottom = tableZone.bottom;
25456
- break;
25457
- case "#Data":
25458
- refZone.top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
25459
- refZone.bottom = table.totalsRowCount ? tableZone.bottom + 1 : tableZone.bottom;
25460
- break;
25461
- case "#This Row":
25462
- refZone.top = refZone.bottom = toCartesian(cellXc).row;
25463
- break;
25464
- case "#Headers":
25465
- refZone.top = refZone.bottom = tableZone.top;
25466
- if (!table.headerRowCount) {
25467
- isReferencedZoneValid = false;
25468
- }
25469
- break;
25470
- case "#Totals":
25471
- refZone.top = refZone.bottom = tableZone.bottom;
25472
- if (!table.totalsRowCount) {
25473
- isReferencedZoneValid = false;
25571
+ else {
25572
+ const columns = part
25573
+ .split(":")
25574
+ .map((part) => part.trim())
25575
+ .map(removeBrackets);
25576
+ if (colIndexes.length) {
25577
+ return CellErrorType.InvalidReference;
25578
+ }
25579
+ const colRelativeIndex = table.cols.findIndex((col) => col.name === columns[0]);
25580
+ if (colRelativeIndex === -1) {
25581
+ return CellErrorType.InvalidReference;
25582
+ }
25583
+ colIndexes.push(colRelativeIndex + tableZone.left);
25584
+ if (columns[1]) {
25585
+ const colRelativeIndex2 = table.cols.findIndex((col) => col.name === columns[1]);
25586
+ if (colRelativeIndex2 === -1) {
25587
+ return CellErrorType.InvalidReference;
25474
25588
  }
25475
- break;
25589
+ colIndexes.push(colRelativeIndex2 + tableZone.left);
25590
+ }
25476
25591
  }
25477
- const colRef = refElements[1].slice(1, refElements[1].length - 1);
25478
- const colRelativeIndex = table.cols.findIndex((col) => col.name === colRef);
25479
- refZone.left = refZone.right = colRelativeIndex + tableZone.left;
25480
25592
  }
25481
- if (!isReferencedZoneValid) {
25593
+ if (!areKeywordsCompatible(foundKeywords)) {
25482
25594
  return CellErrorType.InvalidReference;
25483
25595
  }
25484
- return refZone.top !== refZone.bottom ? zoneToXc(refZone) : toXC(refZone.left, refZone.top);
25596
+ if (rowIndexes.length === 0) {
25597
+ const top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
25598
+ const bottom = table.totalsRowCount
25599
+ ? tableZone.bottom - table.totalsRowCount
25600
+ : tableZone.bottom;
25601
+ rowIndexes.push(top, bottom);
25602
+ }
25603
+ if (colIndexes.length === 0) {
25604
+ colIndexes.push(tableZone.left, tableZone.right);
25605
+ }
25606
+ const refZone = {
25607
+ top: Math.min(...rowIndexes),
25608
+ left: Math.min(...colIndexes),
25609
+ bottom: Math.max(...rowIndexes),
25610
+ right: Math.max(...colIndexes),
25611
+ };
25612
+ return sheetPrefix + zoneToXc(refZone);
25613
+ }
25614
+ function removeBrackets(str) {
25615
+ return str.startsWith("[") && str.endsWith("]") ? str.slice(1, str.length - 1) : str;
25616
+ }
25617
+ function areKeywordsCompatible(keywords) {
25618
+ if (keywords.length < 2) {
25619
+ return true;
25620
+ }
25621
+ else if (keywords.length > 2) {
25622
+ return false;
25623
+ }
25624
+ else if (keywords.includes("#Data") && keywords.includes("#Totals")) {
25625
+ return true;
25626
+ }
25627
+ else if (keywords.includes("#Headers") && keywords.includes("#Data")) {
25628
+ return true;
25629
+ }
25630
+ return false;
25485
25631
  }
25486
25632
 
25487
25633
  // -------------------------------------
@@ -26135,7 +26281,7 @@ stores.inject(MyMetaStore, storeInstance);
26135
26281
  title: { text: chartTitle },
26136
26282
  type: CHART_TYPE_CONVERSION_MAP[chartType],
26137
26283
  dataSets: this.extractChartDatasets(this.querySelectorAll(rootChartElement, `c:${chartType}`), chartType),
26138
- labelRange: this.extractChildTextContent(rootChartElement, `c:ser ${chartType === "scatterChart" ? "c:numRef" : "c:cat"} c:f`),
26284
+ labelRange: this.extractLabelRange(chartType, rootChartElement),
26139
26285
  backgroundColor: this.extractChildAttr(rootChartElement, "c:chartSpace > c:spPr a:srgbClr", "val", {
26140
26286
  default: "ffffff",
26141
26287
  }).asString(),
@@ -26147,6 +26293,13 @@ stores.inject(MyMetaStore, storeInstance);
26147
26293
  };
26148
26294
  })[0];
26149
26295
  }
26296
+ extractLabelRange(chartType, rootChartElement) {
26297
+ if (chartType === "scatterChart") {
26298
+ return (this.extractChildTextContent(rootChartElement, `c:ser c:strRef c:f`) ||
26299
+ this.extractChildTextContent(rootChartElement, `c:ser c:numRef c:f`));
26300
+ }
26301
+ return this.extractChildTextContent(rootChartElement, `c:ser c:cat c:f`);
26302
+ }
26150
26303
  extractComboChart(chartElement) {
26151
26304
  // Title can be separated into multiple xml elements (for styling and such), we only import the text
26152
26305
  const chartTitle = this.mapOnElements({ parent: chartElement, query: "c:title a:t" }, (textElement) => {
@@ -28649,11 +28802,12 @@ stores.inject(MyMetaStore, storeInstance);
28649
28802
  }
28650
28803
  let missingTimeAdapterAlreadyWarned = false;
28651
28804
  function isLuxonTimeAdapterInstalled() {
28652
- if (!window.Chart) {
28805
+ const Chart = getChartJSConstructor();
28806
+ if (!Chart) {
28653
28807
  return false;
28654
28808
  }
28655
28809
  // @ts-ignore
28656
- const adapter = new window.Chart._adapters._date({});
28810
+ const adapter = new Chart._adapters._date({});
28657
28811
  const isInstalled = adapter._id === "luxon";
28658
28812
  if (!isInstalled && !missingTimeAdapterAlreadyWarned) {
28659
28813
  missingTimeAdapterAlreadyWarned = true;
@@ -32507,10 +32661,6 @@ stores.inject(MyMetaStore, storeInstance);
32507
32661
  this.currentDisplayValue = newDisplay;
32508
32662
  if (!anchor)
32509
32663
  return;
32510
- el.style.top = "";
32511
- el.style.left = "";
32512
- el.style["max-height"] = "";
32513
- el.style["max-width"] = "";
32514
32664
  const propsMaxSize = { width: this.props.maxWidth, height: this.props.maxHeight };
32515
32665
  let elDims = {
32516
32666
  width: el.getBoundingClientRect().width,
@@ -34062,6 +34212,7 @@ stores.inject(MyMetaStore, storeInstance);
34062
34212
  duplicateLabelRangeInDuplicatedSheet: duplicateLabelRangeInDuplicatedSheet,
34063
34213
  formatChartDatasetValue: formatChartDatasetValue,
34064
34214
  formatTickValue: formatTickValue,
34215
+ getChartJSConstructor: getChartJSConstructor,
34065
34216
  getChartPositionAtCenterOfViewport: getChartPositionAtCenterOfViewport,
34066
34217
  getDefinedAxis: getDefinedAxis,
34067
34218
  getPieColors: getPieColors,
@@ -36321,6 +36472,7 @@ stores.inject(MyMetaStore, storeInstance);
36321
36472
  fingerprintStore.enable();
36322
36473
  }
36323
36474
  },
36475
+ isReadonlyAllowed: true,
36324
36476
  icon: "o-spreadsheet-Icon.IRREGULARITY_MAP",
36325
36477
  };
36326
36478
  const viewFormulas = {
@@ -37862,6 +38014,11 @@ stores.inject(MyMetaStore, storeInstance);
37862
38014
  }
37863
38015
  updateColors(colors) {
37864
38016
  this.colors = colors;
38017
+ const colorGenerator = new ColorGenerator(this.ranges.length, this.colors);
38018
+ this.ranges = this.ranges.map((range) => ({
38019
+ ...range,
38020
+ color: colorGenerator.next(),
38021
+ }));
37865
38022
  }
37866
38023
  confirm() {
37867
38024
  for (const range of this.selectionInputs) {
@@ -37896,12 +38053,11 @@ stores.inject(MyMetaStore, storeInstance);
37896
38053
  * e.g. ["A1", "Sheet2!B3", "E12"]
37897
38054
  */
37898
38055
  get selectionInputs() {
37899
- const generator = new ColorGenerator(this.ranges.length, this.colors);
37900
38056
  return this.ranges.map((input, index) => Object.assign({}, input, {
37901
38057
  color: this.hasMainFocus &&
37902
38058
  this.focusedRangeIndex !== null &&
37903
38059
  this.getters.isRangeValid(input.xc)
37904
- ? generator.next()
38060
+ ? input.color
37905
38061
  : null,
37906
38062
  isFocused: this.hasMainFocus && this.focusedRangeIndex === index,
37907
38063
  isValidRange: input.xc === "" || this.getters.isRangeValid(input.xc),
@@ -38171,10 +38327,10 @@ stores.inject(MyMetaStore, storeInstance);
38171
38327
  if (originalIndex === finalIndex) {
38172
38328
  return;
38173
38329
  }
38174
- const draggedItems = [...draggableIds];
38175
- draggedItems.splice(originalIndex, 1);
38176
- draggedItems.splice(finalIndex, 0, rangeId);
38177
- this.props.onSelectionReordered?.(this.store.selectionInputs.map((range) => draggedItems.indexOf(range.id)));
38330
+ const indexes = range(0, draggableIds.length);
38331
+ indexes.splice(originalIndex, 1);
38332
+ indexes.splice(finalIndex, 0, originalIndex);
38333
+ this.props.onSelectionReordered?.(indexes);
38178
38334
  this.props.onSelectionConfirmed?.();
38179
38335
  this.store.confirm();
38180
38336
  },
@@ -38452,6 +38608,9 @@ stores.inject(MyMetaStore, storeInstance);
38452
38608
  this.state.datasetDispatchResult = this.props.updateChart(this.props.figureId, {
38453
38609
  dataSets: this.dataSets,
38454
38610
  });
38611
+ if (this.state.datasetDispatchResult.isSuccessful) {
38612
+ this.dataSets = this.env.model.getters.getChartDefinition(this.props.figureId).dataSets;
38613
+ }
38455
38614
  }
38456
38615
  getDataSeriesRanges() {
38457
38616
  return this.dataSets;
@@ -39868,8 +40027,16 @@ stores.inject(MyMetaStore, storeInstance);
39868
40027
  }
39869
40028
  let startNode = this.findChildAtCharacterIndex(start);
39870
40029
  let endNode = this.findChildAtCharacterIndex(end);
39871
- range.setStart(startNode.node, startNode.offset);
39872
- range.setEnd(endNode.node, endNode.offset);
40030
+ // setEnd (setStart) will result in a collapsed range if the end point is before the start point
40031
+ // https://developer.mozilla.org/en-US/docs/Web/API/Range/setEnd
40032
+ if (start <= end) {
40033
+ range.setStart(startNode.node, startNode.offset);
40034
+ range.setEnd(endNode.node, endNode.offset);
40035
+ }
40036
+ else {
40037
+ range.setStart(endNode.node, endNode.offset);
40038
+ range.setEnd(startNode.node, startNode.offset);
40039
+ }
39873
40040
  }
39874
40041
  }
39875
40042
  /**
@@ -40171,8 +40338,7 @@ stores.inject(MyMetaStore, storeInstance);
40171
40338
  }
40172
40339
 
40173
40340
  .o-composer-assistant {
40174
- position: absolute;
40175
- margin: 1px 4px;
40341
+ margin-top: 1px;
40176
40342
 
40177
40343
  .o-semi-bold {
40178
40344
  /* FIXME: to remove in favor of Bootstrap
@@ -40223,10 +40389,11 @@ stores.inject(MyMetaStore, storeInstance);
40223
40389
  });
40224
40390
  compositionActive = false;
40225
40391
  spreadsheetRect = useSpreadsheetRect();
40226
- get assistantStyle() {
40392
+ get assistantStyleProperties() {
40227
40393
  const composerRect = this.composerRef.el.getBoundingClientRect();
40228
40394
  const assistantStyle = {};
40229
- assistantStyle["min-width"] = `${this.props.rect?.width || ASSISTANT_WIDTH}px`;
40395
+ const minWidth = Math.min(this.props.rect?.width || Infinity, ASSISTANT_WIDTH);
40396
+ assistantStyle["min-width"] = `${minWidth}px`;
40230
40397
  const proposals = this.autoCompleteState.provider?.proposals;
40231
40398
  const proposalsHaveDescription = proposals?.some((proposal) => proposal.description);
40232
40399
  if (this.functionDescriptionState.showDescription || proposalsHaveDescription) {
@@ -40250,13 +40417,29 @@ stores.inject(MyMetaStore, storeInstance);
40250
40417
  }
40251
40418
  }
40252
40419
  else {
40253
- assistantStyle["max-height"] = `${this.spreadsheetRect.height - composerRect.bottom}px`;
40420
+ assistantStyle["max-height"] = `${this.spreadsheetRect.height - composerRect.bottom - 1}px`; // -1: margin
40254
40421
  if (composerRect.left + ASSISTANT_WIDTH + SCROLLBAR_WIDTH + CLOSE_ICON_RADIUS >
40255
40422
  this.spreadsheetRect.width) {
40256
40423
  assistantStyle.right = `${CLOSE_ICON_RADIUS}px`;
40257
40424
  }
40258
40425
  }
40259
- return cssPropertiesToCss(assistantStyle);
40426
+ return assistantStyle;
40427
+ }
40428
+ get assistantStyle() {
40429
+ const allProperties = this.assistantStyleProperties;
40430
+ return cssPropertiesToCss({
40431
+ "max-height": allProperties["max-height"],
40432
+ width: allProperties["width"],
40433
+ "min-width": allProperties["min-width"],
40434
+ });
40435
+ }
40436
+ get assistantContainerStyle() {
40437
+ const allProperties = this.assistantStyleProperties;
40438
+ return cssPropertiesToCss({
40439
+ top: allProperties["top"],
40440
+ right: allProperties["right"],
40441
+ transform: allProperties["transform"],
40442
+ });
40260
40443
  }
40261
40444
  // we can't allow input events to be triggered while we remove and add back the content of the composer in processContent
40262
40445
  shouldProcessInputEvents = false;
@@ -46730,9 +46913,7 @@ stores.inject(MyMetaStore, storeInstance);
46730
46913
  pivot: this.draft,
46731
46914
  });
46732
46915
  this.draft = null;
46733
- if (!this.alreadyNotified &&
46734
- !this.isDynamicPivotInViewport() &&
46735
- this.isStaticPivotInViewport()) {
46916
+ if (!this.alreadyNotified && this.isUpdatedPivotVisibleInViewportOnlyAsStaticPivot()) {
46736
46917
  const formulaId = this.getters.getPivotFormulaId(this.pivotId);
46737
46918
  const pivotExample = `=PIVOT(${formulaId})`;
46738
46919
  this.alreadyNotified = true;
@@ -46788,26 +46969,33 @@ stores.inject(MyMetaStore, storeInstance);
46788
46969
  this.applyUpdate();
46789
46970
  }
46790
46971
  }
46791
- isDynamicPivotInViewport() {
46792
- for (const position of this.getters.getVisibleCellPositions()) {
46793
- const isDynamicPivot = this.getters.isSpillPivotFormula(position);
46794
- if (isDynamicPivot) {
46795
- return true;
46796
- }
46797
- }
46798
- return false;
46799
- }
46800
- isStaticPivotInViewport() {
46972
+ /**
46973
+ * @returns true if the updated pivot is visible in the viewport only as a
46974
+ * static pivot and not as a dynamic pivot
46975
+ */
46976
+ isUpdatedPivotVisibleInViewportOnlyAsStaticPivot() {
46977
+ let staticPivotCount = 0;
46978
+ const updatedPivotFormulaId = this.getters.getPivotFormulaId(this.pivotId);
46801
46979
  for (const position of this.getters.getVisibleCellPositions()) {
46802
46980
  const cell = this.getters.getCell(position);
46803
46981
  if (cell?.isFormula) {
46804
46982
  const pivotFunction = getFirstPivotFunction(cell.compiledFormula.tokens);
46805
- if (pivotFunction && pivotFunction.functionName !== "PIVOT") {
46806
- return true;
46983
+ const pivotFormulaId = pivotFunction?.args[0]?.value;
46984
+ if (pivotFunction && updatedPivotFormulaId === pivotFormulaId.toString()) {
46985
+ if (pivotFunction.functionName === "PIVOT") {
46986
+ // if we have at least one dynamic pivot visible inserted the viewport
46987
+ // we return false
46988
+ return false;
46989
+ }
46990
+ else {
46991
+ staticPivotCount++;
46992
+ }
46807
46993
  }
46808
46994
  }
46809
46995
  }
46810
- return false;
46996
+ // we return true if there are only static pivots visible inserted the viewport,
46997
+ // otherwise false
46998
+ return staticPivotCount > 0;
46811
46999
  }
46812
47000
  addDefaultDateTimeGranularity(fields, definition) {
46813
47001
  const { columns, rows } = definition;
@@ -50346,6 +50534,71 @@ stores.inject(MyMetaStore, storeInstance);
50346
50534
  }
50347
50535
  }
50348
50536
 
50537
+ class UnhideRowHeaders extends owl.Component {
50538
+ static template = "o-spreadsheet-UnhideRowHeaders";
50539
+ static props = {
50540
+ headersGroups: Array,
50541
+ headerRange: Object,
50542
+ offset: { type: Number, optional: true },
50543
+ };
50544
+ static defaultProps = { offset: 0 };
50545
+ get sheetId() {
50546
+ return this.env.model.getters.getActiveSheetId();
50547
+ }
50548
+ getUnhidePreviousButtonStyle(hiddenIndex) {
50549
+ const rect = this.env.model.getters.getRect(positionToZone({ col: 0, row: hiddenIndex }));
50550
+ const y = rect.y + rect.height - HEADER_HEIGHT;
50551
+ return cssPropertiesToCss({ top: y - this.props.offset + "px", "margin-right": "1px" });
50552
+ }
50553
+ getUnhideNextButtonStyle(hiddenIndex) {
50554
+ const rect = this.env.model.getters.getRect(positionToZone({ col: 0, row: hiddenIndex }));
50555
+ const y = rect.y - HEADER_HEIGHT;
50556
+ return cssPropertiesToCss({ top: y - this.props.offset + "px", "margin-right": "1px" });
50557
+ }
50558
+ unhide(hiddenElements) {
50559
+ this.env.model.dispatch("UNHIDE_COLUMNS_ROWS", {
50560
+ sheetId: this.sheetId,
50561
+ dimension: "ROW",
50562
+ elements: hiddenElements,
50563
+ });
50564
+ }
50565
+ isVisible(header) {
50566
+ return header >= this.props.headerRange.start && header <= this.props.headerRange.end;
50567
+ }
50568
+ }
50569
+ class UnhideColumnHeaders extends owl.Component {
50570
+ static template = "o-spreadsheet-UnhideColumnHeaders";
50571
+ static props = {
50572
+ headersGroups: Array,
50573
+ headerRange: Object,
50574
+ offset: { type: Number, optional: true },
50575
+ };
50576
+ static defaultProps = { offset: 0 };
50577
+ get sheetId() {
50578
+ return this.env.model.getters.getActiveSheetId();
50579
+ }
50580
+ getUnhidePreviousButtonStyle(hiddenIndex) {
50581
+ const rect = this.env.model.getters.getRect(positionToZone({ col: hiddenIndex, row: 0 }));
50582
+ const x = rect.x + rect.width - HEADER_WIDTH;
50583
+ return cssPropertiesToCss({ left: x - this.props.offset + "px" });
50584
+ }
50585
+ getUnhideNextButtonStyle(hiddenIndex) {
50586
+ const rect = this.env.model.getters.getRect(positionToZone({ col: hiddenIndex, row: 0 }));
50587
+ const x = rect.x - HEADER_WIDTH;
50588
+ return cssPropertiesToCss({ left: x - this.props.offset + "px" });
50589
+ }
50590
+ unhide(hiddenElements) {
50591
+ this.env.model.dispatch("UNHIDE_COLUMNS_ROWS", {
50592
+ sheetId: this.sheetId,
50593
+ dimension: "COL",
50594
+ elements: hiddenElements,
50595
+ });
50596
+ }
50597
+ isVisible(header) {
50598
+ return header >= this.props.headerRange.start && header <= this.props.headerRange.end;
50599
+ }
50600
+ }
50601
+
50349
50602
  class AbstractResizer extends owl.Component {
50350
50603
  static props = {
50351
50604
  onOpenContextMenu: Function,
@@ -50564,6 +50817,7 @@ stores.inject(MyMetaStore, storeInstance);
50564
50817
  left: ${HEADER_WIDTH}px;
50565
50818
  right: 0;
50566
50819
  height: ${HEADER_HEIGHT}px;
50820
+ width: calc(100% - ${HEADER_WIDTH + SCROLLBAR_WIDTH}px);
50567
50821
  &.o-dragging {
50568
50822
  cursor: grabbing;
50569
50823
  }
@@ -50613,6 +50867,7 @@ stores.inject(MyMetaStore, storeInstance);
50613
50867
  onOpenContextMenu: Function,
50614
50868
  };
50615
50869
  static template = "o-spreadsheet-ColResizer";
50870
+ static components = { UnhideColumnHeaders };
50616
50871
  colResizerRef;
50617
50872
  setup() {
50618
50873
  super.setup();
@@ -50621,6 +50876,9 @@ stores.inject(MyMetaStore, storeInstance);
50621
50876
  this.MAX_SIZE_MARGIN = 90;
50622
50877
  this.MIN_ELEMENT_SIZE = MIN_COL_WIDTH;
50623
50878
  }
50879
+ get sheetId() {
50880
+ return this.env.model.getters.getActiveSheetId();
50881
+ }
50624
50882
  _getEvOffset(ev) {
50625
50883
  return ev.offsetX;
50626
50884
  }
@@ -50643,10 +50901,10 @@ stores.inject(MyMetaStore, storeInstance);
50643
50901
  return this.env.model.getters.getEdgeScrollCol(position, position, position);
50644
50902
  }
50645
50903
  _getDimensionsInViewport(index) {
50646
- return this.env.model.getters.getColDimensionsInViewport(this.env.model.getters.getActiveSheetId(), index);
50904
+ return this.env.model.getters.getColDimensionsInViewport(this.sheetId, index);
50647
50905
  }
50648
50906
  _getElementSize(index) {
50649
- return this.env.model.getters.getColSize(this.env.model.getters.getActiveSheetId(), index);
50907
+ return this.env.model.getters.getColSize(this.sheetId, index);
50650
50908
  }
50651
50909
  _getMaxSize() {
50652
50910
  return this.colResizerRef.el.clientWidth;
@@ -50657,7 +50915,7 @@ stores.inject(MyMetaStore, storeInstance);
50657
50915
  const cols = this.env.model.getters.getActiveCols();
50658
50916
  this.env.model.dispatch("RESIZE_COLUMNS_ROWS", {
50659
50917
  dimension: "COL",
50660
- sheetId: this.env.model.getters.getActiveSheetId(),
50918
+ sheetId: this.sheetId,
50661
50919
  elements: cols.has(index) ? [...cols] : [index],
50662
50920
  size,
50663
50921
  });
@@ -50670,7 +50928,7 @@ stores.inject(MyMetaStore, storeInstance);
50670
50928
  elements.push(colIndex);
50671
50929
  }
50672
50930
  const result = this.env.model.dispatch("MOVE_COLUMNS_ROWS", {
50673
- sheetId: this.env.model.getters.getActiveSheetId(),
50931
+ sheetId: this.sheetId,
50674
50932
  dimension: "COL",
50675
50933
  base: this.state.base,
50676
50934
  elements,
@@ -50689,7 +50947,7 @@ stores.inject(MyMetaStore, storeInstance);
50689
50947
  _fitElementSize(index) {
50690
50948
  const cols = this.env.model.getters.getActiveCols();
50691
50949
  this.env.model.dispatch("AUTORESIZE_COLUMNS", {
50692
- sheetId: this.env.model.getters.getActiveSheetId(),
50950
+ sheetId: this.sheetId,
50693
50951
  cols: cols.has(index) ? [...cols] : [index],
50694
50952
  });
50695
50953
  }
@@ -50700,7 +50958,7 @@ stores.inject(MyMetaStore, storeInstance);
50700
50958
  return this.env.model.getters.getActiveCols();
50701
50959
  }
50702
50960
  _getPreviousVisibleElement(index) {
50703
- const sheetId = this.env.model.getters.getActiveSheetId();
50961
+ const sheetId = this.sheetId;
50704
50962
  let row;
50705
50963
  for (row = index - 1; row >= 0; row--) {
50706
50964
  if (!this.env.model.getters.isColHidden(sheetId, row)) {
@@ -50711,13 +50969,38 @@ stores.inject(MyMetaStore, storeInstance);
50711
50969
  }
50712
50970
  unhide(hiddenElements) {
50713
50971
  this.env.model.dispatch("UNHIDE_COLUMNS_ROWS", {
50714
- sheetId: this.env.model.getters.getActiveSheetId(),
50972
+ sheetId: this.sheetId,
50715
50973
  elements: hiddenElements,
50716
50974
  dimension: "COL",
50717
50975
  });
50718
50976
  }
50719
- getUnhideButtonStyle(hiddenIndex) {
50720
- return cssPropertiesToCss({ left: this._getDimensionsInViewport(hiddenIndex).start + "px" });
50977
+ get mainUnhideHeadersProps() {
50978
+ const { left, right } = this.env.model.getters.getActiveMainViewport();
50979
+ const { xSplit } = this.env.model.getters.getPaneDivisions(this.sheetId);
50980
+ const hiddenGroups = this.env.model.getters.getHiddenColsGroups(this.sheetId);
50981
+ const index = hiddenGroups.findIndex((group) => group[0] >= xSplit - 1);
50982
+ return {
50983
+ headersGroups: hiddenGroups.slice(index),
50984
+ offset: this.env.model.getters.getMainViewportCoordinates().x,
50985
+ headerRange: { start: left, end: right },
50986
+ };
50987
+ }
50988
+ get frozenUnhideHeadersProps() {
50989
+ const { xSplit } = this.env.model.getters.getPaneDivisions(this.sheetId);
50990
+ const hiddenGroups = this.env.model.getters.getHiddenColsGroups(this.sheetId);
50991
+ const index = hiddenGroups.findIndex((group) => group[0] >= xSplit - 1);
50992
+ return {
50993
+ headersGroups: hiddenGroups.slice(0, index + 1),
50994
+ headerRange: { start: 0, end: xSplit - 1 },
50995
+ };
50996
+ }
50997
+ get frozenContainerStyle() {
50998
+ return cssPropertiesToCss({
50999
+ width: this.env.model.getters.getMainViewportCoordinates().x + "px",
51000
+ });
51001
+ }
51002
+ get hasFrozenPane() {
51003
+ return this.env.model.getters.getPaneDivisions(this.sheetId).xSplit > 0;
50721
51004
  }
50722
51005
  }
50723
51006
  css /* scss */ `
@@ -50727,7 +51010,7 @@ stores.inject(MyMetaStore, storeInstance);
50727
51010
  left: 0;
50728
51011
  right: 0;
50729
51012
  width: ${HEADER_WIDTH}px;
50730
- height: 100%;
51013
+ height: calc(100% - ${HEADER_HEIGHT + SCROLLBAR_WIDTH}px);
50731
51014
  &.o-dragging {
50732
51015
  cursor: grabbing;
50733
51016
  }
@@ -50777,6 +51060,7 @@ stores.inject(MyMetaStore, storeInstance);
50777
51060
  onOpenContextMenu: Function,
50778
51061
  };
50779
51062
  static template = "o-spreadsheet-RowResizer";
51063
+ static components = { UnhideRowHeaders };
50780
51064
  setup() {
50781
51065
  super.setup();
50782
51066
  this.rowResizerRef = owl.useRef("rowResizer");
@@ -50785,6 +51069,9 @@ stores.inject(MyMetaStore, storeInstance);
50785
51069
  this.MIN_ELEMENT_SIZE = MIN_ROW_HEIGHT;
50786
51070
  }
50787
51071
  rowResizerRef;
51072
+ get sheetId() {
51073
+ return this.env.model.getters.getActiveSheetId();
51074
+ }
50788
51075
  _getEvOffset(ev) {
50789
51076
  return ev.offsetY;
50790
51077
  }
@@ -50807,10 +51094,10 @@ stores.inject(MyMetaStore, storeInstance);
50807
51094
  return this.env.model.getters.getEdgeScrollRow(position, position, position);
50808
51095
  }
50809
51096
  _getDimensionsInViewport(index) {
50810
- return this.env.model.getters.getRowDimensionsInViewport(this.env.model.getters.getActiveSheetId(), index);
51097
+ return this.env.model.getters.getRowDimensionsInViewport(this.sheetId, index);
50811
51098
  }
50812
51099
  _getElementSize(index) {
50813
- return this.env.model.getters.getRowSize(this.env.model.getters.getActiveSheetId(), index);
51100
+ return this.env.model.getters.getRowSize(this.sheetId, index);
50814
51101
  }
50815
51102
  _getMaxSize() {
50816
51103
  return this.rowResizerRef.el.clientHeight;
@@ -50821,7 +51108,7 @@ stores.inject(MyMetaStore, storeInstance);
50821
51108
  const rows = this.env.model.getters.getActiveRows();
50822
51109
  this.env.model.dispatch("RESIZE_COLUMNS_ROWS", {
50823
51110
  dimension: "ROW",
50824
- sheetId: this.env.model.getters.getActiveSheetId(),
51111
+ sheetId: this.sheetId,
50825
51112
  elements: rows.has(index) ? [...rows] : [index],
50826
51113
  size,
50827
51114
  });
@@ -50834,7 +51121,7 @@ stores.inject(MyMetaStore, storeInstance);
50834
51121
  elements.push(rowIndex);
50835
51122
  }
50836
51123
  const result = this.env.model.dispatch("MOVE_COLUMNS_ROWS", {
50837
- sheetId: this.env.model.getters.getActiveSheetId(),
51124
+ sheetId: this.sheetId,
50838
51125
  dimension: "ROW",
50839
51126
  base: this.state.base,
50840
51127
  elements,
@@ -50853,7 +51140,7 @@ stores.inject(MyMetaStore, storeInstance);
50853
51140
  _fitElementSize(index) {
50854
51141
  const rows = this.env.model.getters.getActiveRows();
50855
51142
  this.env.model.dispatch("AUTORESIZE_ROWS", {
50856
- sheetId: this.env.model.getters.getActiveSheetId(),
51143
+ sheetId: this.sheetId,
50857
51144
  rows: rows.has(index) ? [...rows] : [index],
50858
51145
  });
50859
51146
  }
@@ -50864,7 +51151,7 @@ stores.inject(MyMetaStore, storeInstance);
50864
51151
  return this.env.model.getters.getActiveRows();
50865
51152
  }
50866
51153
  _getPreviousVisibleElement(index) {
50867
- const sheetId = this.env.model.getters.getActiveSheetId();
51154
+ const sheetId = this.sheetId;
50868
51155
  let row;
50869
51156
  for (row = index - 1; row >= 0; row--) {
50870
51157
  if (!this.env.model.getters.isRowHidden(sheetId, row)) {
@@ -50873,15 +51160,33 @@ stores.inject(MyMetaStore, storeInstance);
50873
51160
  }
50874
51161
  return row;
50875
51162
  }
50876
- unhide(hiddenElements) {
50877
- this.env.model.dispatch("UNHIDE_COLUMNS_ROWS", {
50878
- sheetId: this.env.model.getters.getActiveSheetId(),
50879
- dimension: "ROW",
50880
- elements: hiddenElements,
51163
+ get mainUnhideHeadersProps() {
51164
+ const { top, bottom } = this.env.model.getters.getActiveMainViewport();
51165
+ const { ySplit } = this.env.model.getters.getPaneDivisions(this.sheetId);
51166
+ const hiddenGroups = this.env.model.getters.getHiddenRowsGroups(this.sheetId);
51167
+ const index = hiddenGroups.findIndex((group) => group[0] >= ySplit - 1);
51168
+ return {
51169
+ headersGroups: hiddenGroups.slice(index),
51170
+ offset: this.env.model.getters.getMainViewportCoordinates().y,
51171
+ headerRange: { start: top, end: bottom },
51172
+ };
51173
+ }
51174
+ get frozenUnhideHeadersProps() {
51175
+ const { ySplit } = this.env.model.getters.getPaneDivisions(this.sheetId);
51176
+ const hiddenGroups = this.env.model.getters.getHiddenRowsGroups(this.sheetId);
51177
+ const index = hiddenGroups.findIndex((group) => group[0] >= ySplit - 1);
51178
+ return {
51179
+ headersGroups: hiddenGroups.slice(0, index + 1),
51180
+ headerRange: { start: 0, end: ySplit - 1 },
51181
+ };
51182
+ }
51183
+ get frozenContainerStyle() {
51184
+ return cssPropertiesToCss({
51185
+ height: this.env.model.getters.getMainViewportCoordinates().y + "px",
50881
51186
  });
50882
51187
  }
50883
- getUnhideButtonStyle(hiddenIndex) {
50884
- return cssPropertiesToCss({ top: this._getDimensionsInViewport(hiddenIndex).start + "px" });
51188
+ get hasFrozenPane() {
51189
+ return this.env.model.getters.getPaneDivisions(this.sheetId).ySplit > 0;
50885
51190
  }
50886
51191
  }
50887
51192
  css /* scss */ `
@@ -60661,6 +60966,7 @@ stores.inject(MyMetaStore, storeInstance);
60661
60966
  exportForExcel(data) {
60662
60967
  for (const sheet of data.sheets) {
60663
60968
  sheet.cellValues = {};
60969
+ sheet.formulaSpillRanges = {};
60664
60970
  }
60665
60971
  for (const position of this.evaluator.getEvaluatedPositions()) {
60666
60972
  const evaluatedCell = this.evaluator.getEvaluatedCell(position);
@@ -60672,8 +60978,9 @@ stores.inject(MyMetaStore, storeInstance);
60672
60978
  const exportedSheetData = data.sheets.find((sheet) => sheet.id === position.sheetId);
60673
60979
  const formulaCell = this.getCorrespondingFormulaCell(position);
60674
60980
  if (formulaCell) {
60981
+ const cell = this.getters.getCell(position);
60675
60982
  isExported = isExportableToExcel(formulaCell.compiledFormula.tokens);
60676
- isFormula = isExported;
60983
+ isFormula = isExported && cell?.content === formulaCell.content;
60677
60984
  // If the cell contains a non-exported formula and that is evaluates to
60678
60985
  // nothing* ,we don't export it.
60679
60986
  // * non-falsy value are relevant and so are 0 and FALSE, which only leaves
@@ -60696,7 +61003,11 @@ stores.inject(MyMetaStore, storeInstance);
60696
61003
  content = !isExported ? newContent : exportedCellData;
60697
61004
  }
60698
61005
  exportedSheetData.cells[xc] = content;
60699
- exportedSheetData.cellValues[xc] = value;
61006
+ exportedSheetData.cellValues[xc] = evaluatedCell.type !== "error" ? value : undefined;
61007
+ const spillZone = this.getSpreadZone(position);
61008
+ if (spillZone) {
61009
+ exportedSheetData.formulaSpillRanges[xc] = this.getters.getRangeString(this.getters.getRangeFromZone(position.sheetId, spillZone), position.sheetId);
61010
+ }
60700
61011
  }
60701
61012
  }
60702
61013
  /**
@@ -62924,7 +63235,7 @@ stores.inject(MyMetaStore, storeInstance);
62924
63235
  getRule(cell, cells) {
62925
63236
  const rules = autofillRulesRegistry.getAll().sort((a, b) => a.sequence - b.sequence);
62926
63237
  const rule = rules.find((rule) => rule.condition(cell, cells));
62927
- return rule && rule.generateRule(cell, cells);
63238
+ return rule && this.direction && rule.generateRule(cell, cells, this.direction);
62928
63239
  }
62929
63240
  /**
62930
63241
  * Create the generator to be able to autofill the next cells.
@@ -67645,7 +67956,7 @@ stores.inject(MyMetaStore, storeInstance);
67645
67956
  *
67646
67957
  */
67647
67958
  getFullRect(zone) {
67648
- const targetZone = intersection(zone, this);
67959
+ const targetZone = intersection(zone, this.boundaries);
67649
67960
  const scrollDeltaX = this.snapCorrection.x;
67650
67961
  const scrollDeltaY = this.snapCorrection.y;
67651
67962
  if (targetZone) {
@@ -68113,7 +68424,8 @@ stores.inject(MyMetaStore, storeInstance);
68113
68424
  ? this.getters.getSheetViewVisibleCols()
68114
68425
  : this.getters.getSheetViewVisibleRows();
68115
68426
  const startIndex = visibleHeaders.findIndex((header) => referenceHeaderIndex >= header);
68116
- const endIndex = visibleHeaders.findIndex((header) => targetHeaderIndex <= header);
68427
+ let endIndex = visibleHeaders.findIndex((header) => targetHeaderIndex <= header);
68428
+ endIndex = endIndex === -1 ? visibleHeaders.length : endIndex;
68117
68429
  const relevantIndexes = visibleHeaders.slice(startIndex, endIndex);
68118
68430
  let offset = 0;
68119
68431
  for (const i of relevantIndexes) {
@@ -68238,11 +68550,12 @@ stores.inject(MyMetaStore, storeInstance);
68238
68550
  * column of the current viewport
68239
68551
  */
68240
68552
  getColDimensionsInViewport(sheetId, col) {
68553
+ const { top } = this.getMainInternalViewport(sheetId);
68241
68554
  const zone = {
68242
68555
  left: col,
68243
68556
  right: col,
68244
- top: 0,
68245
- bottom: this.getters.getNumberRows(sheetId) - 1,
68557
+ top: top,
68558
+ bottom: top,
68246
68559
  };
68247
68560
  const { x, width } = this.getVisibleRect(zone);
68248
68561
  const start = x - this.gridOffsetX;
@@ -68253,9 +68566,10 @@ stores.inject(MyMetaStore, storeInstance);
68253
68566
  * of the current viewport
68254
68567
  */
68255
68568
  getRowDimensionsInViewport(sheetId, row) {
68569
+ const { left } = this.getMainInternalViewport(sheetId);
68256
68570
  const zone = {
68257
68571
  left: 0,
68258
- right: this.getters.getNumberCols(sheetId) - 1,
68572
+ right: left,
68259
68573
  top: row,
68260
68574
  bottom: row,
68261
68575
  };
@@ -73406,7 +73720,7 @@ stores.inject(MyMetaStore, storeInstance);
73406
73720
  `;
73407
73721
  }
73408
73722
 
73409
- function addFormula(formula, value) {
73723
+ function addFormula(formula, value, formulaSpillRange) {
73410
73724
  if (!formula) {
73411
73725
  return { attrs: [], node: escapeXml `` };
73412
73726
  }
@@ -73414,10 +73728,17 @@ stores.inject(MyMetaStore, storeInstance);
73414
73728
  if (type === undefined) {
73415
73729
  return { attrs: [], node: escapeXml `` };
73416
73730
  }
73417
- const attrs = [["t", type]];
73731
+ const attrs = [
73732
+ ["cm", "1"],
73733
+ ["t", type],
73734
+ ];
73418
73735
  const XlsxFormula = adaptFormulaToExcel(formula);
73419
73736
  const exportedValue = adaptFormulaValueToExcel(value);
73420
- const node = escapeXml /*xml*/ `<f>${XlsxFormula}</f><v>${exportedValue}</v>`;
73737
+ // We treat all formulas as array formulas (a simple formula
73738
+ // is an array formula that spills on only one cell) to avoid
73739
+ // trying to detect spilling sub-formulas which is not a trivial task.
73740
+ let node;
73741
+ node = escapeXml /*xml*/ `<f t="array" ref="${formulaSpillRange}">${XlsxFormula}</f><v>${exportedValue}</v>`;
73421
73742
  return { attrs, node };
73422
73743
  }
73423
73744
  function addContent(content, sharedStrings, forceString = false) {
@@ -74200,7 +74521,7 @@ stores.inject(MyMetaStore, storeInstance);
74200
74521
  }
74201
74522
  if (alignAttrs.length > 0) {
74202
74523
  attributes.push(["applyAlignment", "1"]); // for Libre Office
74203
- styleNodes.push(escapeXml /*xml*/ `<xf ${formatAttributes(attributes)}>${escapeXml /*xml*/ `<alignment ${formatAttributes(alignAttrs)} />`}</xf> `);
74524
+ styleNodes.push(escapeXml /*xml*/ `<xf ${formatAttributes(attributes)}><alignment ${formatAttributes(alignAttrs)} /></xf> `);
74204
74525
  }
74205
74526
  else {
74206
74527
  styleNodes.push(escapeXml /*xml*/ `<xf ${formatAttributes(attributes)} />`);
@@ -74368,6 +74689,9 @@ stores.inject(MyMetaStore, storeInstance);
74368
74689
  }
74369
74690
  function addRows(construct, data, sheet) {
74370
74691
  const rowNodes = [];
74692
+ const styles = new PositionMap(iterateItemIdsPositions(sheet.id, sheet.styles));
74693
+ const borders = new PositionMap(iterateItemIdsPositions(sheet.id, sheet.borders));
74694
+ const formats = new PositionMap(iterateItemIdsPositions(sheet.id, sheet.formats));
74371
74695
  for (let r = 0; r < sheet.rowNumber; r++) {
74372
74696
  const rowAttrs = [["r", r + 1]];
74373
74697
  const row = sheet.rows[r] || {};
@@ -74383,9 +74707,6 @@ stores.inject(MyMetaStore, storeInstance);
74383
74707
  if (row.collapsed) {
74384
74708
  rowAttrs.push(["collapsed", 1]);
74385
74709
  }
74386
- const styles = new PositionMap(iterateItemIdsPositions(sheet.id, sheet.styles));
74387
- const borders = new PositionMap(iterateItemIdsPositions(sheet.id, sheet.borders));
74388
- const formats = new PositionMap(iterateItemIdsPositions(sheet.id, sheet.formats));
74389
74710
  const cellNodes = [];
74390
74711
  for (let c = 0; c < sheet.colNumber; c++) {
74391
74712
  const xc = toXC(c, r);
@@ -74407,7 +74728,7 @@ stores.inject(MyMetaStore, storeInstance);
74407
74728
  let cellNode = escapeXml ``;
74408
74729
  // Either formula or static value inside the cell
74409
74730
  if (content?.startsWith("=") && value !== undefined) {
74410
- const res = addFormula(content, value);
74731
+ const res = addFormula(content, value, sheet.formulaSpillRanges[xc] ?? xc);
74411
74732
  if (!res) {
74412
74733
  continue;
74413
74734
  }
@@ -74693,6 +75014,30 @@ stores.inject(MyMetaStore, storeInstance);
74693
75014
  `;
74694
75015
  files.push(createXMLFile(parseXML(sheetXml), `xl/worksheets/sheet${sheetIndex}.xml`, "sheet"));
74695
75016
  }
75017
+ const sheetMetadataXml = escapeXml /*xml*/ `
75018
+ <metadata xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:xda="http://schemas.microsoft.com/office/spreadsheetml/2017/dynamicarray">
75019
+ <metadataTypes count="1">
75020
+ <metadataType name="XLDAPR" minSupportedVersion="120000" copy="1" pasteAll="1"
75021
+ pasteValues="1" merge="1" splitFirst="1" rowColShift="1" clearFormats="1"
75022
+ clearComments="1" assign="1" coerce="1" cellMeta="1" />
75023
+ </metadataTypes>
75024
+ <futureMetadata name="XLDAPR" count="1">
75025
+ <bk>
75026
+ <extLst>
75027
+ <ext uri="{${ARRAY_FORMULA_URI}}">
75028
+ <xda:dynamicArrayProperties fDynamic="1" fCollapsed="0" />
75029
+ </ext>
75030
+ </extLst>
75031
+ </bk>
75032
+ </futureMetadata>
75033
+ <cellMetadata count="1">
75034
+ <bk>
75035
+ <rc t="1" v="0" />
75036
+ </bk>
75037
+ </cellMetadata>
75038
+ </metadata>
75039
+ `;
75040
+ files.push(createXMLFile(parseXML(sheetMetadataXml), "xl/metadata.xml", "metadata"));
74696
75041
  addRelsToFile(construct.relsFiles, "xl/_rels/workbook.xml.rels", {
74697
75042
  type: XLSX_RELATION_TYPE.sharedStrings,
74698
75043
  target: "sharedStrings.xml",
@@ -74701,6 +75046,10 @@ stores.inject(MyMetaStore, storeInstance);
74701
75046
  type: XLSX_RELATION_TYPE.styles,
74702
75047
  target: "styles.xml",
74703
75048
  });
75049
+ addRelsToFile(construct.relsFiles, "xl/_rels/workbook.xml.rels", {
75050
+ type: XLSX_RELATION_TYPE.metadata,
75051
+ target: "metadata.xml",
75052
+ });
74704
75053
  return files;
74705
75054
  }
74706
75055
  /**
@@ -75662,9 +76011,9 @@ stores.inject(MyMetaStore, storeInstance);
75662
76011
  exports.tokenize = tokenize;
75663
76012
 
75664
76013
 
75665
- __info__.version = "18.2.1";
75666
- __info__.date = "2025-02-25T06:03:13.262Z";
75667
- __info__.hash = "3b4b5c9";
76014
+ __info__.version = "18.2.3";
76015
+ __info__.date = "2025-03-12T15:32:36.274Z";
76016
+ __info__.hash = "81b0e08";
75668
76017
 
75669
76018
 
75670
76019
  })(this.o_spreadsheet = this.o_spreadsheet || {}, owl);