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