@odoo/o-spreadsheet 18.2.0 → 18.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,9 +2,9 @@
2
2
  /**
3
3
  * This file is generated by o-spreadsheet build tools. Do not edit it.
4
4
  * @see https://github.com/odoo/o-spreadsheet
5
- * @version 18.2.0
6
- * @date 2025-02-18T08:27:07.101Z
7
- * @hash d708714
5
+ * @version 18.2.2
6
+ * @date 2025-03-07T10:41:04.411Z
7
+ * @hash f567932
8
8
  */
9
9
 
10
10
  (function (exports, owl) {
@@ -4459,7 +4459,7 @@
4459
4459
  * @param reverseSearch if true, search in the array starting from the end.
4460
4460
 
4461
4461
  */
4462
- function linearSearch(data, target, mode, numberOfValues, getValueInData, reverseSearch = false) {
4462
+ function linearSearch(data, target, mode, numberOfValues, getValueInData, lookupCaches, reverseSearch = false) {
4463
4463
  if (target === undefined || target.value === null) {
4464
4464
  return -1;
4465
4465
  }
@@ -4468,17 +4468,48 @@
4468
4468
  }
4469
4469
  const _target = normalizeValue(target.value);
4470
4470
  const getValue = reverseSearch
4471
- ? (data, i) => getValueInData(data, numberOfValues - i - 1)
4472
- : getValueInData;
4471
+ ? (data, i) => normalizeValue(getValueInData(data, numberOfValues - i - 1))
4472
+ : (data, i) => normalizeValue(getValueInData(data, i));
4473
+ // first check if the target is in the cache
4474
+ const isNotWildcardTarget = mode !== "wildcard" ||
4475
+ typeof _target !== "string" ||
4476
+ !(_target.includes("*") || _target.includes("?"));
4477
+ if (lookupCaches && isNotWildcardTarget) {
4478
+ const searchMode = reverseSearch ? "reverseSearch" : "forwardSearch";
4479
+ let cache = lookupCaches[searchMode].get(data);
4480
+ if (cache === undefined) {
4481
+ // build the cache for all the values
4482
+ cache = new Map();
4483
+ for (let i = 0; i < numberOfValues; i++) {
4484
+ const value = getValue(data, i) ?? null;
4485
+ if (!cache.has(value)) {
4486
+ cache.set(value, i);
4487
+ }
4488
+ }
4489
+ lookupCaches[searchMode].set(data, cache);
4490
+ }
4491
+ if (cache.has(_target)) {
4492
+ const resultIndex = cache.get(_target);
4493
+ return reverseSearch ? numberOfValues - resultIndex - 1 : resultIndex;
4494
+ }
4495
+ if (mode === "strict") {
4496
+ return -1;
4497
+ }
4498
+ }
4499
+ // else perform the linear search
4500
+ const resultIndex = _linearSearch(data, _target, mode, numberOfValues, getValue);
4501
+ return reverseSearch && resultIndex !== -1 ? numberOfValues - resultIndex - 1 : resultIndex;
4502
+ }
4503
+ function _linearSearch(data, _target, mode, numberOfValues, getNormalizeValue) {
4473
4504
  let indexMatchTarget = (i) => {
4474
- return normalizeValue(getValue(data, i)) === _target;
4505
+ return getNormalizeValue(data, i) === _target;
4475
4506
  };
4476
4507
  if (mode === "wildcard" &&
4477
4508
  typeof _target === "string" &&
4478
4509
  (_target.includes("*") || _target.includes("?"))) {
4479
4510
  const regExp = wildcardToRegExp(_target);
4480
4511
  indexMatchTarget = (i) => {
4481
- const value = normalizeValue(getValue(data, i));
4512
+ const value = getNormalizeValue(data, i);
4482
4513
  if (typeof value === "string") {
4483
4514
  return regExp.test(value);
4484
4515
  }
@@ -4489,7 +4520,7 @@
4489
4520
  let closestMatchIndex = -1;
4490
4521
  if (mode === "nextSmaller") {
4491
4522
  indexMatchTarget = (i) => {
4492
- const value = normalizeValue(getValue(data, i));
4523
+ const value = getNormalizeValue(data, i);
4493
4524
  if ((!closestMatch && compareCellValues(_target, value) >= 0) ||
4494
4525
  (compareCellValues(_target, value) >= 0 && compareCellValues(value, closestMatch) > 0)) {
4495
4526
  closestMatch = value;
@@ -4500,7 +4531,7 @@
4500
4531
  }
4501
4532
  if (mode === "nextGreater") {
4502
4533
  indexMatchTarget = (i) => {
4503
- const value = normalizeValue(getValue(data, i));
4534
+ const value = getNormalizeValue(data, i);
4504
4535
  if ((!closestMatch && compareCellValues(_target, value) <= 0) ||
4505
4536
  (compareCellValues(_target, value) <= 0 && compareCellValues(value, closestMatch) < 0)) {
4506
4537
  closestMatch = value;
@@ -4511,12 +4542,10 @@
4511
4542
  }
4512
4543
  for (let i = 0; i < numberOfValues; i++) {
4513
4544
  if (indexMatchTarget(i)) {
4514
- return reverseSearch ? numberOfValues - i - 1 : i;
4545
+ return i;
4515
4546
  }
4516
4547
  }
4517
- return reverseSearch && closestMatchIndex !== -1
4518
- ? numberOfValues - closestMatchIndex - 1
4519
- : closestMatchIndex;
4548
+ return closestMatchIndex;
4520
4549
  }
4521
4550
  /**
4522
4551
  * Normalize a value.
@@ -6072,8 +6101,9 @@
6072
6101
  if (zone.bottom !== zone.top && zone.left != zone.right) {
6073
6102
  if (zone.right) {
6074
6103
  for (let j = zone.left; j <= zone.right; ++j) {
6104
+ const datasetOptions = j === zone.left ? dataSet : { yAxisId: dataSet.yAxisId };
6075
6105
  postProcessedRanges.push({
6076
- ...dataSet,
6106
+ ...datasetOptions,
6077
6107
  dataRange: `${sheetPrefix}${zoneToXc({
6078
6108
  left: j,
6079
6109
  right: j,
@@ -6085,8 +6115,9 @@
6085
6115
  }
6086
6116
  else {
6087
6117
  for (let j = zone.top; j <= zone.bottom; ++j) {
6118
+ const datasetOptions = j === zone.top ? dataSet : { yAxisId: dataSet.yAxisId };
6088
6119
  postProcessedRanges.push({
6089
- ...dataSet,
6120
+ ...datasetOptions,
6090
6121
  dataRange: `${sheetPrefix}${zoneToXc({
6091
6122
  left: zone.left,
6092
6123
  right: zone.right,
@@ -6490,10 +6521,11 @@
6490
6521
  *
6491
6522
  */
6492
6523
  smallUuid() {
6493
- //@ts-ignore
6494
- if (window.crypto && window.crypto.getRandomValues) {
6495
- //@ts-ignore
6496
- return ([1e7] + -1e3).replace(/[018]/g, (c) => (c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16));
6524
+ if (window.crypto) {
6525
+ return "10000000-1000".replace(/[01]/g, (c) => {
6526
+ const n = Number(c);
6527
+ return (n ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (n / 4)))).toString(16);
6528
+ });
6497
6529
  }
6498
6530
  else {
6499
6531
  // mainly for jest and other browsers that do not have the crypto functionality
@@ -6508,10 +6540,11 @@
6508
6540
  * This method should be used when you need to avoid collisions at all costs, like the id of a revision.
6509
6541
  */
6510
6542
  uuidv4() {
6511
- //@ts-ignore
6512
- if (window.crypto && window.crypto.getRandomValues) {
6513
- //@ts-ignore
6514
- return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) => (c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16));
6543
+ if (window.crypto) {
6544
+ return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, (c) => {
6545
+ const n = Number(c);
6546
+ return (n ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (n / 4)))).toString(16);
6547
+ });
6515
6548
  }
6516
6549
  else {
6517
6550
  // mainly for jest and other browsers that do not have the crypto functionality
@@ -9502,150 +9535,6 @@ stores.inject(MyMetaStore, storeInstance);
9502
9535
  }
9503
9536
  }
9504
9537
 
9505
- /**
9506
- * This file is largely inspired by owl 1.
9507
- * `css` tag has been removed from owl 2 without workaround to manage css.
9508
- * So, the solution was to import the behavior of owl 1 directly in our
9509
- * codebase, with one difference: the css is added to the sheet as soon as the
9510
- * css tag is executed. In owl 1, the css was added as soon as a Component was
9511
- * created for the first time.
9512
- */
9513
- const STYLESHEETS = {};
9514
- let nextId = 0;
9515
- /**
9516
- * CSS tag helper for defining inline stylesheets. With this, one can simply define
9517
- * an inline stylesheet with just the following code:
9518
- * ```js
9519
- * css`.component-a { color: red; }`;
9520
- * ```
9521
- */
9522
- function css(strings, ...args) {
9523
- const name = `__sheet__${nextId++}`;
9524
- const value = String.raw(strings, ...args);
9525
- registerSheet(name, value);
9526
- activateSheet(name);
9527
- return name;
9528
- }
9529
- function processSheet(str) {
9530
- const tokens = str.split(/(\{|\}|;)/).map((s) => s.trim());
9531
- const selectorStack = [];
9532
- const parts = [];
9533
- let rules = [];
9534
- function generateSelector(stackIndex, parentSelector) {
9535
- const parts = [];
9536
- for (const selector of selectorStack[stackIndex]) {
9537
- let part = (parentSelector && parentSelector + " " + selector) || selector;
9538
- if (part.includes("&")) {
9539
- part = selector.replace(/&/g, parentSelector || "");
9540
- }
9541
- if (stackIndex < selectorStack.length - 1) {
9542
- part = generateSelector(stackIndex + 1, part);
9543
- }
9544
- parts.push(part);
9545
- }
9546
- return parts.join(", ");
9547
- }
9548
- function generateRules() {
9549
- if (rules.length) {
9550
- parts.push(generateSelector(0) + " {");
9551
- parts.push(...rules);
9552
- parts.push("}");
9553
- rules = [];
9554
- }
9555
- }
9556
- while (tokens.length) {
9557
- let token = tokens.shift();
9558
- if (token === "}") {
9559
- generateRules();
9560
- selectorStack.pop();
9561
- }
9562
- else {
9563
- if (tokens[0] === "{") {
9564
- generateRules();
9565
- selectorStack.push(token.split(/\s*,\s*/));
9566
- tokens.shift();
9567
- }
9568
- if (tokens[0] === ";") {
9569
- rules.push(" " + token + ";");
9570
- }
9571
- }
9572
- }
9573
- return parts.join("\n");
9574
- }
9575
- function registerSheet(id, css) {
9576
- const sheet = document.createElement("style");
9577
- sheet.textContent = processSheet(css);
9578
- STYLESHEETS[id] = sheet;
9579
- }
9580
- function activateSheet(id) {
9581
- const sheet = STYLESHEETS[id];
9582
- sheet.setAttribute("component", id);
9583
- document.head.appendChild(sheet);
9584
- }
9585
- function getTextDecoration({ strikethrough, underline, }) {
9586
- if (!strikethrough && !underline) {
9587
- return "none";
9588
- }
9589
- return `${strikethrough ? "line-through" : ""} ${underline ? "underline" : ""}`;
9590
- }
9591
- /**
9592
- * Convert the cell style to CSS properties.
9593
- */
9594
- function cellStyleToCss(style) {
9595
- const attributes = cellTextStyleToCss(style);
9596
- if (!style)
9597
- return attributes;
9598
- if (style.fillColor) {
9599
- attributes["background"] = style.fillColor;
9600
- }
9601
- return attributes;
9602
- }
9603
- /**
9604
- * Convert the cell text style to CSS properties.
9605
- */
9606
- function cellTextStyleToCss(style) {
9607
- const attributes = {};
9608
- if (!style)
9609
- return attributes;
9610
- if (style.bold) {
9611
- attributes["font-weight"] = "bold";
9612
- }
9613
- if (style.italic) {
9614
- attributes["font-style"] = "italic";
9615
- }
9616
- if (style.strikethrough || style.underline) {
9617
- let decoration = style.strikethrough ? "line-through" : "";
9618
- decoration = style.underline ? decoration + " underline" : decoration;
9619
- attributes["text-decoration"] = decoration;
9620
- }
9621
- if (style.textColor) {
9622
- attributes["color"] = style.textColor;
9623
- }
9624
- return attributes;
9625
- }
9626
- /**
9627
- * Transform CSS properties into a CSS string.
9628
- */
9629
- function cssPropertiesToCss(attributes) {
9630
- let styleStr = "";
9631
- for (const attName in attributes) {
9632
- if (!attributes[attName]) {
9633
- continue;
9634
- }
9635
- styleStr += `${attName}:${attributes[attName]}; `;
9636
- }
9637
- return styleStr;
9638
- }
9639
- function getElementMargins(el) {
9640
- const style = window.getComputedStyle(el);
9641
- return {
9642
- top: parseInt(style.marginTop, 10) || 0,
9643
- bottom: parseInt(style.marginBottom, 10) || 0,
9644
- left: parseInt(style.marginLeft, 10) || 0,
9645
- right: parseInt(style.marginRight, 10) || 0,
9646
- };
9647
- }
9648
-
9649
9538
  const TREND_LINE_XAXIS_ID = "x1";
9650
9539
  /**
9651
9540
  * This file contains helpers that are common to different charts (mainly
@@ -10190,79 +10079,341 @@ stores.inject(MyMetaStore, storeInstance);
10190
10079
  return bars.find((bar, i) => i > startIndex && bar.height !== 0);
10191
10080
  }
10192
10081
 
10193
- window.Chart?.register(waterfallLinesPlugin);
10194
- window.Chart?.register(chartShowValuesPlugin);
10195
- css /* scss */ `
10196
- .o-spreadsheet {
10197
- .o-chart-custom-tooltip {
10198
- font-size: 12px;
10199
- background-color: #fff;
10200
- z-index: ${ComponentsImportance.FigureTooltip};
10201
- }
10082
+ const GAUGE_PADDING_SIDE = 30;
10083
+ const GAUGE_PADDING_TOP = 10;
10084
+ const GAUGE_PADDING_BOTTOM = 20;
10085
+ const GAUGE_LABELS_FONT_SIZE = 12;
10086
+ const GAUGE_DEFAULT_VALUE_FONT_SIZE = 80;
10087
+ const GAUGE_BACKGROUND_COLOR = "#F3F2F1";
10088
+ const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
10089
+ const GAUGE_TITLE_SECTION_HEIGHT = 25;
10090
+ function drawGaugeChart(canvas, runtime) {
10091
+ const canvasBoundingRect = canvas.getBoundingClientRect();
10092
+ canvas.width = canvasBoundingRect.width;
10093
+ canvas.height = canvasBoundingRect.height;
10094
+ const ctx = canvas.getContext("2d");
10095
+ const config = getGaugeRenderingConfig(canvasBoundingRect, runtime, ctx);
10096
+ drawBackground(ctx, config);
10097
+ drawGauge(ctx, config);
10098
+ drawInflectionValues(ctx, config);
10099
+ drawLabels(ctx, config);
10100
+ drawTitle(ctx, config);
10202
10101
  }
10203
- `;
10204
- class ChartJsComponent extends owl.Component {
10205
- static template = "o-spreadsheet-ChartJsComponent";
10206
- static props = {
10207
- figure: Object,
10102
+ function drawGauge(ctx, config) {
10103
+ ctx.save();
10104
+ const gauge = config.gauge;
10105
+ const arcCenterX = gauge.rect.x + gauge.rect.width / 2;
10106
+ const arcCenterY = gauge.rect.y + gauge.rect.height;
10107
+ const arcRadius = gauge.rect.height - gauge.arcWidth / 2;
10108
+ if (arcRadius < 0) {
10109
+ return;
10110
+ }
10111
+ const gaugeAngle = gauge.percentage === 1 ? 0 : Math.PI * (1 + gauge.percentage);
10112
+ // Gauge background
10113
+ ctx.strokeStyle = GAUGE_BACKGROUND_COLOR;
10114
+ ctx.beginPath();
10115
+ ctx.lineWidth = gauge.arcWidth;
10116
+ ctx.arc(arcCenterX, arcCenterY, arcRadius, gaugeAngle, 0);
10117
+ ctx.stroke();
10118
+ // Gauge value
10119
+ ctx.strokeStyle = gauge.color;
10120
+ ctx.beginPath();
10121
+ ctx.arc(arcCenterX, arcCenterY, arcRadius, Math.PI, gaugeAngle);
10122
+ ctx.stroke();
10123
+ ctx.restore();
10124
+ }
10125
+ function drawBackground(ctx, config) {
10126
+ ctx.save();
10127
+ ctx.fillStyle = config.backgroundColor;
10128
+ ctx.fillRect(0, 0, config.width, config.height);
10129
+ ctx.restore();
10130
+ }
10131
+ function drawLabels(ctx, config) {
10132
+ for (const label of [config.minLabel, config.maxLabel, config.gaugeValue]) {
10133
+ ctx.save();
10134
+ ctx.textAlign = "center";
10135
+ ctx.fillStyle = label.color;
10136
+ ctx.font = `${label.fontSize}px ${DEFAULT_FONT}`;
10137
+ ctx.fillText(label.label, label.textPosition.x, label.textPosition.y);
10138
+ ctx.restore();
10139
+ }
10140
+ }
10141
+ function drawInflectionValues(ctx, config) {
10142
+ const { x: rectX, y: rectY, width, height } = config.gauge.rect;
10143
+ for (const inflectionValue of config.inflectionValues) {
10144
+ ctx.save();
10145
+ ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment
10146
+ ctx.rotate(Math.PI / 2 - inflectionValue.rotation);
10147
+ ctx.lineWidth = 2;
10148
+ ctx.strokeStyle = chartMutedFontColor(config.backgroundColor) + "aa";
10149
+ ctx.beginPath();
10150
+ ctx.moveTo(0, -(height - config.gauge.arcWidth));
10151
+ ctx.lineTo(0, -height - 3);
10152
+ ctx.stroke();
10153
+ ctx.textAlign = "center";
10154
+ ctx.font = `${inflectionValue.fontSize}px ${DEFAULT_FONT}`;
10155
+ ctx.fillStyle = inflectionValue.color;
10156
+ const textY = -height - GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN - inflectionValue.offset;
10157
+ ctx.fillText(inflectionValue.label, 0, textY);
10158
+ ctx.restore();
10159
+ }
10160
+ }
10161
+ function drawTitle(ctx, config) {
10162
+ ctx.save();
10163
+ const title = config.title;
10164
+ ctx.font = getDefaultContextFont(title.fontSize, title.bold, title.italic);
10165
+ ctx.textBaseline = "middle";
10166
+ ctx.fillStyle = title.color;
10167
+ ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
10168
+ ctx.restore();
10169
+ }
10170
+ function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
10171
+ const maxValue = runtime.maxValue;
10172
+ const minValue = runtime.minValue;
10173
+ const gaugeValue = runtime.gaugeValue;
10174
+ const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
10175
+ const gaugeArcWidth = gaugeRect.width / 6;
10176
+ const gaugePercentage = gaugeValue
10177
+ ? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
10178
+ : 0;
10179
+ const gaugeValuePosition = {
10180
+ x: boundingRect.width / 2,
10181
+ y: gaugeRect.y + gaugeRect.height - gaugeRect.height / 12,
10208
10182
  };
10209
- canvas = owl.useRef("graphContainer");
10210
- chart;
10211
- currentRuntime;
10212
- get background() {
10213
- return this.chartRuntime.background;
10183
+ let gaugeValueFontSize = GAUGE_DEFAULT_VALUE_FONT_SIZE;
10184
+ // Scale down the font size if the gaugeRect is too small
10185
+ if (gaugeRect.height < 300) {
10186
+ gaugeValueFontSize = gaugeValueFontSize * (gaugeRect.height / 300);
10214
10187
  }
10215
- get canvasStyle() {
10216
- return `background-color: ${this.background}`;
10188
+ // Scale down the font size if the text is too long
10189
+ const maxTextWidth = gaugeRect.width / 2;
10190
+ const gaugeLabel = gaugeValue?.label || "-";
10191
+ if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
10192
+ gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
10217
10193
  }
10218
- get chartRuntime() {
10219
- const runtime = this.env.model.getters.getChartRuntime(this.props.figure.id);
10220
- if (!("chartJsConfig" in runtime)) {
10221
- throw new Error("Unsupported chart runtime");
10222
- }
10223
- return runtime;
10194
+ const minLabelPosition = {
10195
+ x: gaugeRect.x + gaugeArcWidth / 2,
10196
+ y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
10197
+ };
10198
+ const maxLabelPosition = {
10199
+ x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
10200
+ y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
10201
+ };
10202
+ const textColor = chartMutedFontColor(runtime.background);
10203
+ const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);
10204
+ let x = 0, titleWidth = 0, titleHeight = 0;
10205
+ if (runtime.title.text) {
10206
+ ({ width: titleWidth, height: titleHeight } = computeTextDimension(ctx, runtime.title.text, { fontSize: CHART_TITLE_FONT_SIZE, ...runtime.title }, "px"));
10224
10207
  }
10225
- setup() {
10226
- owl.onMounted(() => {
10227
- const runtime = this.chartRuntime;
10228
- this.currentRuntime = runtime;
10229
- // Note: chartJS modify the runtime in place, so it's important to give it a copy
10230
- this.createChart(deepCopy(runtime.chartJsConfig));
10231
- });
10232
- owl.onWillUnmount(() => this.chart?.destroy());
10233
- owl.useEffect(() => {
10234
- const runtime = this.chartRuntime;
10235
- if (runtime !== this.currentRuntime) {
10236
- if (runtime.chartJsConfig.type !== this.currentRuntime.chartJsConfig.type) {
10237
- this.chart?.destroy();
10238
- this.createChart(deepCopy(runtime.chartJsConfig));
10239
- }
10240
- else {
10241
- this.updateChartJs(deepCopy(runtime));
10242
- }
10243
- this.currentRuntime = runtime;
10244
- }
10208
+ switch (runtime.title.align) {
10209
+ case "right":
10210
+ x = boundingRect.width - titleWidth - CHART_PADDING$1;
10211
+ break;
10212
+ case "center":
10213
+ x = (boundingRect.width - titleWidth) / 2;
10214
+ break;
10215
+ case "left":
10216
+ default:
10217
+ x = CHART_PADDING$1;
10218
+ break;
10219
+ }
10220
+ return {
10221
+ width: boundingRect.width,
10222
+ height: boundingRect.height,
10223
+ title: {
10224
+ label: runtime.title.text ?? "",
10225
+ fontSize: runtime.title.fontSize ?? CHART_TITLE_FONT_SIZE,
10226
+ textPosition: {
10227
+ x,
10228
+ y: CHART_PADDING_TOP + titleHeight / 2,
10229
+ },
10230
+ color: runtime.title.color ?? textColor,
10231
+ bold: runtime.title.bold,
10232
+ italic: runtime.title.italic,
10233
+ },
10234
+ backgroundColor: runtime.background,
10235
+ gauge: {
10236
+ rect: gaugeRect,
10237
+ arcWidth: gaugeArcWidth,
10238
+ percentage: clip(gaugePercentage, 0, 1),
10239
+ color: getGaugeColor(runtime),
10240
+ },
10241
+ inflectionValues,
10242
+ gaugeValue: {
10243
+ label: gaugeLabel,
10244
+ textPosition: gaugeValuePosition,
10245
+ fontSize: gaugeValueFontSize,
10246
+ color: textColor,
10247
+ },
10248
+ minLabel: {
10249
+ label: runtime.minValue.label,
10250
+ textPosition: minLabelPosition,
10251
+ fontSize: GAUGE_LABELS_FONT_SIZE,
10252
+ color: textColor,
10253
+ },
10254
+ maxLabel: {
10255
+ label: runtime.maxValue.label,
10256
+ textPosition: maxLabelPosition,
10257
+ fontSize: GAUGE_LABELS_FONT_SIZE,
10258
+ color: textColor,
10259
+ },
10260
+ };
10261
+ }
10262
+ /**
10263
+ * Get the rectangle in which the gauge will be drawn, based on the bounding rectangle of the canvas and leaving
10264
+ * space for the title and labels.
10265
+ */
10266
+ function getGaugeRect(boundingRect, title) {
10267
+ const titleHeight = title ? GAUGE_TITLE_SECTION_HEIGHT : 0;
10268
+ const drawHeight = boundingRect.height - GAUGE_PADDING_BOTTOM - titleHeight - GAUGE_PADDING_TOP;
10269
+ const drawWidth = boundingRect.width - GAUGE_PADDING_SIDE * 2;
10270
+ let gaugeWidth;
10271
+ let gaugeHeight;
10272
+ if (drawWidth > 2 * drawHeight) {
10273
+ gaugeWidth = 2 * drawHeight;
10274
+ gaugeHeight = drawHeight;
10275
+ }
10276
+ else {
10277
+ gaugeWidth = drawWidth;
10278
+ gaugeHeight = drawWidth / 2;
10279
+ }
10280
+ const gaugeX = GAUGE_PADDING_SIDE + (drawWidth - gaugeWidth) / 2;
10281
+ const gaugeY = titleHeight + GAUGE_PADDING_TOP + (drawHeight - gaugeHeight) / 2;
10282
+ return {
10283
+ x: gaugeX,
10284
+ y: gaugeY,
10285
+ width: gaugeWidth,
10286
+ height: gaugeHeight,
10287
+ };
10288
+ }
10289
+ /**
10290
+ * 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).
10291
+ *
10292
+ * Also compute an offset for the text so that it doesn't overlap with other text.
10293
+ */
10294
+ function getInflectionValues(runtime, gaugeRect, textColor, ctx) {
10295
+ const maxValue = runtime.maxValue;
10296
+ const minValue = runtime.minValue;
10297
+ const gaugeCircleCenter = {
10298
+ x: gaugeRect.x + gaugeRect.width / 2,
10299
+ y: gaugeRect.y + gaugeRect.height,
10300
+ };
10301
+ const textStyle = { fontSize: GAUGE_LABELS_FONT_SIZE };
10302
+ const inflectionValues = [];
10303
+ const inflectionValuesTextRects = [];
10304
+ for (const inflectionValue of runtime.inflectionValues) {
10305
+ const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
10306
+ const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
10307
+ const angle = Math.PI - Math.PI * percentage;
10308
+ const textRect = getRectangleTangentToCircle(angle, // angle between X axis and the point where the rectangle is tangent to the circle
10309
+ gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, // radius of the gauge circle + margin below text
10310
+ gaugeCircleCenter.x, // center of the gauge circle
10311
+ gaugeCircleCenter.y, // center of the gauge circle
10312
+ labelWidth + 2, // width of the text + some margin
10313
+ GAUGE_LABELS_FONT_SIZE // height of the text
10314
+ );
10315
+ let offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect))
10316
+ ? GAUGE_LABELS_FONT_SIZE
10317
+ : 0;
10318
+ inflectionValuesTextRects.push(textRect);
10319
+ inflectionValues.push({
10320
+ rotation: angle,
10321
+ label: inflectionValue.label,
10322
+ fontSize: GAUGE_LABELS_FONT_SIZE,
10323
+ color: textColor,
10324
+ offset,
10245
10325
  });
10246
10326
  }
10247
- createChart(chartData) {
10248
- const canvas = this.canvas.el;
10249
- const ctx = canvas.getContext("2d");
10250
- this.chart = new window.Chart(ctx, chartData);
10327
+ return inflectionValues;
10328
+ }
10329
+ function getGaugeColor(runtime) {
10330
+ const gaugeValue = runtime.gaugeValue?.value;
10331
+ if (gaugeValue === undefined) {
10332
+ return GAUGE_BACKGROUND_COLOR;
10251
10333
  }
10252
- updateChartJs(chartRuntime) {
10253
- const chartData = chartRuntime.chartJsConfig;
10254
- if (chartData.data && chartData.data.datasets) {
10255
- this.chart.data = chartData.data;
10256
- if (chartData.options?.plugins?.title) {
10257
- this.chart.config.options.plugins.title = chartData.options.plugins.title;
10258
- }
10334
+ for (let i = 0; i < runtime.inflectionValues.length; i++) {
10335
+ const inflectionValue = runtime.inflectionValues[i];
10336
+ if (inflectionValue.operator === "<" && gaugeValue < inflectionValue.value) {
10337
+ return runtime.colors[i];
10259
10338
  }
10260
- else {
10261
- this.chart.data.datasets = [];
10339
+ else if (inflectionValue.operator === "<=" && gaugeValue <= inflectionValue.value) {
10340
+ return runtime.colors[i];
10262
10341
  }
10263
- this.chart.config.options = chartData.options;
10264
- this.chart.update();
10265
10342
  }
10343
+ return runtime.colors.at(-1);
10344
+ }
10345
+ function getSegmentsOfRectangle(rectangle) {
10346
+ return [
10347
+ { start: rectangle.topLeft, end: rectangle.topRight },
10348
+ { start: rectangle.topRight, end: rectangle.bottomRight },
10349
+ { start: rectangle.bottomRight, end: rectangle.bottomLeft },
10350
+ { start: rectangle.bottomLeft, end: rectangle.topLeft },
10351
+ ];
10352
+ }
10353
+ /**
10354
+ * Check if two segment intersect. The case where the segments are colinear (both segments on the same line)
10355
+ * is not handled.
10356
+ */
10357
+ function doSegmentIntersect(segment1, segment2) {
10358
+ const A = segment1.start;
10359
+ const B = segment1.end;
10360
+ const C = segment2.start;
10361
+ const D = segment2.end;
10362
+ /**
10363
+ * Line segment intersection algorithm
10364
+ * https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
10365
+ */
10366
+ function ccw(a, b, c) {
10367
+ return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
10368
+ }
10369
+ return ccw(A, C, D) !== ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D);
10370
+ }
10371
+ function doRectanglesIntersect(rect1, rect2) {
10372
+ const segments1 = getSegmentsOfRectangle(rect1);
10373
+ const segments2 = getSegmentsOfRectangle(rect2);
10374
+ for (const segment1 of segments1) {
10375
+ for (const segment2 of segments2) {
10376
+ if (doSegmentIntersect(segment1, segment2)) {
10377
+ return true;
10378
+ }
10379
+ }
10380
+ }
10381
+ return false;
10382
+ }
10383
+ /**
10384
+ * Get the rectangle that is tangent to a circle at a given angle.
10385
+ *
10386
+ * @param angle angle between X axis and the point where the rectangle is tangent to the circle
10387
+ */
10388
+ function getRectangleTangentToCircle(angle, radius, circleCenterX, circleCenterY, rectWidth, rectHeight) {
10389
+ const cos = Math.cos(angle);
10390
+ const sin = Math.sin(angle);
10391
+ // x, y are the distance from the center of the circle to the point where the rectangle is tangent to the circle
10392
+ const x = cos * radius;
10393
+ const y = sin * radius;
10394
+ // x2, y2 are the distance from the point the rectangle is tangent to the circle to the bottom left corner of the rectangle
10395
+ const x2 = sin * (rectWidth / 2); // cos(angle + 90°) = sin(angle)
10396
+ const y2 = cos * (rectWidth / 2);
10397
+ const bottomRight = {
10398
+ x: x + x2 + circleCenterX,
10399
+ y: circleCenterY - (y - y2),
10400
+ };
10401
+ const bottomLeft = {
10402
+ x: x - x2 + circleCenterX,
10403
+ y: circleCenterY - (y + y2),
10404
+ };
10405
+ // Same as above but for the top corners of the rectangle (radius + rectangle height instead of radius)
10406
+ const xp = cos * (radius + rectHeight);
10407
+ const yp = sin * (radius + rectHeight);
10408
+ const topLeft = {
10409
+ x: xp - x2 + circleCenterX,
10410
+ y: circleCenterY - (yp + y2),
10411
+ };
10412
+ const topRight = {
10413
+ x: xp + x2 + circleCenterX,
10414
+ y: circleCenterY - (yp - y2),
10415
+ };
10416
+ return { bottomLeft, bottomRight, topRight, topLeft };
10266
10417
  }
10267
10418
 
10268
10419
  /**
@@ -10844,6 +10995,299 @@ stores.inject(MyMetaStore, storeInstance);
10844
10995
  }
10845
10996
  }
10846
10997
 
10998
+ const CHART_COMMON_OPTIONS = {
10999
+ // https://www.chartjs.org/docs/latest/general/responsive.html
11000
+ responsive: true, // will resize when its container is resized
11001
+ maintainAspectRatio: false, // doesn't maintain the aspect ratio (width/height =2 by default) so the user has the choice of the exact layout
11002
+ elements: {
11003
+ line: {
11004
+ fill: false, // do not fill the area under line charts
11005
+ },
11006
+ point: {
11007
+ hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
11008
+ },
11009
+ },
11010
+ animation: false,
11011
+ };
11012
+ function chartToImage(runtime, figure, type) {
11013
+ // wrap the canvas in a div with a fixed size because chart.js would
11014
+ // fill the whole page otherwise
11015
+ const div = document.createElement("div");
11016
+ div.style.width = `${figure.width}px`;
11017
+ div.style.height = `${figure.height}px`;
11018
+ const canvas = document.createElement("canvas");
11019
+ div.append(canvas);
11020
+ canvas.setAttribute("width", figure.width.toString());
11021
+ canvas.setAttribute("height", figure.height.toString());
11022
+ // we have to add the canvas to the DOM otherwise it won't be rendered
11023
+ document.body.append(div);
11024
+ if ("chartJsConfig" in runtime) {
11025
+ const config = deepCopy(runtime.chartJsConfig);
11026
+ config.plugins = [backgroundColorChartJSPlugin];
11027
+ const Chart = getChartJSConstructor();
11028
+ const chart = new Chart(canvas, config);
11029
+ const imgContent = chart.toBase64Image();
11030
+ chart.destroy();
11031
+ div.remove();
11032
+ return imgContent;
11033
+ }
11034
+ else if (type === "scorecard") {
11035
+ const design = getScorecardConfiguration(figure, runtime);
11036
+ drawScoreChart(design, canvas);
11037
+ const imgContent = canvas.toDataURL();
11038
+ div.remove();
11039
+ return imgContent;
11040
+ }
11041
+ else if (type === "gauge") {
11042
+ drawGaugeChart(canvas, runtime);
11043
+ const imgContent = canvas.toDataURL();
11044
+ div.remove();
11045
+ return imgContent;
11046
+ }
11047
+ return undefined;
11048
+ }
11049
+ /**
11050
+ * Custom chart.js plugin to set the background color of the canvas
11051
+ * https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md
11052
+ */
11053
+ const backgroundColorChartJSPlugin = {
11054
+ id: "customCanvasBackgroundColor",
11055
+ beforeDraw: (chart) => {
11056
+ const { ctx } = chart;
11057
+ ctx.save();
11058
+ ctx.globalCompositeOperation = "destination-over";
11059
+ ctx.fillStyle = "#ffffff";
11060
+ ctx.fillRect(0, 0, chart.width, chart.height);
11061
+ ctx.restore();
11062
+ },
11063
+ };
11064
+ /** Return window.Chart, making sure all our extensions are loaded in ChartJS */
11065
+ function getChartJSConstructor() {
11066
+ if (window.Chart && !window.Chart?.registry.plugins.get("chartShowValuesPlugin")) {
11067
+ window.Chart.register(chartShowValuesPlugin);
11068
+ window.Chart.register(waterfallLinesPlugin);
11069
+ }
11070
+ return window.Chart;
11071
+ }
11072
+
11073
+ /**
11074
+ * This file is largely inspired by owl 1.
11075
+ * `css` tag has been removed from owl 2 without workaround to manage css.
11076
+ * So, the solution was to import the behavior of owl 1 directly in our
11077
+ * codebase, with one difference: the css is added to the sheet as soon as the
11078
+ * css tag is executed. In owl 1, the css was added as soon as a Component was
11079
+ * created for the first time.
11080
+ */
11081
+ const STYLESHEETS = {};
11082
+ let nextId = 0;
11083
+ /**
11084
+ * CSS tag helper for defining inline stylesheets. With this, one can simply define
11085
+ * an inline stylesheet with just the following code:
11086
+ * ```js
11087
+ * css`.component-a { color: red; }`;
11088
+ * ```
11089
+ */
11090
+ function css(strings, ...args) {
11091
+ const name = `__sheet__${nextId++}`;
11092
+ const value = String.raw(strings, ...args);
11093
+ registerSheet(name, value);
11094
+ activateSheet(name);
11095
+ return name;
11096
+ }
11097
+ function processSheet(str) {
11098
+ const tokens = str.split(/(\{|\}|;)/).map((s) => s.trim());
11099
+ const selectorStack = [];
11100
+ const parts = [];
11101
+ let rules = [];
11102
+ function generateSelector(stackIndex, parentSelector) {
11103
+ const parts = [];
11104
+ for (const selector of selectorStack[stackIndex]) {
11105
+ let part = (parentSelector && parentSelector + " " + selector) || selector;
11106
+ if (part.includes("&")) {
11107
+ part = selector.replace(/&/g, parentSelector || "");
11108
+ }
11109
+ if (stackIndex < selectorStack.length - 1) {
11110
+ part = generateSelector(stackIndex + 1, part);
11111
+ }
11112
+ parts.push(part);
11113
+ }
11114
+ return parts.join(", ");
11115
+ }
11116
+ function generateRules() {
11117
+ if (rules.length) {
11118
+ parts.push(generateSelector(0) + " {");
11119
+ parts.push(...rules);
11120
+ parts.push("}");
11121
+ rules = [];
11122
+ }
11123
+ }
11124
+ while (tokens.length) {
11125
+ let token = tokens.shift();
11126
+ if (token === "}") {
11127
+ generateRules();
11128
+ selectorStack.pop();
11129
+ }
11130
+ else {
11131
+ if (tokens[0] === "{") {
11132
+ generateRules();
11133
+ selectorStack.push(token.split(/\s*,\s*/));
11134
+ tokens.shift();
11135
+ }
11136
+ if (tokens[0] === ";") {
11137
+ rules.push(" " + token + ";");
11138
+ }
11139
+ }
11140
+ }
11141
+ return parts.join("\n");
11142
+ }
11143
+ function registerSheet(id, css) {
11144
+ const sheet = document.createElement("style");
11145
+ sheet.textContent = processSheet(css);
11146
+ STYLESHEETS[id] = sheet;
11147
+ }
11148
+ function activateSheet(id) {
11149
+ const sheet = STYLESHEETS[id];
11150
+ sheet.setAttribute("component", id);
11151
+ document.head.appendChild(sheet);
11152
+ }
11153
+ function getTextDecoration({ strikethrough, underline, }) {
11154
+ if (!strikethrough && !underline) {
11155
+ return "none";
11156
+ }
11157
+ return `${strikethrough ? "line-through" : ""} ${underline ? "underline" : ""}`;
11158
+ }
11159
+ /**
11160
+ * Convert the cell style to CSS properties.
11161
+ */
11162
+ function cellStyleToCss(style) {
11163
+ const attributes = cellTextStyleToCss(style);
11164
+ if (!style)
11165
+ return attributes;
11166
+ if (style.fillColor) {
11167
+ attributes["background"] = style.fillColor;
11168
+ }
11169
+ return attributes;
11170
+ }
11171
+ /**
11172
+ * Convert the cell text style to CSS properties.
11173
+ */
11174
+ function cellTextStyleToCss(style) {
11175
+ const attributes = {};
11176
+ if (!style)
11177
+ return attributes;
11178
+ if (style.bold) {
11179
+ attributes["font-weight"] = "bold";
11180
+ }
11181
+ if (style.italic) {
11182
+ attributes["font-style"] = "italic";
11183
+ }
11184
+ if (style.strikethrough || style.underline) {
11185
+ let decoration = style.strikethrough ? "line-through" : "";
11186
+ decoration = style.underline ? decoration + " underline" : decoration;
11187
+ attributes["text-decoration"] = decoration;
11188
+ }
11189
+ if (style.textColor) {
11190
+ attributes["color"] = style.textColor;
11191
+ }
11192
+ return attributes;
11193
+ }
11194
+ /**
11195
+ * Transform CSS properties into a CSS string.
11196
+ */
11197
+ function cssPropertiesToCss(attributes) {
11198
+ let styleStr = "";
11199
+ for (const attName in attributes) {
11200
+ if (!attributes[attName]) {
11201
+ continue;
11202
+ }
11203
+ styleStr += `${attName}:${attributes[attName]}; `;
11204
+ }
11205
+ return styleStr;
11206
+ }
11207
+ function getElementMargins(el) {
11208
+ const style = window.getComputedStyle(el);
11209
+ return {
11210
+ top: parseInt(style.marginTop, 10) || 0,
11211
+ bottom: parseInt(style.marginBottom, 10) || 0,
11212
+ left: parseInt(style.marginLeft, 10) || 0,
11213
+ right: parseInt(style.marginRight, 10) || 0,
11214
+ };
11215
+ }
11216
+
11217
+ css /* scss */ `
11218
+ .o-spreadsheet {
11219
+ .o-chart-custom-tooltip {
11220
+ font-size: 12px;
11221
+ background-color: #fff;
11222
+ z-index: ${ComponentsImportance.FigureTooltip};
11223
+ }
11224
+ }
11225
+ `;
11226
+ class ChartJsComponent extends owl.Component {
11227
+ static template = "o-spreadsheet-ChartJsComponent";
11228
+ static props = {
11229
+ figure: Object,
11230
+ };
11231
+ canvas = owl.useRef("graphContainer");
11232
+ chart;
11233
+ currentRuntime;
11234
+ get background() {
11235
+ return this.chartRuntime.background;
11236
+ }
11237
+ get canvasStyle() {
11238
+ return `background-color: ${this.background}`;
11239
+ }
11240
+ get chartRuntime() {
11241
+ const runtime = this.env.model.getters.getChartRuntime(this.props.figure.id);
11242
+ if (!("chartJsConfig" in runtime)) {
11243
+ throw new Error("Unsupported chart runtime");
11244
+ }
11245
+ return runtime;
11246
+ }
11247
+ setup() {
11248
+ owl.onMounted(() => {
11249
+ const runtime = this.chartRuntime;
11250
+ this.currentRuntime = runtime;
11251
+ // Note: chartJS modify the runtime in place, so it's important to give it a copy
11252
+ this.createChart(deepCopy(runtime.chartJsConfig));
11253
+ });
11254
+ owl.onWillUnmount(() => this.chart?.destroy());
11255
+ owl.useEffect(() => {
11256
+ const runtime = this.chartRuntime;
11257
+ if (runtime !== this.currentRuntime) {
11258
+ if (runtime.chartJsConfig.type !== this.currentRuntime.chartJsConfig.type) {
11259
+ this.chart?.destroy();
11260
+ this.createChart(deepCopy(runtime.chartJsConfig));
11261
+ }
11262
+ else {
11263
+ this.updateChartJs(deepCopy(runtime));
11264
+ }
11265
+ this.currentRuntime = runtime;
11266
+ }
11267
+ });
11268
+ }
11269
+ createChart(chartData) {
11270
+ const canvas = this.canvas.el;
11271
+ const ctx = canvas.getContext("2d");
11272
+ const Chart = getChartJSConstructor();
11273
+ this.chart = new Chart(ctx, chartData);
11274
+ }
11275
+ updateChartJs(chartRuntime) {
11276
+ const chartData = chartRuntime.chartJsConfig;
11277
+ if (chartData.data && chartData.data.datasets) {
11278
+ this.chart.data = chartData.data;
11279
+ if (chartData.options?.plugins?.title) {
11280
+ this.chart.config.options.plugins.title = chartData.options.plugins.title;
11281
+ }
11282
+ }
11283
+ else {
11284
+ this.chart.data.datasets = [];
11285
+ }
11286
+ this.chart.config.options = chartData.options;
11287
+ this.chart.update();
11288
+ }
11289
+ }
11290
+
10847
11291
  class ScorecardChart extends owl.Component {
10848
11292
  static template = "o-spreadsheet-ScorecardChart";
10849
11293
  static props = {
@@ -18685,7 +19129,7 @@ stores.inject(MyMetaStore, storeInstance);
18685
19129
  const _isSorted = toBoolean(isSorted.value);
18686
19130
  const colIndex = _isSorted
18687
19131
  ? dichotomicSearch(_range, searchKey, "nextSmaller", "asc", _range.length, getValueFromRange)
18688
- : linearSearch(_range, searchKey, "wildcard", _range.length, getValueFromRange);
19132
+ : linearSearch(_range, searchKey, "wildcard", _range.length, getValueFromRange, this.lookupCaches);
18689
19133
  const col = _range[colIndex];
18690
19134
  if (col === undefined) {
18691
19135
  return valueNotAvailable(searchKey);
@@ -18840,7 +19284,7 @@ stores.inject(MyMetaStore, storeInstance);
18840
19284
  index = dichotomicSearch(_range, searchKey, "nextSmaller", "asc", rangeLen, getElement);
18841
19285
  break;
18842
19286
  case 0:
18843
- index = linearSearch(_range, searchKey, "wildcard", rangeLen, getElement);
19287
+ index = linearSearch(_range, searchKey, "wildcard", rangeLen, getElement, this.lookupCaches);
18844
19288
  break;
18845
19289
  case -1:
18846
19290
  index = dichotomicSearch(_range, searchKey, "nextGreater", "desc", rangeLen, getElement);
@@ -18908,7 +19352,7 @@ stores.inject(MyMetaStore, storeInstance);
18908
19352
  const _isSorted = toBoolean(isSorted.value);
18909
19353
  const rowIndex = _isSorted
18910
19354
  ? dichotomicSearch(_range, searchKey, "nextSmaller", "asc", _range[0].length, getValueFromRange)
18911
- : linearSearch(_range, searchKey, "wildcard", _range[0].length, getValueFromRange);
19355
+ : linearSearch(_range, searchKey, "wildcard", _range[0].length, getValueFromRange, this.lookupCaches);
18912
19356
  const value = _range[_index - 1][rowIndex];
18913
19357
  if (value === undefined) {
18914
19358
  return valueNotAvailable(searchKey);
@@ -18964,7 +19408,7 @@ stores.inject(MyMetaStore, storeInstance);
18964
19408
  const reverseSearch = _searchMode === -1;
18965
19409
  const index = _searchMode === 2 || _searchMode === -2
18966
19410
  ? dichotomicSearch(_lookupRange, searchKey, mode, _searchMode === 2 ? "asc" : "desc", rangeLen, getElement)
18967
- : linearSearch(_lookupRange, searchKey, mode, rangeLen, getElement, reverseSearch);
19411
+ : linearSearch(_lookupRange, searchKey, mode, rangeLen, getElement, this.lookupCaches, reverseSearch);
18968
19412
  if (index !== -1) {
18969
19413
  return lookupDirection === "col"
18970
19414
  ? _returnRange.map((col) => [col[index]])
@@ -22296,7 +22740,7 @@ stores.inject(MyMetaStore, storeInstance);
22296
22740
  condition: (cell) => !cell.isFormula &&
22297
22741
  evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.text &&
22298
22742
  alphaNumericValueRegExp.test(cell.content),
22299
- generateRule: (cell, cells) => {
22743
+ generateRule: (cell, cells, direction) => {
22300
22744
  const numberPostfix = parseInt(cell.content.match(numberPostfixRegExp)[0]);
22301
22745
  const prefix = cell.content.match(stringPrefixRegExp)[0];
22302
22746
  const numberPostfixLength = cell.content.length - prefix.length;
@@ -22304,7 +22748,10 @@ stores.inject(MyMetaStore, storeInstance);
22304
22748
  alphaNumericValueRegExp.test(evaluatedCell.value)) // get consecutive alphanumeric cells, no matter what the prefix is
22305
22749
  .filter((cell) => prefix === (cell.value ?? "").toString().match(stringPrefixRegExp)[0])
22306
22750
  .map((cell) => parseInt((cell.value ?? "").toString().match(numberPostfixRegExp)[0]));
22307
- const increment = calculateIncrementBasedOnGroup(group);
22751
+ let increment = calculateIncrementBasedOnGroup(group);
22752
+ if (["up", "left"].includes(direction) && group.length === 1) {
22753
+ increment = -increment;
22754
+ }
22308
22755
  return {
22309
22756
  type: "ALPHANUMERIC_INCREMENT_MODIFIER",
22310
22757
  prefix,
@@ -22367,10 +22814,13 @@ stores.inject(MyMetaStore, storeInstance);
22367
22814
  .add("increment_number", {
22368
22815
  condition: (cell) => !cell.isFormula &&
22369
22816
  evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.number,
22370
- generateRule: (cell, cells) => {
22817
+ generateRule: (cell, cells, direction) => {
22371
22818
  const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number &&
22372
22819
  !isDateTimeFormat(evaluatedCell.format || "")).map((cell) => Number(cell.value));
22373
- const increment = calculateIncrementBasedOnGroup(group);
22820
+ let increment = calculateIncrementBasedOnGroup(group);
22821
+ if (["up", "left"].includes(direction) && group.length === 1) {
22822
+ increment = -increment;
22823
+ }
22374
22824
  const evaluation = evaluateLiteral(cell, { locale: DEFAULT_LOCALE });
22375
22825
  return {
22376
22826
  type: "INCREMENT_MODIFIER",
@@ -22414,343 +22864,6 @@ stores.inject(MyMetaStore, storeInstance);
22414
22864
 
22415
22865
  const cellPopoverRegistry = new Registry();
22416
22866
 
22417
- const GAUGE_PADDING_SIDE = 30;
22418
- const GAUGE_PADDING_TOP = 10;
22419
- const GAUGE_PADDING_BOTTOM = 20;
22420
- const GAUGE_LABELS_FONT_SIZE = 12;
22421
- const GAUGE_DEFAULT_VALUE_FONT_SIZE = 80;
22422
- const GAUGE_BACKGROUND_COLOR = "#F3F2F1";
22423
- const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
22424
- const GAUGE_TITLE_SECTION_HEIGHT = 25;
22425
- function drawGaugeChart(canvas, runtime) {
22426
- const canvasBoundingRect = canvas.getBoundingClientRect();
22427
- canvas.width = canvasBoundingRect.width;
22428
- canvas.height = canvasBoundingRect.height;
22429
- const ctx = canvas.getContext("2d");
22430
- const config = getGaugeRenderingConfig(canvasBoundingRect, runtime, ctx);
22431
- drawBackground(ctx, config);
22432
- drawGauge(ctx, config);
22433
- drawInflectionValues(ctx, config);
22434
- drawLabels(ctx, config);
22435
- drawTitle(ctx, config);
22436
- }
22437
- function drawGauge(ctx, config) {
22438
- ctx.save();
22439
- const gauge = config.gauge;
22440
- const arcCenterX = gauge.rect.x + gauge.rect.width / 2;
22441
- const arcCenterY = gauge.rect.y + gauge.rect.height;
22442
- const arcRadius = gauge.rect.height - gauge.arcWidth / 2;
22443
- if (arcRadius < 0) {
22444
- return;
22445
- }
22446
- const gaugeAngle = gauge.percentage === 1 ? 0 : Math.PI * (1 + gauge.percentage);
22447
- // Gauge background
22448
- ctx.strokeStyle = GAUGE_BACKGROUND_COLOR;
22449
- ctx.beginPath();
22450
- ctx.lineWidth = gauge.arcWidth;
22451
- ctx.arc(arcCenterX, arcCenterY, arcRadius, gaugeAngle, 0);
22452
- ctx.stroke();
22453
- // Gauge value
22454
- ctx.strokeStyle = gauge.color;
22455
- ctx.beginPath();
22456
- ctx.arc(arcCenterX, arcCenterY, arcRadius, Math.PI, gaugeAngle);
22457
- ctx.stroke();
22458
- ctx.restore();
22459
- }
22460
- function drawBackground(ctx, config) {
22461
- ctx.save();
22462
- ctx.fillStyle = config.backgroundColor;
22463
- ctx.fillRect(0, 0, config.width, config.height);
22464
- ctx.restore();
22465
- }
22466
- function drawLabels(ctx, config) {
22467
- for (const label of [config.minLabel, config.maxLabel, config.gaugeValue]) {
22468
- ctx.save();
22469
- ctx.textAlign = "center";
22470
- ctx.fillStyle = label.color;
22471
- ctx.font = `${label.fontSize}px ${DEFAULT_FONT}`;
22472
- ctx.fillText(label.label, label.textPosition.x, label.textPosition.y);
22473
- ctx.restore();
22474
- }
22475
- }
22476
- function drawInflectionValues(ctx, config) {
22477
- const { x: rectX, y: rectY, width, height } = config.gauge.rect;
22478
- for (const inflectionValue of config.inflectionValues) {
22479
- ctx.save();
22480
- ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment
22481
- ctx.rotate(Math.PI / 2 - inflectionValue.rotation);
22482
- ctx.lineWidth = 2;
22483
- ctx.strokeStyle = chartMutedFontColor(config.backgroundColor) + "aa";
22484
- ctx.beginPath();
22485
- ctx.moveTo(0, -(height - config.gauge.arcWidth));
22486
- ctx.lineTo(0, -height - 3);
22487
- ctx.stroke();
22488
- ctx.textAlign = "center";
22489
- ctx.font = `${inflectionValue.fontSize}px ${DEFAULT_FONT}`;
22490
- ctx.fillStyle = inflectionValue.color;
22491
- const textY = -height - GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN - inflectionValue.offset;
22492
- ctx.fillText(inflectionValue.label, 0, textY);
22493
- ctx.restore();
22494
- }
22495
- }
22496
- function drawTitle(ctx, config) {
22497
- ctx.save();
22498
- const title = config.title;
22499
- ctx.font = getDefaultContextFont(title.fontSize, title.bold, title.italic);
22500
- ctx.textBaseline = "middle";
22501
- ctx.fillStyle = title.color;
22502
- ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
22503
- ctx.restore();
22504
- }
22505
- function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
22506
- const maxValue = runtime.maxValue;
22507
- const minValue = runtime.minValue;
22508
- const gaugeValue = runtime.gaugeValue;
22509
- const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
22510
- const gaugeArcWidth = gaugeRect.width / 6;
22511
- const gaugePercentage = gaugeValue
22512
- ? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
22513
- : 0;
22514
- const gaugeValuePosition = {
22515
- x: boundingRect.width / 2,
22516
- y: gaugeRect.y + gaugeRect.height - gaugeRect.height / 12,
22517
- };
22518
- let gaugeValueFontSize = GAUGE_DEFAULT_VALUE_FONT_SIZE;
22519
- // Scale down the font size if the gaugeRect is too small
22520
- if (gaugeRect.height < 300) {
22521
- gaugeValueFontSize = gaugeValueFontSize * (gaugeRect.height / 300);
22522
- }
22523
- // Scale down the font size if the text is too long
22524
- const maxTextWidth = gaugeRect.width / 2;
22525
- const gaugeLabel = gaugeValue?.label || "-";
22526
- if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
22527
- gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
22528
- }
22529
- const minLabelPosition = {
22530
- x: gaugeRect.x + gaugeArcWidth / 2,
22531
- y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
22532
- };
22533
- const maxLabelPosition = {
22534
- x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
22535
- y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
22536
- };
22537
- const textColor = chartMutedFontColor(runtime.background);
22538
- const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);
22539
- let x = 0, titleWidth = 0, titleHeight = 0;
22540
- if (runtime.title.text) {
22541
- ({ width: titleWidth, height: titleHeight } = computeTextDimension(ctx, runtime.title.text, { fontSize: CHART_TITLE_FONT_SIZE, ...runtime.title }, "px"));
22542
- }
22543
- switch (runtime.title.align) {
22544
- case "right":
22545
- x = boundingRect.width - titleWidth - CHART_PADDING$1;
22546
- break;
22547
- case "center":
22548
- x = (boundingRect.width - titleWidth) / 2;
22549
- break;
22550
- case "left":
22551
- default:
22552
- x = CHART_PADDING$1;
22553
- break;
22554
- }
22555
- return {
22556
- width: boundingRect.width,
22557
- height: boundingRect.height,
22558
- title: {
22559
- label: runtime.title.text ?? "",
22560
- fontSize: runtime.title.fontSize ?? CHART_TITLE_FONT_SIZE,
22561
- textPosition: {
22562
- x,
22563
- y: CHART_PADDING_TOP + titleHeight / 2,
22564
- },
22565
- color: runtime.title.color ?? textColor,
22566
- bold: runtime.title.bold,
22567
- italic: runtime.title.italic,
22568
- },
22569
- backgroundColor: runtime.background,
22570
- gauge: {
22571
- rect: gaugeRect,
22572
- arcWidth: gaugeArcWidth,
22573
- percentage: clip(gaugePercentage, 0, 1),
22574
- color: getGaugeColor(runtime),
22575
- },
22576
- inflectionValues,
22577
- gaugeValue: {
22578
- label: gaugeLabel,
22579
- textPosition: gaugeValuePosition,
22580
- fontSize: gaugeValueFontSize,
22581
- color: textColor,
22582
- },
22583
- minLabel: {
22584
- label: runtime.minValue.label,
22585
- textPosition: minLabelPosition,
22586
- fontSize: GAUGE_LABELS_FONT_SIZE,
22587
- color: textColor,
22588
- },
22589
- maxLabel: {
22590
- label: runtime.maxValue.label,
22591
- textPosition: maxLabelPosition,
22592
- fontSize: GAUGE_LABELS_FONT_SIZE,
22593
- color: textColor,
22594
- },
22595
- };
22596
- }
22597
- /**
22598
- * Get the rectangle in which the gauge will be drawn, based on the bounding rectangle of the canvas and leaving
22599
- * space for the title and labels.
22600
- */
22601
- function getGaugeRect(boundingRect, title) {
22602
- const titleHeight = title ? GAUGE_TITLE_SECTION_HEIGHT : 0;
22603
- const drawHeight = boundingRect.height - GAUGE_PADDING_BOTTOM - titleHeight - GAUGE_PADDING_TOP;
22604
- const drawWidth = boundingRect.width - GAUGE_PADDING_SIDE * 2;
22605
- let gaugeWidth;
22606
- let gaugeHeight;
22607
- if (drawWidth > 2 * drawHeight) {
22608
- gaugeWidth = 2 * drawHeight;
22609
- gaugeHeight = drawHeight;
22610
- }
22611
- else {
22612
- gaugeWidth = drawWidth;
22613
- gaugeHeight = drawWidth / 2;
22614
- }
22615
- const gaugeX = GAUGE_PADDING_SIDE + (drawWidth - gaugeWidth) / 2;
22616
- const gaugeY = titleHeight + GAUGE_PADDING_TOP + (drawHeight - gaugeHeight) / 2;
22617
- return {
22618
- x: gaugeX,
22619
- y: gaugeY,
22620
- width: gaugeWidth,
22621
- height: gaugeHeight,
22622
- };
22623
- }
22624
- /**
22625
- * 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).
22626
- *
22627
- * Also compute an offset for the text so that it doesn't overlap with other text.
22628
- */
22629
- function getInflectionValues(runtime, gaugeRect, textColor, ctx) {
22630
- const maxValue = runtime.maxValue;
22631
- const minValue = runtime.minValue;
22632
- const gaugeCircleCenter = {
22633
- x: gaugeRect.x + gaugeRect.width / 2,
22634
- y: gaugeRect.y + gaugeRect.height,
22635
- };
22636
- const textStyle = { fontSize: GAUGE_LABELS_FONT_SIZE };
22637
- const inflectionValues = [];
22638
- const inflectionValuesTextRects = [];
22639
- for (const inflectionValue of runtime.inflectionValues) {
22640
- const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
22641
- const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
22642
- const angle = Math.PI - Math.PI * percentage;
22643
- const textRect = getRectangleTangentToCircle(angle, // angle between X axis and the point where the rectangle is tangent to the circle
22644
- gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, // radius of the gauge circle + margin below text
22645
- gaugeCircleCenter.x, // center of the gauge circle
22646
- gaugeCircleCenter.y, // center of the gauge circle
22647
- labelWidth + 2, // width of the text + some margin
22648
- GAUGE_LABELS_FONT_SIZE // height of the text
22649
- );
22650
- let offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect))
22651
- ? GAUGE_LABELS_FONT_SIZE
22652
- : 0;
22653
- inflectionValuesTextRects.push(textRect);
22654
- inflectionValues.push({
22655
- rotation: angle,
22656
- label: inflectionValue.label,
22657
- fontSize: GAUGE_LABELS_FONT_SIZE,
22658
- color: textColor,
22659
- offset,
22660
- });
22661
- }
22662
- return inflectionValues;
22663
- }
22664
- function getGaugeColor(runtime) {
22665
- const gaugeValue = runtime.gaugeValue?.value;
22666
- if (gaugeValue === undefined) {
22667
- return GAUGE_BACKGROUND_COLOR;
22668
- }
22669
- for (let i = 0; i < runtime.inflectionValues.length; i++) {
22670
- const inflectionValue = runtime.inflectionValues[i];
22671
- if (inflectionValue.operator === "<" && gaugeValue < inflectionValue.value) {
22672
- return runtime.colors[i];
22673
- }
22674
- else if (inflectionValue.operator === "<=" && gaugeValue <= inflectionValue.value) {
22675
- return runtime.colors[i];
22676
- }
22677
- }
22678
- return runtime.colors.at(-1);
22679
- }
22680
- function getSegmentsOfRectangle(rectangle) {
22681
- return [
22682
- { start: rectangle.topLeft, end: rectangle.topRight },
22683
- { start: rectangle.topRight, end: rectangle.bottomRight },
22684
- { start: rectangle.bottomRight, end: rectangle.bottomLeft },
22685
- { start: rectangle.bottomLeft, end: rectangle.topLeft },
22686
- ];
22687
- }
22688
- /**
22689
- * Check if two segment intersect. The case where the segments are colinear (both segments on the same line)
22690
- * is not handled.
22691
- */
22692
- function doSegmentIntersect(segment1, segment2) {
22693
- const A = segment1.start;
22694
- const B = segment1.end;
22695
- const C = segment2.start;
22696
- const D = segment2.end;
22697
- /**
22698
- * Line segment intersection algorithm
22699
- * https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
22700
- */
22701
- function ccw(a, b, c) {
22702
- return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
22703
- }
22704
- return ccw(A, C, D) !== ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D);
22705
- }
22706
- function doRectanglesIntersect(rect1, rect2) {
22707
- const segments1 = getSegmentsOfRectangle(rect1);
22708
- const segments2 = getSegmentsOfRectangle(rect2);
22709
- for (const segment1 of segments1) {
22710
- for (const segment2 of segments2) {
22711
- if (doSegmentIntersect(segment1, segment2)) {
22712
- return true;
22713
- }
22714
- }
22715
- }
22716
- return false;
22717
- }
22718
- /**
22719
- * Get the rectangle that is tangent to a circle at a given angle.
22720
- *
22721
- * @param angle angle between X axis and the point where the rectangle is tangent to the circle
22722
- */
22723
- function getRectangleTangentToCircle(angle, radius, circleCenterX, circleCenterY, rectWidth, rectHeight) {
22724
- const cos = Math.cos(angle);
22725
- const sin = Math.sin(angle);
22726
- // x, y are the distance from the center of the circle to the point where the rectangle is tangent to the circle
22727
- const x = cos * radius;
22728
- const y = sin * radius;
22729
- // x2, y2 are the distance from the point the rectangle is tangent to the circle to the bottom left corner of the rectangle
22730
- const x2 = sin * (rectWidth / 2); // cos(angle + 90°) = sin(angle)
22731
- const y2 = cos * (rectWidth / 2);
22732
- const bottomRight = {
22733
- x: x + x2 + circleCenterX,
22734
- y: circleCenterY - (y - y2),
22735
- };
22736
- const bottomLeft = {
22737
- x: x - x2 + circleCenterX,
22738
- y: circleCenterY - (y + y2),
22739
- };
22740
- // Same as above but for the top corners of the rectangle (radius + rectangle height instead of radius)
22741
- const xp = cos * (radius + rectHeight);
22742
- const yp = sin * (radius + rectHeight);
22743
- const topLeft = {
22744
- x: xp - x2 + circleCenterX,
22745
- y: circleCenterY - (yp + y2),
22746
- };
22747
- const topRight = {
22748
- x: xp + x2 + circleCenterX,
22749
- y: circleCenterY - (yp - y2),
22750
- };
22751
- return { bottomLeft, bottomRight, topRight, topLeft };
22752
- }
22753
-
22754
22867
  class GaugeChartComponent extends owl.Component {
22755
22868
  static template = "o-spreadsheet-GaugeChartComponent";
22756
22869
  canvas = owl.useRef("chartContainer");
@@ -22783,72 +22896,6 @@ stores.inject(MyMetaStore, storeInstance);
22783
22896
  return color;
22784
22897
  }
22785
22898
 
22786
- const CHART_COMMON_OPTIONS = {
22787
- // https://www.chartjs.org/docs/latest/general/responsive.html
22788
- responsive: true, // will resize when its container is resized
22789
- maintainAspectRatio: false, // doesn't maintain the aspect ratio (width/height =2 by default) so the user has the choice of the exact layout
22790
- elements: {
22791
- line: {
22792
- fill: false, // do not fill the area under line charts
22793
- },
22794
- point: {
22795
- hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
22796
- },
22797
- },
22798
- animation: false,
22799
- };
22800
- function chartToImage(runtime, figure, type) {
22801
- // wrap the canvas in a div with a fixed size because chart.js would
22802
- // fill the whole page otherwise
22803
- const div = document.createElement("div");
22804
- div.style.width = `${figure.width}px`;
22805
- div.style.height = `${figure.height}px`;
22806
- const canvas = document.createElement("canvas");
22807
- div.append(canvas);
22808
- canvas.setAttribute("width", figure.width.toString());
22809
- canvas.setAttribute("height", figure.height.toString());
22810
- // we have to add the canvas to the DOM otherwise it won't be rendered
22811
- document.body.append(div);
22812
- if ("chartJsConfig" in runtime) {
22813
- const config = deepCopy(runtime.chartJsConfig);
22814
- config.plugins = [backgroundColorChartJSPlugin];
22815
- const chart = new window.Chart(canvas, config);
22816
- const imgContent = chart.toBase64Image();
22817
- chart.destroy();
22818
- div.remove();
22819
- return imgContent;
22820
- }
22821
- else if (type === "scorecard") {
22822
- const design = getScorecardConfiguration(figure, runtime);
22823
- drawScoreChart(design, canvas);
22824
- const imgContent = canvas.toDataURL();
22825
- div.remove();
22826
- return imgContent;
22827
- }
22828
- else if (type === "gauge") {
22829
- drawGaugeChart(canvas, runtime);
22830
- const imgContent = canvas.toDataURL();
22831
- div.remove();
22832
- return imgContent;
22833
- }
22834
- return undefined;
22835
- }
22836
- /**
22837
- * Custom chart.js plugin to set the background color of the canvas
22838
- * https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md
22839
- */
22840
- const backgroundColorChartJSPlugin = {
22841
- id: "customCanvasBackgroundColor",
22842
- beforeDraw: (chart) => {
22843
- const { ctx } = chart;
22844
- ctx.save();
22845
- ctx.globalCompositeOperation = "destination-over";
22846
- ctx.fillStyle = "#ffffff";
22847
- ctx.fillRect(0, 0, chart.width, chart.height);
22848
- ctx.restore();
22849
- },
22850
- };
22851
-
22852
22899
  /**
22853
22900
  * Represent a raw XML string
22854
22901
  */
@@ -22914,6 +22961,7 @@ stores.inject(MyMetaStore, storeInstance);
22914
22961
  macroEnabledTemplateWorkbook: "application/vnd.ms-excel.template.macroEnabled.main+xml",
22915
22962
  excelAddInWorkbook: "application/vnd.ms-excel.addin.macroEnabled.main+xml",
22916
22963
  sheet: "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml",
22964
+ metadata: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheetMetadata+xml",
22917
22965
  sharedStrings: "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml",
22918
22966
  styles: "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml",
22919
22967
  drawing: "application/vnd.openxmlformats-officedocument.drawing+xml",
@@ -22926,6 +22974,7 @@ stores.inject(MyMetaStore, storeInstance);
22926
22974
  const XLSX_RELATION_TYPE = {
22927
22975
  document: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument",
22928
22976
  sheet: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet",
22977
+ metadata: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sheetMetadata",
22929
22978
  sharedStrings: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings",
22930
22979
  styles: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles",
22931
22980
  drawing: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing",
@@ -22935,6 +22984,7 @@ stores.inject(MyMetaStore, storeInstance);
22935
22984
  hyperlink: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
22936
22985
  image: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
22937
22986
  };
22987
+ const ARRAY_FORMULA_URI = "bdbb8cdc-fa1e-496e-a857-3c3f30c029c3";
22938
22988
  const RELATIONSHIP_NSR = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
22939
22989
  const HEIGHT_FACTOR = 0.75; // 100px => 75 u
22940
22990
  /**
@@ -25356,29 +25406,34 @@ stores.inject(MyMetaStore, storeInstance);
25356
25406
  * In all the sheets, replace the table-only references in the formula cells with standard references.
25357
25407
  */
25358
25408
  function convertTableFormulaReferences(convertedSheets, xlsxSheets) {
25359
- for (let sheet of convertedSheets) {
25360
- const tables = xlsxSheets.find((s) => s.sheetName === sheet.name).tables;
25409
+ for (let tableSheet of convertedSheets) {
25410
+ const tables = xlsxSheets.find((s) => s.sheetName === tableSheet.name).tables;
25361
25411
  for (let table of tables) {
25362
25412
  const tabRef = table.name + "[";
25363
- for (let position of positions(toZone(table.ref))) {
25364
- const xc = toXC(position.col, position.row);
25365
- let cellContent = sheet.cells[xc];
25366
- if (cellContent?.startsWith("=")) {
25367
- let refIndex;
25368
- while ((refIndex = cellContent.indexOf(tabRef)) !== -1) {
25369
- let reference = cellContent.slice(refIndex + tabRef.length);
25370
- // Expression can either be tableName[colName] or tableName[[#This Row], [colName]]
25371
- let endIndex = reference.indexOf("]");
25372
- if (reference.startsWith(`[`)) {
25373
- endIndex = reference.indexOf("]", endIndex + 1);
25374
- endIndex = reference.indexOf("]", endIndex + 1);
25413
+ for (let sheet of convertedSheets) {
25414
+ for (let xc in sheet.cells) {
25415
+ const cell = sheet.cells[xc];
25416
+ let cellContent = sheet.cells[xc];
25417
+ if (cell && cellContent && cellContent.startsWith("=")) {
25418
+ let refIndex;
25419
+ while ((refIndex = cellContent.indexOf(tabRef)) !== -1) {
25420
+ let endIndex = refIndex + tabRef.length;
25421
+ let openBrackets = 1;
25422
+ while (openBrackets > 0 && endIndex < cellContent.length) {
25423
+ if (cellContent[endIndex] === "[") {
25424
+ openBrackets++;
25425
+ }
25426
+ else if (cellContent[endIndex] === "]") {
25427
+ openBrackets--;
25428
+ }
25429
+ endIndex++;
25430
+ }
25431
+ let reference = cellContent.slice(refIndex + tabRef.length, endIndex - 1);
25432
+ const sheetPrefix = tableSheet.id === sheet.id ? "" : tableSheet.name + "!";
25433
+ const convertedRef = convertTableReference(sheetPrefix, reference, table, xc);
25434
+ cellContent =
25435
+ cellContent.slice(0, refIndex) + convertedRef + cellContent.slice(endIndex);
25375
25436
  }
25376
- reference = reference.slice(0, endIndex);
25377
- const convertedRef = convertTableReference(reference, table, xc);
25378
- cellContent =
25379
- cellContent.slice(0, refIndex) +
25380
- convertedRef +
25381
- cellContent.slice(tabRef.length + refIndex + endIndex + 1);
25382
25437
  }
25383
25438
  sheet.cells[xc] = cellContent;
25384
25439
  }
@@ -25387,11 +25442,17 @@ stores.inject(MyMetaStore, storeInstance);
25387
25442
  }
25388
25443
  }
25389
25444
  /**
25390
- * Convert table-specific references in formulas into standard references.
25445
+ * Convert table-specific references in formulas into standard references. A table reference is composed of columns names,
25446
+ * and of keywords determining the rows of the table to reference.
25391
25447
  *
25392
25448
  * A reference in a table can have the form (only the part between brackets should be given to this function):
25393
25449
  * - tableName[colName] : reference to the whole column "colName"
25450
+ * - tableName[#keyword] : reference to the whatever row the keyword refers to
25394
25451
  * - tableName[[#keyword], [colName]] : reference to some of the element(s) of the column colName
25452
+ * - tableName[[#keyword], [colName]:[col2Name]] : reference to some of the element(s) of the columns colName to col2Name
25453
+ * - tableName[[#keyword1], [#keyword2], [colName]] : reference to all the rows referenced by the keywords in the column colName
25454
+ * - tableName[[#keyword1], [colName], [#keyword2]]: the keywords and colName can be in any order
25455
+ *
25395
25456
  *
25396
25457
  * The available keywords are :
25397
25458
  * - #All : all the column (including totals)
@@ -25399,58 +25460,109 @@ stores.inject(MyMetaStore, storeInstance);
25399
25460
  * - #Headers : only the header of the column
25400
25461
  * - #Totals : only the totals of the column
25401
25462
  * - #This Row : only the element in the same row as the cell
25463
+ *
25464
+ * Note that the only valid combination of multiple keywords are #Data + #Totals and #Headers + #Data.
25402
25465
  */
25403
- function convertTableReference(expr, table, cellXc) {
25404
- const refElements = expr.split(",");
25466
+ function convertTableReference(sheetPrefix, expr, table, cellXc) {
25467
+ // TODO: Ideally we'd want to make a real tokenizer, this simple approach won't work if for example the column name
25468
+ // contain # or , characters. But that's probably an edge case that we can ignore for now.
25469
+ const parts = expr.split(",").map((part) => part.trim());
25405
25470
  const tableZone = toZone(table.ref);
25406
- const refZone = { ...tableZone };
25407
- let isReferencedZoneValid = true;
25408
- // Single column reference
25409
- if (refElements.length === 1) {
25410
- const colRelativeIndex = table.cols.findIndex((col) => col.name === refElements[0]);
25411
- refZone.left = refZone.right = colRelativeIndex + tableZone.left;
25412
- if (table.headerRowCount) {
25413
- refZone.top += table.headerRowCount;
25414
- }
25415
- if (table.totalsRowCount) {
25416
- refZone.bottom -= 1;
25471
+ const colIndexes = [];
25472
+ const rowIndexes = [];
25473
+ const foundKeywords = [];
25474
+ for (const part of parts) {
25475
+ if (removeBrackets(part).startsWith("#")) {
25476
+ const keyWord = removeBrackets(part);
25477
+ foundKeywords.push(keyWord);
25478
+ switch (keyWord) {
25479
+ case "#All":
25480
+ rowIndexes.push(tableZone.top, tableZone.bottom);
25481
+ break;
25482
+ case "#Data":
25483
+ const top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
25484
+ const bottom = table.totalsRowCount
25485
+ ? tableZone.bottom - table.totalsRowCount
25486
+ : tableZone.bottom;
25487
+ rowIndexes.push(top, bottom);
25488
+ break;
25489
+ case "#This Row":
25490
+ rowIndexes.push(toCartesian(cellXc).row);
25491
+ break;
25492
+ case "#Headers":
25493
+ if (!table.headerRowCount) {
25494
+ return CellErrorType.InvalidReference;
25495
+ }
25496
+ rowIndexes.push(tableZone.top);
25497
+ break;
25498
+ case "#Totals":
25499
+ if (!table.totalsRowCount) {
25500
+ return CellErrorType.InvalidReference;
25501
+ }
25502
+ rowIndexes.push(tableZone.bottom);
25503
+ break;
25504
+ }
25417
25505
  }
25418
- }
25419
- // Other references
25420
- else {
25421
- switch (refElements[0].slice(1, refElements[0].length - 1)) {
25422
- case "#All":
25423
- refZone.top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
25424
- refZone.bottom = tableZone.bottom;
25425
- break;
25426
- case "#Data":
25427
- refZone.top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
25428
- refZone.bottom = table.totalsRowCount ? tableZone.bottom + 1 : tableZone.bottom;
25429
- break;
25430
- case "#This Row":
25431
- refZone.top = refZone.bottom = toCartesian(cellXc).row;
25432
- break;
25433
- case "#Headers":
25434
- refZone.top = refZone.bottom = tableZone.top;
25435
- if (!table.headerRowCount) {
25436
- isReferencedZoneValid = false;
25437
- }
25438
- break;
25439
- case "#Totals":
25440
- refZone.top = refZone.bottom = tableZone.bottom;
25441
- if (!table.totalsRowCount) {
25442
- isReferencedZoneValid = false;
25506
+ else {
25507
+ const columns = part
25508
+ .split(":")
25509
+ .map((part) => part.trim())
25510
+ .map(removeBrackets);
25511
+ if (colIndexes.length) {
25512
+ return CellErrorType.InvalidReference;
25513
+ }
25514
+ const colRelativeIndex = table.cols.findIndex((col) => col.name === columns[0]);
25515
+ if (colRelativeIndex === -1) {
25516
+ return CellErrorType.InvalidReference;
25517
+ }
25518
+ colIndexes.push(colRelativeIndex + tableZone.left);
25519
+ if (columns[1]) {
25520
+ const colRelativeIndex2 = table.cols.findIndex((col) => col.name === columns[1]);
25521
+ if (colRelativeIndex2 === -1) {
25522
+ return CellErrorType.InvalidReference;
25443
25523
  }
25444
- break;
25524
+ colIndexes.push(colRelativeIndex2 + tableZone.left);
25525
+ }
25445
25526
  }
25446
- const colRef = refElements[1].slice(1, refElements[1].length - 1);
25447
- const colRelativeIndex = table.cols.findIndex((col) => col.name === colRef);
25448
- refZone.left = refZone.right = colRelativeIndex + tableZone.left;
25449
25527
  }
25450
- if (!isReferencedZoneValid) {
25528
+ if (!areKeywordsCompatible(foundKeywords)) {
25451
25529
  return CellErrorType.InvalidReference;
25452
25530
  }
25453
- return refZone.top !== refZone.bottom ? zoneToXc(refZone) : toXC(refZone.left, refZone.top);
25531
+ if (rowIndexes.length === 0) {
25532
+ const top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
25533
+ const bottom = table.totalsRowCount
25534
+ ? tableZone.bottom - table.totalsRowCount
25535
+ : tableZone.bottom;
25536
+ rowIndexes.push(top, bottom);
25537
+ }
25538
+ if (colIndexes.length === 0) {
25539
+ colIndexes.push(tableZone.left, tableZone.right);
25540
+ }
25541
+ const refZone = {
25542
+ top: Math.min(...rowIndexes),
25543
+ left: Math.min(...colIndexes),
25544
+ bottom: Math.max(...rowIndexes),
25545
+ right: Math.max(...colIndexes),
25546
+ };
25547
+ return sheetPrefix + zoneToXc(refZone);
25548
+ }
25549
+ function removeBrackets(str) {
25550
+ return str.startsWith("[") && str.endsWith("]") ? str.slice(1, str.length - 1) : str;
25551
+ }
25552
+ function areKeywordsCompatible(keywords) {
25553
+ if (keywords.length < 2) {
25554
+ return true;
25555
+ }
25556
+ else if (keywords.length > 2) {
25557
+ return false;
25558
+ }
25559
+ else if (keywords.includes("#Data") && keywords.includes("#Totals")) {
25560
+ return true;
25561
+ }
25562
+ else if (keywords.includes("#Headers") && keywords.includes("#Data")) {
25563
+ return true;
25564
+ }
25565
+ return false;
25454
25566
  }
25455
25567
 
25456
25568
  // -------------------------------------
@@ -28303,7 +28415,7 @@ stores.inject(MyMetaStore, storeInstance);
28303
28415
  }
28304
28416
  function getPyramidChartData(definition, dataSets, labelRange, getters) {
28305
28417
  const barChartData = getBarChartData(definition, dataSets, labelRange, getters);
28306
- const barDataset = barChartData.dataSetsValues;
28418
+ const barDataset = barChartData.dataSetsValues.filter((ds) => !ds.hidden);
28307
28419
  const pyramidDatasetValues = [];
28308
28420
  if (barDataset[0]) {
28309
28421
  const pyramidData = barDataset[0].data.map((value) => (value > 0 ? value : 0));
@@ -28618,11 +28730,12 @@ stores.inject(MyMetaStore, storeInstance);
28618
28730
  }
28619
28731
  let missingTimeAdapterAlreadyWarned = false;
28620
28732
  function isLuxonTimeAdapterInstalled() {
28621
- if (!window.Chart) {
28733
+ const Chart = getChartJSConstructor();
28734
+ if (!Chart) {
28622
28735
  return false;
28623
28736
  }
28624
28737
  // @ts-ignore
28625
- const adapter = new window.Chart._adapters._date({});
28738
+ const adapter = new Chart._adapters._date({});
28626
28739
  const isInstalled = adapter._id === "luxon";
28627
28740
  if (!isInstalled && !missingTimeAdapterAlreadyWarned) {
28628
28741
  missingTimeAdapterAlreadyWarned = true;
@@ -28780,10 +28893,8 @@ stores.inject(MyMetaStore, storeInstance);
28780
28893
  function getChartDatasetValues(getters, dataSets) {
28781
28894
  const datasetValues = [];
28782
28895
  for (const [dsIndex, ds] of Object.entries(dataSets)) {
28783
- if (getters.isColHidden(ds.dataRange.sheetId, ds.dataRange.zone.left)) {
28784
- continue;
28785
- }
28786
28896
  let label;
28897
+ let hidden = getters.isColHidden(ds.dataRange.sheetId, ds.dataRange.zone.left);
28787
28898
  if (ds.labelCell) {
28788
28899
  const labelRange = ds.labelCell;
28789
28900
  const cell = labelRange
@@ -28810,9 +28921,9 @@ stores.inject(MyMetaStore, storeInstance);
28810
28921
  data.fill(1);
28811
28922
  }
28812
28923
  else if (data.every((cell) => cell === undefined || cell === null || !isNumber(cell.toString(), DEFAULT_LOCALE))) {
28813
- continue;
28924
+ hidden = true;
28814
28925
  }
28815
- datasetValues.push({ data, label });
28926
+ datasetValues.push({ data, label, hidden });
28816
28927
  }
28817
28928
  return datasetValues;
28818
28929
  }
@@ -28823,12 +28934,13 @@ stores.inject(MyMetaStore, storeInstance);
28823
28934
  const colors = getChartColorsGenerator(definition, dataSetsValues.length);
28824
28935
  const trendDatasets = [];
28825
28936
  for (const index in dataSetsValues) {
28826
- let { label, data } = dataSetsValues[index];
28937
+ let { label, data, hidden } = dataSetsValues[index];
28827
28938
  label = definition.dataSets?.[index].label || label;
28828
28939
  const backgroundColor = colors.next();
28829
28940
  const dataset = {
28830
28941
  label,
28831
28942
  data,
28943
+ hidden,
28832
28944
  borderColor: definition.background || BACKGROUND_CHART_COLOR,
28833
28945
  borderWidth: definition.stacked ? 1 : 0,
28834
28946
  backgroundColor,
@@ -28861,6 +28973,9 @@ stores.inject(MyMetaStore, storeInstance);
28861
28973
  const labelsWithSubTotals = [];
28862
28974
  let lastValue = 0;
28863
28975
  for (const dataSetsValue of dataSetsValues) {
28976
+ if (dataSetsValue.hidden) {
28977
+ continue;
28978
+ }
28864
28979
  for (let i = 0; i < dataSetsValue.data.length; i++) {
28865
28980
  const data = dataSetsValue.data[i];
28866
28981
  labelsWithSubTotals.push(labels[i]);
@@ -28896,7 +29011,7 @@ stores.inject(MyMetaStore, storeInstance);
28896
29011
  const trendDatasets = [];
28897
29012
  const colors = getChartColorsGenerator(definition, dataSetsValues.length);
28898
29013
  for (let index = 0; index < dataSetsValues.length; index++) {
28899
- let { label, data } = dataSetsValues[index];
29014
+ let { label, data, hidden } = dataSetsValues[index];
28900
29015
  label = definition.dataSets?.[index].label || label;
28901
29016
  const color = colors.next();
28902
29017
  if (axisType && ["linear", "time"].includes(axisType)) {
@@ -28906,6 +29021,7 @@ stores.inject(MyMetaStore, storeInstance);
28906
29021
  const dataset = {
28907
29022
  label,
28908
29023
  data,
29024
+ hidden,
28909
29025
  tension: 0, // 0 -> render straight lines, which is much faster
28910
29026
  borderColor: color,
28911
29027
  backgroundColor: areaChart ? setColorAlpha(color, LINE_FILL_TRANSPARENCY) : color,
@@ -28938,7 +29054,9 @@ stores.inject(MyMetaStore, storeInstance);
28938
29054
  const dataSets = [];
28939
29055
  const dataSetsLength = Math.max(0, ...dataSetsValues.map((ds) => ds?.data?.length ?? 0));
28940
29056
  const backgroundColor = getPieColors(new ColorGenerator(dataSetsLength), dataSetsValues);
28941
- for (const { label, data } of dataSetsValues) {
29057
+ for (const { label, data, hidden } of dataSetsValues) {
29058
+ if (hidden)
29059
+ continue;
28942
29060
  const dataset = {
28943
29061
  label,
28944
29062
  data,
@@ -28956,7 +29074,7 @@ stores.inject(MyMetaStore, storeInstance);
28956
29074
  const colors = getChartColorsGenerator(definition, dataSetsValues.length);
28957
29075
  const trendDatasets = [];
28958
29076
  for (let index = 0; index < dataSetsValues.length; index++) {
28959
- let { label, data } = dataSetsValues[index];
29077
+ let { label, data, hidden } = dataSetsValues[index];
28960
29078
  label = definition.dataSets?.[index].label || label;
28961
29079
  const design = definition.dataSets?.[index];
28962
29080
  const color = colors.next();
@@ -28964,6 +29082,7 @@ stores.inject(MyMetaStore, storeInstance);
28964
29082
  const dataset = {
28965
29083
  label: label,
28966
29084
  data,
29085
+ hidden,
28967
29086
  borderColor: color,
28968
29087
  backgroundColor: color,
28969
29088
  yAxisID: definition.dataSets?.[index].yAxisId || "y",
@@ -28988,7 +29107,7 @@ stores.inject(MyMetaStore, storeInstance);
28988
29107
  const fill = definition.fillArea ?? false;
28989
29108
  const colors = getChartColorsGenerator(definition, dataSetsValues.length);
28990
29109
  for (let i = 0; i < dataSetsValues.length; i++) {
28991
- let { label, data } = dataSetsValues[i];
29110
+ let { label, data, hidden } = dataSetsValues[i];
28992
29111
  if (definition.dataSets?.[i]?.label) {
28993
29112
  label = definition.dataSets[i].label;
28994
29113
  }
@@ -28996,6 +29115,7 @@ stores.inject(MyMetaStore, storeInstance);
28996
29115
  const dataset = {
28997
29116
  label,
28998
29117
  data,
29118
+ hidden,
28999
29119
  borderColor,
29000
29120
  backgroundColor: borderColor,
29001
29121
  };
@@ -29141,6 +29261,11 @@ stores.inject(MyMetaStore, storeInstance);
29141
29261
  hidden: false,
29142
29262
  lineWidth: 2,
29143
29263
  })),
29264
+ filter: (legendItem, data) => {
29265
+ return "datasetIndex" in legendItem
29266
+ ? !data.datasets[legendItem.datasetIndex].hidden
29267
+ : true;
29268
+ },
29144
29269
  },
29145
29270
  };
29146
29271
  }
@@ -29202,6 +29327,11 @@ stores.inject(MyMetaStore, storeInstance);
29202
29327
  }
29203
29328
  return legendValues;
29204
29329
  },
29330
+ filter: (legendItem, data) => {
29331
+ return "datasetIndex" in legendItem
29332
+ ? !data.datasets[legendItem.datasetIndex].hidden
29333
+ : true;
29334
+ },
29205
29335
  },
29206
29336
  onClick: () => { }, // Disables click interaction with the waterfall chart legend items
29207
29337
  };
@@ -29285,6 +29415,11 @@ stores.inject(MyMetaStore, storeInstance);
29285
29415
  ...legendLabelConfig,
29286
29416
  };
29287
29417
  }),
29418
+ filter: (legendItem, data) => {
29419
+ return "datasetIndex" in legendItem
29420
+ ? !data.datasets[legendItem.datasetIndex].hidden
29421
+ : true;
29422
+ },
29288
29423
  },
29289
29424
  };
29290
29425
  }
@@ -29618,7 +29753,7 @@ stores.inject(MyMetaStore, storeInstance);
29618
29753
  <div
29619
29754
  class="o-chart-custom-tooltip border rounded px-2 py-1 pe-none mw-100 position-absolute text-nowrap shadow opacity-100">
29620
29755
  <table class="overflow-hidden m-0">
29621
- <thead>
29756
+ <thead t-if="title">
29622
29757
  <tr>
29623
29758
  <th class="o-tooltip-title align-baseline border-0 text-truncate" t-esc="title" t-attf-style="max-width: {{ labelsMaxWidth }}"/>
29624
29759
  </tr>
@@ -29679,8 +29814,8 @@ stores.inject(MyMetaStore, storeInstance);
29679
29814
  ? undefined
29680
29815
  : "";
29681
29816
  },
29817
+ beforeLabel: (tooltipItem) => tooltipItem.dataset?.label || tooltipItem.label,
29682
29818
  label: function (tooltipItem) {
29683
- const xLabel = tooltipItem.dataset?.label || tooltipItem.label;
29684
29819
  const horizontalChart = definition.horizontal;
29685
29820
  let yLabel = horizontalChart ? tooltipItem.parsed.x : tooltipItem.parsed.y;
29686
29821
  if (yLabel === undefined || yLabel === null) {
@@ -29688,7 +29823,7 @@ stores.inject(MyMetaStore, storeInstance);
29688
29823
  }
29689
29824
  const axisId = horizontalChart ? tooltipItem.dataset.xAxisID : tooltipItem.dataset.yAxisID;
29690
29825
  const yLabelStr = formatChartDatasetValue(args.axisFormats, args.locale)(yLabel, axisId);
29691
- return xLabel ? `${xLabel}: ${yLabelStr}` : yLabelStr;
29826
+ return yLabelStr;
29692
29827
  },
29693
29828
  },
29694
29829
  };
@@ -29713,21 +29848,18 @@ stores.inject(MyMetaStore, storeInstance);
29713
29848
  const formattedX = formatValue(label, { locale, format: labelFormat });
29714
29849
  const axisId = tooltipItem.dataset.yAxisID || "y";
29715
29850
  const formattedY = formatValue(dataSetPoint, { locale, format: axisFormats?.[axisId] });
29716
- const dataSetTitle = tooltipItem.dataset.label;
29717
- return formattedX
29718
- ? `${dataSetTitle}: (${formattedX}, ${formattedY})`
29719
- : `${dataSetTitle}: ${formattedY}`;
29851
+ return formattedX ? `(${formattedX}, ${formattedY})` : `${formattedY}`;
29720
29852
  };
29721
29853
  }
29722
29854
  else {
29723
29855
  tooltip.callbacks.label = function (tooltipItem) {
29724
- const xLabel = tooltipItem.dataset?.label || tooltipItem.label;
29725
29856
  const yLabel = tooltipItem.parsed.y;
29726
29857
  const axisId = tooltipItem.dataset.yAxisID;
29727
29858
  const yLabelStr = formatChartDatasetValue(axisFormats, locale)(yLabel, axisId);
29728
- return xLabel ? `${xLabel}: ${yLabelStr}` : yLabelStr;
29859
+ return yLabelStr;
29729
29860
  };
29730
29861
  }
29862
+ tooltip.callbacks.beforeLabel = (tooltipItem) => tooltipItem.dataset?.label || tooltipItem.label;
29731
29863
  tooltip.callbacks.title = function (tooltipItems) {
29732
29864
  const displayTooltipTitle = axisType !== "linear" &&
29733
29865
  tooltipItems.some((item) => item.dataset.xAxisID !== TREND_LINE_XAXIS_ID);
@@ -29745,17 +29877,15 @@ stores.inject(MyMetaStore, storeInstance);
29745
29877
  title: function (tooltipItems) {
29746
29878
  return tooltipItems[0].dataset.label;
29747
29879
  },
29880
+ beforeLabel: (tooltipItem) => tooltipItem.label || tooltipItem.dataset.label,
29748
29881
  label: function (tooltipItem) {
29749
29882
  const data = tooltipItem.dataset.data;
29750
29883
  const dataIndex = tooltipItem.dataIndex;
29751
29884
  const percentage = calculatePercentage(data, dataIndex);
29752
- const xLabel = tooltipItem.label || tooltipItem.dataset.label;
29753
29885
  const yLabel = tooltipItem.parsed.y ?? tooltipItem.parsed;
29754
29886
  const toolTipFormat = !format && yLabel >= 1000 ? "#,##" : format;
29755
29887
  const yLabelStr = formatValue(yLabel, { format: toolTipFormat, locale });
29756
- return xLabel
29757
- ? `${xLabel}: ${yLabelStr} (${percentage}%)`
29758
- : `${yLabelStr} (${percentage}%)`;
29888
+ return `${yLabelStr} (${percentage}%)`;
29759
29889
  },
29760
29890
  },
29761
29891
  };
@@ -29768,16 +29898,17 @@ stores.inject(MyMetaStore, storeInstance);
29768
29898
  enabled: false,
29769
29899
  external: customTooltipHandler,
29770
29900
  callbacks: {
29771
- label: function (tooltipItem) {
29772
- const [lastValue, currentValue] = tooltipItem.raw;
29773
- const yLabel = currentValue - lastValue;
29901
+ beforeLabel: function (tooltipItem) {
29774
29902
  const dataSeriesIndex = labels.length
29775
29903
  ? Math.floor(tooltipItem.dataIndex / labels.length)
29776
29904
  : 0;
29777
- const dataSeriesLabel = dataSeriesLabels[dataSeriesIndex];
29905
+ return dataSeriesLabels[dataSeriesIndex];
29906
+ },
29907
+ label: function (tooltipItem) {
29908
+ const [lastValue, currentValue] = tooltipItem.raw;
29909
+ const yLabel = currentValue - lastValue;
29778
29910
  const toolTipFormat = !format && Math.abs(yLabel) > 1000 ? "#,##" : format;
29779
- const yLabelStr = formatValue(yLabel, { format: toolTipFormat, locale });
29780
- return dataSeriesLabel ? `${dataSeriesLabel}: ${yLabelStr}` : yLabelStr;
29911
+ return formatValue(yLabel, { format: toolTipFormat, locale });
29781
29912
  },
29782
29913
  },
29783
29914
  };
@@ -29801,11 +29932,10 @@ stores.inject(MyMetaStore, storeInstance);
29801
29932
  enabled: false,
29802
29933
  external: customTooltipHandler,
29803
29934
  callbacks: {
29935
+ beforeLabel: (tooltipItem) => tooltipItem.dataset?.label || tooltipItem.label,
29804
29936
  label: function (tooltipItem) {
29805
- const xLabel = tooltipItem.dataset?.label || tooltipItem.label;
29806
29937
  const yLabel = tooltipItem.parsed.r;
29807
- const formattedY = formatValue(yLabel, { format: axisFormats?.r, locale });
29808
- return xLabel ? `${xLabel}: ${formattedY}` : formattedY;
29938
+ return formatValue(yLabel, { format: axisFormats?.r, locale });
29809
29939
  },
29810
29940
  },
29811
29941
  };
@@ -29820,13 +29950,12 @@ stores.inject(MyMetaStore, storeInstance);
29820
29950
  return tooltipItem.raw.value !== undefined;
29821
29951
  },
29822
29952
  callbacks: {
29953
+ beforeLabel: (tooltipItem) => tooltipItem.raw.feature.properties.name,
29823
29954
  label: function (tooltipItem) {
29824
29955
  const rawItem = tooltipItem.raw;
29825
- const xLabel = rawItem.feature.properties.name;
29826
29956
  const yLabel = rawItem.value;
29827
29957
  const toolTipFormat = !format && Math.abs(yLabel) >= 1000 ? "#,##" : format;
29828
- const yLabelStr = formatValue(yLabel, { format: toolTipFormat, locale });
29829
- return xLabel ? `${xLabel}: ${yLabelStr}` : yLabelStr;
29958
+ return formatValue(yLabel, { format: toolTipFormat, locale });
29830
29959
  },
29831
29960
  },
29832
29961
  };
@@ -29846,7 +29975,8 @@ stores.inject(MyMetaStore, storeInstance);
29846
29975
  return;
29847
29976
  }
29848
29977
  const tooltipItems = tooltip.body.map((body, index) => {
29849
- let [label, value] = body.lines[0].split(":").map((str) => str.trim());
29978
+ let label = body.before[0];
29979
+ let value = body.lines[0];
29850
29980
  if (!value) {
29851
29981
  value = label;
29852
29982
  label = "";
@@ -32459,10 +32589,6 @@ stores.inject(MyMetaStore, storeInstance);
32459
32589
  this.currentDisplayValue = newDisplay;
32460
32590
  if (!anchor)
32461
32591
  return;
32462
- el.style.top = "";
32463
- el.style.left = "";
32464
- el.style["max-height"] = "";
32465
- el.style["max-width"] = "";
32466
32592
  const propsMaxSize = { width: this.props.maxWidth, height: this.props.maxHeight };
32467
32593
  let elDims = {
32468
32594
  width: el.getBoundingClientRect().width,
@@ -34014,6 +34140,7 @@ stores.inject(MyMetaStore, storeInstance);
34014
34140
  duplicateLabelRangeInDuplicatedSheet: duplicateLabelRangeInDuplicatedSheet,
34015
34141
  formatChartDatasetValue: formatChartDatasetValue,
34016
34142
  formatTickValue: formatTickValue,
34143
+ getChartJSConstructor: getChartJSConstructor,
34017
34144
  getChartPositionAtCenterOfViewport: getChartPositionAtCenterOfViewport,
34018
34145
  getDefinedAxis: getDefinedAxis,
34019
34146
  getPieColors: getPieColors,
@@ -37814,6 +37941,11 @@ stores.inject(MyMetaStore, storeInstance);
37814
37941
  }
37815
37942
  updateColors(colors) {
37816
37943
  this.colors = colors;
37944
+ const colorGenerator = new ColorGenerator(this.ranges.length, this.colors);
37945
+ this.ranges = this.ranges.map((range) => ({
37946
+ ...range,
37947
+ color: colorGenerator.next(),
37948
+ }));
37817
37949
  }
37818
37950
  confirm() {
37819
37951
  for (const range of this.selectionInputs) {
@@ -37848,12 +37980,11 @@ stores.inject(MyMetaStore, storeInstance);
37848
37980
  * e.g. ["A1", "Sheet2!B3", "E12"]
37849
37981
  */
37850
37982
  get selectionInputs() {
37851
- const generator = new ColorGenerator(this.ranges.length, this.colors);
37852
37983
  return this.ranges.map((input, index) => Object.assign({}, input, {
37853
37984
  color: this.hasMainFocus &&
37854
37985
  this.focusedRangeIndex !== null &&
37855
37986
  this.getters.isRangeValid(input.xc)
37856
- ? generator.next()
37987
+ ? input.color
37857
37988
  : null,
37858
37989
  isFocused: this.hasMainFocus && this.focusedRangeIndex === index,
37859
37990
  isValidRange: input.xc === "" || this.getters.isRangeValid(input.xc),
@@ -38123,10 +38254,10 @@ stores.inject(MyMetaStore, storeInstance);
38123
38254
  if (originalIndex === finalIndex) {
38124
38255
  return;
38125
38256
  }
38126
- const draggedItems = [...draggableIds];
38127
- draggedItems.splice(originalIndex, 1);
38128
- draggedItems.splice(finalIndex, 0, rangeId);
38129
- this.props.onSelectionReordered?.(this.store.selectionInputs.map((range) => draggedItems.indexOf(range.id)));
38257
+ const indexes = range(0, draggableIds.length);
38258
+ indexes.splice(originalIndex, 1);
38259
+ indexes.splice(finalIndex, 0, originalIndex);
38260
+ this.props.onSelectionReordered?.(indexes);
38130
38261
  this.props.onSelectionConfirmed?.();
38131
38262
  this.store.confirm();
38132
38263
  },
@@ -38404,6 +38535,9 @@ stores.inject(MyMetaStore, storeInstance);
38404
38535
  this.state.datasetDispatchResult = this.props.updateChart(this.props.figureId, {
38405
38536
  dataSets: this.dataSets,
38406
38537
  });
38538
+ if (this.state.datasetDispatchResult.isSuccessful) {
38539
+ this.dataSets = this.env.model.getters.getChartDefinition(this.props.figureId).dataSets;
38540
+ }
38407
38541
  }
38408
38542
  getDataSeriesRanges() {
38409
38543
  return this.dataSets;
@@ -39820,8 +39954,16 @@ stores.inject(MyMetaStore, storeInstance);
39820
39954
  }
39821
39955
  let startNode = this.findChildAtCharacterIndex(start);
39822
39956
  let endNode = this.findChildAtCharacterIndex(end);
39823
- range.setStart(startNode.node, startNode.offset);
39824
- range.setEnd(endNode.node, endNode.offset);
39957
+ // setEnd (setStart) will result in a collapsed range if the end point is before the start point
39958
+ // https://developer.mozilla.org/en-US/docs/Web/API/Range/setEnd
39959
+ if (start <= end) {
39960
+ range.setStart(startNode.node, startNode.offset);
39961
+ range.setEnd(endNode.node, endNode.offset);
39962
+ }
39963
+ else {
39964
+ range.setStart(endNode.node, endNode.offset);
39965
+ range.setEnd(startNode.node, startNode.offset);
39966
+ }
39825
39967
  }
39826
39968
  }
39827
39969
  /**
@@ -40123,8 +40265,7 @@ stores.inject(MyMetaStore, storeInstance);
40123
40265
  }
40124
40266
 
40125
40267
  .o-composer-assistant {
40126
- position: absolute;
40127
- margin: 1px 4px;
40268
+ margin-top: 1px;
40128
40269
 
40129
40270
  .o-semi-bold {
40130
40271
  /* FIXME: to remove in favor of Bootstrap
@@ -40175,10 +40316,11 @@ stores.inject(MyMetaStore, storeInstance);
40175
40316
  });
40176
40317
  compositionActive = false;
40177
40318
  spreadsheetRect = useSpreadsheetRect();
40178
- get assistantStyle() {
40319
+ get assistantStyleProperties() {
40179
40320
  const composerRect = this.composerRef.el.getBoundingClientRect();
40180
40321
  const assistantStyle = {};
40181
- assistantStyle["min-width"] = `${this.props.rect?.width || ASSISTANT_WIDTH}px`;
40322
+ const minWidth = Math.min(this.props.rect?.width || Infinity, ASSISTANT_WIDTH);
40323
+ assistantStyle["min-width"] = `${minWidth}px`;
40182
40324
  const proposals = this.autoCompleteState.provider?.proposals;
40183
40325
  const proposalsHaveDescription = proposals?.some((proposal) => proposal.description);
40184
40326
  if (this.functionDescriptionState.showDescription || proposalsHaveDescription) {
@@ -40202,13 +40344,29 @@ stores.inject(MyMetaStore, storeInstance);
40202
40344
  }
40203
40345
  }
40204
40346
  else {
40205
- assistantStyle["max-height"] = `${this.spreadsheetRect.height - composerRect.bottom}px`;
40347
+ assistantStyle["max-height"] = `${this.spreadsheetRect.height - composerRect.bottom - 1}px`; // -1: margin
40206
40348
  if (composerRect.left + ASSISTANT_WIDTH + SCROLLBAR_WIDTH + CLOSE_ICON_RADIUS >
40207
40349
  this.spreadsheetRect.width) {
40208
40350
  assistantStyle.right = `${CLOSE_ICON_RADIUS}px`;
40209
40351
  }
40210
40352
  }
40211
- return cssPropertiesToCss(assistantStyle);
40353
+ return assistantStyle;
40354
+ }
40355
+ get assistantStyle() {
40356
+ const allProperties = this.assistantStyleProperties;
40357
+ return cssPropertiesToCss({
40358
+ "max-height": allProperties["max-height"],
40359
+ width: allProperties["width"],
40360
+ "min-width": allProperties["min-width"],
40361
+ });
40362
+ }
40363
+ get assistantContainerStyle() {
40364
+ const allProperties = this.assistantStyleProperties;
40365
+ return cssPropertiesToCss({
40366
+ top: allProperties["top"],
40367
+ right: allProperties["right"],
40368
+ transform: allProperties["transform"],
40369
+ });
40212
40370
  }
40213
40371
  // we can't allow input events to be triggered while we remove and add back the content of the composer in processContent
40214
40372
  shouldProcessInputEvents = false;
@@ -46682,9 +46840,7 @@ stores.inject(MyMetaStore, storeInstance);
46682
46840
  pivot: this.draft,
46683
46841
  });
46684
46842
  this.draft = null;
46685
- if (!this.alreadyNotified &&
46686
- !this.isDynamicPivotInViewport() &&
46687
- this.isStaticPivotInViewport()) {
46843
+ if (!this.alreadyNotified && this.isUpdatedPivotVisibleInViewportOnlyAsStaticPivot()) {
46688
46844
  const formulaId = this.getters.getPivotFormulaId(this.pivotId);
46689
46845
  const pivotExample = `=PIVOT(${formulaId})`;
46690
46846
  this.alreadyNotified = true;
@@ -46740,26 +46896,33 @@ stores.inject(MyMetaStore, storeInstance);
46740
46896
  this.applyUpdate();
46741
46897
  }
46742
46898
  }
46743
- isDynamicPivotInViewport() {
46744
- for (const position of this.getters.getVisibleCellPositions()) {
46745
- const isDynamicPivot = this.getters.isSpillPivotFormula(position);
46746
- if (isDynamicPivot) {
46747
- return true;
46748
- }
46749
- }
46750
- return false;
46751
- }
46752
- isStaticPivotInViewport() {
46899
+ /**
46900
+ * @returns true if the updated pivot is visible in the viewport only as a
46901
+ * static pivot and not as a dynamic pivot
46902
+ */
46903
+ isUpdatedPivotVisibleInViewportOnlyAsStaticPivot() {
46904
+ let staticPivotCount = 0;
46905
+ const updatedPivotFormulaId = this.getters.getPivotFormulaId(this.pivotId);
46753
46906
  for (const position of this.getters.getVisibleCellPositions()) {
46754
46907
  const cell = this.getters.getCell(position);
46755
46908
  if (cell?.isFormula) {
46756
46909
  const pivotFunction = getFirstPivotFunction(cell.compiledFormula.tokens);
46757
- if (pivotFunction && pivotFunction.functionName !== "PIVOT") {
46758
- return true;
46910
+ const pivotFormulaId = pivotFunction?.args[0]?.value;
46911
+ if (pivotFunction && updatedPivotFormulaId === pivotFormulaId.toString()) {
46912
+ if (pivotFunction.functionName === "PIVOT") {
46913
+ // if we have at least one dynamic pivot visible inserted the viewport
46914
+ // we return false
46915
+ return false;
46916
+ }
46917
+ else {
46918
+ staticPivotCount++;
46919
+ }
46759
46920
  }
46760
46921
  }
46761
46922
  }
46762
- return false;
46923
+ // we return true if there are only static pivots visible inserted the viewport,
46924
+ // otherwise false
46925
+ return staticPivotCount > 0;
46763
46926
  }
46764
46927
  addDefaultDateTimeGranularity(fields, definition) {
46765
46928
  const { columns, rows } = definition;
@@ -51638,8 +51801,8 @@ stores.inject(MyMetaStore, storeInstance);
51638
51801
  css /* scss */ `
51639
51802
  .o-corner {
51640
51803
  position: absolute;
51641
- height: 6px;
51642
- width: 6px;
51804
+ height: 8px;
51805
+ width: 8px;
51643
51806
  border: 1px solid white;
51644
51807
  }
51645
51808
  .o-corner-nw,
@@ -60000,6 +60163,10 @@ stores.inject(MyMetaStore, storeInstance);
60000
60163
  this.compilationParams = buildCompilationParameters(this.context, this.getters, this.computeAndSave.bind(this));
60001
60164
  this.compilationParams.evalContext.updateDependencies = this.updateDependencies.bind(this);
60002
60165
  this.compilationParams.evalContext.addDependencies = this.addDependencies.bind(this);
60166
+ this.compilationParams.evalContext.lookupCaches = {
60167
+ forwardSearch: new Map(),
60168
+ reverseSearch: new Map(),
60169
+ };
60003
60170
  }
60004
60171
  createEmptyPositionSet() {
60005
60172
  const sheetSizes = {};
@@ -60609,6 +60776,7 @@ stores.inject(MyMetaStore, storeInstance);
60609
60776
  exportForExcel(data) {
60610
60777
  for (const sheet of data.sheets) {
60611
60778
  sheet.cellValues = {};
60779
+ sheet.formulaSpillRanges = {};
60612
60780
  }
60613
60781
  for (const position of this.evaluator.getEvaluatedPositions()) {
60614
60782
  const evaluatedCell = this.evaluator.getEvaluatedCell(position);
@@ -60620,8 +60788,9 @@ stores.inject(MyMetaStore, storeInstance);
60620
60788
  const exportedSheetData = data.sheets.find((sheet) => sheet.id === position.sheetId);
60621
60789
  const formulaCell = this.getCorrespondingFormulaCell(position);
60622
60790
  if (formulaCell) {
60791
+ const cell = this.getters.getCell(position);
60623
60792
  isExported = isExportableToExcel(formulaCell.compiledFormula.tokens);
60624
- isFormula = isExported;
60793
+ isFormula = isExported && cell?.content === formulaCell.content;
60625
60794
  // If the cell contains a non-exported formula and that is evaluates to
60626
60795
  // nothing* ,we don't export it.
60627
60796
  // * non-falsy value are relevant and so are 0 and FALSE, which only leaves
@@ -60644,7 +60813,11 @@ stores.inject(MyMetaStore, storeInstance);
60644
60813
  content = !isExported ? newContent : exportedCellData;
60645
60814
  }
60646
60815
  exportedSheetData.cells[xc] = content;
60647
- exportedSheetData.cellValues[xc] = value;
60816
+ exportedSheetData.cellValues[xc] = evaluatedCell.type !== "error" ? value : undefined;
60817
+ const spillZone = this.getSpreadZone(position);
60818
+ if (spillZone) {
60819
+ exportedSheetData.formulaSpillRanges[xc] = this.getters.getRangeString(this.getters.getRangeFromZone(position.sheetId, spillZone), position.sheetId);
60820
+ }
60648
60821
  }
60649
60822
  }
60650
60823
  /**
@@ -62872,7 +63045,7 @@ stores.inject(MyMetaStore, storeInstance);
62872
63045
  getRule(cell, cells) {
62873
63046
  const rules = autofillRulesRegistry.getAll().sort((a, b) => a.sequence - b.sequence);
62874
63047
  const rule = rules.find((rule) => rule.condition(cell, cells));
62875
- return rule && rule.generateRule(cell, cells);
63048
+ return rule && this.direction && rule.generateRule(cell, cells, this.direction);
62876
63049
  }
62877
63050
  /**
62878
63051
  * Create the generator to be able to autofill the next cells.
@@ -73354,7 +73527,7 @@ stores.inject(MyMetaStore, storeInstance);
73354
73527
  `;
73355
73528
  }
73356
73529
 
73357
- function addFormula(formula, value) {
73530
+ function addFormula(formula, value, formulaSpillRange) {
73358
73531
  if (!formula) {
73359
73532
  return { attrs: [], node: escapeXml `` };
73360
73533
  }
@@ -73362,10 +73535,17 @@ stores.inject(MyMetaStore, storeInstance);
73362
73535
  if (type === undefined) {
73363
73536
  return { attrs: [], node: escapeXml `` };
73364
73537
  }
73365
- const attrs = [["t", type]];
73538
+ const attrs = [
73539
+ ["cm", "1"],
73540
+ ["t", type],
73541
+ ];
73366
73542
  const XlsxFormula = adaptFormulaToExcel(formula);
73367
73543
  const exportedValue = adaptFormulaValueToExcel(value);
73368
- const node = escapeXml /*xml*/ `<f>${XlsxFormula}</f><v>${exportedValue}</v>`;
73544
+ // We treat all formulas as array formulas (a simple formula
73545
+ // is an array formula that spills on only one cell) to avoid
73546
+ // trying to detect spilling sub-formulas which is not a trivial task.
73547
+ let node;
73548
+ node = escapeXml /*xml*/ `<f t="array" ref="${formulaSpillRange}">${XlsxFormula}</f><v>${exportedValue}</v>`;
73369
73549
  return { attrs, node };
73370
73550
  }
73371
73551
  function addContent(content, sharedStrings, forceString = false) {
@@ -74355,7 +74535,7 @@ stores.inject(MyMetaStore, storeInstance);
74355
74535
  let cellNode = escapeXml ``;
74356
74536
  // Either formula or static value inside the cell
74357
74537
  if (content?.startsWith("=") && value !== undefined) {
74358
- const res = addFormula(content, value);
74538
+ const res = addFormula(content, value, sheet.formulaSpillRanges[xc] ?? xc);
74359
74539
  if (!res) {
74360
74540
  continue;
74361
74541
  }
@@ -74641,6 +74821,30 @@ stores.inject(MyMetaStore, storeInstance);
74641
74821
  `;
74642
74822
  files.push(createXMLFile(parseXML(sheetXml), `xl/worksheets/sheet${sheetIndex}.xml`, "sheet"));
74643
74823
  }
74824
+ const sheetMetadataXml = escapeXml /*xml*/ `
74825
+ <metadata xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:xda="http://schemas.microsoft.com/office/spreadsheetml/2017/dynamicarray">
74826
+ <metadataTypes count="1">
74827
+ <metadataType name="XLDAPR" minSupportedVersion="120000" copy="1" pasteAll="1"
74828
+ pasteValues="1" merge="1" splitFirst="1" rowColShift="1" clearFormats="1"
74829
+ clearComments="1" assign="1" coerce="1" cellMeta="1" />
74830
+ </metadataTypes>
74831
+ <futureMetadata name="XLDAPR" count="1">
74832
+ <bk>
74833
+ <extLst>
74834
+ <ext uri="{${ARRAY_FORMULA_URI}}">
74835
+ <xda:dynamicArrayProperties fDynamic="1" fCollapsed="0" />
74836
+ </ext>
74837
+ </extLst>
74838
+ </bk>
74839
+ </futureMetadata>
74840
+ <cellMetadata count="1">
74841
+ <bk>
74842
+ <rc t="1" v="0" />
74843
+ </bk>
74844
+ </cellMetadata>
74845
+ </metadata>
74846
+ `;
74847
+ files.push(createXMLFile(parseXML(sheetMetadataXml), "xl/metadata.xml", "metadata"));
74644
74848
  addRelsToFile(construct.relsFiles, "xl/_rels/workbook.xml.rels", {
74645
74849
  type: XLSX_RELATION_TYPE.sharedStrings,
74646
74850
  target: "sharedStrings.xml",
@@ -74649,6 +74853,10 @@ stores.inject(MyMetaStore, storeInstance);
74649
74853
  type: XLSX_RELATION_TYPE.styles,
74650
74854
  target: "styles.xml",
74651
74855
  });
74856
+ addRelsToFile(construct.relsFiles, "xl/_rels/workbook.xml.rels", {
74857
+ type: XLSX_RELATION_TYPE.metadata,
74858
+ target: "metadata.xml",
74859
+ });
74652
74860
  return files;
74653
74861
  }
74654
74862
  /**
@@ -75610,9 +75818,9 @@ stores.inject(MyMetaStore, storeInstance);
75610
75818
  exports.tokenize = tokenize;
75611
75819
 
75612
75820
 
75613
- __info__.version = "18.2.0";
75614
- __info__.date = "2025-02-18T08:27:07.101Z";
75615
- __info__.hash = "d708714";
75821
+ __info__.version = "18.2.2";
75822
+ __info__.date = "2025-03-07T10:41:04.411Z";
75823
+ __info__.hash = "f567932";
75616
75824
 
75617
75825
 
75618
75826
  })(this.o_spreadsheet = this.o_spreadsheet || {}, owl);