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