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