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