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