@odoo/o-spreadsheet 18.1.13 → 18.1.14

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.1.13
6
- * @date 2025-03-26T12:48:31.680Z
7
- * @hash 45ec54c
5
+ * @version 18.1.14
6
+ * @date 2025-04-04T08:42:40.149Z
7
+ * @hash 63b2fb7
8
8
  */
9
9
 
10
10
  (function (exports, owl) {
@@ -804,8 +804,7 @@
804
804
  *
805
805
  * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Character_Classes
806
806
  */
807
- const whiteSpaceSpecialCharacters = [
808
- " ",
807
+ const specialWhiteSpaceSpecialCharacters = [
809
808
  "\t",
810
809
  "\f",
811
810
  "\v",
@@ -820,7 +819,7 @@
820
819
  String.fromCharCode(parseInt("3000", 16)),
821
820
  String.fromCharCode(parseInt("feff", 16)),
822
821
  ];
823
- const whiteSpaceRegexp = new RegExp(whiteSpaceSpecialCharacters.join("|"), "g");
822
+ const specialWhiteSpaceRegexp = new RegExp(specialWhiteSpaceSpecialCharacters.join("|"), "g");
824
823
  const newLineRegexp = /(\r\n|\r)/g;
825
824
  /**
826
825
  * Replace all different newlines characters by \n
@@ -6789,8 +6788,12 @@
6789
6788
  str = replaceNewLines(str);
6790
6789
  const chars = new TokenizingChars(str);
6791
6790
  const result = [];
6791
+ const tokenizeSpace = specialWhiteSpaceRegexp.test(str)
6792
+ ? tokenizeSpecialCharacterSpace
6793
+ : tokenizeSimpleSpace;
6792
6794
  while (!chars.isOver()) {
6793
- let token = tokenizeSpace(chars) ||
6795
+ let token = tokenizeNewLine(chars) ||
6796
+ tokenizeSpace(chars) ||
6794
6797
  tokenizeArgsSeparator(chars, locale) ||
6795
6798
  tokenizeParenthesis(chars) ||
6796
6799
  tokenizeOperator(chars) ||
@@ -6924,17 +6927,19 @@
6924
6927
  }
6925
6928
  return null;
6926
6929
  }
6927
- function tokenizeSpace(chars) {
6928
- let length = 0;
6929
- while (chars.current === NEWLINE) {
6930
- length++;
6931
- chars.shift();
6930
+ function tokenizeSpecialCharacterSpace(chars) {
6931
+ let spaces = "";
6932
+ while (chars.current === " " || (chars.current && chars.current.match(specialWhiteSpaceRegexp))) {
6933
+ spaces += chars.shift();
6932
6934
  }
6933
- if (length) {
6934
- return { type: "SPACE", value: NEWLINE.repeat(length) };
6935
+ if (spaces) {
6936
+ return { type: "SPACE", value: spaces };
6935
6937
  }
6938
+ return null;
6939
+ }
6940
+ function tokenizeSimpleSpace(chars) {
6936
6941
  let spaces = "";
6937
- while (chars.current && chars.current.match(whiteSpaceRegexp)) {
6942
+ while (chars.current === " ") {
6938
6943
  spaces += chars.shift();
6939
6944
  }
6940
6945
  if (spaces) {
@@ -6942,6 +6947,17 @@
6942
6947
  }
6943
6948
  return null;
6944
6949
  }
6950
+ function tokenizeNewLine(chars) {
6951
+ let length = 0;
6952
+ while (chars.current === NEWLINE) {
6953
+ length++;
6954
+ chars.shift();
6955
+ }
6956
+ if (length) {
6957
+ return { type: "SPACE", value: NEWLINE.repeat(length) };
6958
+ }
6959
+ return null;
6960
+ }
6945
6961
  function tokenizeInvalidRange(chars) {
6946
6962
  if (chars.currentStartsWith(CellErrorType.InvalidReference)) {
6947
6963
  chars.advanceBy(CellErrorType.InvalidReference.length);
@@ -9567,6 +9583,15 @@ stores.inject(MyMetaStore, storeInstance);
9567
9583
  }
9568
9584
  }
9569
9585
 
9586
+ const chartJsExtensionRegistry = new Registry();
9587
+ /** Return window.Chart, making sure all our extensions are loaded in ChartJS */
9588
+ function getChartJSConstructor() {
9589
+ if (window.Chart && !window.Chart?.registry.plugins.get("chartShowValuesPlugin")) {
9590
+ window.Chart.register(...chartJsExtensionRegistry.getAll());
9591
+ }
9592
+ return window.Chart;
9593
+ }
9594
+
9570
9595
  const TREND_LINE_XAXIS_ID = "x1";
9571
9596
  const MOVING_AVERAGE_TREND_LINE_XAXIS_ID = "xMovingAverage";
9572
9597
  /**
@@ -10105,341 +10130,70 @@ stores.inject(MyMetaStore, storeInstance);
10105
10130
  return bars.find((bar, i) => i > startIndex && bar.height !== 0);
10106
10131
  }
10107
10132
 
10108
- const GAUGE_PADDING_SIDE = 30;
10109
- const GAUGE_PADDING_TOP = 10;
10110
- const GAUGE_PADDING_BOTTOM = 20;
10111
- const GAUGE_LABELS_FONT_SIZE = 12;
10112
- const GAUGE_DEFAULT_VALUE_FONT_SIZE = 80;
10113
- const GAUGE_BACKGROUND_COLOR = "#F3F2F1";
10114
- const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
10115
- const GAUGE_TITLE_SECTION_HEIGHT = 25;
10116
- function drawGaugeChart(canvas, runtime) {
10117
- const canvasBoundingRect = canvas.getBoundingClientRect();
10118
- canvas.width = canvasBoundingRect.width;
10119
- canvas.height = canvasBoundingRect.height;
10120
- const ctx = canvas.getContext("2d");
10121
- const config = getGaugeRenderingConfig(canvasBoundingRect, runtime, ctx);
10122
- drawBackground(ctx, config);
10123
- drawGauge(ctx, config);
10124
- drawInflectionValues(ctx, config);
10125
- drawLabels(ctx, config);
10126
- drawTitle(ctx, config);
10127
- }
10128
- function drawGauge(ctx, config) {
10129
- ctx.save();
10130
- const gauge = config.gauge;
10131
- const arcCenterX = gauge.rect.x + gauge.rect.width / 2;
10132
- const arcCenterY = gauge.rect.y + gauge.rect.height;
10133
- const arcRadius = gauge.rect.height - gauge.arcWidth / 2;
10134
- if (arcRadius < 0) {
10135
- return;
10136
- }
10137
- const gaugeAngle = gauge.percentage === 1 ? 0 : Math.PI * (1 + gauge.percentage);
10138
- // Gauge background
10139
- ctx.strokeStyle = GAUGE_BACKGROUND_COLOR;
10140
- ctx.beginPath();
10141
- ctx.lineWidth = gauge.arcWidth;
10142
- ctx.arc(arcCenterX, arcCenterY, arcRadius, gaugeAngle, 0);
10143
- ctx.stroke();
10144
- // Gauge value
10145
- ctx.strokeStyle = gauge.color;
10146
- ctx.beginPath();
10147
- ctx.arc(arcCenterX, arcCenterY, arcRadius, Math.PI, gaugeAngle);
10148
- ctx.stroke();
10149
- ctx.restore();
10150
- }
10151
- function drawBackground(ctx, config) {
10152
- ctx.save();
10153
- ctx.fillStyle = config.backgroundColor;
10154
- ctx.fillRect(0, 0, config.width, config.height);
10155
- ctx.restore();
10156
- }
10157
- function drawLabels(ctx, config) {
10158
- for (const label of [config.minLabel, config.maxLabel, config.gaugeValue]) {
10159
- ctx.save();
10160
- ctx.textAlign = "center";
10161
- ctx.fillStyle = label.color;
10162
- ctx.font = `${label.fontSize}px ${DEFAULT_FONT}`;
10163
- ctx.fillText(label.label, label.textPosition.x, label.textPosition.y);
10164
- ctx.restore();
10165
- }
10166
- }
10167
- function drawInflectionValues(ctx, config) {
10168
- const { x: rectX, y: rectY, width, height } = config.gauge.rect;
10169
- for (const inflectionValue of config.inflectionValues) {
10170
- ctx.save();
10171
- ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment
10172
- ctx.rotate(Math.PI / 2 - inflectionValue.rotation);
10173
- ctx.lineWidth = 2;
10174
- ctx.strokeStyle = chartMutedFontColor(config.backgroundColor) + "aa";
10175
- ctx.beginPath();
10176
- ctx.moveTo(0, -(height - config.gauge.arcWidth));
10177
- ctx.lineTo(0, -height - 3);
10178
- ctx.stroke();
10179
- ctx.textAlign = "center";
10180
- ctx.font = `${inflectionValue.fontSize}px ${DEFAULT_FONT}`;
10181
- ctx.fillStyle = inflectionValue.color;
10182
- const textY = -height - GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN - inflectionValue.offset;
10183
- ctx.fillText(inflectionValue.label, 0, textY);
10184
- ctx.restore();
10185
- }
10186
- }
10187
- function drawTitle(ctx, config) {
10188
- ctx.save();
10189
- const title = config.title;
10190
- ctx.font = getDefaultContextFont(title.fontSize, title.bold, title.italic);
10191
- ctx.textBaseline = "middle";
10192
- ctx.fillStyle = title.color;
10193
- ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
10194
- ctx.restore();
10195
- }
10196
- function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
10197
- const maxValue = runtime.maxValue;
10198
- const minValue = runtime.minValue;
10199
- const gaugeValue = runtime.gaugeValue;
10200
- const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
10201
- const gaugeArcWidth = gaugeRect.width / 6;
10202
- const gaugePercentage = gaugeValue
10203
- ? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
10204
- : 0;
10205
- const gaugeValuePosition = {
10206
- x: boundingRect.width / 2,
10207
- y: gaugeRect.y + gaugeRect.height - gaugeRect.height / 12,
10208
- };
10209
- let gaugeValueFontSize = GAUGE_DEFAULT_VALUE_FONT_SIZE;
10210
- // Scale down the font size if the gaugeRect is too small
10211
- if (gaugeRect.height < 300) {
10212
- gaugeValueFontSize = gaugeValueFontSize * (gaugeRect.height / 300);
10213
- }
10214
- // Scale down the font size if the text is too long
10215
- const maxTextWidth = gaugeRect.width / 2;
10216
- const gaugeLabel = gaugeValue?.label || "-";
10217
- if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
10218
- gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
10219
- }
10220
- const minLabelPosition = {
10221
- x: gaugeRect.x + gaugeArcWidth / 2,
10222
- y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
10223
- };
10224
- const maxLabelPosition = {
10225
- x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
10226
- y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
10133
+ chartJsExtensionRegistry.add("chartShowValuesPlugin", chartShowValuesPlugin);
10134
+ chartJsExtensionRegistry.add("waterfallLinesPlugin", waterfallLinesPlugin);
10135
+ class ChartJsComponent extends owl.Component {
10136
+ static template = "o-spreadsheet-ChartJsComponent";
10137
+ static props = {
10138
+ figure: Object,
10227
10139
  };
10228
- const textColor = chartMutedFontColor(runtime.background);
10229
- const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);
10230
- let x = 0, titleWidth = 0, titleHeight = 0;
10231
- if (runtime.title.text) {
10232
- ({ width: titleWidth, height: titleHeight } = computeTextDimension(ctx, runtime.title.text, { fontSize: CHART_TITLE_FONT_SIZE, ...runtime.title }, "px"));
10233
- }
10234
- switch (runtime.title.align) {
10235
- case "right":
10236
- x = boundingRect.width - titleWidth - CHART_PADDING$1;
10237
- break;
10238
- case "center":
10239
- x = (boundingRect.width - titleWidth) / 2;
10240
- break;
10241
- case "left":
10242
- default:
10243
- x = CHART_PADDING$1;
10244
- break;
10140
+ canvas = owl.useRef("graphContainer");
10141
+ chart;
10142
+ currentRuntime;
10143
+ get background() {
10144
+ return this.chartRuntime.background;
10245
10145
  }
10246
- return {
10247
- width: boundingRect.width,
10248
- height: boundingRect.height,
10249
- title: {
10250
- label: runtime.title.text ?? "",
10251
- fontSize: runtime.title.fontSize ?? CHART_TITLE_FONT_SIZE,
10252
- textPosition: {
10253
- x,
10254
- y: CHART_PADDING_TOP + titleHeight / 2,
10255
- },
10256
- color: runtime.title.color ?? textColor,
10257
- bold: runtime.title.bold,
10258
- italic: runtime.title.italic,
10259
- },
10260
- backgroundColor: runtime.background,
10261
- gauge: {
10262
- rect: gaugeRect,
10263
- arcWidth: gaugeArcWidth,
10264
- percentage: clip(gaugePercentage, 0, 1),
10265
- color: getGaugeColor(runtime),
10266
- },
10267
- inflectionValues,
10268
- gaugeValue: {
10269
- label: gaugeLabel,
10270
- textPosition: gaugeValuePosition,
10271
- fontSize: gaugeValueFontSize,
10272
- color: textColor,
10273
- },
10274
- minLabel: {
10275
- label: runtime.minValue.label,
10276
- textPosition: minLabelPosition,
10277
- fontSize: GAUGE_LABELS_FONT_SIZE,
10278
- color: textColor,
10279
- },
10280
- maxLabel: {
10281
- label: runtime.maxValue.label,
10282
- textPosition: maxLabelPosition,
10283
- fontSize: GAUGE_LABELS_FONT_SIZE,
10284
- color: textColor,
10285
- },
10286
- };
10287
- }
10288
- /**
10289
- * Get the rectangle in which the gauge will be drawn, based on the bounding rectangle of the canvas and leaving
10290
- * space for the title and labels.
10291
- */
10292
- function getGaugeRect(boundingRect, title) {
10293
- const titleHeight = title ? GAUGE_TITLE_SECTION_HEIGHT : 0;
10294
- const drawHeight = boundingRect.height - GAUGE_PADDING_BOTTOM - titleHeight - GAUGE_PADDING_TOP;
10295
- const drawWidth = boundingRect.width - GAUGE_PADDING_SIDE * 2;
10296
- let gaugeWidth;
10297
- let gaugeHeight;
10298
- if (drawWidth > 2 * drawHeight) {
10299
- gaugeWidth = 2 * drawHeight;
10300
- gaugeHeight = drawHeight;
10146
+ get canvasStyle() {
10147
+ return `background-color: ${this.background}`;
10301
10148
  }
10302
- else {
10303
- gaugeWidth = drawWidth;
10304
- gaugeHeight = drawWidth / 2;
10149
+ get chartRuntime() {
10150
+ const runtime = this.env.model.getters.getChartRuntime(this.props.figure.id);
10151
+ if (!("chartJsConfig" in runtime)) {
10152
+ throw new Error("Unsupported chart runtime");
10153
+ }
10154
+ return runtime;
10305
10155
  }
10306
- const gaugeX = GAUGE_PADDING_SIDE + (drawWidth - gaugeWidth) / 2;
10307
- const gaugeY = titleHeight + GAUGE_PADDING_TOP + (drawHeight - gaugeHeight) / 2;
10308
- return {
10309
- x: gaugeX,
10310
- y: gaugeY,
10311
- width: gaugeWidth,
10312
- height: gaugeHeight,
10313
- };
10314
- }
10315
- /**
10316
- * 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).
10317
- *
10318
- * Also compute an offset for the text so that it doesn't overlap with other text.
10319
- */
10320
- function getInflectionValues(runtime, gaugeRect, textColor, ctx) {
10321
- const maxValue = runtime.maxValue;
10322
- const minValue = runtime.minValue;
10323
- const gaugeCircleCenter = {
10324
- x: gaugeRect.x + gaugeRect.width / 2,
10325
- y: gaugeRect.y + gaugeRect.height,
10326
- };
10327
- const textStyle = { fontSize: GAUGE_LABELS_FONT_SIZE };
10328
- const inflectionValues = [];
10329
- const inflectionValuesTextRects = [];
10330
- for (const inflectionValue of runtime.inflectionValues) {
10331
- const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
10332
- const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
10333
- const angle = Math.PI - Math.PI * percentage;
10334
- const textRect = getRectangleTangentToCircle(angle, // angle between X axis and the point where the rectangle is tangent to the circle
10335
- gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, // radius of the gauge circle + margin below text
10336
- gaugeCircleCenter.x, // center of the gauge circle
10337
- gaugeCircleCenter.y, // center of the gauge circle
10338
- labelWidth + 2, // width of the text + some margin
10339
- GAUGE_LABELS_FONT_SIZE // height of the text
10340
- );
10341
- let offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect))
10342
- ? GAUGE_LABELS_FONT_SIZE
10343
- : 0;
10344
- inflectionValuesTextRects.push(textRect);
10345
- inflectionValues.push({
10346
- rotation: angle,
10347
- label: inflectionValue.label,
10348
- fontSize: GAUGE_LABELS_FONT_SIZE,
10349
- color: textColor,
10350
- offset,
10156
+ setup() {
10157
+ owl.onMounted(() => {
10158
+ const runtime = this.chartRuntime;
10159
+ this.currentRuntime = runtime;
10160
+ // Note: chartJS modify the runtime in place, so it's important to give it a copy
10161
+ this.createChart(deepCopy(runtime.chartJsConfig));
10162
+ });
10163
+ owl.onWillUnmount(() => this.chart?.destroy());
10164
+ owl.useEffect(() => {
10165
+ const runtime = this.chartRuntime;
10166
+ if (runtime !== this.currentRuntime) {
10167
+ if (runtime.chartJsConfig.type !== this.currentRuntime.chartJsConfig.type) {
10168
+ this.chart?.destroy();
10169
+ this.createChart(deepCopy(runtime.chartJsConfig));
10170
+ }
10171
+ else {
10172
+ this.updateChartJs(deepCopy(runtime.chartJsConfig));
10173
+ }
10174
+ this.currentRuntime = runtime;
10175
+ }
10351
10176
  });
10352
10177
  }
10353
- return inflectionValues;
10354
- }
10355
- function getGaugeColor(runtime) {
10356
- const gaugeValue = runtime.gaugeValue?.value;
10357
- if (gaugeValue === undefined) {
10358
- return GAUGE_BACKGROUND_COLOR;
10359
- }
10360
- for (let i = 0; i < runtime.inflectionValues.length; i++) {
10361
- const inflectionValue = runtime.inflectionValues[i];
10362
- if (inflectionValue.operator === "<" && gaugeValue < inflectionValue.value) {
10363
- return runtime.colors[i];
10364
- }
10365
- else if (inflectionValue.operator === "<=" && gaugeValue <= inflectionValue.value) {
10366
- return runtime.colors[i];
10367
- }
10368
- }
10369
- return runtime.colors.at(-1);
10370
- }
10371
- function getSegmentsOfRectangle(rectangle) {
10372
- return [
10373
- { start: rectangle.topLeft, end: rectangle.topRight },
10374
- { start: rectangle.topRight, end: rectangle.bottomRight },
10375
- { start: rectangle.bottomRight, end: rectangle.bottomLeft },
10376
- { start: rectangle.bottomLeft, end: rectangle.topLeft },
10377
- ];
10378
- }
10379
- /**
10380
- * Check if two segment intersect. The case where the segments are colinear (both segments on the same line)
10381
- * is not handled.
10382
- */
10383
- function doSegmentIntersect(segment1, segment2) {
10384
- const A = segment1.start;
10385
- const B = segment1.end;
10386
- const C = segment2.start;
10387
- const D = segment2.end;
10388
- /**
10389
- * Line segment intersection algorithm
10390
- * https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
10391
- */
10392
- function ccw(a, b, c) {
10393
- return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
10178
+ createChart(chartData) {
10179
+ const canvas = this.canvas.el;
10180
+ const ctx = canvas.getContext("2d");
10181
+ const Chart = getChartJSConstructor();
10182
+ this.chart = new Chart(ctx, chartData);
10394
10183
  }
10395
- return ccw(A, C, D) !== ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D);
10396
- }
10397
- function doRectanglesIntersect(rect1, rect2) {
10398
- const segments1 = getSegmentsOfRectangle(rect1);
10399
- const segments2 = getSegmentsOfRectangle(rect2);
10400
- for (const segment1 of segments1) {
10401
- for (const segment2 of segments2) {
10402
- if (doSegmentIntersect(segment1, segment2)) {
10403
- return true;
10184
+ updateChartJs(chartData) {
10185
+ if (chartData.data && chartData.data.datasets) {
10186
+ this.chart.data = chartData.data;
10187
+ if (chartData.options?.plugins?.title) {
10188
+ this.chart.config.options.plugins.title = chartData.options.plugins.title;
10404
10189
  }
10405
10190
  }
10191
+ else {
10192
+ this.chart.data.datasets = [];
10193
+ }
10194
+ this.chart.config.options = chartData.options;
10195
+ this.chart.update();
10406
10196
  }
10407
- return false;
10408
- }
10409
- /**
10410
- * Get the rectangle that is tangent to a circle at a given angle.
10411
- *
10412
- * @param angle angle between X axis and the point where the rectangle is tangent to the circle
10413
- */
10414
- function getRectangleTangentToCircle(angle, radius, circleCenterX, circleCenterY, rectWidth, rectHeight) {
10415
- const cos = Math.cos(angle);
10416
- const sin = Math.sin(angle);
10417
- // x, y are the distance from the center of the circle to the point where the rectangle is tangent to the circle
10418
- const x = cos * radius;
10419
- const y = sin * radius;
10420
- // x2, y2 are the distance from the point the rectangle is tangent to the circle to the bottom left corner of the rectangle
10421
- const x2 = sin * (rectWidth / 2); // cos(angle + 90°) = sin(angle)
10422
- const y2 = cos * (rectWidth / 2);
10423
- const bottomRight = {
10424
- x: x + x2 + circleCenterX,
10425
- y: circleCenterY - (y - y2),
10426
- };
10427
- const bottomLeft = {
10428
- x: x - x2 + circleCenterX,
10429
- y: circleCenterY - (y + y2),
10430
- };
10431
- // Same as above but for the top corners of the rectangle (radius + rectangle height instead of radius)
10432
- const xp = cos * (radius + rectHeight);
10433
- const yp = sin * (radius + rectHeight);
10434
- const topLeft = {
10435
- x: xp - x2 + circleCenterX,
10436
- y: circleCenterY - (yp + y2),
10437
- };
10438
- const topRight = {
10439
- x: xp + x2 + circleCenterX,
10440
- y: circleCenterY - (yp - y2),
10441
- };
10442
- return { bottomLeft, bottomRight, topRight, topLeft };
10443
10197
  }
10444
10198
 
10445
10199
  /**
@@ -11021,155 +10775,6 @@ stores.inject(MyMetaStore, storeInstance);
11021
10775
  }
11022
10776
  }
11023
10777
 
11024
- const CHART_COMMON_OPTIONS = {
11025
- // https://www.chartjs.org/docs/latest/general/responsive.html
11026
- responsive: true, // will resize when its container is resized
11027
- maintainAspectRatio: false, // doesn't maintain the aspect ratio (width/height =2 by default) so the user has the choice of the exact layout
11028
- elements: {
11029
- line: {
11030
- fill: false, // do not fill the area under line charts
11031
- },
11032
- point: {
11033
- hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
11034
- },
11035
- },
11036
- animation: false,
11037
- };
11038
- function truncateLabel(label) {
11039
- if (!label) {
11040
- return "";
11041
- }
11042
- if (label.length > MAX_CHAR_LABEL) {
11043
- return label.substring(0, MAX_CHAR_LABEL) + "…";
11044
- }
11045
- return label;
11046
- }
11047
- function chartToImage(runtime, figure, type) {
11048
- // wrap the canvas in a div with a fixed size because chart.js would
11049
- // fill the whole page otherwise
11050
- const div = document.createElement("div");
11051
- div.style.width = `${figure.width}px`;
11052
- div.style.height = `${figure.height}px`;
11053
- const canvas = document.createElement("canvas");
11054
- div.append(canvas);
11055
- canvas.setAttribute("width", figure.width.toString());
11056
- canvas.setAttribute("height", figure.height.toString());
11057
- // we have to add the canvas to the DOM otherwise it won't be rendered
11058
- document.body.append(div);
11059
- if ("chartJsConfig" in runtime) {
11060
- const config = deepCopy(runtime.chartJsConfig);
11061
- config.plugins = [backgroundColorChartJSPlugin];
11062
- const Chart = getChartJSConstructor();
11063
- const chart = new Chart(canvas, config);
11064
- const imgContent = chart.toBase64Image();
11065
- chart.destroy();
11066
- div.remove();
11067
- return imgContent;
11068
- }
11069
- else if (type === "scorecard") {
11070
- const design = getScorecardConfiguration(figure, runtime);
11071
- drawScoreChart(design, canvas);
11072
- const imgContent = canvas.toDataURL();
11073
- div.remove();
11074
- return imgContent;
11075
- }
11076
- else if (type === "gauge") {
11077
- drawGaugeChart(canvas, runtime);
11078
- const imgContent = canvas.toDataURL();
11079
- div.remove();
11080
- return imgContent;
11081
- }
11082
- return undefined;
11083
- }
11084
- /**
11085
- * Custom chart.js plugin to set the background color of the canvas
11086
- * https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md
11087
- */
11088
- const backgroundColorChartJSPlugin = {
11089
- id: "customCanvasBackgroundColor",
11090
- beforeDraw: (chart) => {
11091
- const { ctx } = chart;
11092
- ctx.save();
11093
- ctx.globalCompositeOperation = "destination-over";
11094
- ctx.fillStyle = "#ffffff";
11095
- ctx.fillRect(0, 0, chart.width, chart.height);
11096
- ctx.restore();
11097
- },
11098
- };
11099
- /** Return window.Chart, making sure all our extensions are loaded in ChartJS */
11100
- function getChartJSConstructor() {
11101
- if (window.Chart && !window.Chart?.registry.plugins.get("chartShowValuesPlugin")) {
11102
- window.Chart.register(chartShowValuesPlugin);
11103
- window.Chart.register(waterfallLinesPlugin);
11104
- }
11105
- return window.Chart;
11106
- }
11107
-
11108
- class ChartJsComponent extends owl.Component {
11109
- static template = "o-spreadsheet-ChartJsComponent";
11110
- static props = {
11111
- figure: Object,
11112
- };
11113
- canvas = owl.useRef("graphContainer");
11114
- chart;
11115
- currentRuntime;
11116
- get background() {
11117
- return this.chartRuntime.background;
11118
- }
11119
- get canvasStyle() {
11120
- return `background-color: ${this.background}`;
11121
- }
11122
- get chartRuntime() {
11123
- const runtime = this.env.model.getters.getChartRuntime(this.props.figure.id);
11124
- if (!("chartJsConfig" in runtime)) {
11125
- throw new Error("Unsupported chart runtime");
11126
- }
11127
- return runtime;
11128
- }
11129
- setup() {
11130
- owl.onMounted(() => {
11131
- const runtime = this.chartRuntime;
11132
- this.currentRuntime = runtime;
11133
- // Note: chartJS modify the runtime in place, so it's important to give it a copy
11134
- this.createChart(deepCopy(runtime.chartJsConfig));
11135
- });
11136
- owl.onWillUnmount(() => this.chart?.destroy());
11137
- owl.useEffect(() => {
11138
- const runtime = this.chartRuntime;
11139
- if (runtime !== this.currentRuntime) {
11140
- if (runtime.chartJsConfig.type !== this.currentRuntime.chartJsConfig.type) {
11141
- this.chart?.destroy();
11142
- this.createChart(deepCopy(runtime.chartJsConfig));
11143
- }
11144
- else {
11145
- this.updateChartJs(deepCopy(runtime));
11146
- }
11147
- this.currentRuntime = runtime;
11148
- }
11149
- });
11150
- }
11151
- createChart(chartData) {
11152
- const canvas = this.canvas.el;
11153
- const ctx = canvas.getContext("2d");
11154
- const Chart = getChartJSConstructor();
11155
- this.chart = new Chart(ctx, chartData);
11156
- }
11157
- updateChartJs(chartRuntime) {
11158
- const chartData = chartRuntime.chartJsConfig;
11159
- if (chartData.data && chartData.data.datasets) {
11160
- this.chart.data = chartData.data;
11161
- if (chartData.options?.plugins?.title) {
11162
- this.chart.config.options.plugins.title = chartData.options.plugins.title;
11163
- }
11164
- }
11165
- else {
11166
- this.chart.data.datasets = [];
11167
- }
11168
- this.chart.config.options = chartData.options;
11169
- this.chart.update();
11170
- }
11171
- }
11172
-
11173
10778
  class ScorecardChart extends owl.Component {
11174
10779
  static template = "o-spreadsheet-ScorecardChart";
11175
10780
  static props = {
@@ -22767,6 +22372,343 @@ stores.inject(MyMetaStore, storeInstance);
22767
22372
 
22768
22373
  const cellPopoverRegistry = new Registry();
22769
22374
 
22375
+ const GAUGE_PADDING_SIDE = 30;
22376
+ const GAUGE_PADDING_TOP = 10;
22377
+ const GAUGE_PADDING_BOTTOM = 20;
22378
+ const GAUGE_LABELS_FONT_SIZE = 12;
22379
+ const GAUGE_DEFAULT_VALUE_FONT_SIZE = 80;
22380
+ const GAUGE_BACKGROUND_COLOR = "#F3F2F1";
22381
+ const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
22382
+ const GAUGE_TITLE_SECTION_HEIGHT = 25;
22383
+ function drawGaugeChart(canvas, runtime) {
22384
+ const canvasBoundingRect = canvas.getBoundingClientRect();
22385
+ canvas.width = canvasBoundingRect.width;
22386
+ canvas.height = canvasBoundingRect.height;
22387
+ const ctx = canvas.getContext("2d");
22388
+ const config = getGaugeRenderingConfig(canvasBoundingRect, runtime, ctx);
22389
+ drawBackground(ctx, config);
22390
+ drawGauge(ctx, config);
22391
+ drawInflectionValues(ctx, config);
22392
+ drawLabels(ctx, config);
22393
+ drawTitle(ctx, config);
22394
+ }
22395
+ function drawGauge(ctx, config) {
22396
+ ctx.save();
22397
+ const gauge = config.gauge;
22398
+ const arcCenterX = gauge.rect.x + gauge.rect.width / 2;
22399
+ const arcCenterY = gauge.rect.y + gauge.rect.height;
22400
+ const arcRadius = gauge.rect.height - gauge.arcWidth / 2;
22401
+ if (arcRadius < 0) {
22402
+ return;
22403
+ }
22404
+ const gaugeAngle = gauge.percentage === 1 ? 0 : Math.PI * (1 + gauge.percentage);
22405
+ // Gauge background
22406
+ ctx.strokeStyle = GAUGE_BACKGROUND_COLOR;
22407
+ ctx.beginPath();
22408
+ ctx.lineWidth = gauge.arcWidth;
22409
+ ctx.arc(arcCenterX, arcCenterY, arcRadius, gaugeAngle, 0);
22410
+ ctx.stroke();
22411
+ // Gauge value
22412
+ ctx.strokeStyle = gauge.color;
22413
+ ctx.beginPath();
22414
+ ctx.arc(arcCenterX, arcCenterY, arcRadius, Math.PI, gaugeAngle);
22415
+ ctx.stroke();
22416
+ ctx.restore();
22417
+ }
22418
+ function drawBackground(ctx, config) {
22419
+ ctx.save();
22420
+ ctx.fillStyle = config.backgroundColor;
22421
+ ctx.fillRect(0, 0, config.width, config.height);
22422
+ ctx.restore();
22423
+ }
22424
+ function drawLabels(ctx, config) {
22425
+ for (const label of [config.minLabel, config.maxLabel, config.gaugeValue]) {
22426
+ ctx.save();
22427
+ ctx.textAlign = "center";
22428
+ ctx.fillStyle = label.color;
22429
+ ctx.font = `${label.fontSize}px ${DEFAULT_FONT}`;
22430
+ ctx.fillText(label.label, label.textPosition.x, label.textPosition.y);
22431
+ ctx.restore();
22432
+ }
22433
+ }
22434
+ function drawInflectionValues(ctx, config) {
22435
+ const { x: rectX, y: rectY, width, height } = config.gauge.rect;
22436
+ for (const inflectionValue of config.inflectionValues) {
22437
+ ctx.save();
22438
+ ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment
22439
+ ctx.rotate(Math.PI / 2 - inflectionValue.rotation);
22440
+ ctx.lineWidth = 2;
22441
+ ctx.strokeStyle = chartMutedFontColor(config.backgroundColor) + "aa";
22442
+ ctx.beginPath();
22443
+ ctx.moveTo(0, -(height - config.gauge.arcWidth));
22444
+ ctx.lineTo(0, -height - 3);
22445
+ ctx.stroke();
22446
+ ctx.textAlign = "center";
22447
+ ctx.font = `${inflectionValue.fontSize}px ${DEFAULT_FONT}`;
22448
+ ctx.fillStyle = inflectionValue.color;
22449
+ const textY = -height - GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN - inflectionValue.offset;
22450
+ ctx.fillText(inflectionValue.label, 0, textY);
22451
+ ctx.restore();
22452
+ }
22453
+ }
22454
+ function drawTitle(ctx, config) {
22455
+ ctx.save();
22456
+ const title = config.title;
22457
+ ctx.font = getDefaultContextFont(title.fontSize, title.bold, title.italic);
22458
+ ctx.textBaseline = "middle";
22459
+ ctx.fillStyle = title.color;
22460
+ ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
22461
+ ctx.restore();
22462
+ }
22463
+ function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
22464
+ const maxValue = runtime.maxValue;
22465
+ const minValue = runtime.minValue;
22466
+ const gaugeValue = runtime.gaugeValue;
22467
+ const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
22468
+ const gaugeArcWidth = gaugeRect.width / 6;
22469
+ const gaugePercentage = gaugeValue
22470
+ ? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
22471
+ : 0;
22472
+ const gaugeValuePosition = {
22473
+ x: boundingRect.width / 2,
22474
+ y: gaugeRect.y + gaugeRect.height - gaugeRect.height / 12,
22475
+ };
22476
+ let gaugeValueFontSize = GAUGE_DEFAULT_VALUE_FONT_SIZE;
22477
+ // Scale down the font size if the gaugeRect is too small
22478
+ if (gaugeRect.height < 300) {
22479
+ gaugeValueFontSize = gaugeValueFontSize * (gaugeRect.height / 300);
22480
+ }
22481
+ // Scale down the font size if the text is too long
22482
+ const maxTextWidth = gaugeRect.width / 2;
22483
+ const gaugeLabel = gaugeValue?.label || "-";
22484
+ if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
22485
+ gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
22486
+ }
22487
+ const minLabelPosition = {
22488
+ x: gaugeRect.x + gaugeArcWidth / 2,
22489
+ y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
22490
+ };
22491
+ const maxLabelPosition = {
22492
+ x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
22493
+ y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
22494
+ };
22495
+ const textColor = chartMutedFontColor(runtime.background);
22496
+ const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);
22497
+ let x = 0, titleWidth = 0, titleHeight = 0;
22498
+ if (runtime.title.text) {
22499
+ ({ width: titleWidth, height: titleHeight } = computeTextDimension(ctx, runtime.title.text, { fontSize: CHART_TITLE_FONT_SIZE, ...runtime.title }, "px"));
22500
+ }
22501
+ switch (runtime.title.align) {
22502
+ case "right":
22503
+ x = boundingRect.width - titleWidth - CHART_PADDING$1;
22504
+ break;
22505
+ case "center":
22506
+ x = (boundingRect.width - titleWidth) / 2;
22507
+ break;
22508
+ case "left":
22509
+ default:
22510
+ x = CHART_PADDING$1;
22511
+ break;
22512
+ }
22513
+ return {
22514
+ width: boundingRect.width,
22515
+ height: boundingRect.height,
22516
+ title: {
22517
+ label: runtime.title.text ?? "",
22518
+ fontSize: runtime.title.fontSize ?? CHART_TITLE_FONT_SIZE,
22519
+ textPosition: {
22520
+ x,
22521
+ y: CHART_PADDING_TOP + titleHeight / 2,
22522
+ },
22523
+ color: runtime.title.color ?? textColor,
22524
+ bold: runtime.title.bold,
22525
+ italic: runtime.title.italic,
22526
+ },
22527
+ backgroundColor: runtime.background,
22528
+ gauge: {
22529
+ rect: gaugeRect,
22530
+ arcWidth: gaugeArcWidth,
22531
+ percentage: clip(gaugePercentage, 0, 1),
22532
+ color: getGaugeColor(runtime),
22533
+ },
22534
+ inflectionValues,
22535
+ gaugeValue: {
22536
+ label: gaugeLabel,
22537
+ textPosition: gaugeValuePosition,
22538
+ fontSize: gaugeValueFontSize,
22539
+ color: textColor,
22540
+ },
22541
+ minLabel: {
22542
+ label: runtime.minValue.label,
22543
+ textPosition: minLabelPosition,
22544
+ fontSize: GAUGE_LABELS_FONT_SIZE,
22545
+ color: textColor,
22546
+ },
22547
+ maxLabel: {
22548
+ label: runtime.maxValue.label,
22549
+ textPosition: maxLabelPosition,
22550
+ fontSize: GAUGE_LABELS_FONT_SIZE,
22551
+ color: textColor,
22552
+ },
22553
+ };
22554
+ }
22555
+ /**
22556
+ * Get the rectangle in which the gauge will be drawn, based on the bounding rectangle of the canvas and leaving
22557
+ * space for the title and labels.
22558
+ */
22559
+ function getGaugeRect(boundingRect, title) {
22560
+ const titleHeight = title ? GAUGE_TITLE_SECTION_HEIGHT : 0;
22561
+ const drawHeight = boundingRect.height - GAUGE_PADDING_BOTTOM - titleHeight - GAUGE_PADDING_TOP;
22562
+ const drawWidth = boundingRect.width - GAUGE_PADDING_SIDE * 2;
22563
+ let gaugeWidth;
22564
+ let gaugeHeight;
22565
+ if (drawWidth > 2 * drawHeight) {
22566
+ gaugeWidth = 2 * drawHeight;
22567
+ gaugeHeight = drawHeight;
22568
+ }
22569
+ else {
22570
+ gaugeWidth = drawWidth;
22571
+ gaugeHeight = drawWidth / 2;
22572
+ }
22573
+ const gaugeX = GAUGE_PADDING_SIDE + (drawWidth - gaugeWidth) / 2;
22574
+ const gaugeY = titleHeight + GAUGE_PADDING_TOP + (drawHeight - gaugeHeight) / 2;
22575
+ return {
22576
+ x: gaugeX,
22577
+ y: gaugeY,
22578
+ width: gaugeWidth,
22579
+ height: gaugeHeight,
22580
+ };
22581
+ }
22582
+ /**
22583
+ * 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).
22584
+ *
22585
+ * Also compute an offset for the text so that it doesn't overlap with other text.
22586
+ */
22587
+ function getInflectionValues(runtime, gaugeRect, textColor, ctx) {
22588
+ const maxValue = runtime.maxValue;
22589
+ const minValue = runtime.minValue;
22590
+ const gaugeCircleCenter = {
22591
+ x: gaugeRect.x + gaugeRect.width / 2,
22592
+ y: gaugeRect.y + gaugeRect.height,
22593
+ };
22594
+ const textStyle = { fontSize: GAUGE_LABELS_FONT_SIZE };
22595
+ const inflectionValues = [];
22596
+ const inflectionValuesTextRects = [];
22597
+ for (const inflectionValue of runtime.inflectionValues) {
22598
+ const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
22599
+ const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
22600
+ const angle = Math.PI - Math.PI * percentage;
22601
+ const textRect = getRectangleTangentToCircle(angle, // angle between X axis and the point where the rectangle is tangent to the circle
22602
+ gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, // radius of the gauge circle + margin below text
22603
+ gaugeCircleCenter.x, // center of the gauge circle
22604
+ gaugeCircleCenter.y, // center of the gauge circle
22605
+ labelWidth + 2, // width of the text + some margin
22606
+ GAUGE_LABELS_FONT_SIZE // height of the text
22607
+ );
22608
+ let offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect))
22609
+ ? GAUGE_LABELS_FONT_SIZE
22610
+ : 0;
22611
+ inflectionValuesTextRects.push(textRect);
22612
+ inflectionValues.push({
22613
+ rotation: angle,
22614
+ label: inflectionValue.label,
22615
+ fontSize: GAUGE_LABELS_FONT_SIZE,
22616
+ color: textColor,
22617
+ offset,
22618
+ });
22619
+ }
22620
+ return inflectionValues;
22621
+ }
22622
+ function getGaugeColor(runtime) {
22623
+ const gaugeValue = runtime.gaugeValue?.value;
22624
+ if (gaugeValue === undefined) {
22625
+ return GAUGE_BACKGROUND_COLOR;
22626
+ }
22627
+ for (let i = 0; i < runtime.inflectionValues.length; i++) {
22628
+ const inflectionValue = runtime.inflectionValues[i];
22629
+ if (inflectionValue.operator === "<" && gaugeValue < inflectionValue.value) {
22630
+ return runtime.colors[i];
22631
+ }
22632
+ else if (inflectionValue.operator === "<=" && gaugeValue <= inflectionValue.value) {
22633
+ return runtime.colors[i];
22634
+ }
22635
+ }
22636
+ return runtime.colors.at(-1);
22637
+ }
22638
+ function getSegmentsOfRectangle(rectangle) {
22639
+ return [
22640
+ { start: rectangle.topLeft, end: rectangle.topRight },
22641
+ { start: rectangle.topRight, end: rectangle.bottomRight },
22642
+ { start: rectangle.bottomRight, end: rectangle.bottomLeft },
22643
+ { start: rectangle.bottomLeft, end: rectangle.topLeft },
22644
+ ];
22645
+ }
22646
+ /**
22647
+ * Check if two segment intersect. The case where the segments are colinear (both segments on the same line)
22648
+ * is not handled.
22649
+ */
22650
+ function doSegmentIntersect(segment1, segment2) {
22651
+ const A = segment1.start;
22652
+ const B = segment1.end;
22653
+ const C = segment2.start;
22654
+ const D = segment2.end;
22655
+ /**
22656
+ * Line segment intersection algorithm
22657
+ * https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
22658
+ */
22659
+ function ccw(a, b, c) {
22660
+ return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
22661
+ }
22662
+ return ccw(A, C, D) !== ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D);
22663
+ }
22664
+ function doRectanglesIntersect(rect1, rect2) {
22665
+ const segments1 = getSegmentsOfRectangle(rect1);
22666
+ const segments2 = getSegmentsOfRectangle(rect2);
22667
+ for (const segment1 of segments1) {
22668
+ for (const segment2 of segments2) {
22669
+ if (doSegmentIntersect(segment1, segment2)) {
22670
+ return true;
22671
+ }
22672
+ }
22673
+ }
22674
+ return false;
22675
+ }
22676
+ /**
22677
+ * Get the rectangle that is tangent to a circle at a given angle.
22678
+ *
22679
+ * @param angle angle between X axis and the point where the rectangle is tangent to the circle
22680
+ */
22681
+ function getRectangleTangentToCircle(angle, radius, circleCenterX, circleCenterY, rectWidth, rectHeight) {
22682
+ const cos = Math.cos(angle);
22683
+ const sin = Math.sin(angle);
22684
+ // x, y are the distance from the center of the circle to the point where the rectangle is tangent to the circle
22685
+ const x = cos * radius;
22686
+ const y = sin * radius;
22687
+ // x2, y2 are the distance from the point the rectangle is tangent to the circle to the bottom left corner of the rectangle
22688
+ const x2 = sin * (rectWidth / 2); // cos(angle + 90°) = sin(angle)
22689
+ const y2 = cos * (rectWidth / 2);
22690
+ const bottomRight = {
22691
+ x: x + x2 + circleCenterX,
22692
+ y: circleCenterY - (y - y2),
22693
+ };
22694
+ const bottomLeft = {
22695
+ x: x - x2 + circleCenterX,
22696
+ y: circleCenterY - (y + y2),
22697
+ };
22698
+ // Same as above but for the top corners of the rectangle (radius + rectangle height instead of radius)
22699
+ const xp = cos * (radius + rectHeight);
22700
+ const yp = sin * (radius + rectHeight);
22701
+ const topLeft = {
22702
+ x: xp - x2 + circleCenterX,
22703
+ y: circleCenterY - (yp + y2),
22704
+ };
22705
+ const topRight = {
22706
+ x: xp + x2 + circleCenterX,
22707
+ y: circleCenterY - (yp - y2),
22708
+ };
22709
+ return { bottomLeft, bottomRight, topRight, topLeft };
22710
+ }
22711
+
22770
22712
  class GaugeChartComponent extends owl.Component {
22771
22713
  static template = "o-spreadsheet-GaugeChartComponent";
22772
22714
  canvas = owl.useRef("chartContainer");
@@ -22799,6 +22741,82 @@ stores.inject(MyMetaStore, storeInstance);
22799
22741
  return color;
22800
22742
  }
22801
22743
 
22744
+ const CHART_COMMON_OPTIONS = {
22745
+ // https://www.chartjs.org/docs/latest/general/responsive.html
22746
+ responsive: true, // will resize when its container is resized
22747
+ maintainAspectRatio: false, // doesn't maintain the aspect ratio (width/height =2 by default) so the user has the choice of the exact layout
22748
+ elements: {
22749
+ line: {
22750
+ fill: false, // do not fill the area under line charts
22751
+ },
22752
+ point: {
22753
+ hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
22754
+ },
22755
+ },
22756
+ animation: false,
22757
+ };
22758
+ function truncateLabel(label) {
22759
+ if (!label) {
22760
+ return "";
22761
+ }
22762
+ if (label.length > MAX_CHAR_LABEL) {
22763
+ return label.substring(0, MAX_CHAR_LABEL) + "…";
22764
+ }
22765
+ return label;
22766
+ }
22767
+ function chartToImage(runtime, figure, type) {
22768
+ // wrap the canvas in a div with a fixed size because chart.js would
22769
+ // fill the whole page otherwise
22770
+ const div = document.createElement("div");
22771
+ div.style.width = `${figure.width}px`;
22772
+ div.style.height = `${figure.height}px`;
22773
+ const canvas = document.createElement("canvas");
22774
+ div.append(canvas);
22775
+ canvas.setAttribute("width", figure.width.toString());
22776
+ canvas.setAttribute("height", figure.height.toString());
22777
+ // we have to add the canvas to the DOM otherwise it won't be rendered
22778
+ document.body.append(div);
22779
+ if ("chartJsConfig" in runtime) {
22780
+ const config = deepCopy(runtime.chartJsConfig);
22781
+ config.plugins = [backgroundColorChartJSPlugin];
22782
+ const Chart = getChartJSConstructor();
22783
+ const chart = new Chart(canvas, config);
22784
+ const imgContent = chart.toBase64Image();
22785
+ chart.destroy();
22786
+ div.remove();
22787
+ return imgContent;
22788
+ }
22789
+ else if (type === "scorecard") {
22790
+ const design = getScorecardConfiguration(figure, runtime);
22791
+ drawScoreChart(design, canvas);
22792
+ const imgContent = canvas.toDataURL();
22793
+ div.remove();
22794
+ return imgContent;
22795
+ }
22796
+ else if (type === "gauge") {
22797
+ drawGaugeChart(canvas, runtime);
22798
+ const imgContent = canvas.toDataURL();
22799
+ div.remove();
22800
+ return imgContent;
22801
+ }
22802
+ return undefined;
22803
+ }
22804
+ /**
22805
+ * Custom chart.js plugin to set the background color of the canvas
22806
+ * https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md
22807
+ */
22808
+ const backgroundColorChartJSPlugin = {
22809
+ id: "customCanvasBackgroundColor",
22810
+ beforeDraw: (chart) => {
22811
+ const { ctx } = chart;
22812
+ ctx.save();
22813
+ ctx.globalCompositeOperation = "destination-over";
22814
+ ctx.fillStyle = "#ffffff";
22815
+ ctx.fillRect(0, 0, chart.width, chart.height);
22816
+ ctx.restore();
22817
+ },
22818
+ };
22819
+
22802
22820
  /**
22803
22821
  * Represent a raw XML string
22804
22822
  */
@@ -34038,7 +34056,6 @@ stores.inject(MyMetaStore, storeInstance);
34038
34056
  drawScoreChart: drawScoreChart,
34039
34057
  formatChartDatasetValue: formatChartDatasetValue,
34040
34058
  formatTickValue: formatTickValue,
34041
- getChartJSConstructor: getChartJSConstructor,
34042
34059
  getChartPositionAtCenterOfViewport: getChartPositionAtCenterOfViewport,
34043
34060
  getDefinedAxis: getDefinedAxis,
34044
34061
  getPieColors: getPieColors,
@@ -44748,7 +44765,8 @@ stores.inject(MyMetaStore, storeInstance);
44748
44765
  &.pivot-dimension-invalid {
44749
44766
  background-color: #ffdddd;
44750
44767
  border-color: red !important;
44751
- select {
44768
+ select,
44769
+ input {
44752
44770
  background-color: #ffdddd;
44753
44771
  }
44754
44772
  }
@@ -46611,7 +46629,7 @@ stores.inject(MyMetaStore, storeInstance);
46611
46629
  this.notification.notifyUser({
46612
46630
  type: "info",
46613
46631
  text: _t("Pivot updates only work with dynamic pivot tables. Use %s or re-insert the static pivot from the Data menu.", pivotExample),
46614
- sticky: false,
46632
+ sticky: true,
46615
46633
  });
46616
46634
  }
46617
46635
  }
@@ -75427,6 +75445,7 @@ stores.inject(MyMetaStore, storeInstance);
75427
75445
  supportedPivotPositionalFormulaRegistry,
75428
75446
  pivotToFunctionValueRegistry,
75429
75447
  migrationStepRegistry,
75448
+ chartJsExtensionRegistry,
75430
75449
  };
75431
75450
  const helpers = {
75432
75451
  arg,
@@ -75625,9 +75644,9 @@ stores.inject(MyMetaStore, storeInstance);
75625
75644
  exports.tokenize = tokenize;
75626
75645
 
75627
75646
 
75628
- __info__.version = "18.1.13";
75629
- __info__.date = "2025-03-26T12:48:31.680Z";
75630
- __info__.hash = "45ec54c";
75647
+ __info__.version = "18.1.14";
75648
+ __info__.date = "2025-04-04T08:42:40.149Z";
75649
+ __info__.hash = "63b2fb7";
75631
75650
 
75632
75651
 
75633
75652
  })(this.o_spreadsheet = this.o_spreadsheet || {}, owl);