@odoo/o-spreadsheet 18.2.4 → 18.2.6

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.4
6
- * @date 2025-03-19T08:20:57.717Z
7
- * @hash 958936a
5
+ * @version 18.2.6
6
+ * @date 2025-04-04T08:41:26.115Z
7
+ * @hash faa00e2
8
8
  */
9
9
 
10
10
  'use strict';
@@ -806,8 +806,7 @@ function removeFalsyAttributes(obj) {
806
806
  *
807
807
  * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Character_Classes
808
808
  */
809
- const whiteSpaceSpecialCharacters = [
810
- " ",
809
+ const specialWhiteSpaceSpecialCharacters = [
811
810
  "\t",
812
811
  "\f",
813
812
  "\v",
@@ -822,7 +821,7 @@ const whiteSpaceSpecialCharacters = [
822
821
  String.fromCharCode(parseInt("3000", 16)),
823
822
  String.fromCharCode(parseInt("feff", 16)),
824
823
  ];
825
- const whiteSpaceRegexp = new RegExp(whiteSpaceSpecialCharacters.join("|"), "g");
824
+ const specialWhiteSpaceRegexp = new RegExp(specialWhiteSpaceSpecialCharacters.join("|"), "g");
826
825
  const newLineRegexp = /(\r\n|\r)/g;
827
826
  /**
828
827
  * Replace all different newlines characters by \n
@@ -1133,7 +1132,10 @@ function rgbaStringToHex(color) {
1133
1132
  }
1134
1133
  else if (stringVals.length === 4) {
1135
1134
  const alpha = parseFloat(stringVals.pop() || "1");
1136
- alphaHex = Math.round((alpha || 1) * 255);
1135
+ if (isNaN(alpha)) {
1136
+ throw new Error("invalid alpha value");
1137
+ }
1138
+ alphaHex = Math.round(alpha * 255);
1137
1139
  }
1138
1140
  const vals = stringVals.map((val) => parseInt(val, 10));
1139
1141
  if (alphaHex !== 255) {
@@ -6796,8 +6798,12 @@ function tokenize(str, locale = DEFAULT_LOCALE) {
6796
6798
  str = replaceNewLines(str);
6797
6799
  const chars = new TokenizingChars(str);
6798
6800
  const result = [];
6801
+ const tokenizeSpace = specialWhiteSpaceRegexp.test(str)
6802
+ ? tokenizeSpecialCharacterSpace
6803
+ : tokenizeSimpleSpace;
6799
6804
  while (!chars.isOver()) {
6800
- let token = tokenizeSpace(chars) ||
6805
+ let token = tokenizeNewLine(chars) ||
6806
+ tokenizeSpace(chars) ||
6801
6807
  tokenizeArgsSeparator(chars, locale) ||
6802
6808
  tokenizeParenthesis(chars) ||
6803
6809
  tokenizeOperator(chars) ||
@@ -6931,17 +6937,19 @@ function tokenizeSymbol(chars) {
6931
6937
  }
6932
6938
  return null;
6933
6939
  }
6934
- function tokenizeSpace(chars) {
6935
- let length = 0;
6936
- while (chars.current === NEWLINE) {
6937
- length++;
6938
- chars.shift();
6940
+ function tokenizeSpecialCharacterSpace(chars) {
6941
+ let spaces = "";
6942
+ while (chars.current === " " || (chars.current && chars.current.match(specialWhiteSpaceRegexp))) {
6943
+ spaces += chars.shift();
6939
6944
  }
6940
- if (length) {
6941
- return { type: "SPACE", value: NEWLINE.repeat(length) };
6945
+ if (spaces) {
6946
+ return { type: "SPACE", value: spaces };
6942
6947
  }
6948
+ return null;
6949
+ }
6950
+ function tokenizeSimpleSpace(chars) {
6943
6951
  let spaces = "";
6944
- while (chars.current && chars.current.match(whiteSpaceRegexp)) {
6952
+ while (chars.current === " ") {
6945
6953
  spaces += chars.shift();
6946
6954
  }
6947
6955
  if (spaces) {
@@ -6949,6 +6957,17 @@ function tokenizeSpace(chars) {
6949
6957
  }
6950
6958
  return null;
6951
6959
  }
6960
+ function tokenizeNewLine(chars) {
6961
+ let length = 0;
6962
+ while (chars.current === NEWLINE) {
6963
+ length++;
6964
+ chars.shift();
6965
+ }
6966
+ if (length) {
6967
+ return { type: "SPACE", value: NEWLINE.repeat(length) };
6968
+ }
6969
+ return null;
6970
+ }
6952
6971
  function tokenizeInvalidRange(chars) {
6953
6972
  if (chars.currentStartsWith(CellErrorType.InvalidReference)) {
6954
6973
  chars.advanceBy(CellErrorType.InvalidReference.length);
@@ -7001,7 +7020,7 @@ function isValidLocale(locale) {
7001
7020
  */
7002
7021
  function canonicalizeNumberContent(content, locale) {
7003
7022
  return content.startsWith("=")
7004
- ? canonicalizeFormula$1(content, locale)
7023
+ ? canonicalizeFormula(content, locale)
7005
7024
  : canonicalizeNumberLiteral(content, locale);
7006
7025
  }
7007
7026
  /**
@@ -7016,7 +7035,7 @@ function canonicalizeNumberContent(content, locale) {
7016
7035
  */
7017
7036
  function canonicalizeContent(content, locale) {
7018
7037
  return content.startsWith("=")
7019
- ? canonicalizeFormula$1(content, locale)
7038
+ ? canonicalizeFormula(content, locale)
7020
7039
  : canonicalizeLiteral(content, locale);
7021
7040
  }
7022
7041
  /**
@@ -7032,15 +7051,21 @@ function localizeContent(content, locale) {
7032
7051
  ? localizeFormula(content, locale)
7033
7052
  : localizeLiteral(content, locale);
7034
7053
  }
7054
+ /** Change a number string to its canonical form (en_US locale) */
7055
+ function canonicalizeNumberValue(content, locale) {
7056
+ return content.startsWith("=")
7057
+ ? canonicalizeFormula(content, locale)
7058
+ : canonicalizeNumberLiteral(content, locale);
7059
+ }
7035
7060
  /** Change a formula to its canonical form (en_US locale) */
7036
- function canonicalizeFormula$1(formula, locale) {
7037
- return _localizeFormula$1(formula, locale, DEFAULT_LOCALE);
7061
+ function canonicalizeFormula(formula, locale) {
7062
+ return _localizeFormula(formula, locale, DEFAULT_LOCALE);
7038
7063
  }
7039
7064
  /** Change a formula from the canonical form to the given locale */
7040
7065
  function localizeFormula(formula, locale) {
7041
- return _localizeFormula$1(formula, DEFAULT_LOCALE, locale);
7066
+ return _localizeFormula(formula, DEFAULT_LOCALE, locale);
7042
7067
  }
7043
- function _localizeFormula$1(formula, fromLocale, toLocale) {
7068
+ function _localizeFormula(formula, fromLocale, toLocale) {
7044
7069
  if (fromLocale.formulaArgSeparator === toLocale.formulaArgSeparator &&
7045
7070
  fromLocale.decimalSeparator === toLocale.decimalSeparator) {
7046
7071
  return formula;
@@ -7195,37 +7220,6 @@ function getDateTimeFormat(locale) {
7195
7220
  return locale.dateFormat + " " + locale.timeFormat;
7196
7221
  }
7197
7222
 
7198
- /** Change a number string to its canonical form (en_US locale) */
7199
- function canonicalizeNumberValue(content, locale) {
7200
- return content.startsWith("=")
7201
- ? canonicalizeFormula(content, locale)
7202
- : canonicalizeNumberLiteral(content, locale);
7203
- }
7204
- /** Change a formula to its canonical form (en_US locale) */
7205
- function canonicalizeFormula(formula, locale) {
7206
- return _localizeFormula(formula, locale, DEFAULT_LOCALE);
7207
- }
7208
- function _localizeFormula(formula, fromLocale, toLocale) {
7209
- if (fromLocale.formulaArgSeparator === toLocale.formulaArgSeparator &&
7210
- fromLocale.decimalSeparator === toLocale.decimalSeparator) {
7211
- return formula;
7212
- }
7213
- const tokens = tokenize(formula, fromLocale);
7214
- let localizedFormula = "";
7215
- for (const token of tokens) {
7216
- if (token.type === "NUMBER") {
7217
- localizedFormula += token.value.replace(fromLocale.decimalSeparator, toLocale.decimalSeparator);
7218
- }
7219
- else if (token.type === "ARG_SEPARATOR") {
7220
- localizedFormula += toLocale.formulaArgSeparator;
7221
- }
7222
- else {
7223
- localizedFormula += token.value;
7224
- }
7225
- }
7226
- return localizedFormula;
7227
- }
7228
-
7229
7223
  function boolAnd(args) {
7230
7224
  let foundBoolean = false;
7231
7225
  let acc = true;
@@ -9600,7 +9594,161 @@ class ComposerFocusStore extends SpreadsheetStore {
9600
9594
  }
9601
9595
  }
9602
9596
 
9597
+ /**
9598
+ * This file is largely inspired by owl 1.
9599
+ * `css` tag has been removed from owl 2 without workaround to manage css.
9600
+ * So, the solution was to import the behavior of owl 1 directly in our
9601
+ * codebase, with one difference: the css is added to the sheet as soon as the
9602
+ * css tag is executed. In owl 1, the css was added as soon as a Component was
9603
+ * created for the first time.
9604
+ */
9605
+ const STYLESHEETS = {};
9606
+ let nextId = 0;
9607
+ /**
9608
+ * CSS tag helper for defining inline stylesheets. With this, one can simply define
9609
+ * an inline stylesheet with just the following code:
9610
+ * ```js
9611
+ * css`.component-a { color: red; }`;
9612
+ * ```
9613
+ */
9614
+ function css(strings, ...args) {
9615
+ const name = `__sheet__${nextId++}`;
9616
+ const value = String.raw(strings, ...args);
9617
+ registerSheet(name, value);
9618
+ activateSheet(name);
9619
+ return name;
9620
+ }
9621
+ function processSheet(str) {
9622
+ const tokens = str.split(/(\{|\}|;)/).map((s) => s.trim());
9623
+ const selectorStack = [];
9624
+ const parts = [];
9625
+ let rules = [];
9626
+ function generateSelector(stackIndex, parentSelector) {
9627
+ const parts = [];
9628
+ for (const selector of selectorStack[stackIndex]) {
9629
+ let part = (parentSelector && parentSelector + " " + selector) || selector;
9630
+ if (part.includes("&")) {
9631
+ part = selector.replace(/&/g, parentSelector || "");
9632
+ }
9633
+ if (stackIndex < selectorStack.length - 1) {
9634
+ part = generateSelector(stackIndex + 1, part);
9635
+ }
9636
+ parts.push(part);
9637
+ }
9638
+ return parts.join(", ");
9639
+ }
9640
+ function generateRules() {
9641
+ if (rules.length) {
9642
+ parts.push(generateSelector(0) + " {");
9643
+ parts.push(...rules);
9644
+ parts.push("}");
9645
+ rules = [];
9646
+ }
9647
+ }
9648
+ while (tokens.length) {
9649
+ let token = tokens.shift();
9650
+ if (token === "}") {
9651
+ generateRules();
9652
+ selectorStack.pop();
9653
+ }
9654
+ else {
9655
+ if (tokens[0] === "{") {
9656
+ generateRules();
9657
+ selectorStack.push(token.split(/\s*,\s*/));
9658
+ tokens.shift();
9659
+ }
9660
+ if (tokens[0] === ";") {
9661
+ rules.push(" " + token + ";");
9662
+ }
9663
+ }
9664
+ }
9665
+ return parts.join("\n");
9666
+ }
9667
+ function registerSheet(id, css) {
9668
+ const sheet = document.createElement("style");
9669
+ sheet.textContent = processSheet(css);
9670
+ STYLESHEETS[id] = sheet;
9671
+ }
9672
+ function activateSheet(id) {
9673
+ const sheet = STYLESHEETS[id];
9674
+ sheet.setAttribute("component", id);
9675
+ document.head.appendChild(sheet);
9676
+ }
9677
+ function getTextDecoration({ strikethrough, underline, }) {
9678
+ if (!strikethrough && !underline) {
9679
+ return "none";
9680
+ }
9681
+ return `${strikethrough ? "line-through" : ""} ${underline ? "underline" : ""}`;
9682
+ }
9683
+ /**
9684
+ * Convert the cell style to CSS properties.
9685
+ */
9686
+ function cellStyleToCss(style) {
9687
+ const attributes = cellTextStyleToCss(style);
9688
+ if (!style)
9689
+ return attributes;
9690
+ if (style.fillColor) {
9691
+ attributes["background"] = style.fillColor;
9692
+ }
9693
+ return attributes;
9694
+ }
9695
+ /**
9696
+ * Convert the cell text style to CSS properties.
9697
+ */
9698
+ function cellTextStyleToCss(style) {
9699
+ const attributes = {};
9700
+ if (!style)
9701
+ return attributes;
9702
+ if (style.bold) {
9703
+ attributes["font-weight"] = "bold";
9704
+ }
9705
+ if (style.italic) {
9706
+ attributes["font-style"] = "italic";
9707
+ }
9708
+ if (style.strikethrough || style.underline) {
9709
+ let decoration = style.strikethrough ? "line-through" : "";
9710
+ decoration = style.underline ? decoration + " underline" : decoration;
9711
+ attributes["text-decoration"] = decoration;
9712
+ }
9713
+ if (style.textColor) {
9714
+ attributes["color"] = style.textColor;
9715
+ }
9716
+ return attributes;
9717
+ }
9718
+ /**
9719
+ * Transform CSS properties into a CSS string.
9720
+ */
9721
+ function cssPropertiesToCss(attributes) {
9722
+ let styleStr = "";
9723
+ for (const attName in attributes) {
9724
+ if (!attributes[attName]) {
9725
+ continue;
9726
+ }
9727
+ styleStr += `${attName}:${attributes[attName]}; `;
9728
+ }
9729
+ return styleStr;
9730
+ }
9731
+ function getElementMargins(el) {
9732
+ const style = window.getComputedStyle(el);
9733
+ return {
9734
+ top: parseInt(style.marginTop, 10) || 0,
9735
+ bottom: parseInt(style.marginBottom, 10) || 0,
9736
+ left: parseInt(style.marginLeft, 10) || 0,
9737
+ right: parseInt(style.marginRight, 10) || 0,
9738
+ };
9739
+ }
9740
+
9741
+ const chartJsExtensionRegistry = new Registry();
9742
+ /** Return window.Chart, making sure all our extensions are loaded in ChartJS */
9743
+ function getChartJSConstructor() {
9744
+ if (window.Chart && !window.Chart?.registry.plugins.get("chartShowValuesPlugin")) {
9745
+ window.Chart.register(...chartJsExtensionRegistry.getAll());
9746
+ }
9747
+ return window.Chart;
9748
+ }
9749
+
9603
9750
  const TREND_LINE_XAXIS_ID = "x1";
9751
+ const MOVING_AVERAGE_TREND_LINE_XAXIS_ID = "xMovingAverage";
9604
9752
  /**
9605
9753
  * This file contains helpers that are common to different charts (mainly
9606
9754
  * line, bar and pie charts)
@@ -9951,6 +10099,9 @@ function truncateLabel(label) {
9951
10099
  }
9952
10100
  return label;
9953
10101
  }
10102
+ function isTrendLineAxis(axisID) {
10103
+ return axisID === TREND_LINE_XAXIS_ID || axisID === MOVING_AVERAGE_TREND_LINE_XAXIS_ID;
10104
+ }
9954
10105
 
9955
10106
  /** This is a chartJS plugin that will draw the values of each data next to the point/bar/pie slice */
9956
10107
  const chartShowValuesPlugin = {
@@ -9995,7 +10146,7 @@ function drawLineOrBarOrRadarChartValues(chart, options, ctx) {
9995
10146
  const yMin = chart.chartArea.top;
9996
10147
  const textsPositions = {};
9997
10148
  for (const dataset of chart._metasets) {
9998
- if (dataset.xAxisID === TREND_LINE_XAXIS_ID || dataset.hidden) {
10149
+ if (isTrendLineAxis(dataset.axisID) || dataset.hidden) {
9999
10150
  continue;
10000
10151
  }
10001
10152
  for (let i = 0; i < dataset._parsed.length; i++) {
@@ -10038,7 +10189,7 @@ function drawHorizontalBarChartValues(chart, options, ctx) {
10038
10189
  const xMin = chart.chartArea.left;
10039
10190
  const textsPositions = {};
10040
10191
  for (const dataset of chart._metasets) {
10041
- if (dataset.xAxisID === TREND_LINE_XAXIS_ID) {
10192
+ if (isTrendLineAxis(dataset.axisID)) {
10042
10193
  return; // ignore trend lines
10043
10194
  }
10044
10195
  for (let i = 0; i < dataset._parsed.length; i++) {
@@ -10144,341 +10295,79 @@ function getNextNonEmptyBar(bars, startIndex) {
10144
10295
  return bars.find((bar, i) => i > startIndex && bar.height !== 0);
10145
10296
  }
10146
10297
 
10147
- const GAUGE_PADDING_SIDE = 30;
10148
- const GAUGE_PADDING_TOP = 10;
10149
- const GAUGE_PADDING_BOTTOM = 20;
10150
- const GAUGE_LABELS_FONT_SIZE = 12;
10151
- const GAUGE_DEFAULT_VALUE_FONT_SIZE = 80;
10152
- const GAUGE_BACKGROUND_COLOR = "#F3F2F1";
10153
- const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
10154
- const GAUGE_TITLE_SECTION_HEIGHT = 25;
10155
- function drawGaugeChart(canvas, runtime) {
10156
- const canvasBoundingRect = canvas.getBoundingClientRect();
10157
- canvas.width = canvasBoundingRect.width;
10158
- canvas.height = canvasBoundingRect.height;
10159
- const ctx = canvas.getContext("2d");
10160
- const config = getGaugeRenderingConfig(canvasBoundingRect, runtime, ctx);
10161
- drawBackground(ctx, config);
10162
- drawGauge(ctx, config);
10163
- drawInflectionValues(ctx, config);
10164
- drawLabels(ctx, config);
10165
- drawTitle(ctx, config);
10166
- }
10167
- function drawGauge(ctx, config) {
10168
- ctx.save();
10169
- const gauge = config.gauge;
10170
- const arcCenterX = gauge.rect.x + gauge.rect.width / 2;
10171
- const arcCenterY = gauge.rect.y + gauge.rect.height;
10172
- const arcRadius = gauge.rect.height - gauge.arcWidth / 2;
10173
- if (arcRadius < 0) {
10174
- return;
10175
- }
10176
- const gaugeAngle = gauge.percentage === 1 ? 0 : Math.PI * (1 + gauge.percentage);
10177
- // Gauge background
10178
- ctx.strokeStyle = GAUGE_BACKGROUND_COLOR;
10179
- ctx.beginPath();
10180
- ctx.lineWidth = gauge.arcWidth;
10181
- ctx.arc(arcCenterX, arcCenterY, arcRadius, gaugeAngle, 0);
10182
- ctx.stroke();
10183
- // Gauge value
10184
- ctx.strokeStyle = gauge.color;
10185
- ctx.beginPath();
10186
- ctx.arc(arcCenterX, arcCenterY, arcRadius, Math.PI, gaugeAngle);
10187
- ctx.stroke();
10188
- ctx.restore();
10189
- }
10190
- function drawBackground(ctx, config) {
10191
- ctx.save();
10192
- ctx.fillStyle = config.backgroundColor;
10193
- ctx.fillRect(0, 0, config.width, config.height);
10194
- ctx.restore();
10195
- }
10196
- function drawLabels(ctx, config) {
10197
- for (const label of [config.minLabel, config.maxLabel, config.gaugeValue]) {
10198
- ctx.save();
10199
- ctx.textAlign = "center";
10200
- ctx.fillStyle = label.color;
10201
- ctx.font = `${label.fontSize}px ${DEFAULT_FONT}`;
10202
- ctx.fillText(label.label, label.textPosition.x, label.textPosition.y);
10203
- ctx.restore();
10204
- }
10205
- }
10206
- function drawInflectionValues(ctx, config) {
10207
- const { x: rectX, y: rectY, width, height } = config.gauge.rect;
10208
- for (const inflectionValue of config.inflectionValues) {
10209
- ctx.save();
10210
- ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment
10211
- ctx.rotate(Math.PI / 2 - inflectionValue.rotation);
10212
- ctx.lineWidth = 2;
10213
- ctx.strokeStyle = chartMutedFontColor(config.backgroundColor) + "aa";
10214
- ctx.beginPath();
10215
- ctx.moveTo(0, -(height - config.gauge.arcWidth));
10216
- ctx.lineTo(0, -height - 3);
10217
- ctx.stroke();
10218
- ctx.textAlign = "center";
10219
- ctx.font = `${inflectionValue.fontSize}px ${DEFAULT_FONT}`;
10220
- ctx.fillStyle = inflectionValue.color;
10221
- const textY = -height - GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN - inflectionValue.offset;
10222
- ctx.fillText(inflectionValue.label, 0, textY);
10223
- ctx.restore();
10224
- }
10225
- }
10226
- function drawTitle(ctx, config) {
10227
- ctx.save();
10228
- const title = config.title;
10229
- ctx.font = getDefaultContextFont(title.fontSize, title.bold, title.italic);
10230
- ctx.textBaseline = "middle";
10231
- ctx.fillStyle = title.color;
10232
- ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
10233
- ctx.restore();
10234
- }
10235
- function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
10236
- const maxValue = runtime.maxValue;
10237
- const minValue = runtime.minValue;
10238
- const gaugeValue = runtime.gaugeValue;
10239
- const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
10240
- const gaugeArcWidth = gaugeRect.width / 6;
10241
- const gaugePercentage = gaugeValue
10242
- ? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
10243
- : 0;
10244
- const gaugeValuePosition = {
10245
- x: boundingRect.width / 2,
10246
- y: gaugeRect.y + gaugeRect.height - gaugeRect.height / 12,
10247
- };
10248
- let gaugeValueFontSize = GAUGE_DEFAULT_VALUE_FONT_SIZE;
10249
- // Scale down the font size if the gaugeRect is too small
10250
- if (gaugeRect.height < 300) {
10251
- gaugeValueFontSize = gaugeValueFontSize * (gaugeRect.height / 300);
10252
- }
10253
- // Scale down the font size if the text is too long
10254
- const maxTextWidth = gaugeRect.width / 2;
10255
- const gaugeLabel = gaugeValue?.label || "-";
10256
- if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
10257
- gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
10298
+ css /* scss */ `
10299
+ .o-spreadsheet {
10300
+ .o-chart-custom-tooltip {
10301
+ font-size: 12px;
10302
+ background-color: #fff;
10303
+ z-index: ${ComponentsImportance.FigureTooltip};
10258
10304
  }
10259
- const minLabelPosition = {
10260
- x: gaugeRect.x + gaugeArcWidth / 2,
10261
- y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
10262
- };
10263
- const maxLabelPosition = {
10264
- x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
10265
- y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
10305
+ }
10306
+ `;
10307
+ chartJsExtensionRegistry.add("chartShowValuesPlugin", chartShowValuesPlugin);
10308
+ chartJsExtensionRegistry.add("waterfallLinesPlugin", waterfallLinesPlugin);
10309
+ class ChartJsComponent extends owl.Component {
10310
+ static template = "o-spreadsheet-ChartJsComponent";
10311
+ static props = {
10312
+ figure: Object,
10266
10313
  };
10267
- const textColor = chartMutedFontColor(runtime.background);
10268
- const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);
10269
- let x = 0, titleWidth = 0, titleHeight = 0;
10270
- if (runtime.title.text) {
10271
- ({ width: titleWidth, height: titleHeight } = computeTextDimension(ctx, runtime.title.text, { fontSize: CHART_TITLE_FONT_SIZE, ...runtime.title }, "px"));
10272
- }
10273
- switch (runtime.title.align) {
10274
- case "right":
10275
- x = boundingRect.width - titleWidth - CHART_PADDING$1;
10276
- break;
10277
- case "center":
10278
- x = (boundingRect.width - titleWidth) / 2;
10279
- break;
10280
- case "left":
10281
- default:
10282
- x = CHART_PADDING$1;
10283
- break;
10314
+ canvas = owl.useRef("graphContainer");
10315
+ chart;
10316
+ currentRuntime;
10317
+ get background() {
10318
+ return this.chartRuntime.background;
10284
10319
  }
10285
- return {
10286
- width: boundingRect.width,
10287
- height: boundingRect.height,
10288
- title: {
10289
- label: runtime.title.text ?? "",
10290
- fontSize: runtime.title.fontSize ?? CHART_TITLE_FONT_SIZE,
10291
- textPosition: {
10292
- x,
10293
- y: CHART_PADDING_TOP + titleHeight / 2,
10294
- },
10295
- color: runtime.title.color ?? textColor,
10296
- bold: runtime.title.bold,
10297
- italic: runtime.title.italic,
10298
- },
10299
- backgroundColor: runtime.background,
10300
- gauge: {
10301
- rect: gaugeRect,
10302
- arcWidth: gaugeArcWidth,
10303
- percentage: clip(gaugePercentage, 0, 1),
10304
- color: getGaugeColor(runtime),
10305
- },
10306
- inflectionValues,
10307
- gaugeValue: {
10308
- label: gaugeLabel,
10309
- textPosition: gaugeValuePosition,
10310
- fontSize: gaugeValueFontSize,
10311
- color: textColor,
10312
- },
10313
- minLabel: {
10314
- label: runtime.minValue.label,
10315
- textPosition: minLabelPosition,
10316
- fontSize: GAUGE_LABELS_FONT_SIZE,
10317
- color: textColor,
10318
- },
10319
- maxLabel: {
10320
- label: runtime.maxValue.label,
10321
- textPosition: maxLabelPosition,
10322
- fontSize: GAUGE_LABELS_FONT_SIZE,
10323
- color: textColor,
10324
- },
10325
- };
10326
- }
10327
- /**
10328
- * Get the rectangle in which the gauge will be drawn, based on the bounding rectangle of the canvas and leaving
10329
- * space for the title and labels.
10330
- */
10331
- function getGaugeRect(boundingRect, title) {
10332
- const titleHeight = title ? GAUGE_TITLE_SECTION_HEIGHT : 0;
10333
- const drawHeight = boundingRect.height - GAUGE_PADDING_BOTTOM - titleHeight - GAUGE_PADDING_TOP;
10334
- const drawWidth = boundingRect.width - GAUGE_PADDING_SIDE * 2;
10335
- let gaugeWidth;
10336
- let gaugeHeight;
10337
- if (drawWidth > 2 * drawHeight) {
10338
- gaugeWidth = 2 * drawHeight;
10339
- gaugeHeight = drawHeight;
10320
+ get canvasStyle() {
10321
+ return `background-color: ${this.background}`;
10340
10322
  }
10341
- else {
10342
- gaugeWidth = drawWidth;
10343
- gaugeHeight = drawWidth / 2;
10323
+ get chartRuntime() {
10324
+ const runtime = this.env.model.getters.getChartRuntime(this.props.figure.id);
10325
+ if (!("chartJsConfig" in runtime)) {
10326
+ throw new Error("Unsupported chart runtime");
10327
+ }
10328
+ return runtime;
10344
10329
  }
10345
- const gaugeX = GAUGE_PADDING_SIDE + (drawWidth - gaugeWidth) / 2;
10346
- const gaugeY = titleHeight + GAUGE_PADDING_TOP + (drawHeight - gaugeHeight) / 2;
10347
- return {
10348
- x: gaugeX,
10349
- y: gaugeY,
10350
- width: gaugeWidth,
10351
- height: gaugeHeight,
10352
- };
10353
- }
10354
- /**
10355
- * 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).
10356
- *
10357
- * Also compute an offset for the text so that it doesn't overlap with other text.
10358
- */
10359
- function getInflectionValues(runtime, gaugeRect, textColor, ctx) {
10360
- const maxValue = runtime.maxValue;
10361
- const minValue = runtime.minValue;
10362
- const gaugeCircleCenter = {
10363
- x: gaugeRect.x + gaugeRect.width / 2,
10364
- y: gaugeRect.y + gaugeRect.height,
10365
- };
10366
- const textStyle = { fontSize: GAUGE_LABELS_FONT_SIZE };
10367
- const inflectionValues = [];
10368
- const inflectionValuesTextRects = [];
10369
- for (const inflectionValue of runtime.inflectionValues) {
10370
- const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
10371
- const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
10372
- const angle = Math.PI - Math.PI * percentage;
10373
- const textRect = getRectangleTangentToCircle(angle, // angle between X axis and the point where the rectangle is tangent to the circle
10374
- gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, // radius of the gauge circle + margin below text
10375
- gaugeCircleCenter.x, // center of the gauge circle
10376
- gaugeCircleCenter.y, // center of the gauge circle
10377
- labelWidth + 2, // width of the text + some margin
10378
- GAUGE_LABELS_FONT_SIZE // height of the text
10379
- );
10380
- let offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect))
10381
- ? GAUGE_LABELS_FONT_SIZE
10382
- : 0;
10383
- inflectionValuesTextRects.push(textRect);
10384
- inflectionValues.push({
10385
- rotation: angle,
10386
- label: inflectionValue.label,
10387
- fontSize: GAUGE_LABELS_FONT_SIZE,
10388
- color: textColor,
10389
- offset,
10330
+ setup() {
10331
+ owl.onMounted(() => {
10332
+ const runtime = this.chartRuntime;
10333
+ this.currentRuntime = runtime;
10334
+ // Note: chartJS modify the runtime in place, so it's important to give it a copy
10335
+ this.createChart(deepCopy(runtime.chartJsConfig));
10336
+ });
10337
+ owl.onWillUnmount(() => this.chart?.destroy());
10338
+ owl.useEffect(() => {
10339
+ const runtime = this.chartRuntime;
10340
+ if (runtime !== this.currentRuntime) {
10341
+ if (runtime.chartJsConfig.type !== this.currentRuntime.chartJsConfig.type) {
10342
+ this.chart?.destroy();
10343
+ this.createChart(deepCopy(runtime.chartJsConfig));
10344
+ }
10345
+ else {
10346
+ this.updateChartJs(deepCopy(runtime.chartJsConfig));
10347
+ }
10348
+ this.currentRuntime = runtime;
10349
+ }
10390
10350
  });
10391
10351
  }
10392
- return inflectionValues;
10393
- }
10394
- function getGaugeColor(runtime) {
10395
- const gaugeValue = runtime.gaugeValue?.value;
10396
- if (gaugeValue === undefined) {
10397
- return GAUGE_BACKGROUND_COLOR;
10398
- }
10399
- for (let i = 0; i < runtime.inflectionValues.length; i++) {
10400
- const inflectionValue = runtime.inflectionValues[i];
10401
- if (inflectionValue.operator === "<" && gaugeValue < inflectionValue.value) {
10402
- return runtime.colors[i];
10403
- }
10404
- else if (inflectionValue.operator === "<=" && gaugeValue <= inflectionValue.value) {
10405
- return runtime.colors[i];
10406
- }
10407
- }
10408
- return runtime.colors.at(-1);
10409
- }
10410
- function getSegmentsOfRectangle(rectangle) {
10411
- return [
10412
- { start: rectangle.topLeft, end: rectangle.topRight },
10413
- { start: rectangle.topRight, end: rectangle.bottomRight },
10414
- { start: rectangle.bottomRight, end: rectangle.bottomLeft },
10415
- { start: rectangle.bottomLeft, end: rectangle.topLeft },
10416
- ];
10417
- }
10418
- /**
10419
- * Check if two segment intersect. The case where the segments are colinear (both segments on the same line)
10420
- * is not handled.
10421
- */
10422
- function doSegmentIntersect(segment1, segment2) {
10423
- const A = segment1.start;
10424
- const B = segment1.end;
10425
- const C = segment2.start;
10426
- const D = segment2.end;
10427
- /**
10428
- * Line segment intersection algorithm
10429
- * https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
10430
- */
10431
- function ccw(a, b, c) {
10432
- return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
10352
+ createChart(chartData) {
10353
+ const canvas = this.canvas.el;
10354
+ const ctx = canvas.getContext("2d");
10355
+ const Chart = getChartJSConstructor();
10356
+ this.chart = new Chart(ctx, chartData);
10433
10357
  }
10434
- return ccw(A, C, D) !== ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D);
10435
- }
10436
- function doRectanglesIntersect(rect1, rect2) {
10437
- const segments1 = getSegmentsOfRectangle(rect1);
10438
- const segments2 = getSegmentsOfRectangle(rect2);
10439
- for (const segment1 of segments1) {
10440
- for (const segment2 of segments2) {
10441
- if (doSegmentIntersect(segment1, segment2)) {
10442
- return true;
10358
+ updateChartJs(chartData) {
10359
+ if (chartData.data && chartData.data.datasets) {
10360
+ this.chart.data = chartData.data;
10361
+ if (chartData.options?.plugins?.title) {
10362
+ this.chart.config.options.plugins.title = chartData.options.plugins.title;
10443
10363
  }
10444
10364
  }
10365
+ else {
10366
+ this.chart.data.datasets = [];
10367
+ }
10368
+ this.chart.config.options = chartData.options;
10369
+ this.chart.update();
10445
10370
  }
10446
- return false;
10447
- }
10448
- /**
10449
- * Get the rectangle that is tangent to a circle at a given angle.
10450
- *
10451
- * @param angle angle between X axis and the point where the rectangle is tangent to the circle
10452
- */
10453
- function getRectangleTangentToCircle(angle, radius, circleCenterX, circleCenterY, rectWidth, rectHeight) {
10454
- const cos = Math.cos(angle);
10455
- const sin = Math.sin(angle);
10456
- // x, y are the distance from the center of the circle to the point where the rectangle is tangent to the circle
10457
- const x = cos * radius;
10458
- const y = sin * radius;
10459
- // x2, y2 are the distance from the point the rectangle is tangent to the circle to the bottom left corner of the rectangle
10460
- const x2 = sin * (rectWidth / 2); // cos(angle + 90°) = sin(angle)
10461
- const y2 = cos * (rectWidth / 2);
10462
- const bottomRight = {
10463
- x: x + x2 + circleCenterX,
10464
- y: circleCenterY - (y - y2),
10465
- };
10466
- const bottomLeft = {
10467
- x: x - x2 + circleCenterX,
10468
- y: circleCenterY - (y + y2),
10469
- };
10470
- // Same as above but for the top corners of the rectangle (radius + rectangle height instead of radius)
10471
- const xp = cos * (radius + rectHeight);
10472
- const yp = sin * (radius + rectHeight);
10473
- const topLeft = {
10474
- x: xp - x2 + circleCenterX,
10475
- y: circleCenterY - (yp + y2),
10476
- };
10477
- const topRight = {
10478
- x: xp + x2 + circleCenterX,
10479
- y: circleCenterY - (yp - y2),
10480
- };
10481
- return { bottomLeft, bottomRight, topRight, topLeft };
10482
10371
  }
10483
10372
 
10484
10373
  /**
@@ -11060,299 +10949,6 @@ class ScorecardChartConfigBuilder {
11060
10949
  }
11061
10950
  }
11062
10951
 
11063
- const CHART_COMMON_OPTIONS = {
11064
- // https://www.chartjs.org/docs/latest/general/responsive.html
11065
- responsive: true, // will resize when its container is resized
11066
- maintainAspectRatio: false, // doesn't maintain the aspect ratio (width/height =2 by default) so the user has the choice of the exact layout
11067
- elements: {
11068
- line: {
11069
- fill: false, // do not fill the area under line charts
11070
- },
11071
- point: {
11072
- hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
11073
- },
11074
- },
11075
- animation: false,
11076
- };
11077
- function chartToImage(runtime, figure, type) {
11078
- // wrap the canvas in a div with a fixed size because chart.js would
11079
- // fill the whole page otherwise
11080
- const div = document.createElement("div");
11081
- div.style.width = `${figure.width}px`;
11082
- div.style.height = `${figure.height}px`;
11083
- const canvas = document.createElement("canvas");
11084
- div.append(canvas);
11085
- canvas.setAttribute("width", figure.width.toString());
11086
- canvas.setAttribute("height", figure.height.toString());
11087
- // we have to add the canvas to the DOM otherwise it won't be rendered
11088
- document.body.append(div);
11089
- if ("chartJsConfig" in runtime) {
11090
- const config = deepCopy(runtime.chartJsConfig);
11091
- config.plugins = [backgroundColorChartJSPlugin];
11092
- const Chart = getChartJSConstructor();
11093
- const chart = new Chart(canvas, config);
11094
- const imgContent = chart.toBase64Image();
11095
- chart.destroy();
11096
- div.remove();
11097
- return imgContent;
11098
- }
11099
- else if (type === "scorecard") {
11100
- const design = getScorecardConfiguration(figure, runtime);
11101
- drawScoreChart(design, canvas);
11102
- const imgContent = canvas.toDataURL();
11103
- div.remove();
11104
- return imgContent;
11105
- }
11106
- else if (type === "gauge") {
11107
- drawGaugeChart(canvas, runtime);
11108
- const imgContent = canvas.toDataURL();
11109
- div.remove();
11110
- return imgContent;
11111
- }
11112
- return undefined;
11113
- }
11114
- /**
11115
- * Custom chart.js plugin to set the background color of the canvas
11116
- * https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md
11117
- */
11118
- const backgroundColorChartJSPlugin = {
11119
- id: "customCanvasBackgroundColor",
11120
- beforeDraw: (chart) => {
11121
- const { ctx } = chart;
11122
- ctx.save();
11123
- ctx.globalCompositeOperation = "destination-over";
11124
- ctx.fillStyle = "#ffffff";
11125
- ctx.fillRect(0, 0, chart.width, chart.height);
11126
- ctx.restore();
11127
- },
11128
- };
11129
- /** Return window.Chart, making sure all our extensions are loaded in ChartJS */
11130
- function getChartJSConstructor() {
11131
- if (window.Chart && !window.Chart?.registry.plugins.get("chartShowValuesPlugin")) {
11132
- window.Chart.register(chartShowValuesPlugin);
11133
- window.Chart.register(waterfallLinesPlugin);
11134
- }
11135
- return window.Chart;
11136
- }
11137
-
11138
- /**
11139
- * This file is largely inspired by owl 1.
11140
- * `css` tag has been removed from owl 2 without workaround to manage css.
11141
- * So, the solution was to import the behavior of owl 1 directly in our
11142
- * codebase, with one difference: the css is added to the sheet as soon as the
11143
- * css tag is executed. In owl 1, the css was added as soon as a Component was
11144
- * created for the first time.
11145
- */
11146
- const STYLESHEETS = {};
11147
- let nextId = 0;
11148
- /**
11149
- * CSS tag helper for defining inline stylesheets. With this, one can simply define
11150
- * an inline stylesheet with just the following code:
11151
- * ```js
11152
- * css`.component-a { color: red; }`;
11153
- * ```
11154
- */
11155
- function css(strings, ...args) {
11156
- const name = `__sheet__${nextId++}`;
11157
- const value = String.raw(strings, ...args);
11158
- registerSheet(name, value);
11159
- activateSheet(name);
11160
- return name;
11161
- }
11162
- function processSheet(str) {
11163
- const tokens = str.split(/(\{|\}|;)/).map((s) => s.trim());
11164
- const selectorStack = [];
11165
- const parts = [];
11166
- let rules = [];
11167
- function generateSelector(stackIndex, parentSelector) {
11168
- const parts = [];
11169
- for (const selector of selectorStack[stackIndex]) {
11170
- let part = (parentSelector && parentSelector + " " + selector) || selector;
11171
- if (part.includes("&")) {
11172
- part = selector.replace(/&/g, parentSelector || "");
11173
- }
11174
- if (stackIndex < selectorStack.length - 1) {
11175
- part = generateSelector(stackIndex + 1, part);
11176
- }
11177
- parts.push(part);
11178
- }
11179
- return parts.join(", ");
11180
- }
11181
- function generateRules() {
11182
- if (rules.length) {
11183
- parts.push(generateSelector(0) + " {");
11184
- parts.push(...rules);
11185
- parts.push("}");
11186
- rules = [];
11187
- }
11188
- }
11189
- while (tokens.length) {
11190
- let token = tokens.shift();
11191
- if (token === "}") {
11192
- generateRules();
11193
- selectorStack.pop();
11194
- }
11195
- else {
11196
- if (tokens[0] === "{") {
11197
- generateRules();
11198
- selectorStack.push(token.split(/\s*,\s*/));
11199
- tokens.shift();
11200
- }
11201
- if (tokens[0] === ";") {
11202
- rules.push(" " + token + ";");
11203
- }
11204
- }
11205
- }
11206
- return parts.join("\n");
11207
- }
11208
- function registerSheet(id, css) {
11209
- const sheet = document.createElement("style");
11210
- sheet.textContent = processSheet(css);
11211
- STYLESHEETS[id] = sheet;
11212
- }
11213
- function activateSheet(id) {
11214
- const sheet = STYLESHEETS[id];
11215
- sheet.setAttribute("component", id);
11216
- document.head.appendChild(sheet);
11217
- }
11218
- function getTextDecoration({ strikethrough, underline, }) {
11219
- if (!strikethrough && !underline) {
11220
- return "none";
11221
- }
11222
- return `${strikethrough ? "line-through" : ""} ${underline ? "underline" : ""}`;
11223
- }
11224
- /**
11225
- * Convert the cell style to CSS properties.
11226
- */
11227
- function cellStyleToCss(style) {
11228
- const attributes = cellTextStyleToCss(style);
11229
- if (!style)
11230
- return attributes;
11231
- if (style.fillColor) {
11232
- attributes["background"] = style.fillColor;
11233
- }
11234
- return attributes;
11235
- }
11236
- /**
11237
- * Convert the cell text style to CSS properties.
11238
- */
11239
- function cellTextStyleToCss(style) {
11240
- const attributes = {};
11241
- if (!style)
11242
- return attributes;
11243
- if (style.bold) {
11244
- attributes["font-weight"] = "bold";
11245
- }
11246
- if (style.italic) {
11247
- attributes["font-style"] = "italic";
11248
- }
11249
- if (style.strikethrough || style.underline) {
11250
- let decoration = style.strikethrough ? "line-through" : "";
11251
- decoration = style.underline ? decoration + " underline" : decoration;
11252
- attributes["text-decoration"] = decoration;
11253
- }
11254
- if (style.textColor) {
11255
- attributes["color"] = style.textColor;
11256
- }
11257
- return attributes;
11258
- }
11259
- /**
11260
- * Transform CSS properties into a CSS string.
11261
- */
11262
- function cssPropertiesToCss(attributes) {
11263
- let styleStr = "";
11264
- for (const attName in attributes) {
11265
- if (!attributes[attName]) {
11266
- continue;
11267
- }
11268
- styleStr += `${attName}:${attributes[attName]}; `;
11269
- }
11270
- return styleStr;
11271
- }
11272
- function getElementMargins(el) {
11273
- const style = window.getComputedStyle(el);
11274
- return {
11275
- top: parseInt(style.marginTop, 10) || 0,
11276
- bottom: parseInt(style.marginBottom, 10) || 0,
11277
- left: parseInt(style.marginLeft, 10) || 0,
11278
- right: parseInt(style.marginRight, 10) || 0,
11279
- };
11280
- }
11281
-
11282
- css /* scss */ `
11283
- .o-spreadsheet {
11284
- .o-chart-custom-tooltip {
11285
- font-size: 12px;
11286
- background-color: #fff;
11287
- z-index: ${ComponentsImportance.FigureTooltip};
11288
- }
11289
- }
11290
- `;
11291
- class ChartJsComponent extends owl.Component {
11292
- static template = "o-spreadsheet-ChartJsComponent";
11293
- static props = {
11294
- figure: Object,
11295
- };
11296
- canvas = owl.useRef("graphContainer");
11297
- chart;
11298
- currentRuntime;
11299
- get background() {
11300
- return this.chartRuntime.background;
11301
- }
11302
- get canvasStyle() {
11303
- return `background-color: ${this.background}`;
11304
- }
11305
- get chartRuntime() {
11306
- const runtime = this.env.model.getters.getChartRuntime(this.props.figure.id);
11307
- if (!("chartJsConfig" in runtime)) {
11308
- throw new Error("Unsupported chart runtime");
11309
- }
11310
- return runtime;
11311
- }
11312
- setup() {
11313
- owl.onMounted(() => {
11314
- const runtime = this.chartRuntime;
11315
- this.currentRuntime = runtime;
11316
- // Note: chartJS modify the runtime in place, so it's important to give it a copy
11317
- this.createChart(deepCopy(runtime.chartJsConfig));
11318
- });
11319
- owl.onWillUnmount(() => this.chart?.destroy());
11320
- owl.useEffect(() => {
11321
- const runtime = this.chartRuntime;
11322
- if (runtime !== this.currentRuntime) {
11323
- if (runtime.chartJsConfig.type !== this.currentRuntime.chartJsConfig.type) {
11324
- this.chart?.destroy();
11325
- this.createChart(deepCopy(runtime.chartJsConfig));
11326
- }
11327
- else {
11328
- this.updateChartJs(deepCopy(runtime));
11329
- }
11330
- this.currentRuntime = runtime;
11331
- }
11332
- });
11333
- }
11334
- createChart(chartData) {
11335
- const canvas = this.canvas.el;
11336
- const ctx = canvas.getContext("2d");
11337
- const Chart = getChartJSConstructor();
11338
- this.chart = new Chart(ctx, chartData);
11339
- }
11340
- updateChartJs(chartRuntime) {
11341
- const chartData = chartRuntime.chartJsConfig;
11342
- if (chartData.data && chartData.data.datasets) {
11343
- this.chart.data = chartData.data;
11344
- if (chartData.options?.plugins?.title) {
11345
- this.chart.config.options.plugins.title = chartData.options.plugins.title;
11346
- }
11347
- }
11348
- else {
11349
- this.chart.data.datasets = [];
11350
- }
11351
- this.chart.config.options = chartData.options;
11352
- this.chart.update();
11353
- }
11354
- }
11355
-
11356
10952
  class ScorecardChart extends owl.Component {
11357
10953
  static template = "o-spreadsheet-ScorecardChart";
11358
10954
  static props = {
@@ -20546,11 +20142,26 @@ const SEARCH = {
20546
20142
  const _searchFor = toString(searchFor).toLowerCase();
20547
20143
  const _textToSearch = toString(textToSearch).toLowerCase();
20548
20144
  const _startingAt = toNumber(startingAt, this.locale);
20549
- assert(() => _textToSearch !== "", _t("The text_to_search must be non-empty."));
20550
- assert(() => _startingAt >= 1, _t("The starting_at (%s) must be greater than or equal to 1.", _startingAt.toString()));
20145
+ if (_textToSearch === "") {
20146
+ return {
20147
+ value: CellErrorType.GenericError,
20148
+ message: _t("The text_to_search must be non-empty."),
20149
+ };
20150
+ }
20151
+ if (_startingAt < 1) {
20152
+ return {
20153
+ value: CellErrorType.GenericError,
20154
+ message: _t("The starting_at (%s) must be greater than or equal to 1.", _startingAt),
20155
+ };
20156
+ }
20551
20157
  const result = _textToSearch.indexOf(_searchFor, _startingAt - 1);
20552
- assert(() => result >= 0, _t("In [[FUNCTION_NAME]] evaluation, cannot find '%s' within '%s'.", _searchFor, _textToSearch));
20553
- return result + 1;
20158
+ if (result === -1) {
20159
+ return {
20160
+ value: CellErrorType.GenericError,
20161
+ message: _t("In [[FUNCTION_NAME]] evaluation, cannot find '%s' within '%s'.", _searchFor, _textToSearch),
20162
+ };
20163
+ }
20164
+ return { value: result + 1 };
20554
20165
  },
20555
20166
  isExported: true,
20556
20167
  };
@@ -21885,11 +21496,14 @@ function compileTokens(tokens) {
21885
21496
  }
21886
21497
  }
21887
21498
  function compileTokensOrThrow(tokens) {
21888
- const { dependencies, constantValues, symbols } = formulaArguments(tokens);
21889
- const cacheKey = compilationCacheKey(tokens, dependencies, constantValues);
21499
+ const { dependencies, literalValues, symbols } = formulaArguments(tokens);
21500
+ const cacheKey = compilationCacheKey(tokens);
21890
21501
  if (!functionCache[cacheKey]) {
21891
21502
  const ast = parseTokens([...tokens]);
21892
21503
  const scope = new Scope();
21504
+ let stringCount = 0;
21505
+ let numberCount = 0;
21506
+ let dependencyCount = 0;
21893
21507
  if (ast.type === "BIN_OPERATION" && ast.value === ":") {
21894
21508
  throw new BadExpressionError(_t("Invalid formula"));
21895
21509
  }
@@ -21963,16 +21577,15 @@ function compileTokensOrThrow(tokens) {
21963
21577
  case "BOOLEAN":
21964
21578
  return code.return(`{ value: ${ast.value} }`);
21965
21579
  case "NUMBER":
21966
- return code.return(`{ value: this.constantValues.numbers[${constantValues.numbers.indexOf(ast.value)}] }`);
21580
+ return code.return(`this.literalValues.numbers[${numberCount++}]`);
21967
21581
  case "STRING":
21968
- return code.return(`{ value: this.constantValues.strings[${constantValues.strings.indexOf(ast.value)}] }`);
21582
+ return code.return(`this.literalValues.strings[${stringCount++}]`);
21969
21583
  case "REFERENCE":
21970
- const referenceIndex = dependencies.indexOf(ast.value);
21971
21584
  if ((!isMeta && ast.value.includes(":")) || hasRange) {
21972
- return code.return(`range(deps[${referenceIndex}])`);
21585
+ return code.return(`range(deps[${dependencyCount++}])`);
21973
21586
  }
21974
21587
  else {
21975
- return code.return(`ref(deps[${referenceIndex}], ${isMeta ? "true" : "false"})`);
21588
+ return code.return(`ref(deps[${dependencyCount++}], ${isMeta ? "true" : "false"})`);
21976
21589
  }
21977
21590
  case "FUNCALL":
21978
21591
  const args = compileFunctionArgs(ast).map((arg) => arg.assignResultToVariable());
@@ -22004,7 +21617,7 @@ function compileTokensOrThrow(tokens) {
22004
21617
  const compiledFormula = {
22005
21618
  execute: functionCache[cacheKey],
22006
21619
  dependencies,
22007
- constantValues,
21620
+ literalValues,
22008
21621
  symbols,
22009
21622
  tokens,
22010
21623
  isBadExpression: false,
@@ -22017,33 +21630,31 @@ function compileTokensOrThrow(tokens) {
22017
21630
  * References, numbers and strings are replaced with placeholders because
22018
21631
  * the compiled formula does not depend on their actual value.
22019
21632
  * Both `=A1+1+"2"` and `=A2+2+"3"` are compiled to the exact same function.
22020
- *
22021
21633
  * Spaces are also ignored to compute the cache key.
22022
21634
  *
22023
- * A formula `=A1+A2+SUM(2, 2, "2")` have the cache key `=|0|+|1|+SUM(|N0|,|N0|,|S0|)`
21635
+ * A formula `=A1+A2+SUM(2, 2, "2")` have the cache key `=|C|+|C|+SUM(|N|,|N|,|S|)`
22024
21636
  */
22025
- function compilationCacheKey(tokens, dependencies, constantValues, symbols) {
21637
+ function compilationCacheKey(tokens) {
22026
21638
  let cacheKey = "";
22027
21639
  for (const token of tokens) {
22028
21640
  switch (token.type) {
22029
21641
  case "STRING":
22030
- const value = removeStringQuotes(token.value);
22031
- cacheKey += `|S${constantValues.strings.indexOf(value)}|`;
21642
+ cacheKey += "|S|";
22032
21643
  break;
22033
21644
  case "NUMBER":
22034
- cacheKey += `|N${constantValues.numbers.indexOf(parseNumber(token.value, DEFAULT_LOCALE))}|`;
21645
+ cacheKey += "|N|";
22035
21646
  break;
22036
21647
  case "REFERENCE":
22037
21648
  case "INVALID_REFERENCE":
22038
21649
  if (token.value.includes(":")) {
22039
- cacheKey += `R|${dependencies.indexOf(token.value)}|`;
21650
+ cacheKey += "|R|";
22040
21651
  }
22041
21652
  else {
22042
- cacheKey += `C|${dependencies.indexOf(token.value)}|`;
21653
+ cacheKey += "|C|";
22043
21654
  }
22044
21655
  break;
22045
21656
  case "SPACE":
22046
- cacheKey += "";
21657
+ // ignore spaces
22047
21658
  break;
22048
21659
  default:
22049
21660
  cacheKey += token.value;
@@ -22056,7 +21667,7 @@ function compilationCacheKey(tokens, dependencies, constantValues, symbols) {
22056
21667
  * Return formula arguments which are references, strings and numbers.
22057
21668
  */
22058
21669
  function formulaArguments(tokens) {
22059
- const constantValues = {
21670
+ const literalValues = {
22060
21671
  numbers: [],
22061
21672
  strings: [],
22062
21673
  };
@@ -22070,15 +21681,11 @@ function formulaArguments(tokens) {
22070
21681
  break;
22071
21682
  case "STRING":
22072
21683
  const value = removeStringQuotes(token.value);
22073
- if (!constantValues.strings.includes(value)) {
22074
- constantValues.strings.push(value);
22075
- }
21684
+ literalValues.strings.push({ value });
22076
21685
  break;
22077
21686
  case "NUMBER": {
22078
21687
  const value = parseNumber(token.value, DEFAULT_LOCALE);
22079
- if (!constantValues.numbers.includes(value)) {
22080
- constantValues.numbers.push(value);
22081
- }
21688
+ literalValues.numbers.push({ value });
22082
21689
  break;
22083
21690
  }
22084
21691
  case "SYMBOL": {
@@ -22089,7 +21696,7 @@ function formulaArguments(tokens) {
22089
21696
  }
22090
21697
  return {
22091
21698
  dependencies,
22092
- constantValues,
21699
+ literalValues,
22093
21700
  symbols,
22094
21701
  };
22095
21702
  }
@@ -22939,6 +22546,343 @@ function getDateIntervals(dates) {
22939
22546
 
22940
22547
  const cellPopoverRegistry = new Registry();
22941
22548
 
22549
+ const GAUGE_PADDING_SIDE = 30;
22550
+ const GAUGE_PADDING_TOP = 10;
22551
+ const GAUGE_PADDING_BOTTOM = 20;
22552
+ const GAUGE_LABELS_FONT_SIZE = 12;
22553
+ const GAUGE_DEFAULT_VALUE_FONT_SIZE = 80;
22554
+ const GAUGE_BACKGROUND_COLOR = "#F3F2F1";
22555
+ const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
22556
+ const GAUGE_TITLE_SECTION_HEIGHT = 25;
22557
+ function drawGaugeChart(canvas, runtime) {
22558
+ const canvasBoundingRect = canvas.getBoundingClientRect();
22559
+ canvas.width = canvasBoundingRect.width;
22560
+ canvas.height = canvasBoundingRect.height;
22561
+ const ctx = canvas.getContext("2d");
22562
+ const config = getGaugeRenderingConfig(canvasBoundingRect, runtime, ctx);
22563
+ drawBackground(ctx, config);
22564
+ drawGauge(ctx, config);
22565
+ drawInflectionValues(ctx, config);
22566
+ drawLabels(ctx, config);
22567
+ drawTitle(ctx, config);
22568
+ }
22569
+ function drawGauge(ctx, config) {
22570
+ ctx.save();
22571
+ const gauge = config.gauge;
22572
+ const arcCenterX = gauge.rect.x + gauge.rect.width / 2;
22573
+ const arcCenterY = gauge.rect.y + gauge.rect.height;
22574
+ const arcRadius = gauge.rect.height - gauge.arcWidth / 2;
22575
+ if (arcRadius < 0) {
22576
+ return;
22577
+ }
22578
+ const gaugeAngle = gauge.percentage === 1 ? 0 : Math.PI * (1 + gauge.percentage);
22579
+ // Gauge background
22580
+ ctx.strokeStyle = GAUGE_BACKGROUND_COLOR;
22581
+ ctx.beginPath();
22582
+ ctx.lineWidth = gauge.arcWidth;
22583
+ ctx.arc(arcCenterX, arcCenterY, arcRadius, gaugeAngle, 0);
22584
+ ctx.stroke();
22585
+ // Gauge value
22586
+ ctx.strokeStyle = gauge.color;
22587
+ ctx.beginPath();
22588
+ ctx.arc(arcCenterX, arcCenterY, arcRadius, Math.PI, gaugeAngle);
22589
+ ctx.stroke();
22590
+ ctx.restore();
22591
+ }
22592
+ function drawBackground(ctx, config) {
22593
+ ctx.save();
22594
+ ctx.fillStyle = config.backgroundColor;
22595
+ ctx.fillRect(0, 0, config.width, config.height);
22596
+ ctx.restore();
22597
+ }
22598
+ function drawLabels(ctx, config) {
22599
+ for (const label of [config.minLabel, config.maxLabel, config.gaugeValue]) {
22600
+ ctx.save();
22601
+ ctx.textAlign = "center";
22602
+ ctx.fillStyle = label.color;
22603
+ ctx.font = `${label.fontSize}px ${DEFAULT_FONT}`;
22604
+ ctx.fillText(label.label, label.textPosition.x, label.textPosition.y);
22605
+ ctx.restore();
22606
+ }
22607
+ }
22608
+ function drawInflectionValues(ctx, config) {
22609
+ const { x: rectX, y: rectY, width, height } = config.gauge.rect;
22610
+ for (const inflectionValue of config.inflectionValues) {
22611
+ ctx.save();
22612
+ ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment
22613
+ ctx.rotate(Math.PI / 2 - inflectionValue.rotation);
22614
+ ctx.lineWidth = 2;
22615
+ ctx.strokeStyle = chartMutedFontColor(config.backgroundColor) + "aa";
22616
+ ctx.beginPath();
22617
+ ctx.moveTo(0, -(height - config.gauge.arcWidth));
22618
+ ctx.lineTo(0, -height - 3);
22619
+ ctx.stroke();
22620
+ ctx.textAlign = "center";
22621
+ ctx.font = `${inflectionValue.fontSize}px ${DEFAULT_FONT}`;
22622
+ ctx.fillStyle = inflectionValue.color;
22623
+ const textY = -height - GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN - inflectionValue.offset;
22624
+ ctx.fillText(inflectionValue.label, 0, textY);
22625
+ ctx.restore();
22626
+ }
22627
+ }
22628
+ function drawTitle(ctx, config) {
22629
+ ctx.save();
22630
+ const title = config.title;
22631
+ ctx.font = getDefaultContextFont(title.fontSize, title.bold, title.italic);
22632
+ ctx.textBaseline = "middle";
22633
+ ctx.fillStyle = title.color;
22634
+ ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
22635
+ ctx.restore();
22636
+ }
22637
+ function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
22638
+ const maxValue = runtime.maxValue;
22639
+ const minValue = runtime.minValue;
22640
+ const gaugeValue = runtime.gaugeValue;
22641
+ const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
22642
+ const gaugeArcWidth = gaugeRect.width / 6;
22643
+ const gaugePercentage = gaugeValue
22644
+ ? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
22645
+ : 0;
22646
+ const gaugeValuePosition = {
22647
+ x: boundingRect.width / 2,
22648
+ y: gaugeRect.y + gaugeRect.height - gaugeRect.height / 12,
22649
+ };
22650
+ let gaugeValueFontSize = GAUGE_DEFAULT_VALUE_FONT_SIZE;
22651
+ // Scale down the font size if the gaugeRect is too small
22652
+ if (gaugeRect.height < 300) {
22653
+ gaugeValueFontSize = gaugeValueFontSize * (gaugeRect.height / 300);
22654
+ }
22655
+ // Scale down the font size if the text is too long
22656
+ const maxTextWidth = gaugeRect.width / 2;
22657
+ const gaugeLabel = gaugeValue?.label || "-";
22658
+ if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
22659
+ gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
22660
+ }
22661
+ const minLabelPosition = {
22662
+ x: gaugeRect.x + gaugeArcWidth / 2,
22663
+ y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
22664
+ };
22665
+ const maxLabelPosition = {
22666
+ x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
22667
+ y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
22668
+ };
22669
+ const textColor = chartMutedFontColor(runtime.background);
22670
+ const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);
22671
+ let x = 0, titleWidth = 0, titleHeight = 0;
22672
+ if (runtime.title.text) {
22673
+ ({ width: titleWidth, height: titleHeight } = computeTextDimension(ctx, runtime.title.text, { fontSize: CHART_TITLE_FONT_SIZE, ...runtime.title }, "px"));
22674
+ }
22675
+ switch (runtime.title.align) {
22676
+ case "right":
22677
+ x = boundingRect.width - titleWidth - CHART_PADDING$1;
22678
+ break;
22679
+ case "center":
22680
+ x = (boundingRect.width - titleWidth) / 2;
22681
+ break;
22682
+ case "left":
22683
+ default:
22684
+ x = CHART_PADDING$1;
22685
+ break;
22686
+ }
22687
+ return {
22688
+ width: boundingRect.width,
22689
+ height: boundingRect.height,
22690
+ title: {
22691
+ label: runtime.title.text ?? "",
22692
+ fontSize: runtime.title.fontSize ?? CHART_TITLE_FONT_SIZE,
22693
+ textPosition: {
22694
+ x,
22695
+ y: CHART_PADDING_TOP + titleHeight / 2,
22696
+ },
22697
+ color: runtime.title.color ?? textColor,
22698
+ bold: runtime.title.bold,
22699
+ italic: runtime.title.italic,
22700
+ },
22701
+ backgroundColor: runtime.background,
22702
+ gauge: {
22703
+ rect: gaugeRect,
22704
+ arcWidth: gaugeArcWidth,
22705
+ percentage: clip(gaugePercentage, 0, 1),
22706
+ color: getGaugeColor(runtime),
22707
+ },
22708
+ inflectionValues,
22709
+ gaugeValue: {
22710
+ label: gaugeLabel,
22711
+ textPosition: gaugeValuePosition,
22712
+ fontSize: gaugeValueFontSize,
22713
+ color: textColor,
22714
+ },
22715
+ minLabel: {
22716
+ label: runtime.minValue.label,
22717
+ textPosition: minLabelPosition,
22718
+ fontSize: GAUGE_LABELS_FONT_SIZE,
22719
+ color: textColor,
22720
+ },
22721
+ maxLabel: {
22722
+ label: runtime.maxValue.label,
22723
+ textPosition: maxLabelPosition,
22724
+ fontSize: GAUGE_LABELS_FONT_SIZE,
22725
+ color: textColor,
22726
+ },
22727
+ };
22728
+ }
22729
+ /**
22730
+ * Get the rectangle in which the gauge will be drawn, based on the bounding rectangle of the canvas and leaving
22731
+ * space for the title and labels.
22732
+ */
22733
+ function getGaugeRect(boundingRect, title) {
22734
+ const titleHeight = title ? GAUGE_TITLE_SECTION_HEIGHT : 0;
22735
+ const drawHeight = boundingRect.height - GAUGE_PADDING_BOTTOM - titleHeight - GAUGE_PADDING_TOP;
22736
+ const drawWidth = boundingRect.width - GAUGE_PADDING_SIDE * 2;
22737
+ let gaugeWidth;
22738
+ let gaugeHeight;
22739
+ if (drawWidth > 2 * drawHeight) {
22740
+ gaugeWidth = 2 * drawHeight;
22741
+ gaugeHeight = drawHeight;
22742
+ }
22743
+ else {
22744
+ gaugeWidth = drawWidth;
22745
+ gaugeHeight = drawWidth / 2;
22746
+ }
22747
+ const gaugeX = GAUGE_PADDING_SIDE + (drawWidth - gaugeWidth) / 2;
22748
+ const gaugeY = titleHeight + GAUGE_PADDING_TOP + (drawHeight - gaugeHeight) / 2;
22749
+ return {
22750
+ x: gaugeX,
22751
+ y: gaugeY,
22752
+ width: gaugeWidth,
22753
+ height: gaugeHeight,
22754
+ };
22755
+ }
22756
+ /**
22757
+ * 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).
22758
+ *
22759
+ * Also compute an offset for the text so that it doesn't overlap with other text.
22760
+ */
22761
+ function getInflectionValues(runtime, gaugeRect, textColor, ctx) {
22762
+ const maxValue = runtime.maxValue;
22763
+ const minValue = runtime.minValue;
22764
+ const gaugeCircleCenter = {
22765
+ x: gaugeRect.x + gaugeRect.width / 2,
22766
+ y: gaugeRect.y + gaugeRect.height,
22767
+ };
22768
+ const textStyle = { fontSize: GAUGE_LABELS_FONT_SIZE };
22769
+ const inflectionValues = [];
22770
+ const inflectionValuesTextRects = [];
22771
+ for (const inflectionValue of runtime.inflectionValues) {
22772
+ const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
22773
+ const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
22774
+ const angle = Math.PI - Math.PI * percentage;
22775
+ const textRect = getRectangleTangentToCircle(angle, // angle between X axis and the point where the rectangle is tangent to the circle
22776
+ gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, // radius of the gauge circle + margin below text
22777
+ gaugeCircleCenter.x, // center of the gauge circle
22778
+ gaugeCircleCenter.y, // center of the gauge circle
22779
+ labelWidth + 2, // width of the text + some margin
22780
+ GAUGE_LABELS_FONT_SIZE // height of the text
22781
+ );
22782
+ let offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect))
22783
+ ? GAUGE_LABELS_FONT_SIZE
22784
+ : 0;
22785
+ inflectionValuesTextRects.push(textRect);
22786
+ inflectionValues.push({
22787
+ rotation: angle,
22788
+ label: inflectionValue.label,
22789
+ fontSize: GAUGE_LABELS_FONT_SIZE,
22790
+ color: textColor,
22791
+ offset,
22792
+ });
22793
+ }
22794
+ return inflectionValues;
22795
+ }
22796
+ function getGaugeColor(runtime) {
22797
+ const gaugeValue = runtime.gaugeValue?.value;
22798
+ if (gaugeValue === undefined) {
22799
+ return GAUGE_BACKGROUND_COLOR;
22800
+ }
22801
+ for (let i = 0; i < runtime.inflectionValues.length; i++) {
22802
+ const inflectionValue = runtime.inflectionValues[i];
22803
+ if (inflectionValue.operator === "<" && gaugeValue < inflectionValue.value) {
22804
+ return runtime.colors[i];
22805
+ }
22806
+ else if (inflectionValue.operator === "<=" && gaugeValue <= inflectionValue.value) {
22807
+ return runtime.colors[i];
22808
+ }
22809
+ }
22810
+ return runtime.colors.at(-1);
22811
+ }
22812
+ function getSegmentsOfRectangle(rectangle) {
22813
+ return [
22814
+ { start: rectangle.topLeft, end: rectangle.topRight },
22815
+ { start: rectangle.topRight, end: rectangle.bottomRight },
22816
+ { start: rectangle.bottomRight, end: rectangle.bottomLeft },
22817
+ { start: rectangle.bottomLeft, end: rectangle.topLeft },
22818
+ ];
22819
+ }
22820
+ /**
22821
+ * Check if two segment intersect. The case where the segments are colinear (both segments on the same line)
22822
+ * is not handled.
22823
+ */
22824
+ function doSegmentIntersect(segment1, segment2) {
22825
+ const A = segment1.start;
22826
+ const B = segment1.end;
22827
+ const C = segment2.start;
22828
+ const D = segment2.end;
22829
+ /**
22830
+ * Line segment intersection algorithm
22831
+ * https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
22832
+ */
22833
+ function ccw(a, b, c) {
22834
+ return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
22835
+ }
22836
+ return ccw(A, C, D) !== ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D);
22837
+ }
22838
+ function doRectanglesIntersect(rect1, rect2) {
22839
+ const segments1 = getSegmentsOfRectangle(rect1);
22840
+ const segments2 = getSegmentsOfRectangle(rect2);
22841
+ for (const segment1 of segments1) {
22842
+ for (const segment2 of segments2) {
22843
+ if (doSegmentIntersect(segment1, segment2)) {
22844
+ return true;
22845
+ }
22846
+ }
22847
+ }
22848
+ return false;
22849
+ }
22850
+ /**
22851
+ * Get the rectangle that is tangent to a circle at a given angle.
22852
+ *
22853
+ * @param angle angle between X axis and the point where the rectangle is tangent to the circle
22854
+ */
22855
+ function getRectangleTangentToCircle(angle, radius, circleCenterX, circleCenterY, rectWidth, rectHeight) {
22856
+ const cos = Math.cos(angle);
22857
+ const sin = Math.sin(angle);
22858
+ // x, y are the distance from the center of the circle to the point where the rectangle is tangent to the circle
22859
+ const x = cos * radius;
22860
+ const y = sin * radius;
22861
+ // x2, y2 are the distance from the point the rectangle is tangent to the circle to the bottom left corner of the rectangle
22862
+ const x2 = sin * (rectWidth / 2); // cos(angle + 90°) = sin(angle)
22863
+ const y2 = cos * (rectWidth / 2);
22864
+ const bottomRight = {
22865
+ x: x + x2 + circleCenterX,
22866
+ y: circleCenterY - (y - y2),
22867
+ };
22868
+ const bottomLeft = {
22869
+ x: x - x2 + circleCenterX,
22870
+ y: circleCenterY - (y + y2),
22871
+ };
22872
+ // Same as above but for the top corners of the rectangle (radius + rectangle height instead of radius)
22873
+ const xp = cos * (radius + rectHeight);
22874
+ const yp = sin * (radius + rectHeight);
22875
+ const topLeft = {
22876
+ x: xp - x2 + circleCenterX,
22877
+ y: circleCenterY - (yp + y2),
22878
+ };
22879
+ const topRight = {
22880
+ x: xp + x2 + circleCenterX,
22881
+ y: circleCenterY - (yp - y2),
22882
+ };
22883
+ return { bottomLeft, bottomRight, topRight, topLeft };
22884
+ }
22885
+
22942
22886
  class GaugeChartComponent extends owl.Component {
22943
22887
  static template = "o-spreadsheet-GaugeChartComponent";
22944
22888
  canvas = owl.useRef("chartContainer");
@@ -22971,6 +22915,73 @@ function toXlsxHexColor(color) {
22971
22915
  return color;
22972
22916
  }
22973
22917
 
22918
+ const CHART_COMMON_OPTIONS = {
22919
+ // https://www.chartjs.org/docs/latest/general/responsive.html
22920
+ responsive: true, // will resize when its container is resized
22921
+ maintainAspectRatio: false, // doesn't maintain the aspect ratio (width/height =2 by default) so the user has the choice of the exact layout
22922
+ elements: {
22923
+ line: {
22924
+ fill: false, // do not fill the area under line charts
22925
+ },
22926
+ point: {
22927
+ hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
22928
+ },
22929
+ },
22930
+ animation: false,
22931
+ };
22932
+ function chartToImage(runtime, figure, type) {
22933
+ // wrap the canvas in a div with a fixed size because chart.js would
22934
+ // fill the whole page otherwise
22935
+ const div = document.createElement("div");
22936
+ div.style.width = `${figure.width}px`;
22937
+ div.style.height = `${figure.height}px`;
22938
+ const canvas = document.createElement("canvas");
22939
+ div.append(canvas);
22940
+ canvas.setAttribute("width", figure.width.toString());
22941
+ canvas.setAttribute("height", figure.height.toString());
22942
+ // we have to add the canvas to the DOM otherwise it won't be rendered
22943
+ document.body.append(div);
22944
+ if ("chartJsConfig" in runtime) {
22945
+ const config = deepCopy(runtime.chartJsConfig);
22946
+ config.plugins = [backgroundColorChartJSPlugin];
22947
+ const Chart = getChartJSConstructor();
22948
+ const chart = new Chart(canvas, config);
22949
+ const imgContent = chart.toBase64Image();
22950
+ chart.destroy();
22951
+ div.remove();
22952
+ return imgContent;
22953
+ }
22954
+ else if (type === "scorecard") {
22955
+ const design = getScorecardConfiguration(figure, runtime);
22956
+ drawScoreChart(design, canvas);
22957
+ const imgContent = canvas.toDataURL();
22958
+ div.remove();
22959
+ return imgContent;
22960
+ }
22961
+ else if (type === "gauge") {
22962
+ drawGaugeChart(canvas, runtime);
22963
+ const imgContent = canvas.toDataURL();
22964
+ div.remove();
22965
+ return imgContent;
22966
+ }
22967
+ return undefined;
22968
+ }
22969
+ /**
22970
+ * Custom chart.js plugin to set the background color of the canvas
22971
+ * https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md
22972
+ */
22973
+ const backgroundColorChartJSPlugin = {
22974
+ id: "customCanvasBackgroundColor",
22975
+ beforeDraw: (chart) => {
22976
+ const { ctx } = chart;
22977
+ ctx.save();
22978
+ ctx.globalCompositeOperation = "destination-over";
22979
+ ctx.fillStyle = "#ffffff";
22980
+ ctx.fillRect(0, 0, chart.width, chart.height);
22981
+ ctx.restore();
22982
+ },
22983
+ };
22984
+
22974
22985
  /**
22975
22986
  * Represent a raw XML string
22976
22987
  */
@@ -23010,9 +23021,9 @@ const XLSX_CHART_TYPES = [
23010
23021
  /** In XLSX color format (no #) */
23011
23022
  const AUTO_COLOR = "000000";
23012
23023
  const XLSX_ICONSET_MAP = {
23013
- arrow: "3Arrows",
23024
+ arrows: "3Arrows",
23014
23025
  smiley: "3Symbols",
23015
- dot: "3TrafficLights1",
23026
+ dots: "3TrafficLights1",
23016
23027
  };
23017
23028
  const NAMESPACE = {
23018
23029
  styleSheet: "http://schemas.openxmlformats.org/spreadsheetml/2006/main",
@@ -23543,6 +23554,7 @@ const ICON_SET_CONVERSION_MAP = {
23543
23554
  };
23544
23555
  /** Map between legend position in XLSX file and human readable position */
23545
23556
  const DRAWING_LEGEND_POSITION_CONVERSION_MAP = {
23557
+ none: "none",
23546
23558
  b: "bottom",
23547
23559
  t: "top",
23548
23560
  l: "left",
@@ -26305,7 +26317,7 @@ class XlsxChartExtractor extends XlsxBaseExtractor {
26305
26317
  default: "ffffff",
26306
26318
  }).asString(),
26307
26319
  legendPosition: DRAWING_LEGEND_POSITION_CONVERSION_MAP[this.extractChildAttr(rootChartElement, "c:legendPos", "val", {
26308
- default: "b",
26320
+ default: "none",
26309
26321
  }).asString()],
26310
26322
  stacked: barChartGrouping === "stacked",
26311
26323
  fontColor: "000000",
@@ -26339,7 +26351,7 @@ class XlsxChartExtractor extends XlsxBaseExtractor {
26339
26351
  default: "ffffff",
26340
26352
  }).asString(),
26341
26353
  legendPosition: DRAWING_LEGEND_POSITION_CONVERSION_MAP[this.extractChildAttr(chartElement, "c:legendPos", "val", {
26342
- default: "b",
26354
+ default: "none",
26343
26355
  }).asString()],
26344
26356
  stacked: barChartGrouping === "stacked",
26345
26357
  fontColor: "000000",
@@ -28951,7 +28963,8 @@ function getChartLabelValues(getters, dataSets, labelRange) {
28951
28963
  }
28952
28964
  }
28953
28965
  else if (dataSets.length === 1) {
28954
- for (let i = 0; i < getData(getters, dataSets[0]).length; i++) {
28966
+ const dataLength = getData(getters, dataSets[0]).length;
28967
+ for (let i = 0; i < dataLength; i++) {
28955
28968
  labels.formattedValues.push("");
28956
28969
  labels.values.push("");
28957
28970
  }
@@ -29134,7 +29147,7 @@ function getLineChartDatasets(definition, args) {
29134
29147
  function getScatterChartDatasets(definition, args) {
29135
29148
  const dataSets = getLineChartDatasets(definition, args);
29136
29149
  for (const dataSet of dataSets) {
29137
- if (dataSet.xAxisID !== TREND_LINE_XAXIS_ID) {
29150
+ if (!isTrendLineAxis(dataSet.xAxisID)) {
29138
29151
  dataSet.showLine = false;
29139
29152
  }
29140
29153
  }
@@ -29261,7 +29274,9 @@ function getTrendingLineDataSet(dataset, config, data) {
29261
29274
  const borderColor = config.color || lightenColor(rgbaToHex(defaultBorderColor), 0.5);
29262
29275
  return {
29263
29276
  type: "line",
29264
- xAxisID: TREND_LINE_XAXIS_ID,
29277
+ xAxisID: config.type === "trailingMovingAverage"
29278
+ ? MOVING_AVERAGE_TREND_LINE_XAXIS_ID
29279
+ : TREND_LINE_XAXIS_ID,
29265
29280
  yAxisID: dataset.yAxisID,
29266
29281
  label: dataset.label ? _t("Trend line for %s", dataset.label) : "",
29267
29282
  data,
@@ -29336,22 +29351,19 @@ function getPieChartLegend(definition, args) {
29336
29351
  const { dataSetsValues } = args;
29337
29352
  const dataSetsLength = Math.max(0, ...dataSetsValues.map((ds) => ds?.data?.length ?? 0));
29338
29353
  const colors = getPieColors(new ColorGenerator(dataSetsLength), dataSetsValues);
29354
+ const fontColor = chartFontColor(definition.background);
29339
29355
  return {
29340
29356
  ...getLegendDisplayOptions(definition),
29341
29357
  labels: {
29342
- color: chartFontColor(definition.background),
29343
29358
  usePointStyle: true,
29344
- //@ts-ignore
29345
- generateLabels: (c) =>
29346
- //@ts-ignore
29347
- c.data.labels.map((label, index) => ({
29359
+ generateLabels: (c) => c.data.labels?.map((label, index) => ({
29348
29360
  text: truncateLabel(String(label)),
29349
29361
  strokeStyle: colors[index],
29350
29362
  fillStyle: colors[index],
29351
29363
  pointStyle: "rect",
29352
- hidden: false,
29353
29364
  lineWidth: 2,
29354
- })),
29365
+ fontColor,
29366
+ })) || [],
29355
29367
  filter: (legendItem, data) => {
29356
29368
  return "datasetIndex" in legendItem
29357
29369
  ? !data.datasets[legendItem.datasetIndex].hidden
@@ -29484,7 +29496,7 @@ function getCustomLegendLabels(fontColor, legendLabelConfig) {
29484
29496
  color: fontColor,
29485
29497
  usePointStyle: true,
29486
29498
  generateLabels: (chart) => chart.data.datasets.map((dataset, index) => {
29487
- if (dataset["xAxisID"] === TREND_LINE_XAXIS_ID) {
29499
+ if (isTrendLineAxis(dataset["xAxisID"])) {
29488
29500
  return {
29489
29501
  text: truncateLabel(dataset.label),
29490
29502
  fontColor,
@@ -29542,6 +29554,11 @@ function getBarChartScales(definition, args) {
29542
29554
  offset: false,
29543
29555
  display: false,
29544
29556
  };
29557
+ scales[MOVING_AVERAGE_TREND_LINE_XAXIS_ID] = {
29558
+ ...scales.x,
29559
+ offset: false,
29560
+ display: false,
29561
+ };
29545
29562
  }
29546
29563
  return scales;
29547
29564
  }
@@ -29575,6 +29592,10 @@ function getLineChartScales(definition, args) {
29575
29592
  ...scales.x,
29576
29593
  display: false,
29577
29594
  };
29595
+ scales[MOVING_AVERAGE_TREND_LINE_XAXIS_ID] = {
29596
+ ...scales.x,
29597
+ display: false,
29598
+ };
29578
29599
  if (axisType === "category" || axisType === "time") {
29579
29600
  /* We add a second x axis here to draw the trend lines, with the labels length being
29580
29601
  * set so that the second axis points match the classical x axis
@@ -29583,6 +29604,8 @@ function getLineChartScales(definition, args) {
29583
29604
  scales[TREND_LINE_XAXIS_ID]["type"] = "category";
29584
29605
  scales[TREND_LINE_XAXIS_ID]["labels"] = range(0, maxLength).map((x) => x.toString());
29585
29606
  scales[TREND_LINE_XAXIS_ID]["offset"] = false;
29607
+ scales[MOVING_AVERAGE_TREND_LINE_XAXIS_ID]["type"] = "category";
29608
+ scales[MOVING_AVERAGE_TREND_LINE_XAXIS_ID]["offset"] = false;
29586
29609
  }
29587
29610
  }
29588
29611
  return scales;
@@ -29901,9 +29924,7 @@ function getBarChartTooltip(definition, args) {
29901
29924
  external: customTooltipHandler,
29902
29925
  callbacks: {
29903
29926
  title: function (tooltipItems) {
29904
- return tooltipItems.some((item) => item.dataset.xAxisID !== TREND_LINE_XAXIS_ID)
29905
- ? undefined
29906
- : "";
29927
+ return tooltipItems.some((item) => !isTrendLineAxis(item.dataset.xAxisID)) ? undefined : "";
29907
29928
  },
29908
29929
  beforeLabel: (tooltipItem) => tooltipItem.dataset?.label || tooltipItem.label,
29909
29930
  label: function (tooltipItem) {
@@ -29930,7 +29951,7 @@ function getLineChartTooltip(definition, args) {
29930
29951
  if (axisType === "linear") {
29931
29952
  tooltip.callbacks.label = (tooltipItem) => {
29932
29953
  const dataSetPoint = tooltipItem.parsed.y;
29933
- let label = tooltipItem.dataset.xAxisID === TREND_LINE_XAXIS_ID
29954
+ let label = isTrendLineAxis(tooltipItem.dataset.xAxisID)
29934
29955
  ? ""
29935
29956
  : tooltipItem.parsed.x;
29936
29957
  if (typeof label === "string" && isNumber(label, locale)) {
@@ -29952,8 +29973,7 @@ function getLineChartTooltip(definition, args) {
29952
29973
  }
29953
29974
  tooltip.callbacks.beforeLabel = (tooltipItem) => tooltipItem.dataset?.label || tooltipItem.label;
29954
29975
  tooltip.callbacks.title = function (tooltipItems) {
29955
- const displayTooltipTitle = axisType !== "linear" &&
29956
- tooltipItems.some((item) => item.dataset.xAxisID !== TREND_LINE_XAXIS_ID);
29976
+ const displayTooltipTitle = axisType !== "linear" && tooltipItems.some((item) => !isTrendLineAxis(item.dataset.xAxisID));
29957
29977
  return displayTooltipTitle ? undefined : "";
29958
29978
  };
29959
29979
  return tooltip;
@@ -34207,6 +34227,7 @@ var CHART_HELPERS = /*#__PURE__*/Object.freeze({
34207
34227
  CHART_COMMON_OPTIONS: CHART_COMMON_OPTIONS,
34208
34228
  GaugeChart: GaugeChart,
34209
34229
  LineChart: LineChart,
34230
+ MOVING_AVERAGE_TREND_LINE_XAXIS_ID: MOVING_AVERAGE_TREND_LINE_XAXIS_ID,
34210
34231
  PieChart: PieChart,
34211
34232
  ScorecardChart: ScorecardChart$1,
34212
34233
  TREND_LINE_XAXIS_ID: TREND_LINE_XAXIS_ID,
@@ -34231,11 +34252,11 @@ var CHART_HELPERS = /*#__PURE__*/Object.freeze({
34231
34252
  duplicateLabelRangeInDuplicatedSheet: duplicateLabelRangeInDuplicatedSheet,
34232
34253
  formatChartDatasetValue: formatChartDatasetValue,
34233
34254
  formatTickValue: formatTickValue,
34234
- getChartJSConstructor: getChartJSConstructor,
34235
34255
  getChartPositionAtCenterOfViewport: getChartPositionAtCenterOfViewport,
34236
34256
  getDefinedAxis: getDefinedAxis,
34237
34257
  getPieColors: getPieColors,
34238
34258
  getSmartChartDefinition: getSmartChartDefinition,
34259
+ isTrendLineAxis: isTrendLineAxis,
34239
34260
  shouldRemoveFirstLabel: shouldRemoveFirstLabel,
34240
34261
  toExcelDataset: toExcelDataset,
34241
34262
  toExcelLabelRange: toExcelLabelRange,
@@ -36283,9 +36304,7 @@ class FormulaFingerprintStore extends SpreadsheetStore {
36283
36304
  }
36284
36305
  }
36285
36306
  }
36286
- // removes the index placeholders from the normalized formula
36287
- // =|N0|+|N1|+|N0| -> =|N|+|N|+|N|
36288
- const normalizedFormula = cell.compiledFormula.normalizedFormula.replace(/(|\w)(\d)(|)/g, "$1$3");
36307
+ const normalizedFormula = cell.compiledFormula.normalizedFormula;
36289
36308
  return hash(fingerprintVector) + normalizedFormula;
36290
36309
  }
36291
36310
  getLiteralFingerprint(position) {
@@ -39630,9 +39649,11 @@ class SeriesDesignEditor extends owl.Component {
39630
39649
  if (!runtime || !("chartJsConfig" in runtime)) {
39631
39650
  return [];
39632
39651
  }
39633
- return runtime.chartJsConfig.data.datasets.map((d) => d.label);
39652
+ return runtime.chartJsConfig.data.datasets
39653
+ .filter((d) => !isTrendLineAxis(d["xAxisID"] ?? ""))
39654
+ .map((d) => d.label);
39634
39655
  }
39635
- updateSerieEditor(ev) {
39656
+ updateEditedSeries(ev) {
39636
39657
  this.state.index = ev.target.selectedIndex;
39637
39658
  }
39638
39659
  updateDataSeriesColor(color) {
@@ -39645,7 +39666,7 @@ class SeriesDesignEditor extends owl.Component {
39645
39666
  };
39646
39667
  this.props.updateChart(this.props.figureId, { dataSets });
39647
39668
  }
39648
- getDataSerieColor() {
39669
+ getDataSeriesColor() {
39649
39670
  const dataSets = this.props.definition.dataSets;
39650
39671
  if (!dataSets?.[this.state.index])
39651
39672
  return "";
@@ -39665,7 +39686,7 @@ class SeriesDesignEditor extends owl.Component {
39665
39686
  };
39666
39687
  this.props.updateChart(this.props.figureId, { dataSets });
39667
39688
  }
39668
- getDataSerieLabel() {
39689
+ getDataSeriesLabel() {
39669
39690
  const dataSets = this.props.definition.dataSets;
39670
39691
  return dataSets[this.state.index]?.label || this.getDataSeries()[this.state.index];
39671
39692
  }
@@ -39778,7 +39799,7 @@ class SeriesWithAxisDesignEditor extends owl.Component {
39778
39799
  }
39779
39800
  this.updateTrendLineValue(index, { window });
39780
39801
  }
39781
- getDataSerieColor(index) {
39802
+ getDataSeriesColor(index) {
39782
39803
  const dataSets = this.props.definition.dataSets;
39783
39804
  if (!dataSets?.[index])
39784
39805
  return "";
@@ -39789,7 +39810,7 @@ class SeriesWithAxisDesignEditor extends owl.Component {
39789
39810
  }
39790
39811
  getTrendLineColor(index) {
39791
39812
  return (this.getTrendLineConfiguration(index)?.color ??
39792
- setColorAlpha(this.getDataSerieColor(index), 0.5));
39813
+ setColorAlpha(this.getDataSeriesColor(index), 0.5));
39793
39814
  }
39794
39815
  updateTrendLineColor(index, color) {
39795
39816
  this.updateTrendLineValue(index, { color });
@@ -45078,7 +45099,8 @@ css /* scss */ `
45078
45099
  &.pivot-dimension-invalid {
45079
45100
  background-color: #ffdddd;
45080
45101
  border-color: red !important;
45081
- select {
45102
+ select,
45103
+ input {
45082
45104
  background-color: #ffdddd;
45083
45105
  }
45084
45106
  }
@@ -46939,7 +46961,7 @@ class PivotSidePanelStore extends SpreadsheetStore {
46939
46961
  this.notification.notifyUser({
46940
46962
  type: "info",
46941
46963
  text: _t("Pivot updates only work with dynamic pivot tables. Use %s or re-insert the static pivot from the Data menu.", pivotExample),
46942
- sticky: false,
46964
+ sticky: true,
46943
46965
  });
46944
46966
  }
46945
46967
  }
@@ -70769,7 +70791,9 @@ css /* scss */ `
70769
70791
  border: 1px solid;
70770
70792
  font-family: ${DEFAULT_FONT};
70771
70793
 
70772
- .o-composer:empty:not(:focus):not(.active)::before {
70794
+ /* In readonly we always show the fx icon if the composer is empty, not matter the focus */
70795
+ .o-composer:empty:not(:focus):not(.active)::before,
70796
+ &.o-topbar-composer-readonly .o-composer:empty::before {
70773
70797
  content: url("data:image/svg+xml,${encodeURIComponent(FX_SVG)}");
70774
70798
  position: relative;
70775
70799
  top: 20%;
@@ -74095,10 +74119,14 @@ function addIconSetRule(cf, rule) {
74095
74119
  continue;
74096
74120
  }
74097
74121
  const cfValueObjectNodes = cfValueObject.map((attrs) => escapeXml /*xml*/ `<cfvo ${formatAttributes(attrs)} />`);
74122
+ const iconSetAttrs = [["iconSet", getIconSet(rule.icons)]];
74123
+ if (isIconSetReversed(rule.icons)) {
74124
+ iconSetAttrs.push(["reverse", "1"]);
74125
+ }
74098
74126
  conditionalFormats.push(escapeXml /*xml*/ `
74099
74127
  <conditionalFormatting sqref="${range}">
74100
74128
  <cfRule ${formatAttributes(ruleAttributes)}>
74101
- <iconSet iconSet="${getIconSet(rule.icons)}">
74129
+ <iconSet ${formatAttributes(iconSetAttrs)}>
74102
74130
  ${joinXmlNodes(cfValueObjectNodes)}
74103
74131
  </iconSet>
74104
74132
  </cfRule>
@@ -74116,9 +74144,21 @@ function commonCfAttributes(cf) {
74116
74144
  ["stopIfTrue", cf.stopIfTrue ? 1 : 0],
74117
74145
  ];
74118
74146
  }
74147
+ function isIconSetReversed(iconSet) {
74148
+ const defaultIconSet = ICON_SETS[detectIconsType(iconSet)];
74149
+ return iconSet.upper === defaultIconSet.bad && iconSet.lower === defaultIconSet.good;
74150
+ }
74119
74151
  function getIconSet(iconSet) {
74120
- return XLSX_ICONSET_MAP[Object.keys(XLSX_ICONSET_MAP).find((key) => iconSet.upper.toLowerCase().startsWith(key)) ||
74121
- "dots"];
74152
+ return XLSX_ICONSET_MAP[detectIconsType(iconSet)];
74153
+ }
74154
+ /**
74155
+ * Partial detection based on "upper" point only.
74156
+ * We support any arbitrary icon in the set, while excel doesn't allow
74157
+ * mixing icons from different types.
74158
+ */
74159
+ function detectIconsType(iconSet) {
74160
+ const type = Object.keys(ICON_SETS).find((type) => Object.values(ICON_SETS[type]).includes(iconSet.upper)) || "dots";
74161
+ return type;
74122
74162
  }
74123
74163
  function thresholdAttributes(threshold, position) {
74124
74164
  const type = getExcelThresholdType(threshold.type, position);
@@ -75868,6 +75908,7 @@ const registries = {
75868
75908
  supportedPivotPositionalFormulaRegistry,
75869
75909
  pivotToFunctionValueRegistry,
75870
75910
  migrationStepRegistry,
75911
+ chartJsExtensionRegistry,
75871
75912
  };
75872
75913
  const helpers = {
75873
75914
  arg,
@@ -76068,6 +76109,6 @@ exports.tokenColors = tokenColors;
76068
76109
  exports.tokenize = tokenize;
76069
76110
 
76070
76111
 
76071
- __info__.version = "18.2.4";
76072
- __info__.date = "2025-03-19T08:20:57.717Z";
76073
- __info__.hash = "958936a";
76112
+ __info__.version = "18.2.6";
76113
+ __info__.date = "2025-04-04T08:41:26.115Z";
76114
+ __info__.hash = "faa00e2";