@odoo/o-spreadsheet 18.2.5 → 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.5
6
- * @date 2025-03-26T12:47:44.113Z
7
- * @hash 4675edd
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
@@ -6798,8 +6797,12 @@
6798
6797
  str = replaceNewLines(str);
6799
6798
  const chars = new TokenizingChars(str);
6800
6799
  const result = [];
6800
+ const tokenizeSpace = specialWhiteSpaceRegexp.test(str)
6801
+ ? tokenizeSpecialCharacterSpace
6802
+ : tokenizeSimpleSpace;
6801
6803
  while (!chars.isOver()) {
6802
- let token = tokenizeSpace(chars) ||
6804
+ let token = tokenizeNewLine(chars) ||
6805
+ tokenizeSpace(chars) ||
6803
6806
  tokenizeArgsSeparator(chars, locale) ||
6804
6807
  tokenizeParenthesis(chars) ||
6805
6808
  tokenizeOperator(chars) ||
@@ -6933,17 +6936,19 @@
6933
6936
  }
6934
6937
  return null;
6935
6938
  }
6936
- function tokenizeSpace(chars) {
6937
- let length = 0;
6938
- while (chars.current === NEWLINE) {
6939
- length++;
6940
- chars.shift();
6939
+ function tokenizeSpecialCharacterSpace(chars) {
6940
+ let spaces = "";
6941
+ while (chars.current === " " || (chars.current && chars.current.match(specialWhiteSpaceRegexp))) {
6942
+ spaces += chars.shift();
6941
6943
  }
6942
- if (length) {
6943
- return { type: "SPACE", value: NEWLINE.repeat(length) };
6944
+ if (spaces) {
6945
+ return { type: "SPACE", value: spaces };
6944
6946
  }
6947
+ return null;
6948
+ }
6949
+ function tokenizeSimpleSpace(chars) {
6945
6950
  let spaces = "";
6946
- while (chars.current && chars.current.match(whiteSpaceRegexp)) {
6951
+ while (chars.current === " ") {
6947
6952
  spaces += chars.shift();
6948
6953
  }
6949
6954
  if (spaces) {
@@ -6951,6 +6956,17 @@
6951
6956
  }
6952
6957
  return null;
6953
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
+ }
6954
6970
  function tokenizeInvalidRange(chars) {
6955
6971
  if (chars.currentStartsWith(CellErrorType.InvalidReference)) {
6956
6972
  chars.advanceBy(CellErrorType.InvalidReference.length);
@@ -9577,6 +9593,159 @@ stores.inject(MyMetaStore, storeInstance);
9577
9593
  }
9578
9594
  }
9579
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
+
9580
9749
  const TREND_LINE_XAXIS_ID = "x1";
9581
9750
  const MOVING_AVERAGE_TREND_LINE_XAXIS_ID = "xMovingAverage";
9582
9751
  /**
@@ -10125,341 +10294,79 @@ stores.inject(MyMetaStore, storeInstance);
10125
10294
  return bars.find((bar, i) => i > startIndex && bar.height !== 0);
10126
10295
  }
10127
10296
 
10128
- const GAUGE_PADDING_SIDE = 30;
10129
- const GAUGE_PADDING_TOP = 10;
10130
- const GAUGE_PADDING_BOTTOM = 20;
10131
- const GAUGE_LABELS_FONT_SIZE = 12;
10132
- const GAUGE_DEFAULT_VALUE_FONT_SIZE = 80;
10133
- const GAUGE_BACKGROUND_COLOR = "#F3F2F1";
10134
- const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
10135
- const GAUGE_TITLE_SECTION_HEIGHT = 25;
10136
- function drawGaugeChart(canvas, runtime) {
10137
- const canvasBoundingRect = canvas.getBoundingClientRect();
10138
- canvas.width = canvasBoundingRect.width;
10139
- canvas.height = canvasBoundingRect.height;
10140
- const ctx = canvas.getContext("2d");
10141
- const config = getGaugeRenderingConfig(canvasBoundingRect, runtime, ctx);
10142
- drawBackground(ctx, config);
10143
- drawGauge(ctx, config);
10144
- drawInflectionValues(ctx, config);
10145
- drawLabels(ctx, config);
10146
- drawTitle(ctx, config);
10147
- }
10148
- function drawGauge(ctx, config) {
10149
- ctx.save();
10150
- const gauge = config.gauge;
10151
- const arcCenterX = gauge.rect.x + gauge.rect.width / 2;
10152
- const arcCenterY = gauge.rect.y + gauge.rect.height;
10153
- const arcRadius = gauge.rect.height - gauge.arcWidth / 2;
10154
- if (arcRadius < 0) {
10155
- return;
10156
- }
10157
- const gaugeAngle = gauge.percentage === 1 ? 0 : Math.PI * (1 + gauge.percentage);
10158
- // Gauge background
10159
- ctx.strokeStyle = GAUGE_BACKGROUND_COLOR;
10160
- ctx.beginPath();
10161
- ctx.lineWidth = gauge.arcWidth;
10162
- ctx.arc(arcCenterX, arcCenterY, arcRadius, gaugeAngle, 0);
10163
- ctx.stroke();
10164
- // Gauge value
10165
- ctx.strokeStyle = gauge.color;
10166
- ctx.beginPath();
10167
- ctx.arc(arcCenterX, arcCenterY, arcRadius, Math.PI, gaugeAngle);
10168
- ctx.stroke();
10169
- ctx.restore();
10170
- }
10171
- function drawBackground(ctx, config) {
10172
- ctx.save();
10173
- ctx.fillStyle = config.backgroundColor;
10174
- ctx.fillRect(0, 0, config.width, config.height);
10175
- ctx.restore();
10176
- }
10177
- function drawLabels(ctx, config) {
10178
- for (const label of [config.minLabel, config.maxLabel, config.gaugeValue]) {
10179
- ctx.save();
10180
- ctx.textAlign = "center";
10181
- ctx.fillStyle = label.color;
10182
- ctx.font = `${label.fontSize}px ${DEFAULT_FONT}`;
10183
- ctx.fillText(label.label, label.textPosition.x, label.textPosition.y);
10184
- ctx.restore();
10185
- }
10186
- }
10187
- function drawInflectionValues(ctx, config) {
10188
- const { x: rectX, y: rectY, width, height } = config.gauge.rect;
10189
- for (const inflectionValue of config.inflectionValues) {
10190
- ctx.save();
10191
- ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment
10192
- ctx.rotate(Math.PI / 2 - inflectionValue.rotation);
10193
- ctx.lineWidth = 2;
10194
- ctx.strokeStyle = chartMutedFontColor(config.backgroundColor) + "aa";
10195
- ctx.beginPath();
10196
- ctx.moveTo(0, -(height - config.gauge.arcWidth));
10197
- ctx.lineTo(0, -height - 3);
10198
- ctx.stroke();
10199
- ctx.textAlign = "center";
10200
- ctx.font = `${inflectionValue.fontSize}px ${DEFAULT_FONT}`;
10201
- ctx.fillStyle = inflectionValue.color;
10202
- const textY = -height - GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN - inflectionValue.offset;
10203
- ctx.fillText(inflectionValue.label, 0, textY);
10204
- ctx.restore();
10205
- }
10206
- }
10207
- function drawTitle(ctx, config) {
10208
- ctx.save();
10209
- const title = config.title;
10210
- ctx.font = getDefaultContextFont(title.fontSize, title.bold, title.italic);
10211
- ctx.textBaseline = "middle";
10212
- ctx.fillStyle = title.color;
10213
- ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
10214
- ctx.restore();
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
+ }
10215
10304
  }
10216
- function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
10217
- const maxValue = runtime.maxValue;
10218
- const minValue = runtime.minValue;
10219
- const gaugeValue = runtime.gaugeValue;
10220
- const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
10221
- const gaugeArcWidth = gaugeRect.width / 6;
10222
- const gaugePercentage = gaugeValue
10223
- ? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
10224
- : 0;
10225
- const gaugeValuePosition = {
10226
- x: boundingRect.width / 2,
10227
- y: gaugeRect.y + gaugeRect.height - gaugeRect.height / 12,
10228
- };
10229
- let gaugeValueFontSize = GAUGE_DEFAULT_VALUE_FONT_SIZE;
10230
- // Scale down the font size if the gaugeRect is too small
10231
- if (gaugeRect.height < 300) {
10232
- gaugeValueFontSize = gaugeValueFontSize * (gaugeRect.height / 300);
10233
- }
10234
- // Scale down the font size if the text is too long
10235
- const maxTextWidth = gaugeRect.width / 2;
10236
- const gaugeLabel = gaugeValue?.label || "-";
10237
- if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
10238
- gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
10239
- }
10240
- const minLabelPosition = {
10241
- x: gaugeRect.x + gaugeArcWidth / 2,
10242
- y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
10243
- };
10244
- const maxLabelPosition = {
10245
- x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
10246
- y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
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,
10247
10312
  };
10248
- const textColor = chartMutedFontColor(runtime.background);
10249
- const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);
10250
- let x = 0, titleWidth = 0, titleHeight = 0;
10251
- if (runtime.title.text) {
10252
- ({ width: titleWidth, height: titleHeight } = computeTextDimension(ctx, runtime.title.text, { fontSize: CHART_TITLE_FONT_SIZE, ...runtime.title }, "px"));
10253
- }
10254
- switch (runtime.title.align) {
10255
- case "right":
10256
- x = boundingRect.width - titleWidth - CHART_PADDING$1;
10257
- break;
10258
- case "center":
10259
- x = (boundingRect.width - titleWidth) / 2;
10260
- break;
10261
- case "left":
10262
- default:
10263
- x = CHART_PADDING$1;
10264
- break;
10313
+ canvas = owl.useRef("graphContainer");
10314
+ chart;
10315
+ currentRuntime;
10316
+ get background() {
10317
+ return this.chartRuntime.background;
10265
10318
  }
10266
- return {
10267
- width: boundingRect.width,
10268
- height: boundingRect.height,
10269
- title: {
10270
- label: runtime.title.text ?? "",
10271
- fontSize: runtime.title.fontSize ?? CHART_TITLE_FONT_SIZE,
10272
- textPosition: {
10273
- x,
10274
- y: CHART_PADDING_TOP + titleHeight / 2,
10275
- },
10276
- color: runtime.title.color ?? textColor,
10277
- bold: runtime.title.bold,
10278
- italic: runtime.title.italic,
10279
- },
10280
- backgroundColor: runtime.background,
10281
- gauge: {
10282
- rect: gaugeRect,
10283
- arcWidth: gaugeArcWidth,
10284
- percentage: clip(gaugePercentage, 0, 1),
10285
- color: getGaugeColor(runtime),
10286
- },
10287
- inflectionValues,
10288
- gaugeValue: {
10289
- label: gaugeLabel,
10290
- textPosition: gaugeValuePosition,
10291
- fontSize: gaugeValueFontSize,
10292
- color: textColor,
10293
- },
10294
- minLabel: {
10295
- label: runtime.minValue.label,
10296
- textPosition: minLabelPosition,
10297
- fontSize: GAUGE_LABELS_FONT_SIZE,
10298
- color: textColor,
10299
- },
10300
- maxLabel: {
10301
- label: runtime.maxValue.label,
10302
- textPosition: maxLabelPosition,
10303
- fontSize: GAUGE_LABELS_FONT_SIZE,
10304
- color: textColor,
10305
- },
10306
- };
10307
- }
10308
- /**
10309
- * Get the rectangle in which the gauge will be drawn, based on the bounding rectangle of the canvas and leaving
10310
- * space for the title and labels.
10311
- */
10312
- function getGaugeRect(boundingRect, title) {
10313
- const titleHeight = title ? GAUGE_TITLE_SECTION_HEIGHT : 0;
10314
- const drawHeight = boundingRect.height - GAUGE_PADDING_BOTTOM - titleHeight - GAUGE_PADDING_TOP;
10315
- const drawWidth = boundingRect.width - GAUGE_PADDING_SIDE * 2;
10316
- let gaugeWidth;
10317
- let gaugeHeight;
10318
- if (drawWidth > 2 * drawHeight) {
10319
- gaugeWidth = 2 * drawHeight;
10320
- gaugeHeight = drawHeight;
10319
+ get canvasStyle() {
10320
+ return `background-color: ${this.background}`;
10321
10321
  }
10322
- else {
10323
- gaugeWidth = drawWidth;
10324
- 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;
10325
10328
  }
10326
- const gaugeX = GAUGE_PADDING_SIDE + (drawWidth - gaugeWidth) / 2;
10327
- const gaugeY = titleHeight + GAUGE_PADDING_TOP + (drawHeight - gaugeHeight) / 2;
10328
- return {
10329
- x: gaugeX,
10330
- y: gaugeY,
10331
- width: gaugeWidth,
10332
- height: gaugeHeight,
10333
- };
10334
- }
10335
- /**
10336
- * Get the infliction values of the gauge, and where to draw them (the angle from the center of the gauge at which they are drawn).
10337
- *
10338
- * Also compute an offset for the text so that it doesn't overlap with other text.
10339
- */
10340
- function getInflectionValues(runtime, gaugeRect, textColor, ctx) {
10341
- const maxValue = runtime.maxValue;
10342
- const minValue = runtime.minValue;
10343
- const gaugeCircleCenter = {
10344
- x: gaugeRect.x + gaugeRect.width / 2,
10345
- y: gaugeRect.y + gaugeRect.height,
10346
- };
10347
- const textStyle = { fontSize: GAUGE_LABELS_FONT_SIZE };
10348
- const inflectionValues = [];
10349
- const inflectionValuesTextRects = [];
10350
- for (const inflectionValue of runtime.inflectionValues) {
10351
- const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
10352
- const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
10353
- const angle = Math.PI - Math.PI * percentage;
10354
- const textRect = getRectangleTangentToCircle(angle, // angle between X axis and the point where the rectangle is tangent to the circle
10355
- gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, // radius of the gauge circle + margin below text
10356
- gaugeCircleCenter.x, // center of the gauge circle
10357
- gaugeCircleCenter.y, // center of the gauge circle
10358
- labelWidth + 2, // width of the text + some margin
10359
- GAUGE_LABELS_FONT_SIZE // height of the text
10360
- );
10361
- let offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect))
10362
- ? GAUGE_LABELS_FONT_SIZE
10363
- : 0;
10364
- inflectionValuesTextRects.push(textRect);
10365
- inflectionValues.push({
10366
- rotation: angle,
10367
- label: inflectionValue.label,
10368
- fontSize: GAUGE_LABELS_FONT_SIZE,
10369
- color: textColor,
10370
- offset,
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
+ }
10371
10349
  });
10372
10350
  }
10373
- return inflectionValues;
10374
- }
10375
- function getGaugeColor(runtime) {
10376
- const gaugeValue = runtime.gaugeValue?.value;
10377
- if (gaugeValue === undefined) {
10378
- return GAUGE_BACKGROUND_COLOR;
10379
- }
10380
- for (let i = 0; i < runtime.inflectionValues.length; i++) {
10381
- const inflectionValue = runtime.inflectionValues[i];
10382
- if (inflectionValue.operator === "<" && gaugeValue < inflectionValue.value) {
10383
- return runtime.colors[i];
10384
- }
10385
- else if (inflectionValue.operator === "<=" && gaugeValue <= inflectionValue.value) {
10386
- return runtime.colors[i];
10387
- }
10388
- }
10389
- return runtime.colors.at(-1);
10390
- }
10391
- function getSegmentsOfRectangle(rectangle) {
10392
- return [
10393
- { start: rectangle.topLeft, end: rectangle.topRight },
10394
- { start: rectangle.topRight, end: rectangle.bottomRight },
10395
- { start: rectangle.bottomRight, end: rectangle.bottomLeft },
10396
- { start: rectangle.bottomLeft, end: rectangle.topLeft },
10397
- ];
10398
- }
10399
- /**
10400
- * Check if two segment intersect. The case where the segments are colinear (both segments on the same line)
10401
- * is not handled.
10402
- */
10403
- function doSegmentIntersect(segment1, segment2) {
10404
- const A = segment1.start;
10405
- const B = segment1.end;
10406
- const C = segment2.start;
10407
- const D = segment2.end;
10408
- /**
10409
- * Line segment intersection algorithm
10410
- * https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
10411
- */
10412
- function ccw(a, b, c) {
10413
- return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
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);
10414
10356
  }
10415
- return ccw(A, C, D) !== ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D);
10416
- }
10417
- function doRectanglesIntersect(rect1, rect2) {
10418
- const segments1 = getSegmentsOfRectangle(rect1);
10419
- const segments2 = getSegmentsOfRectangle(rect2);
10420
- for (const segment1 of segments1) {
10421
- for (const segment2 of segments2) {
10422
- if (doSegmentIntersect(segment1, segment2)) {
10423
- return true;
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;
10424
10362
  }
10425
10363
  }
10364
+ else {
10365
+ this.chart.data.datasets = [];
10366
+ }
10367
+ this.chart.config.options = chartData.options;
10368
+ this.chart.update();
10426
10369
  }
10427
- return false;
10428
- }
10429
- /**
10430
- * Get the rectangle that is tangent to a circle at a given angle.
10431
- *
10432
- * @param angle angle between X axis and the point where the rectangle is tangent to the circle
10433
- */
10434
- function getRectangleTangentToCircle(angle, radius, circleCenterX, circleCenterY, rectWidth, rectHeight) {
10435
- const cos = Math.cos(angle);
10436
- const sin = Math.sin(angle);
10437
- // x, y are the distance from the center of the circle to the point where the rectangle is tangent to the circle
10438
- const x = cos * radius;
10439
- const y = sin * radius;
10440
- // x2, y2 are the distance from the point the rectangle is tangent to the circle to the bottom left corner of the rectangle
10441
- const x2 = sin * (rectWidth / 2); // cos(angle + 90°) = sin(angle)
10442
- const y2 = cos * (rectWidth / 2);
10443
- const bottomRight = {
10444
- x: x + x2 + circleCenterX,
10445
- y: circleCenterY - (y - y2),
10446
- };
10447
- const bottomLeft = {
10448
- x: x - x2 + circleCenterX,
10449
- y: circleCenterY - (y + y2),
10450
- };
10451
- // Same as above but for the top corners of the rectangle (radius + rectangle height instead of radius)
10452
- const xp = cos * (radius + rectHeight);
10453
- const yp = sin * (radius + rectHeight);
10454
- const topLeft = {
10455
- x: xp - x2 + circleCenterX,
10456
- y: circleCenterY - (yp + y2),
10457
- };
10458
- const topRight = {
10459
- x: xp + x2 + circleCenterX,
10460
- y: circleCenterY - (yp - y2),
10461
- };
10462
- return { bottomLeft, bottomRight, topRight, topLeft };
10463
10370
  }
10464
10371
 
10465
10372
  /**
@@ -11041,299 +10948,6 @@ stores.inject(MyMetaStore, storeInstance);
11041
10948
  }
11042
10949
  }
11043
10950
 
11044
- const CHART_COMMON_OPTIONS = {
11045
- // https://www.chartjs.org/docs/latest/general/responsive.html
11046
- responsive: true, // will resize when its container is resized
11047
- maintainAspectRatio: false, // doesn't maintain the aspect ratio (width/height =2 by default) so the user has the choice of the exact layout
11048
- elements: {
11049
- line: {
11050
- fill: false, // do not fill the area under line charts
11051
- },
11052
- point: {
11053
- hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
11054
- },
11055
- },
11056
- animation: false,
11057
- };
11058
- function chartToImage(runtime, figure, type) {
11059
- // wrap the canvas in a div with a fixed size because chart.js would
11060
- // fill the whole page otherwise
11061
- const div = document.createElement("div");
11062
- div.style.width = `${figure.width}px`;
11063
- div.style.height = `${figure.height}px`;
11064
- const canvas = document.createElement("canvas");
11065
- div.append(canvas);
11066
- canvas.setAttribute("width", figure.width.toString());
11067
- canvas.setAttribute("height", figure.height.toString());
11068
- // we have to add the canvas to the DOM otherwise it won't be rendered
11069
- document.body.append(div);
11070
- if ("chartJsConfig" in runtime) {
11071
- const config = deepCopy(runtime.chartJsConfig);
11072
- config.plugins = [backgroundColorChartJSPlugin];
11073
- const Chart = getChartJSConstructor();
11074
- const chart = new Chart(canvas, config);
11075
- const imgContent = chart.toBase64Image();
11076
- chart.destroy();
11077
- div.remove();
11078
- return imgContent;
11079
- }
11080
- else if (type === "scorecard") {
11081
- const design = getScorecardConfiguration(figure, runtime);
11082
- drawScoreChart(design, canvas);
11083
- const imgContent = canvas.toDataURL();
11084
- div.remove();
11085
- return imgContent;
11086
- }
11087
- else if (type === "gauge") {
11088
- drawGaugeChart(canvas, runtime);
11089
- const imgContent = canvas.toDataURL();
11090
- div.remove();
11091
- return imgContent;
11092
- }
11093
- return undefined;
11094
- }
11095
- /**
11096
- * Custom chart.js plugin to set the background color of the canvas
11097
- * https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md
11098
- */
11099
- const backgroundColorChartJSPlugin = {
11100
- id: "customCanvasBackgroundColor",
11101
- beforeDraw: (chart) => {
11102
- const { ctx } = chart;
11103
- ctx.save();
11104
- ctx.globalCompositeOperation = "destination-over";
11105
- ctx.fillStyle = "#ffffff";
11106
- ctx.fillRect(0, 0, chart.width, chart.height);
11107
- ctx.restore();
11108
- },
11109
- };
11110
- /** Return window.Chart, making sure all our extensions are loaded in ChartJS */
11111
- function getChartJSConstructor() {
11112
- if (window.Chart && !window.Chart?.registry.plugins.get("chartShowValuesPlugin")) {
11113
- window.Chart.register(chartShowValuesPlugin);
11114
- window.Chart.register(waterfallLinesPlugin);
11115
- }
11116
- return window.Chart;
11117
- }
11118
-
11119
- /**
11120
- * This file is largely inspired by owl 1.
11121
- * `css` tag has been removed from owl 2 without workaround to manage css.
11122
- * So, the solution was to import the behavior of owl 1 directly in our
11123
- * codebase, with one difference: the css is added to the sheet as soon as the
11124
- * css tag is executed. In owl 1, the css was added as soon as a Component was
11125
- * created for the first time.
11126
- */
11127
- const STYLESHEETS = {};
11128
- let nextId = 0;
11129
- /**
11130
- * CSS tag helper for defining inline stylesheets. With this, one can simply define
11131
- * an inline stylesheet with just the following code:
11132
- * ```js
11133
- * css`.component-a { color: red; }`;
11134
- * ```
11135
- */
11136
- function css(strings, ...args) {
11137
- const name = `__sheet__${nextId++}`;
11138
- const value = String.raw(strings, ...args);
11139
- registerSheet(name, value);
11140
- activateSheet(name);
11141
- return name;
11142
- }
11143
- function processSheet(str) {
11144
- const tokens = str.split(/(\{|\}|;)/).map((s) => s.trim());
11145
- const selectorStack = [];
11146
- const parts = [];
11147
- let rules = [];
11148
- function generateSelector(stackIndex, parentSelector) {
11149
- const parts = [];
11150
- for (const selector of selectorStack[stackIndex]) {
11151
- let part = (parentSelector && parentSelector + " " + selector) || selector;
11152
- if (part.includes("&")) {
11153
- part = selector.replace(/&/g, parentSelector || "");
11154
- }
11155
- if (stackIndex < selectorStack.length - 1) {
11156
- part = generateSelector(stackIndex + 1, part);
11157
- }
11158
- parts.push(part);
11159
- }
11160
- return parts.join(", ");
11161
- }
11162
- function generateRules() {
11163
- if (rules.length) {
11164
- parts.push(generateSelector(0) + " {");
11165
- parts.push(...rules);
11166
- parts.push("}");
11167
- rules = [];
11168
- }
11169
- }
11170
- while (tokens.length) {
11171
- let token = tokens.shift();
11172
- if (token === "}") {
11173
- generateRules();
11174
- selectorStack.pop();
11175
- }
11176
- else {
11177
- if (tokens[0] === "{") {
11178
- generateRules();
11179
- selectorStack.push(token.split(/\s*,\s*/));
11180
- tokens.shift();
11181
- }
11182
- if (tokens[0] === ";") {
11183
- rules.push(" " + token + ";");
11184
- }
11185
- }
11186
- }
11187
- return parts.join("\n");
11188
- }
11189
- function registerSheet(id, css) {
11190
- const sheet = document.createElement("style");
11191
- sheet.textContent = processSheet(css);
11192
- STYLESHEETS[id] = sheet;
11193
- }
11194
- function activateSheet(id) {
11195
- const sheet = STYLESHEETS[id];
11196
- sheet.setAttribute("component", id);
11197
- document.head.appendChild(sheet);
11198
- }
11199
- function getTextDecoration({ strikethrough, underline, }) {
11200
- if (!strikethrough && !underline) {
11201
- return "none";
11202
- }
11203
- return `${strikethrough ? "line-through" : ""} ${underline ? "underline" : ""}`;
11204
- }
11205
- /**
11206
- * Convert the cell style to CSS properties.
11207
- */
11208
- function cellStyleToCss(style) {
11209
- const attributes = cellTextStyleToCss(style);
11210
- if (!style)
11211
- return attributes;
11212
- if (style.fillColor) {
11213
- attributes["background"] = style.fillColor;
11214
- }
11215
- return attributes;
11216
- }
11217
- /**
11218
- * Convert the cell text style to CSS properties.
11219
- */
11220
- function cellTextStyleToCss(style) {
11221
- const attributes = {};
11222
- if (!style)
11223
- return attributes;
11224
- if (style.bold) {
11225
- attributes["font-weight"] = "bold";
11226
- }
11227
- if (style.italic) {
11228
- attributes["font-style"] = "italic";
11229
- }
11230
- if (style.strikethrough || style.underline) {
11231
- let decoration = style.strikethrough ? "line-through" : "";
11232
- decoration = style.underline ? decoration + " underline" : decoration;
11233
- attributes["text-decoration"] = decoration;
11234
- }
11235
- if (style.textColor) {
11236
- attributes["color"] = style.textColor;
11237
- }
11238
- return attributes;
11239
- }
11240
- /**
11241
- * Transform CSS properties into a CSS string.
11242
- */
11243
- function cssPropertiesToCss(attributes) {
11244
- let styleStr = "";
11245
- for (const attName in attributes) {
11246
- if (!attributes[attName]) {
11247
- continue;
11248
- }
11249
- styleStr += `${attName}:${attributes[attName]}; `;
11250
- }
11251
- return styleStr;
11252
- }
11253
- function getElementMargins(el) {
11254
- const style = window.getComputedStyle(el);
11255
- return {
11256
- top: parseInt(style.marginTop, 10) || 0,
11257
- bottom: parseInt(style.marginBottom, 10) || 0,
11258
- left: parseInt(style.marginLeft, 10) || 0,
11259
- right: parseInt(style.marginRight, 10) || 0,
11260
- };
11261
- }
11262
-
11263
- css /* scss */ `
11264
- .o-spreadsheet {
11265
- .o-chart-custom-tooltip {
11266
- font-size: 12px;
11267
- background-color: #fff;
11268
- z-index: ${ComponentsImportance.FigureTooltip};
11269
- }
11270
- }
11271
- `;
11272
- class ChartJsComponent extends owl.Component {
11273
- static template = "o-spreadsheet-ChartJsComponent";
11274
- static props = {
11275
- figure: Object,
11276
- };
11277
- canvas = owl.useRef("graphContainer");
11278
- chart;
11279
- currentRuntime;
11280
- get background() {
11281
- return this.chartRuntime.background;
11282
- }
11283
- get canvasStyle() {
11284
- return `background-color: ${this.background}`;
11285
- }
11286
- get chartRuntime() {
11287
- const runtime = this.env.model.getters.getChartRuntime(this.props.figure.id);
11288
- if (!("chartJsConfig" in runtime)) {
11289
- throw new Error("Unsupported chart runtime");
11290
- }
11291
- return runtime;
11292
- }
11293
- setup() {
11294
- owl.onMounted(() => {
11295
- const runtime = this.chartRuntime;
11296
- this.currentRuntime = runtime;
11297
- // Note: chartJS modify the runtime in place, so it's important to give it a copy
11298
- this.createChart(deepCopy(runtime.chartJsConfig));
11299
- });
11300
- owl.onWillUnmount(() => this.chart?.destroy());
11301
- owl.useEffect(() => {
11302
- const runtime = this.chartRuntime;
11303
- if (runtime !== this.currentRuntime) {
11304
- if (runtime.chartJsConfig.type !== this.currentRuntime.chartJsConfig.type) {
11305
- this.chart?.destroy();
11306
- this.createChart(deepCopy(runtime.chartJsConfig));
11307
- }
11308
- else {
11309
- this.updateChartJs(deepCopy(runtime));
11310
- }
11311
- this.currentRuntime = runtime;
11312
- }
11313
- });
11314
- }
11315
- createChart(chartData) {
11316
- const canvas = this.canvas.el;
11317
- const ctx = canvas.getContext("2d");
11318
- const Chart = getChartJSConstructor();
11319
- this.chart = new Chart(ctx, chartData);
11320
- }
11321
- updateChartJs(chartRuntime) {
11322
- const chartData = chartRuntime.chartJsConfig;
11323
- if (chartData.data && chartData.data.datasets) {
11324
- this.chart.data = chartData.data;
11325
- if (chartData.options?.plugins?.title) {
11326
- this.chart.config.options.plugins.title = chartData.options.plugins.title;
11327
- }
11328
- }
11329
- else {
11330
- this.chart.data.datasets = [];
11331
- }
11332
- this.chart.config.options = chartData.options;
11333
- this.chart.update();
11334
- }
11335
- }
11336
-
11337
10951
  class ScorecardChart extends owl.Component {
11338
10952
  static template = "o-spreadsheet-ScorecardChart";
11339
10953
  static props = {
@@ -22931,6 +22545,343 @@ stores.inject(MyMetaStore, storeInstance);
22931
22545
 
22932
22546
  const cellPopoverRegistry = new Registry();
22933
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
+
22934
22885
  class GaugeChartComponent extends owl.Component {
22935
22886
  static template = "o-spreadsheet-GaugeChartComponent";
22936
22887
  canvas = owl.useRef("chartContainer");
@@ -22963,6 +22914,73 @@ stores.inject(MyMetaStore, storeInstance);
22963
22914
  return color;
22964
22915
  }
22965
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
+
22966
22984
  /**
22967
22985
  * Represent a raw XML string
22968
22986
  */
@@ -34233,7 +34251,6 @@ stores.inject(MyMetaStore, storeInstance);
34233
34251
  duplicateLabelRangeInDuplicatedSheet: duplicateLabelRangeInDuplicatedSheet,
34234
34252
  formatChartDatasetValue: formatChartDatasetValue,
34235
34253
  formatTickValue: formatTickValue,
34236
- getChartJSConstructor: getChartJSConstructor,
34237
34254
  getChartPositionAtCenterOfViewport: getChartPositionAtCenterOfViewport,
34238
34255
  getDefinedAxis: getDefinedAxis,
34239
34256
  getPieColors: getPieColors,
@@ -45081,7 +45098,8 @@ stores.inject(MyMetaStore, storeInstance);
45081
45098
  &.pivot-dimension-invalid {
45082
45099
  background-color: #ffdddd;
45083
45100
  border-color: red !important;
45084
- select {
45101
+ select,
45102
+ input {
45085
45103
  background-color: #ffdddd;
45086
45104
  }
45087
45105
  }
@@ -46942,7 +46960,7 @@ stores.inject(MyMetaStore, storeInstance);
46942
46960
  this.notification.notifyUser({
46943
46961
  type: "info",
46944
46962
  text: _t("Pivot updates only work with dynamic pivot tables. Use %s or re-insert the static pivot from the Data menu.", pivotExample),
46945
- sticky: false,
46963
+ sticky: true,
46946
46964
  });
46947
46965
  }
46948
46966
  }
@@ -75889,6 +75907,7 @@ stores.inject(MyMetaStore, storeInstance);
75889
75907
  supportedPivotPositionalFormulaRegistry,
75890
75908
  pivotToFunctionValueRegistry,
75891
75909
  migrationStepRegistry,
75910
+ chartJsExtensionRegistry,
75892
75911
  };
75893
75912
  const helpers = {
75894
75913
  arg,
@@ -76089,9 +76108,9 @@ stores.inject(MyMetaStore, storeInstance);
76089
76108
  exports.tokenize = tokenize;
76090
76109
 
76091
76110
 
76092
- __info__.version = "18.2.5";
76093
- __info__.date = "2025-03-26T12:47:44.113Z";
76094
- __info__.hash = "4675edd";
76111
+ __info__.version = "18.2.6";
76112
+ __info__.date = "2025-04-04T08:41:26.115Z";
76113
+ __info__.hash = "faa00e2";
76095
76114
 
76096
76115
 
76097
76116
  })(this.o_spreadsheet = this.o_spreadsheet || {}, owl);