@odoo/o-spreadsheet 18.2.5 → 18.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/o-spreadsheet.cjs.js +657 -638
- package/dist/o-spreadsheet.d.ts +2 -1
- package/dist/o-spreadsheet.esm.js +657 -638
- package/dist/o-spreadsheet.iife.js +657 -638
- package/dist/o-spreadsheet.iife.min.js +380 -379
- 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.2.
|
|
6
|
-
* @date 2025-
|
|
7
|
-
* @hash
|
|
5
|
+
* @version 18.2.6
|
|
6
|
+
* @date 2025-04-04T08:41:26.115Z
|
|
7
|
+
* @hash faa00e2
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { useEnv, useSubEnv, onWillUnmount, useComponent, status, Component, useRef, onMounted, useEffect, App, blockDom, useState, onPatched, onWillPatch, onWillUpdateProps, useExternalListener, onWillStart, xml, useChildSubEnv, markRaw, toRaw } from '@odoo/owl';
|
|
@@ -804,8 +804,7 @@ function removeFalsyAttributes(obj) {
|
|
|
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 @@ const whiteSpaceSpecialCharacters = [
|
|
|
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
|
|
@@ -6797,8 +6796,12 @@ function tokenize(str, locale = DEFAULT_LOCALE) {
|
|
|
6797
6796
|
str = replaceNewLines(str);
|
|
6798
6797
|
const chars = new TokenizingChars(str);
|
|
6799
6798
|
const result = [];
|
|
6799
|
+
const tokenizeSpace = specialWhiteSpaceRegexp.test(str)
|
|
6800
|
+
? tokenizeSpecialCharacterSpace
|
|
6801
|
+
: tokenizeSimpleSpace;
|
|
6800
6802
|
while (!chars.isOver()) {
|
|
6801
|
-
let token =
|
|
6803
|
+
let token = tokenizeNewLine(chars) ||
|
|
6804
|
+
tokenizeSpace(chars) ||
|
|
6802
6805
|
tokenizeArgsSeparator(chars, locale) ||
|
|
6803
6806
|
tokenizeParenthesis(chars) ||
|
|
6804
6807
|
tokenizeOperator(chars) ||
|
|
@@ -6932,17 +6935,19 @@ function tokenizeSymbol(chars) {
|
|
|
6932
6935
|
}
|
|
6933
6936
|
return null;
|
|
6934
6937
|
}
|
|
6935
|
-
function
|
|
6936
|
-
let
|
|
6937
|
-
while (chars.current ===
|
|
6938
|
-
|
|
6939
|
-
chars.shift();
|
|
6938
|
+
function tokenizeSpecialCharacterSpace(chars) {
|
|
6939
|
+
let spaces = "";
|
|
6940
|
+
while (chars.current === " " || (chars.current && chars.current.match(specialWhiteSpaceRegexp))) {
|
|
6941
|
+
spaces += chars.shift();
|
|
6940
6942
|
}
|
|
6941
|
-
if (
|
|
6942
|
-
return { type: "SPACE", value:
|
|
6943
|
+
if (spaces) {
|
|
6944
|
+
return { type: "SPACE", value: spaces };
|
|
6943
6945
|
}
|
|
6946
|
+
return null;
|
|
6947
|
+
}
|
|
6948
|
+
function tokenizeSimpleSpace(chars) {
|
|
6944
6949
|
let spaces = "";
|
|
6945
|
-
while (chars.current
|
|
6950
|
+
while (chars.current === " ") {
|
|
6946
6951
|
spaces += chars.shift();
|
|
6947
6952
|
}
|
|
6948
6953
|
if (spaces) {
|
|
@@ -6950,6 +6955,17 @@ function tokenizeSpace(chars) {
|
|
|
6950
6955
|
}
|
|
6951
6956
|
return null;
|
|
6952
6957
|
}
|
|
6958
|
+
function tokenizeNewLine(chars) {
|
|
6959
|
+
let length = 0;
|
|
6960
|
+
while (chars.current === NEWLINE) {
|
|
6961
|
+
length++;
|
|
6962
|
+
chars.shift();
|
|
6963
|
+
}
|
|
6964
|
+
if (length) {
|
|
6965
|
+
return { type: "SPACE", value: NEWLINE.repeat(length) };
|
|
6966
|
+
}
|
|
6967
|
+
return null;
|
|
6968
|
+
}
|
|
6953
6969
|
function tokenizeInvalidRange(chars) {
|
|
6954
6970
|
if (chars.currentStartsWith(CellErrorType.InvalidReference)) {
|
|
6955
6971
|
chars.advanceBy(CellErrorType.InvalidReference.length);
|
|
@@ -9576,6 +9592,159 @@ class ComposerFocusStore extends SpreadsheetStore {
|
|
|
9576
9592
|
}
|
|
9577
9593
|
}
|
|
9578
9594
|
|
|
9595
|
+
/**
|
|
9596
|
+
* This file is largely inspired by owl 1.
|
|
9597
|
+
* `css` tag has been removed from owl 2 without workaround to manage css.
|
|
9598
|
+
* So, the solution was to import the behavior of owl 1 directly in our
|
|
9599
|
+
* codebase, with one difference: the css is added to the sheet as soon as the
|
|
9600
|
+
* css tag is executed. In owl 1, the css was added as soon as a Component was
|
|
9601
|
+
* created for the first time.
|
|
9602
|
+
*/
|
|
9603
|
+
const STYLESHEETS = {};
|
|
9604
|
+
let nextId = 0;
|
|
9605
|
+
/**
|
|
9606
|
+
* CSS tag helper for defining inline stylesheets. With this, one can simply define
|
|
9607
|
+
* an inline stylesheet with just the following code:
|
|
9608
|
+
* ```js
|
|
9609
|
+
* css`.component-a { color: red; }`;
|
|
9610
|
+
* ```
|
|
9611
|
+
*/
|
|
9612
|
+
function css(strings, ...args) {
|
|
9613
|
+
const name = `__sheet__${nextId++}`;
|
|
9614
|
+
const value = String.raw(strings, ...args);
|
|
9615
|
+
registerSheet(name, value);
|
|
9616
|
+
activateSheet(name);
|
|
9617
|
+
return name;
|
|
9618
|
+
}
|
|
9619
|
+
function processSheet(str) {
|
|
9620
|
+
const tokens = str.split(/(\{|\}|;)/).map((s) => s.trim());
|
|
9621
|
+
const selectorStack = [];
|
|
9622
|
+
const parts = [];
|
|
9623
|
+
let rules = [];
|
|
9624
|
+
function generateSelector(stackIndex, parentSelector) {
|
|
9625
|
+
const parts = [];
|
|
9626
|
+
for (const selector of selectorStack[stackIndex]) {
|
|
9627
|
+
let part = (parentSelector && parentSelector + " " + selector) || selector;
|
|
9628
|
+
if (part.includes("&")) {
|
|
9629
|
+
part = selector.replace(/&/g, parentSelector || "");
|
|
9630
|
+
}
|
|
9631
|
+
if (stackIndex < selectorStack.length - 1) {
|
|
9632
|
+
part = generateSelector(stackIndex + 1, part);
|
|
9633
|
+
}
|
|
9634
|
+
parts.push(part);
|
|
9635
|
+
}
|
|
9636
|
+
return parts.join(", ");
|
|
9637
|
+
}
|
|
9638
|
+
function generateRules() {
|
|
9639
|
+
if (rules.length) {
|
|
9640
|
+
parts.push(generateSelector(0) + " {");
|
|
9641
|
+
parts.push(...rules);
|
|
9642
|
+
parts.push("}");
|
|
9643
|
+
rules = [];
|
|
9644
|
+
}
|
|
9645
|
+
}
|
|
9646
|
+
while (tokens.length) {
|
|
9647
|
+
let token = tokens.shift();
|
|
9648
|
+
if (token === "}") {
|
|
9649
|
+
generateRules();
|
|
9650
|
+
selectorStack.pop();
|
|
9651
|
+
}
|
|
9652
|
+
else {
|
|
9653
|
+
if (tokens[0] === "{") {
|
|
9654
|
+
generateRules();
|
|
9655
|
+
selectorStack.push(token.split(/\s*,\s*/));
|
|
9656
|
+
tokens.shift();
|
|
9657
|
+
}
|
|
9658
|
+
if (tokens[0] === ";") {
|
|
9659
|
+
rules.push(" " + token + ";");
|
|
9660
|
+
}
|
|
9661
|
+
}
|
|
9662
|
+
}
|
|
9663
|
+
return parts.join("\n");
|
|
9664
|
+
}
|
|
9665
|
+
function registerSheet(id, css) {
|
|
9666
|
+
const sheet = document.createElement("style");
|
|
9667
|
+
sheet.textContent = processSheet(css);
|
|
9668
|
+
STYLESHEETS[id] = sheet;
|
|
9669
|
+
}
|
|
9670
|
+
function activateSheet(id) {
|
|
9671
|
+
const sheet = STYLESHEETS[id];
|
|
9672
|
+
sheet.setAttribute("component", id);
|
|
9673
|
+
document.head.appendChild(sheet);
|
|
9674
|
+
}
|
|
9675
|
+
function getTextDecoration({ strikethrough, underline, }) {
|
|
9676
|
+
if (!strikethrough && !underline) {
|
|
9677
|
+
return "none";
|
|
9678
|
+
}
|
|
9679
|
+
return `${strikethrough ? "line-through" : ""} ${underline ? "underline" : ""}`;
|
|
9680
|
+
}
|
|
9681
|
+
/**
|
|
9682
|
+
* Convert the cell style to CSS properties.
|
|
9683
|
+
*/
|
|
9684
|
+
function cellStyleToCss(style) {
|
|
9685
|
+
const attributes = cellTextStyleToCss(style);
|
|
9686
|
+
if (!style)
|
|
9687
|
+
return attributes;
|
|
9688
|
+
if (style.fillColor) {
|
|
9689
|
+
attributes["background"] = style.fillColor;
|
|
9690
|
+
}
|
|
9691
|
+
return attributes;
|
|
9692
|
+
}
|
|
9693
|
+
/**
|
|
9694
|
+
* Convert the cell text style to CSS properties.
|
|
9695
|
+
*/
|
|
9696
|
+
function cellTextStyleToCss(style) {
|
|
9697
|
+
const attributes = {};
|
|
9698
|
+
if (!style)
|
|
9699
|
+
return attributes;
|
|
9700
|
+
if (style.bold) {
|
|
9701
|
+
attributes["font-weight"] = "bold";
|
|
9702
|
+
}
|
|
9703
|
+
if (style.italic) {
|
|
9704
|
+
attributes["font-style"] = "italic";
|
|
9705
|
+
}
|
|
9706
|
+
if (style.strikethrough || style.underline) {
|
|
9707
|
+
let decoration = style.strikethrough ? "line-through" : "";
|
|
9708
|
+
decoration = style.underline ? decoration + " underline" : decoration;
|
|
9709
|
+
attributes["text-decoration"] = decoration;
|
|
9710
|
+
}
|
|
9711
|
+
if (style.textColor) {
|
|
9712
|
+
attributes["color"] = style.textColor;
|
|
9713
|
+
}
|
|
9714
|
+
return attributes;
|
|
9715
|
+
}
|
|
9716
|
+
/**
|
|
9717
|
+
* Transform CSS properties into a CSS string.
|
|
9718
|
+
*/
|
|
9719
|
+
function cssPropertiesToCss(attributes) {
|
|
9720
|
+
let styleStr = "";
|
|
9721
|
+
for (const attName in attributes) {
|
|
9722
|
+
if (!attributes[attName]) {
|
|
9723
|
+
continue;
|
|
9724
|
+
}
|
|
9725
|
+
styleStr += `${attName}:${attributes[attName]}; `;
|
|
9726
|
+
}
|
|
9727
|
+
return styleStr;
|
|
9728
|
+
}
|
|
9729
|
+
function getElementMargins(el) {
|
|
9730
|
+
const style = window.getComputedStyle(el);
|
|
9731
|
+
return {
|
|
9732
|
+
top: parseInt(style.marginTop, 10) || 0,
|
|
9733
|
+
bottom: parseInt(style.marginBottom, 10) || 0,
|
|
9734
|
+
left: parseInt(style.marginLeft, 10) || 0,
|
|
9735
|
+
right: parseInt(style.marginRight, 10) || 0,
|
|
9736
|
+
};
|
|
9737
|
+
}
|
|
9738
|
+
|
|
9739
|
+
const chartJsExtensionRegistry = new Registry();
|
|
9740
|
+
/** Return window.Chart, making sure all our extensions are loaded in ChartJS */
|
|
9741
|
+
function getChartJSConstructor() {
|
|
9742
|
+
if (window.Chart && !window.Chart?.registry.plugins.get("chartShowValuesPlugin")) {
|
|
9743
|
+
window.Chart.register(...chartJsExtensionRegistry.getAll());
|
|
9744
|
+
}
|
|
9745
|
+
return window.Chart;
|
|
9746
|
+
}
|
|
9747
|
+
|
|
9579
9748
|
const TREND_LINE_XAXIS_ID = "x1";
|
|
9580
9749
|
const MOVING_AVERAGE_TREND_LINE_XAXIS_ID = "xMovingAverage";
|
|
9581
9750
|
/**
|
|
@@ -10124,341 +10293,79 @@ function getNextNonEmptyBar(bars, startIndex) {
|
|
|
10124
10293
|
return bars.find((bar, i) => i > startIndex && bar.height !== 0);
|
|
10125
10294
|
}
|
|
10126
10295
|
|
|
10127
|
-
|
|
10128
|
-
|
|
10129
|
-
|
|
10130
|
-
|
|
10131
|
-
|
|
10132
|
-
|
|
10133
|
-
const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
|
|
10134
|
-
const GAUGE_TITLE_SECTION_HEIGHT = 25;
|
|
10135
|
-
function drawGaugeChart(canvas, runtime) {
|
|
10136
|
-
const canvasBoundingRect = canvas.getBoundingClientRect();
|
|
10137
|
-
canvas.width = canvasBoundingRect.width;
|
|
10138
|
-
canvas.height = canvasBoundingRect.height;
|
|
10139
|
-
const ctx = canvas.getContext("2d");
|
|
10140
|
-
const config = getGaugeRenderingConfig(canvasBoundingRect, runtime, ctx);
|
|
10141
|
-
drawBackground(ctx, config);
|
|
10142
|
-
drawGauge(ctx, config);
|
|
10143
|
-
drawInflectionValues(ctx, config);
|
|
10144
|
-
drawLabels(ctx, config);
|
|
10145
|
-
drawTitle(ctx, config);
|
|
10146
|
-
}
|
|
10147
|
-
function drawGauge(ctx, config) {
|
|
10148
|
-
ctx.save();
|
|
10149
|
-
const gauge = config.gauge;
|
|
10150
|
-
const arcCenterX = gauge.rect.x + gauge.rect.width / 2;
|
|
10151
|
-
const arcCenterY = gauge.rect.y + gauge.rect.height;
|
|
10152
|
-
const arcRadius = gauge.rect.height - gauge.arcWidth / 2;
|
|
10153
|
-
if (arcRadius < 0) {
|
|
10154
|
-
return;
|
|
10155
|
-
}
|
|
10156
|
-
const gaugeAngle = gauge.percentage === 1 ? 0 : Math.PI * (1 + gauge.percentage);
|
|
10157
|
-
// Gauge background
|
|
10158
|
-
ctx.strokeStyle = GAUGE_BACKGROUND_COLOR;
|
|
10159
|
-
ctx.beginPath();
|
|
10160
|
-
ctx.lineWidth = gauge.arcWidth;
|
|
10161
|
-
ctx.arc(arcCenterX, arcCenterY, arcRadius, gaugeAngle, 0);
|
|
10162
|
-
ctx.stroke();
|
|
10163
|
-
// Gauge value
|
|
10164
|
-
ctx.strokeStyle = gauge.color;
|
|
10165
|
-
ctx.beginPath();
|
|
10166
|
-
ctx.arc(arcCenterX, arcCenterY, arcRadius, Math.PI, gaugeAngle);
|
|
10167
|
-
ctx.stroke();
|
|
10168
|
-
ctx.restore();
|
|
10169
|
-
}
|
|
10170
|
-
function drawBackground(ctx, config) {
|
|
10171
|
-
ctx.save();
|
|
10172
|
-
ctx.fillStyle = config.backgroundColor;
|
|
10173
|
-
ctx.fillRect(0, 0, config.width, config.height);
|
|
10174
|
-
ctx.restore();
|
|
10175
|
-
}
|
|
10176
|
-
function drawLabels(ctx, config) {
|
|
10177
|
-
for (const label of [config.minLabel, config.maxLabel, config.gaugeValue]) {
|
|
10178
|
-
ctx.save();
|
|
10179
|
-
ctx.textAlign = "center";
|
|
10180
|
-
ctx.fillStyle = label.color;
|
|
10181
|
-
ctx.font = `${label.fontSize}px ${DEFAULT_FONT}`;
|
|
10182
|
-
ctx.fillText(label.label, label.textPosition.x, label.textPosition.y);
|
|
10183
|
-
ctx.restore();
|
|
10184
|
-
}
|
|
10185
|
-
}
|
|
10186
|
-
function drawInflectionValues(ctx, config) {
|
|
10187
|
-
const { x: rectX, y: rectY, width, height } = config.gauge.rect;
|
|
10188
|
-
for (const inflectionValue of config.inflectionValues) {
|
|
10189
|
-
ctx.save();
|
|
10190
|
-
ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment
|
|
10191
|
-
ctx.rotate(Math.PI / 2 - inflectionValue.rotation);
|
|
10192
|
-
ctx.lineWidth = 2;
|
|
10193
|
-
ctx.strokeStyle = chartMutedFontColor(config.backgroundColor) + "aa";
|
|
10194
|
-
ctx.beginPath();
|
|
10195
|
-
ctx.moveTo(0, -(height - config.gauge.arcWidth));
|
|
10196
|
-
ctx.lineTo(0, -height - 3);
|
|
10197
|
-
ctx.stroke();
|
|
10198
|
-
ctx.textAlign = "center";
|
|
10199
|
-
ctx.font = `${inflectionValue.fontSize}px ${DEFAULT_FONT}`;
|
|
10200
|
-
ctx.fillStyle = inflectionValue.color;
|
|
10201
|
-
const textY = -height - GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN - inflectionValue.offset;
|
|
10202
|
-
ctx.fillText(inflectionValue.label, 0, textY);
|
|
10203
|
-
ctx.restore();
|
|
10204
|
-
}
|
|
10205
|
-
}
|
|
10206
|
-
function drawTitle(ctx, config) {
|
|
10207
|
-
ctx.save();
|
|
10208
|
-
const title = config.title;
|
|
10209
|
-
ctx.font = getDefaultContextFont(title.fontSize, title.bold, title.italic);
|
|
10210
|
-
ctx.textBaseline = "middle";
|
|
10211
|
-
ctx.fillStyle = title.color;
|
|
10212
|
-
ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
|
|
10213
|
-
ctx.restore();
|
|
10214
|
-
}
|
|
10215
|
-
function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
|
|
10216
|
-
const maxValue = runtime.maxValue;
|
|
10217
|
-
const minValue = runtime.minValue;
|
|
10218
|
-
const gaugeValue = runtime.gaugeValue;
|
|
10219
|
-
const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
|
|
10220
|
-
const gaugeArcWidth = gaugeRect.width / 6;
|
|
10221
|
-
const gaugePercentage = gaugeValue
|
|
10222
|
-
? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
|
|
10223
|
-
: 0;
|
|
10224
|
-
const gaugeValuePosition = {
|
|
10225
|
-
x: boundingRect.width / 2,
|
|
10226
|
-
y: gaugeRect.y + gaugeRect.height - gaugeRect.height / 12,
|
|
10227
|
-
};
|
|
10228
|
-
let gaugeValueFontSize = GAUGE_DEFAULT_VALUE_FONT_SIZE;
|
|
10229
|
-
// Scale down the font size if the gaugeRect is too small
|
|
10230
|
-
if (gaugeRect.height < 300) {
|
|
10231
|
-
gaugeValueFontSize = gaugeValueFontSize * (gaugeRect.height / 300);
|
|
10232
|
-
}
|
|
10233
|
-
// Scale down the font size if the text is too long
|
|
10234
|
-
const maxTextWidth = gaugeRect.width / 2;
|
|
10235
|
-
const gaugeLabel = gaugeValue?.label || "-";
|
|
10236
|
-
if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
|
|
10237
|
-
gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
|
|
10296
|
+
css /* scss */ `
|
|
10297
|
+
.o-spreadsheet {
|
|
10298
|
+
.o-chart-custom-tooltip {
|
|
10299
|
+
font-size: 12px;
|
|
10300
|
+
background-color: #fff;
|
|
10301
|
+
z-index: ${ComponentsImportance.FigureTooltip};
|
|
10238
10302
|
}
|
|
10239
|
-
|
|
10240
|
-
|
|
10241
|
-
|
|
10242
|
-
|
|
10243
|
-
|
|
10244
|
-
|
|
10245
|
-
|
|
10303
|
+
}
|
|
10304
|
+
`;
|
|
10305
|
+
chartJsExtensionRegistry.add("chartShowValuesPlugin", chartShowValuesPlugin);
|
|
10306
|
+
chartJsExtensionRegistry.add("waterfallLinesPlugin", waterfallLinesPlugin);
|
|
10307
|
+
class ChartJsComponent extends Component {
|
|
10308
|
+
static template = "o-spreadsheet-ChartJsComponent";
|
|
10309
|
+
static props = {
|
|
10310
|
+
figure: Object,
|
|
10246
10311
|
};
|
|
10247
|
-
|
|
10248
|
-
|
|
10249
|
-
|
|
10250
|
-
|
|
10251
|
-
|
|
10252
|
-
}
|
|
10253
|
-
switch (runtime.title.align) {
|
|
10254
|
-
case "right":
|
|
10255
|
-
x = boundingRect.width - titleWidth - CHART_PADDING$1;
|
|
10256
|
-
break;
|
|
10257
|
-
case "center":
|
|
10258
|
-
x = (boundingRect.width - titleWidth) / 2;
|
|
10259
|
-
break;
|
|
10260
|
-
case "left":
|
|
10261
|
-
default:
|
|
10262
|
-
x = CHART_PADDING$1;
|
|
10263
|
-
break;
|
|
10312
|
+
canvas = useRef("graphContainer");
|
|
10313
|
+
chart;
|
|
10314
|
+
currentRuntime;
|
|
10315
|
+
get background() {
|
|
10316
|
+
return this.chartRuntime.background;
|
|
10264
10317
|
}
|
|
10265
|
-
|
|
10266
|
-
|
|
10267
|
-
height: boundingRect.height,
|
|
10268
|
-
title: {
|
|
10269
|
-
label: runtime.title.text ?? "",
|
|
10270
|
-
fontSize: runtime.title.fontSize ?? CHART_TITLE_FONT_SIZE,
|
|
10271
|
-
textPosition: {
|
|
10272
|
-
x,
|
|
10273
|
-
y: CHART_PADDING_TOP + titleHeight / 2,
|
|
10274
|
-
},
|
|
10275
|
-
color: runtime.title.color ?? textColor,
|
|
10276
|
-
bold: runtime.title.bold,
|
|
10277
|
-
italic: runtime.title.italic,
|
|
10278
|
-
},
|
|
10279
|
-
backgroundColor: runtime.background,
|
|
10280
|
-
gauge: {
|
|
10281
|
-
rect: gaugeRect,
|
|
10282
|
-
arcWidth: gaugeArcWidth,
|
|
10283
|
-
percentage: clip(gaugePercentage, 0, 1),
|
|
10284
|
-
color: getGaugeColor(runtime),
|
|
10285
|
-
},
|
|
10286
|
-
inflectionValues,
|
|
10287
|
-
gaugeValue: {
|
|
10288
|
-
label: gaugeLabel,
|
|
10289
|
-
textPosition: gaugeValuePosition,
|
|
10290
|
-
fontSize: gaugeValueFontSize,
|
|
10291
|
-
color: textColor,
|
|
10292
|
-
},
|
|
10293
|
-
minLabel: {
|
|
10294
|
-
label: runtime.minValue.label,
|
|
10295
|
-
textPosition: minLabelPosition,
|
|
10296
|
-
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
10297
|
-
color: textColor,
|
|
10298
|
-
},
|
|
10299
|
-
maxLabel: {
|
|
10300
|
-
label: runtime.maxValue.label,
|
|
10301
|
-
textPosition: maxLabelPosition,
|
|
10302
|
-
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
10303
|
-
color: textColor,
|
|
10304
|
-
},
|
|
10305
|
-
};
|
|
10306
|
-
}
|
|
10307
|
-
/**
|
|
10308
|
-
* Get the rectangle in which the gauge will be drawn, based on the bounding rectangle of the canvas and leaving
|
|
10309
|
-
* space for the title and labels.
|
|
10310
|
-
*/
|
|
10311
|
-
function getGaugeRect(boundingRect, title) {
|
|
10312
|
-
const titleHeight = title ? GAUGE_TITLE_SECTION_HEIGHT : 0;
|
|
10313
|
-
const drawHeight = boundingRect.height - GAUGE_PADDING_BOTTOM - titleHeight - GAUGE_PADDING_TOP;
|
|
10314
|
-
const drawWidth = boundingRect.width - GAUGE_PADDING_SIDE * 2;
|
|
10315
|
-
let gaugeWidth;
|
|
10316
|
-
let gaugeHeight;
|
|
10317
|
-
if (drawWidth > 2 * drawHeight) {
|
|
10318
|
-
gaugeWidth = 2 * drawHeight;
|
|
10319
|
-
gaugeHeight = drawHeight;
|
|
10318
|
+
get canvasStyle() {
|
|
10319
|
+
return `background-color: ${this.background}`;
|
|
10320
10320
|
}
|
|
10321
|
-
|
|
10322
|
-
|
|
10323
|
-
|
|
10321
|
+
get chartRuntime() {
|
|
10322
|
+
const runtime = this.env.model.getters.getChartRuntime(this.props.figure.id);
|
|
10323
|
+
if (!("chartJsConfig" in runtime)) {
|
|
10324
|
+
throw new Error("Unsupported chart runtime");
|
|
10325
|
+
}
|
|
10326
|
+
return runtime;
|
|
10324
10327
|
}
|
|
10325
|
-
|
|
10326
|
-
|
|
10327
|
-
|
|
10328
|
-
|
|
10329
|
-
|
|
10330
|
-
|
|
10331
|
-
|
|
10332
|
-
|
|
10333
|
-
|
|
10334
|
-
|
|
10335
|
-
|
|
10336
|
-
|
|
10337
|
-
|
|
10338
|
-
|
|
10339
|
-
|
|
10340
|
-
|
|
10341
|
-
|
|
10342
|
-
|
|
10343
|
-
|
|
10344
|
-
|
|
10345
|
-
};
|
|
10346
|
-
const textStyle = { fontSize: GAUGE_LABELS_FONT_SIZE };
|
|
10347
|
-
const inflectionValues = [];
|
|
10348
|
-
const inflectionValuesTextRects = [];
|
|
10349
|
-
for (const inflectionValue of runtime.inflectionValues) {
|
|
10350
|
-
const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
|
|
10351
|
-
const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
|
|
10352
|
-
const angle = Math.PI - Math.PI * percentage;
|
|
10353
|
-
const textRect = getRectangleTangentToCircle(angle, // angle between X axis and the point where the rectangle is tangent to the circle
|
|
10354
|
-
gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, // radius of the gauge circle + margin below text
|
|
10355
|
-
gaugeCircleCenter.x, // center of the gauge circle
|
|
10356
|
-
gaugeCircleCenter.y, // center of the gauge circle
|
|
10357
|
-
labelWidth + 2, // width of the text + some margin
|
|
10358
|
-
GAUGE_LABELS_FONT_SIZE // height of the text
|
|
10359
|
-
);
|
|
10360
|
-
let offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect))
|
|
10361
|
-
? GAUGE_LABELS_FONT_SIZE
|
|
10362
|
-
: 0;
|
|
10363
|
-
inflectionValuesTextRects.push(textRect);
|
|
10364
|
-
inflectionValues.push({
|
|
10365
|
-
rotation: angle,
|
|
10366
|
-
label: inflectionValue.label,
|
|
10367
|
-
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
10368
|
-
color: textColor,
|
|
10369
|
-
offset,
|
|
10328
|
+
setup() {
|
|
10329
|
+
onMounted(() => {
|
|
10330
|
+
const runtime = this.chartRuntime;
|
|
10331
|
+
this.currentRuntime = runtime;
|
|
10332
|
+
// Note: chartJS modify the runtime in place, so it's important to give it a copy
|
|
10333
|
+
this.createChart(deepCopy(runtime.chartJsConfig));
|
|
10334
|
+
});
|
|
10335
|
+
onWillUnmount(() => this.chart?.destroy());
|
|
10336
|
+
useEffect(() => {
|
|
10337
|
+
const runtime = this.chartRuntime;
|
|
10338
|
+
if (runtime !== this.currentRuntime) {
|
|
10339
|
+
if (runtime.chartJsConfig.type !== this.currentRuntime.chartJsConfig.type) {
|
|
10340
|
+
this.chart?.destroy();
|
|
10341
|
+
this.createChart(deepCopy(runtime.chartJsConfig));
|
|
10342
|
+
}
|
|
10343
|
+
else {
|
|
10344
|
+
this.updateChartJs(deepCopy(runtime.chartJsConfig));
|
|
10345
|
+
}
|
|
10346
|
+
this.currentRuntime = runtime;
|
|
10347
|
+
}
|
|
10370
10348
|
});
|
|
10371
10349
|
}
|
|
10372
|
-
|
|
10373
|
-
|
|
10374
|
-
|
|
10375
|
-
|
|
10376
|
-
|
|
10377
|
-
return GAUGE_BACKGROUND_COLOR;
|
|
10378
|
-
}
|
|
10379
|
-
for (let i = 0; i < runtime.inflectionValues.length; i++) {
|
|
10380
|
-
const inflectionValue = runtime.inflectionValues[i];
|
|
10381
|
-
if (inflectionValue.operator === "<" && gaugeValue < inflectionValue.value) {
|
|
10382
|
-
return runtime.colors[i];
|
|
10383
|
-
}
|
|
10384
|
-
else if (inflectionValue.operator === "<=" && gaugeValue <= inflectionValue.value) {
|
|
10385
|
-
return runtime.colors[i];
|
|
10386
|
-
}
|
|
10387
|
-
}
|
|
10388
|
-
return runtime.colors.at(-1);
|
|
10389
|
-
}
|
|
10390
|
-
function getSegmentsOfRectangle(rectangle) {
|
|
10391
|
-
return [
|
|
10392
|
-
{ start: rectangle.topLeft, end: rectangle.topRight },
|
|
10393
|
-
{ start: rectangle.topRight, end: rectangle.bottomRight },
|
|
10394
|
-
{ start: rectangle.bottomRight, end: rectangle.bottomLeft },
|
|
10395
|
-
{ start: rectangle.bottomLeft, end: rectangle.topLeft },
|
|
10396
|
-
];
|
|
10397
|
-
}
|
|
10398
|
-
/**
|
|
10399
|
-
* Check if two segment intersect. The case where the segments are colinear (both segments on the same line)
|
|
10400
|
-
* is not handled.
|
|
10401
|
-
*/
|
|
10402
|
-
function doSegmentIntersect(segment1, segment2) {
|
|
10403
|
-
const A = segment1.start;
|
|
10404
|
-
const B = segment1.end;
|
|
10405
|
-
const C = segment2.start;
|
|
10406
|
-
const D = segment2.end;
|
|
10407
|
-
/**
|
|
10408
|
-
* Line segment intersection algorithm
|
|
10409
|
-
* https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
|
|
10410
|
-
*/
|
|
10411
|
-
function ccw(a, b, c) {
|
|
10412
|
-
return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
|
|
10350
|
+
createChart(chartData) {
|
|
10351
|
+
const canvas = this.canvas.el;
|
|
10352
|
+
const ctx = canvas.getContext("2d");
|
|
10353
|
+
const Chart = getChartJSConstructor();
|
|
10354
|
+
this.chart = new Chart(ctx, chartData);
|
|
10413
10355
|
}
|
|
10414
|
-
|
|
10415
|
-
|
|
10416
|
-
|
|
10417
|
-
|
|
10418
|
-
|
|
10419
|
-
for (const segment1 of segments1) {
|
|
10420
|
-
for (const segment2 of segments2) {
|
|
10421
|
-
if (doSegmentIntersect(segment1, segment2)) {
|
|
10422
|
-
return true;
|
|
10356
|
+
updateChartJs(chartData) {
|
|
10357
|
+
if (chartData.data && chartData.data.datasets) {
|
|
10358
|
+
this.chart.data = chartData.data;
|
|
10359
|
+
if (chartData.options?.plugins?.title) {
|
|
10360
|
+
this.chart.config.options.plugins.title = chartData.options.plugins.title;
|
|
10423
10361
|
}
|
|
10424
10362
|
}
|
|
10363
|
+
else {
|
|
10364
|
+
this.chart.data.datasets = [];
|
|
10365
|
+
}
|
|
10366
|
+
this.chart.config.options = chartData.options;
|
|
10367
|
+
this.chart.update();
|
|
10425
10368
|
}
|
|
10426
|
-
return false;
|
|
10427
|
-
}
|
|
10428
|
-
/**
|
|
10429
|
-
* Get the rectangle that is tangent to a circle at a given angle.
|
|
10430
|
-
*
|
|
10431
|
-
* @param angle angle between X axis and the point where the rectangle is tangent to the circle
|
|
10432
|
-
*/
|
|
10433
|
-
function getRectangleTangentToCircle(angle, radius, circleCenterX, circleCenterY, rectWidth, rectHeight) {
|
|
10434
|
-
const cos = Math.cos(angle);
|
|
10435
|
-
const sin = Math.sin(angle);
|
|
10436
|
-
// x, y are the distance from the center of the circle to the point where the rectangle is tangent to the circle
|
|
10437
|
-
const x = cos * radius;
|
|
10438
|
-
const y = sin * radius;
|
|
10439
|
-
// x2, y2 are the distance from the point the rectangle is tangent to the circle to the bottom left corner of the rectangle
|
|
10440
|
-
const x2 = sin * (rectWidth / 2); // cos(angle + 90°) = sin(angle)
|
|
10441
|
-
const y2 = cos * (rectWidth / 2);
|
|
10442
|
-
const bottomRight = {
|
|
10443
|
-
x: x + x2 + circleCenterX,
|
|
10444
|
-
y: circleCenterY - (y - y2),
|
|
10445
|
-
};
|
|
10446
|
-
const bottomLeft = {
|
|
10447
|
-
x: x - x2 + circleCenterX,
|
|
10448
|
-
y: circleCenterY - (y + y2),
|
|
10449
|
-
};
|
|
10450
|
-
// Same as above but for the top corners of the rectangle (radius + rectangle height instead of radius)
|
|
10451
|
-
const xp = cos * (radius + rectHeight);
|
|
10452
|
-
const yp = sin * (radius + rectHeight);
|
|
10453
|
-
const topLeft = {
|
|
10454
|
-
x: xp - x2 + circleCenterX,
|
|
10455
|
-
y: circleCenterY - (yp + y2),
|
|
10456
|
-
};
|
|
10457
|
-
const topRight = {
|
|
10458
|
-
x: xp + x2 + circleCenterX,
|
|
10459
|
-
y: circleCenterY - (yp - y2),
|
|
10460
|
-
};
|
|
10461
|
-
return { bottomLeft, bottomRight, topRight, topLeft };
|
|
10462
10369
|
}
|
|
10463
10370
|
|
|
10464
10371
|
/**
|
|
@@ -11040,299 +10947,6 @@ class ScorecardChartConfigBuilder {
|
|
|
11040
10947
|
}
|
|
11041
10948
|
}
|
|
11042
10949
|
|
|
11043
|
-
const CHART_COMMON_OPTIONS = {
|
|
11044
|
-
// https://www.chartjs.org/docs/latest/general/responsive.html
|
|
11045
|
-
responsive: true, // will resize when its container is resized
|
|
11046
|
-
maintainAspectRatio: false, // doesn't maintain the aspect ratio (width/height =2 by default) so the user has the choice of the exact layout
|
|
11047
|
-
elements: {
|
|
11048
|
-
line: {
|
|
11049
|
-
fill: false, // do not fill the area under line charts
|
|
11050
|
-
},
|
|
11051
|
-
point: {
|
|
11052
|
-
hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
|
|
11053
|
-
},
|
|
11054
|
-
},
|
|
11055
|
-
animation: false,
|
|
11056
|
-
};
|
|
11057
|
-
function chartToImage(runtime, figure, type) {
|
|
11058
|
-
// wrap the canvas in a div with a fixed size because chart.js would
|
|
11059
|
-
// fill the whole page otherwise
|
|
11060
|
-
const div = document.createElement("div");
|
|
11061
|
-
div.style.width = `${figure.width}px`;
|
|
11062
|
-
div.style.height = `${figure.height}px`;
|
|
11063
|
-
const canvas = document.createElement("canvas");
|
|
11064
|
-
div.append(canvas);
|
|
11065
|
-
canvas.setAttribute("width", figure.width.toString());
|
|
11066
|
-
canvas.setAttribute("height", figure.height.toString());
|
|
11067
|
-
// we have to add the canvas to the DOM otherwise it won't be rendered
|
|
11068
|
-
document.body.append(div);
|
|
11069
|
-
if ("chartJsConfig" in runtime) {
|
|
11070
|
-
const config = deepCopy(runtime.chartJsConfig);
|
|
11071
|
-
config.plugins = [backgroundColorChartJSPlugin];
|
|
11072
|
-
const Chart = getChartJSConstructor();
|
|
11073
|
-
const chart = new Chart(canvas, config);
|
|
11074
|
-
const imgContent = chart.toBase64Image();
|
|
11075
|
-
chart.destroy();
|
|
11076
|
-
div.remove();
|
|
11077
|
-
return imgContent;
|
|
11078
|
-
}
|
|
11079
|
-
else if (type === "scorecard") {
|
|
11080
|
-
const design = getScorecardConfiguration(figure, runtime);
|
|
11081
|
-
drawScoreChart(design, canvas);
|
|
11082
|
-
const imgContent = canvas.toDataURL();
|
|
11083
|
-
div.remove();
|
|
11084
|
-
return imgContent;
|
|
11085
|
-
}
|
|
11086
|
-
else if (type === "gauge") {
|
|
11087
|
-
drawGaugeChart(canvas, runtime);
|
|
11088
|
-
const imgContent = canvas.toDataURL();
|
|
11089
|
-
div.remove();
|
|
11090
|
-
return imgContent;
|
|
11091
|
-
}
|
|
11092
|
-
return undefined;
|
|
11093
|
-
}
|
|
11094
|
-
/**
|
|
11095
|
-
* Custom chart.js plugin to set the background color of the canvas
|
|
11096
|
-
* https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md
|
|
11097
|
-
*/
|
|
11098
|
-
const backgroundColorChartJSPlugin = {
|
|
11099
|
-
id: "customCanvasBackgroundColor",
|
|
11100
|
-
beforeDraw: (chart) => {
|
|
11101
|
-
const { ctx } = chart;
|
|
11102
|
-
ctx.save();
|
|
11103
|
-
ctx.globalCompositeOperation = "destination-over";
|
|
11104
|
-
ctx.fillStyle = "#ffffff";
|
|
11105
|
-
ctx.fillRect(0, 0, chart.width, chart.height);
|
|
11106
|
-
ctx.restore();
|
|
11107
|
-
},
|
|
11108
|
-
};
|
|
11109
|
-
/** Return window.Chart, making sure all our extensions are loaded in ChartJS */
|
|
11110
|
-
function getChartJSConstructor() {
|
|
11111
|
-
if (window.Chart && !window.Chart?.registry.plugins.get("chartShowValuesPlugin")) {
|
|
11112
|
-
window.Chart.register(chartShowValuesPlugin);
|
|
11113
|
-
window.Chart.register(waterfallLinesPlugin);
|
|
11114
|
-
}
|
|
11115
|
-
return window.Chart;
|
|
11116
|
-
}
|
|
11117
|
-
|
|
11118
|
-
/**
|
|
11119
|
-
* This file is largely inspired by owl 1.
|
|
11120
|
-
* `css` tag has been removed from owl 2 without workaround to manage css.
|
|
11121
|
-
* So, the solution was to import the behavior of owl 1 directly in our
|
|
11122
|
-
* codebase, with one difference: the css is added to the sheet as soon as the
|
|
11123
|
-
* css tag is executed. In owl 1, the css was added as soon as a Component was
|
|
11124
|
-
* created for the first time.
|
|
11125
|
-
*/
|
|
11126
|
-
const STYLESHEETS = {};
|
|
11127
|
-
let nextId = 0;
|
|
11128
|
-
/**
|
|
11129
|
-
* CSS tag helper for defining inline stylesheets. With this, one can simply define
|
|
11130
|
-
* an inline stylesheet with just the following code:
|
|
11131
|
-
* ```js
|
|
11132
|
-
* css`.component-a { color: red; }`;
|
|
11133
|
-
* ```
|
|
11134
|
-
*/
|
|
11135
|
-
function css(strings, ...args) {
|
|
11136
|
-
const name = `__sheet__${nextId++}`;
|
|
11137
|
-
const value = String.raw(strings, ...args);
|
|
11138
|
-
registerSheet(name, value);
|
|
11139
|
-
activateSheet(name);
|
|
11140
|
-
return name;
|
|
11141
|
-
}
|
|
11142
|
-
function processSheet(str) {
|
|
11143
|
-
const tokens = str.split(/(\{|\}|;)/).map((s) => s.trim());
|
|
11144
|
-
const selectorStack = [];
|
|
11145
|
-
const parts = [];
|
|
11146
|
-
let rules = [];
|
|
11147
|
-
function generateSelector(stackIndex, parentSelector) {
|
|
11148
|
-
const parts = [];
|
|
11149
|
-
for (const selector of selectorStack[stackIndex]) {
|
|
11150
|
-
let part = (parentSelector && parentSelector + " " + selector) || selector;
|
|
11151
|
-
if (part.includes("&")) {
|
|
11152
|
-
part = selector.replace(/&/g, parentSelector || "");
|
|
11153
|
-
}
|
|
11154
|
-
if (stackIndex < selectorStack.length - 1) {
|
|
11155
|
-
part = generateSelector(stackIndex + 1, part);
|
|
11156
|
-
}
|
|
11157
|
-
parts.push(part);
|
|
11158
|
-
}
|
|
11159
|
-
return parts.join(", ");
|
|
11160
|
-
}
|
|
11161
|
-
function generateRules() {
|
|
11162
|
-
if (rules.length) {
|
|
11163
|
-
parts.push(generateSelector(0) + " {");
|
|
11164
|
-
parts.push(...rules);
|
|
11165
|
-
parts.push("}");
|
|
11166
|
-
rules = [];
|
|
11167
|
-
}
|
|
11168
|
-
}
|
|
11169
|
-
while (tokens.length) {
|
|
11170
|
-
let token = tokens.shift();
|
|
11171
|
-
if (token === "}") {
|
|
11172
|
-
generateRules();
|
|
11173
|
-
selectorStack.pop();
|
|
11174
|
-
}
|
|
11175
|
-
else {
|
|
11176
|
-
if (tokens[0] === "{") {
|
|
11177
|
-
generateRules();
|
|
11178
|
-
selectorStack.push(token.split(/\s*,\s*/));
|
|
11179
|
-
tokens.shift();
|
|
11180
|
-
}
|
|
11181
|
-
if (tokens[0] === ";") {
|
|
11182
|
-
rules.push(" " + token + ";");
|
|
11183
|
-
}
|
|
11184
|
-
}
|
|
11185
|
-
}
|
|
11186
|
-
return parts.join("\n");
|
|
11187
|
-
}
|
|
11188
|
-
function registerSheet(id, css) {
|
|
11189
|
-
const sheet = document.createElement("style");
|
|
11190
|
-
sheet.textContent = processSheet(css);
|
|
11191
|
-
STYLESHEETS[id] = sheet;
|
|
11192
|
-
}
|
|
11193
|
-
function activateSheet(id) {
|
|
11194
|
-
const sheet = STYLESHEETS[id];
|
|
11195
|
-
sheet.setAttribute("component", id);
|
|
11196
|
-
document.head.appendChild(sheet);
|
|
11197
|
-
}
|
|
11198
|
-
function getTextDecoration({ strikethrough, underline, }) {
|
|
11199
|
-
if (!strikethrough && !underline) {
|
|
11200
|
-
return "none";
|
|
11201
|
-
}
|
|
11202
|
-
return `${strikethrough ? "line-through" : ""} ${underline ? "underline" : ""}`;
|
|
11203
|
-
}
|
|
11204
|
-
/**
|
|
11205
|
-
* Convert the cell style to CSS properties.
|
|
11206
|
-
*/
|
|
11207
|
-
function cellStyleToCss(style) {
|
|
11208
|
-
const attributes = cellTextStyleToCss(style);
|
|
11209
|
-
if (!style)
|
|
11210
|
-
return attributes;
|
|
11211
|
-
if (style.fillColor) {
|
|
11212
|
-
attributes["background"] = style.fillColor;
|
|
11213
|
-
}
|
|
11214
|
-
return attributes;
|
|
11215
|
-
}
|
|
11216
|
-
/**
|
|
11217
|
-
* Convert the cell text style to CSS properties.
|
|
11218
|
-
*/
|
|
11219
|
-
function cellTextStyleToCss(style) {
|
|
11220
|
-
const attributes = {};
|
|
11221
|
-
if (!style)
|
|
11222
|
-
return attributes;
|
|
11223
|
-
if (style.bold) {
|
|
11224
|
-
attributes["font-weight"] = "bold";
|
|
11225
|
-
}
|
|
11226
|
-
if (style.italic) {
|
|
11227
|
-
attributes["font-style"] = "italic";
|
|
11228
|
-
}
|
|
11229
|
-
if (style.strikethrough || style.underline) {
|
|
11230
|
-
let decoration = style.strikethrough ? "line-through" : "";
|
|
11231
|
-
decoration = style.underline ? decoration + " underline" : decoration;
|
|
11232
|
-
attributes["text-decoration"] = decoration;
|
|
11233
|
-
}
|
|
11234
|
-
if (style.textColor) {
|
|
11235
|
-
attributes["color"] = style.textColor;
|
|
11236
|
-
}
|
|
11237
|
-
return attributes;
|
|
11238
|
-
}
|
|
11239
|
-
/**
|
|
11240
|
-
* Transform CSS properties into a CSS string.
|
|
11241
|
-
*/
|
|
11242
|
-
function cssPropertiesToCss(attributes) {
|
|
11243
|
-
let styleStr = "";
|
|
11244
|
-
for (const attName in attributes) {
|
|
11245
|
-
if (!attributes[attName]) {
|
|
11246
|
-
continue;
|
|
11247
|
-
}
|
|
11248
|
-
styleStr += `${attName}:${attributes[attName]}; `;
|
|
11249
|
-
}
|
|
11250
|
-
return styleStr;
|
|
11251
|
-
}
|
|
11252
|
-
function getElementMargins(el) {
|
|
11253
|
-
const style = window.getComputedStyle(el);
|
|
11254
|
-
return {
|
|
11255
|
-
top: parseInt(style.marginTop, 10) || 0,
|
|
11256
|
-
bottom: parseInt(style.marginBottom, 10) || 0,
|
|
11257
|
-
left: parseInt(style.marginLeft, 10) || 0,
|
|
11258
|
-
right: parseInt(style.marginRight, 10) || 0,
|
|
11259
|
-
};
|
|
11260
|
-
}
|
|
11261
|
-
|
|
11262
|
-
css /* scss */ `
|
|
11263
|
-
.o-spreadsheet {
|
|
11264
|
-
.o-chart-custom-tooltip {
|
|
11265
|
-
font-size: 12px;
|
|
11266
|
-
background-color: #fff;
|
|
11267
|
-
z-index: ${ComponentsImportance.FigureTooltip};
|
|
11268
|
-
}
|
|
11269
|
-
}
|
|
11270
|
-
`;
|
|
11271
|
-
class ChartJsComponent extends Component {
|
|
11272
|
-
static template = "o-spreadsheet-ChartJsComponent";
|
|
11273
|
-
static props = {
|
|
11274
|
-
figure: Object,
|
|
11275
|
-
};
|
|
11276
|
-
canvas = useRef("graphContainer");
|
|
11277
|
-
chart;
|
|
11278
|
-
currentRuntime;
|
|
11279
|
-
get background() {
|
|
11280
|
-
return this.chartRuntime.background;
|
|
11281
|
-
}
|
|
11282
|
-
get canvasStyle() {
|
|
11283
|
-
return `background-color: ${this.background}`;
|
|
11284
|
-
}
|
|
11285
|
-
get chartRuntime() {
|
|
11286
|
-
const runtime = this.env.model.getters.getChartRuntime(this.props.figure.id);
|
|
11287
|
-
if (!("chartJsConfig" in runtime)) {
|
|
11288
|
-
throw new Error("Unsupported chart runtime");
|
|
11289
|
-
}
|
|
11290
|
-
return runtime;
|
|
11291
|
-
}
|
|
11292
|
-
setup() {
|
|
11293
|
-
onMounted(() => {
|
|
11294
|
-
const runtime = this.chartRuntime;
|
|
11295
|
-
this.currentRuntime = runtime;
|
|
11296
|
-
// Note: chartJS modify the runtime in place, so it's important to give it a copy
|
|
11297
|
-
this.createChart(deepCopy(runtime.chartJsConfig));
|
|
11298
|
-
});
|
|
11299
|
-
onWillUnmount(() => this.chart?.destroy());
|
|
11300
|
-
useEffect(() => {
|
|
11301
|
-
const runtime = this.chartRuntime;
|
|
11302
|
-
if (runtime !== this.currentRuntime) {
|
|
11303
|
-
if (runtime.chartJsConfig.type !== this.currentRuntime.chartJsConfig.type) {
|
|
11304
|
-
this.chart?.destroy();
|
|
11305
|
-
this.createChart(deepCopy(runtime.chartJsConfig));
|
|
11306
|
-
}
|
|
11307
|
-
else {
|
|
11308
|
-
this.updateChartJs(deepCopy(runtime));
|
|
11309
|
-
}
|
|
11310
|
-
this.currentRuntime = runtime;
|
|
11311
|
-
}
|
|
11312
|
-
});
|
|
11313
|
-
}
|
|
11314
|
-
createChart(chartData) {
|
|
11315
|
-
const canvas = this.canvas.el;
|
|
11316
|
-
const ctx = canvas.getContext("2d");
|
|
11317
|
-
const Chart = getChartJSConstructor();
|
|
11318
|
-
this.chart = new Chart(ctx, chartData);
|
|
11319
|
-
}
|
|
11320
|
-
updateChartJs(chartRuntime) {
|
|
11321
|
-
const chartData = chartRuntime.chartJsConfig;
|
|
11322
|
-
if (chartData.data && chartData.data.datasets) {
|
|
11323
|
-
this.chart.data = chartData.data;
|
|
11324
|
-
if (chartData.options?.plugins?.title) {
|
|
11325
|
-
this.chart.config.options.plugins.title = chartData.options.plugins.title;
|
|
11326
|
-
}
|
|
11327
|
-
}
|
|
11328
|
-
else {
|
|
11329
|
-
this.chart.data.datasets = [];
|
|
11330
|
-
}
|
|
11331
|
-
this.chart.config.options = chartData.options;
|
|
11332
|
-
this.chart.update();
|
|
11333
|
-
}
|
|
11334
|
-
}
|
|
11335
|
-
|
|
11336
10950
|
class ScorecardChart extends Component {
|
|
11337
10951
|
static template = "o-spreadsheet-ScorecardChart";
|
|
11338
10952
|
static props = {
|
|
@@ -22930,6 +22544,343 @@ function getDateIntervals(dates) {
|
|
|
22930
22544
|
|
|
22931
22545
|
const cellPopoverRegistry = new Registry();
|
|
22932
22546
|
|
|
22547
|
+
const GAUGE_PADDING_SIDE = 30;
|
|
22548
|
+
const GAUGE_PADDING_TOP = 10;
|
|
22549
|
+
const GAUGE_PADDING_BOTTOM = 20;
|
|
22550
|
+
const GAUGE_LABELS_FONT_SIZE = 12;
|
|
22551
|
+
const GAUGE_DEFAULT_VALUE_FONT_SIZE = 80;
|
|
22552
|
+
const GAUGE_BACKGROUND_COLOR = "#F3F2F1";
|
|
22553
|
+
const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
|
|
22554
|
+
const GAUGE_TITLE_SECTION_HEIGHT = 25;
|
|
22555
|
+
function drawGaugeChart(canvas, runtime) {
|
|
22556
|
+
const canvasBoundingRect = canvas.getBoundingClientRect();
|
|
22557
|
+
canvas.width = canvasBoundingRect.width;
|
|
22558
|
+
canvas.height = canvasBoundingRect.height;
|
|
22559
|
+
const ctx = canvas.getContext("2d");
|
|
22560
|
+
const config = getGaugeRenderingConfig(canvasBoundingRect, runtime, ctx);
|
|
22561
|
+
drawBackground(ctx, config);
|
|
22562
|
+
drawGauge(ctx, config);
|
|
22563
|
+
drawInflectionValues(ctx, config);
|
|
22564
|
+
drawLabels(ctx, config);
|
|
22565
|
+
drawTitle(ctx, config);
|
|
22566
|
+
}
|
|
22567
|
+
function drawGauge(ctx, config) {
|
|
22568
|
+
ctx.save();
|
|
22569
|
+
const gauge = config.gauge;
|
|
22570
|
+
const arcCenterX = gauge.rect.x + gauge.rect.width / 2;
|
|
22571
|
+
const arcCenterY = gauge.rect.y + gauge.rect.height;
|
|
22572
|
+
const arcRadius = gauge.rect.height - gauge.arcWidth / 2;
|
|
22573
|
+
if (arcRadius < 0) {
|
|
22574
|
+
return;
|
|
22575
|
+
}
|
|
22576
|
+
const gaugeAngle = gauge.percentage === 1 ? 0 : Math.PI * (1 + gauge.percentage);
|
|
22577
|
+
// Gauge background
|
|
22578
|
+
ctx.strokeStyle = GAUGE_BACKGROUND_COLOR;
|
|
22579
|
+
ctx.beginPath();
|
|
22580
|
+
ctx.lineWidth = gauge.arcWidth;
|
|
22581
|
+
ctx.arc(arcCenterX, arcCenterY, arcRadius, gaugeAngle, 0);
|
|
22582
|
+
ctx.stroke();
|
|
22583
|
+
// Gauge value
|
|
22584
|
+
ctx.strokeStyle = gauge.color;
|
|
22585
|
+
ctx.beginPath();
|
|
22586
|
+
ctx.arc(arcCenterX, arcCenterY, arcRadius, Math.PI, gaugeAngle);
|
|
22587
|
+
ctx.stroke();
|
|
22588
|
+
ctx.restore();
|
|
22589
|
+
}
|
|
22590
|
+
function drawBackground(ctx, config) {
|
|
22591
|
+
ctx.save();
|
|
22592
|
+
ctx.fillStyle = config.backgroundColor;
|
|
22593
|
+
ctx.fillRect(0, 0, config.width, config.height);
|
|
22594
|
+
ctx.restore();
|
|
22595
|
+
}
|
|
22596
|
+
function drawLabels(ctx, config) {
|
|
22597
|
+
for (const label of [config.minLabel, config.maxLabel, config.gaugeValue]) {
|
|
22598
|
+
ctx.save();
|
|
22599
|
+
ctx.textAlign = "center";
|
|
22600
|
+
ctx.fillStyle = label.color;
|
|
22601
|
+
ctx.font = `${label.fontSize}px ${DEFAULT_FONT}`;
|
|
22602
|
+
ctx.fillText(label.label, label.textPosition.x, label.textPosition.y);
|
|
22603
|
+
ctx.restore();
|
|
22604
|
+
}
|
|
22605
|
+
}
|
|
22606
|
+
function drawInflectionValues(ctx, config) {
|
|
22607
|
+
const { x: rectX, y: rectY, width, height } = config.gauge.rect;
|
|
22608
|
+
for (const inflectionValue of config.inflectionValues) {
|
|
22609
|
+
ctx.save();
|
|
22610
|
+
ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment
|
|
22611
|
+
ctx.rotate(Math.PI / 2 - inflectionValue.rotation);
|
|
22612
|
+
ctx.lineWidth = 2;
|
|
22613
|
+
ctx.strokeStyle = chartMutedFontColor(config.backgroundColor) + "aa";
|
|
22614
|
+
ctx.beginPath();
|
|
22615
|
+
ctx.moveTo(0, -(height - config.gauge.arcWidth));
|
|
22616
|
+
ctx.lineTo(0, -height - 3);
|
|
22617
|
+
ctx.stroke();
|
|
22618
|
+
ctx.textAlign = "center";
|
|
22619
|
+
ctx.font = `${inflectionValue.fontSize}px ${DEFAULT_FONT}`;
|
|
22620
|
+
ctx.fillStyle = inflectionValue.color;
|
|
22621
|
+
const textY = -height - GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN - inflectionValue.offset;
|
|
22622
|
+
ctx.fillText(inflectionValue.label, 0, textY);
|
|
22623
|
+
ctx.restore();
|
|
22624
|
+
}
|
|
22625
|
+
}
|
|
22626
|
+
function drawTitle(ctx, config) {
|
|
22627
|
+
ctx.save();
|
|
22628
|
+
const title = config.title;
|
|
22629
|
+
ctx.font = getDefaultContextFont(title.fontSize, title.bold, title.italic);
|
|
22630
|
+
ctx.textBaseline = "middle";
|
|
22631
|
+
ctx.fillStyle = title.color;
|
|
22632
|
+
ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
|
|
22633
|
+
ctx.restore();
|
|
22634
|
+
}
|
|
22635
|
+
function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
|
|
22636
|
+
const maxValue = runtime.maxValue;
|
|
22637
|
+
const minValue = runtime.minValue;
|
|
22638
|
+
const gaugeValue = runtime.gaugeValue;
|
|
22639
|
+
const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
|
|
22640
|
+
const gaugeArcWidth = gaugeRect.width / 6;
|
|
22641
|
+
const gaugePercentage = gaugeValue
|
|
22642
|
+
? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
|
|
22643
|
+
: 0;
|
|
22644
|
+
const gaugeValuePosition = {
|
|
22645
|
+
x: boundingRect.width / 2,
|
|
22646
|
+
y: gaugeRect.y + gaugeRect.height - gaugeRect.height / 12,
|
|
22647
|
+
};
|
|
22648
|
+
let gaugeValueFontSize = GAUGE_DEFAULT_VALUE_FONT_SIZE;
|
|
22649
|
+
// Scale down the font size if the gaugeRect is too small
|
|
22650
|
+
if (gaugeRect.height < 300) {
|
|
22651
|
+
gaugeValueFontSize = gaugeValueFontSize * (gaugeRect.height / 300);
|
|
22652
|
+
}
|
|
22653
|
+
// Scale down the font size if the text is too long
|
|
22654
|
+
const maxTextWidth = gaugeRect.width / 2;
|
|
22655
|
+
const gaugeLabel = gaugeValue?.label || "-";
|
|
22656
|
+
if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
|
|
22657
|
+
gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
|
|
22658
|
+
}
|
|
22659
|
+
const minLabelPosition = {
|
|
22660
|
+
x: gaugeRect.x + gaugeArcWidth / 2,
|
|
22661
|
+
y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
|
|
22662
|
+
};
|
|
22663
|
+
const maxLabelPosition = {
|
|
22664
|
+
x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
|
|
22665
|
+
y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
|
|
22666
|
+
};
|
|
22667
|
+
const textColor = chartMutedFontColor(runtime.background);
|
|
22668
|
+
const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);
|
|
22669
|
+
let x = 0, titleWidth = 0, titleHeight = 0;
|
|
22670
|
+
if (runtime.title.text) {
|
|
22671
|
+
({ width: titleWidth, height: titleHeight } = computeTextDimension(ctx, runtime.title.text, { fontSize: CHART_TITLE_FONT_SIZE, ...runtime.title }, "px"));
|
|
22672
|
+
}
|
|
22673
|
+
switch (runtime.title.align) {
|
|
22674
|
+
case "right":
|
|
22675
|
+
x = boundingRect.width - titleWidth - CHART_PADDING$1;
|
|
22676
|
+
break;
|
|
22677
|
+
case "center":
|
|
22678
|
+
x = (boundingRect.width - titleWidth) / 2;
|
|
22679
|
+
break;
|
|
22680
|
+
case "left":
|
|
22681
|
+
default:
|
|
22682
|
+
x = CHART_PADDING$1;
|
|
22683
|
+
break;
|
|
22684
|
+
}
|
|
22685
|
+
return {
|
|
22686
|
+
width: boundingRect.width,
|
|
22687
|
+
height: boundingRect.height,
|
|
22688
|
+
title: {
|
|
22689
|
+
label: runtime.title.text ?? "",
|
|
22690
|
+
fontSize: runtime.title.fontSize ?? CHART_TITLE_FONT_SIZE,
|
|
22691
|
+
textPosition: {
|
|
22692
|
+
x,
|
|
22693
|
+
y: CHART_PADDING_TOP + titleHeight / 2,
|
|
22694
|
+
},
|
|
22695
|
+
color: runtime.title.color ?? textColor,
|
|
22696
|
+
bold: runtime.title.bold,
|
|
22697
|
+
italic: runtime.title.italic,
|
|
22698
|
+
},
|
|
22699
|
+
backgroundColor: runtime.background,
|
|
22700
|
+
gauge: {
|
|
22701
|
+
rect: gaugeRect,
|
|
22702
|
+
arcWidth: gaugeArcWidth,
|
|
22703
|
+
percentage: clip(gaugePercentage, 0, 1),
|
|
22704
|
+
color: getGaugeColor(runtime),
|
|
22705
|
+
},
|
|
22706
|
+
inflectionValues,
|
|
22707
|
+
gaugeValue: {
|
|
22708
|
+
label: gaugeLabel,
|
|
22709
|
+
textPosition: gaugeValuePosition,
|
|
22710
|
+
fontSize: gaugeValueFontSize,
|
|
22711
|
+
color: textColor,
|
|
22712
|
+
},
|
|
22713
|
+
minLabel: {
|
|
22714
|
+
label: runtime.minValue.label,
|
|
22715
|
+
textPosition: minLabelPosition,
|
|
22716
|
+
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
22717
|
+
color: textColor,
|
|
22718
|
+
},
|
|
22719
|
+
maxLabel: {
|
|
22720
|
+
label: runtime.maxValue.label,
|
|
22721
|
+
textPosition: maxLabelPosition,
|
|
22722
|
+
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
22723
|
+
color: textColor,
|
|
22724
|
+
},
|
|
22725
|
+
};
|
|
22726
|
+
}
|
|
22727
|
+
/**
|
|
22728
|
+
* Get the rectangle in which the gauge will be drawn, based on the bounding rectangle of the canvas and leaving
|
|
22729
|
+
* space for the title and labels.
|
|
22730
|
+
*/
|
|
22731
|
+
function getGaugeRect(boundingRect, title) {
|
|
22732
|
+
const titleHeight = title ? GAUGE_TITLE_SECTION_HEIGHT : 0;
|
|
22733
|
+
const drawHeight = boundingRect.height - GAUGE_PADDING_BOTTOM - titleHeight - GAUGE_PADDING_TOP;
|
|
22734
|
+
const drawWidth = boundingRect.width - GAUGE_PADDING_SIDE * 2;
|
|
22735
|
+
let gaugeWidth;
|
|
22736
|
+
let gaugeHeight;
|
|
22737
|
+
if (drawWidth > 2 * drawHeight) {
|
|
22738
|
+
gaugeWidth = 2 * drawHeight;
|
|
22739
|
+
gaugeHeight = drawHeight;
|
|
22740
|
+
}
|
|
22741
|
+
else {
|
|
22742
|
+
gaugeWidth = drawWidth;
|
|
22743
|
+
gaugeHeight = drawWidth / 2;
|
|
22744
|
+
}
|
|
22745
|
+
const gaugeX = GAUGE_PADDING_SIDE + (drawWidth - gaugeWidth) / 2;
|
|
22746
|
+
const gaugeY = titleHeight + GAUGE_PADDING_TOP + (drawHeight - gaugeHeight) / 2;
|
|
22747
|
+
return {
|
|
22748
|
+
x: gaugeX,
|
|
22749
|
+
y: gaugeY,
|
|
22750
|
+
width: gaugeWidth,
|
|
22751
|
+
height: gaugeHeight,
|
|
22752
|
+
};
|
|
22753
|
+
}
|
|
22754
|
+
/**
|
|
22755
|
+
* 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).
|
|
22756
|
+
*
|
|
22757
|
+
* Also compute an offset for the text so that it doesn't overlap with other text.
|
|
22758
|
+
*/
|
|
22759
|
+
function getInflectionValues(runtime, gaugeRect, textColor, ctx) {
|
|
22760
|
+
const maxValue = runtime.maxValue;
|
|
22761
|
+
const minValue = runtime.minValue;
|
|
22762
|
+
const gaugeCircleCenter = {
|
|
22763
|
+
x: gaugeRect.x + gaugeRect.width / 2,
|
|
22764
|
+
y: gaugeRect.y + gaugeRect.height,
|
|
22765
|
+
};
|
|
22766
|
+
const textStyle = { fontSize: GAUGE_LABELS_FONT_SIZE };
|
|
22767
|
+
const inflectionValues = [];
|
|
22768
|
+
const inflectionValuesTextRects = [];
|
|
22769
|
+
for (const inflectionValue of runtime.inflectionValues) {
|
|
22770
|
+
const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
|
|
22771
|
+
const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
|
|
22772
|
+
const angle = Math.PI - Math.PI * percentage;
|
|
22773
|
+
const textRect = getRectangleTangentToCircle(angle, // angle between X axis and the point where the rectangle is tangent to the circle
|
|
22774
|
+
gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, // radius of the gauge circle + margin below text
|
|
22775
|
+
gaugeCircleCenter.x, // center of the gauge circle
|
|
22776
|
+
gaugeCircleCenter.y, // center of the gauge circle
|
|
22777
|
+
labelWidth + 2, // width of the text + some margin
|
|
22778
|
+
GAUGE_LABELS_FONT_SIZE // height of the text
|
|
22779
|
+
);
|
|
22780
|
+
let offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect))
|
|
22781
|
+
? GAUGE_LABELS_FONT_SIZE
|
|
22782
|
+
: 0;
|
|
22783
|
+
inflectionValuesTextRects.push(textRect);
|
|
22784
|
+
inflectionValues.push({
|
|
22785
|
+
rotation: angle,
|
|
22786
|
+
label: inflectionValue.label,
|
|
22787
|
+
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
22788
|
+
color: textColor,
|
|
22789
|
+
offset,
|
|
22790
|
+
});
|
|
22791
|
+
}
|
|
22792
|
+
return inflectionValues;
|
|
22793
|
+
}
|
|
22794
|
+
function getGaugeColor(runtime) {
|
|
22795
|
+
const gaugeValue = runtime.gaugeValue?.value;
|
|
22796
|
+
if (gaugeValue === undefined) {
|
|
22797
|
+
return GAUGE_BACKGROUND_COLOR;
|
|
22798
|
+
}
|
|
22799
|
+
for (let i = 0; i < runtime.inflectionValues.length; i++) {
|
|
22800
|
+
const inflectionValue = runtime.inflectionValues[i];
|
|
22801
|
+
if (inflectionValue.operator === "<" && gaugeValue < inflectionValue.value) {
|
|
22802
|
+
return runtime.colors[i];
|
|
22803
|
+
}
|
|
22804
|
+
else if (inflectionValue.operator === "<=" && gaugeValue <= inflectionValue.value) {
|
|
22805
|
+
return runtime.colors[i];
|
|
22806
|
+
}
|
|
22807
|
+
}
|
|
22808
|
+
return runtime.colors.at(-1);
|
|
22809
|
+
}
|
|
22810
|
+
function getSegmentsOfRectangle(rectangle) {
|
|
22811
|
+
return [
|
|
22812
|
+
{ start: rectangle.topLeft, end: rectangle.topRight },
|
|
22813
|
+
{ start: rectangle.topRight, end: rectangle.bottomRight },
|
|
22814
|
+
{ start: rectangle.bottomRight, end: rectangle.bottomLeft },
|
|
22815
|
+
{ start: rectangle.bottomLeft, end: rectangle.topLeft },
|
|
22816
|
+
];
|
|
22817
|
+
}
|
|
22818
|
+
/**
|
|
22819
|
+
* Check if two segment intersect. The case where the segments are colinear (both segments on the same line)
|
|
22820
|
+
* is not handled.
|
|
22821
|
+
*/
|
|
22822
|
+
function doSegmentIntersect(segment1, segment2) {
|
|
22823
|
+
const A = segment1.start;
|
|
22824
|
+
const B = segment1.end;
|
|
22825
|
+
const C = segment2.start;
|
|
22826
|
+
const D = segment2.end;
|
|
22827
|
+
/**
|
|
22828
|
+
* Line segment intersection algorithm
|
|
22829
|
+
* https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
|
|
22830
|
+
*/
|
|
22831
|
+
function ccw(a, b, c) {
|
|
22832
|
+
return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
|
|
22833
|
+
}
|
|
22834
|
+
return ccw(A, C, D) !== ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D);
|
|
22835
|
+
}
|
|
22836
|
+
function doRectanglesIntersect(rect1, rect2) {
|
|
22837
|
+
const segments1 = getSegmentsOfRectangle(rect1);
|
|
22838
|
+
const segments2 = getSegmentsOfRectangle(rect2);
|
|
22839
|
+
for (const segment1 of segments1) {
|
|
22840
|
+
for (const segment2 of segments2) {
|
|
22841
|
+
if (doSegmentIntersect(segment1, segment2)) {
|
|
22842
|
+
return true;
|
|
22843
|
+
}
|
|
22844
|
+
}
|
|
22845
|
+
}
|
|
22846
|
+
return false;
|
|
22847
|
+
}
|
|
22848
|
+
/**
|
|
22849
|
+
* Get the rectangle that is tangent to a circle at a given angle.
|
|
22850
|
+
*
|
|
22851
|
+
* @param angle angle between X axis and the point where the rectangle is tangent to the circle
|
|
22852
|
+
*/
|
|
22853
|
+
function getRectangleTangentToCircle(angle, radius, circleCenterX, circleCenterY, rectWidth, rectHeight) {
|
|
22854
|
+
const cos = Math.cos(angle);
|
|
22855
|
+
const sin = Math.sin(angle);
|
|
22856
|
+
// x, y are the distance from the center of the circle to the point where the rectangle is tangent to the circle
|
|
22857
|
+
const x = cos * radius;
|
|
22858
|
+
const y = sin * radius;
|
|
22859
|
+
// x2, y2 are the distance from the point the rectangle is tangent to the circle to the bottom left corner of the rectangle
|
|
22860
|
+
const x2 = sin * (rectWidth / 2); // cos(angle + 90°) = sin(angle)
|
|
22861
|
+
const y2 = cos * (rectWidth / 2);
|
|
22862
|
+
const bottomRight = {
|
|
22863
|
+
x: x + x2 + circleCenterX,
|
|
22864
|
+
y: circleCenterY - (y - y2),
|
|
22865
|
+
};
|
|
22866
|
+
const bottomLeft = {
|
|
22867
|
+
x: x - x2 + circleCenterX,
|
|
22868
|
+
y: circleCenterY - (y + y2),
|
|
22869
|
+
};
|
|
22870
|
+
// Same as above but for the top corners of the rectangle (radius + rectangle height instead of radius)
|
|
22871
|
+
const xp = cos * (radius + rectHeight);
|
|
22872
|
+
const yp = sin * (radius + rectHeight);
|
|
22873
|
+
const topLeft = {
|
|
22874
|
+
x: xp - x2 + circleCenterX,
|
|
22875
|
+
y: circleCenterY - (yp + y2),
|
|
22876
|
+
};
|
|
22877
|
+
const topRight = {
|
|
22878
|
+
x: xp + x2 + circleCenterX,
|
|
22879
|
+
y: circleCenterY - (yp - y2),
|
|
22880
|
+
};
|
|
22881
|
+
return { bottomLeft, bottomRight, topRight, topLeft };
|
|
22882
|
+
}
|
|
22883
|
+
|
|
22933
22884
|
class GaugeChartComponent extends Component {
|
|
22934
22885
|
static template = "o-spreadsheet-GaugeChartComponent";
|
|
22935
22886
|
canvas = useRef("chartContainer");
|
|
@@ -22962,6 +22913,73 @@ function toXlsxHexColor(color) {
|
|
|
22962
22913
|
return color;
|
|
22963
22914
|
}
|
|
22964
22915
|
|
|
22916
|
+
const CHART_COMMON_OPTIONS = {
|
|
22917
|
+
// https://www.chartjs.org/docs/latest/general/responsive.html
|
|
22918
|
+
responsive: true, // will resize when its container is resized
|
|
22919
|
+
maintainAspectRatio: false, // doesn't maintain the aspect ratio (width/height =2 by default) so the user has the choice of the exact layout
|
|
22920
|
+
elements: {
|
|
22921
|
+
line: {
|
|
22922
|
+
fill: false, // do not fill the area under line charts
|
|
22923
|
+
},
|
|
22924
|
+
point: {
|
|
22925
|
+
hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
|
|
22926
|
+
},
|
|
22927
|
+
},
|
|
22928
|
+
animation: false,
|
|
22929
|
+
};
|
|
22930
|
+
function chartToImage(runtime, figure, type) {
|
|
22931
|
+
// wrap the canvas in a div with a fixed size because chart.js would
|
|
22932
|
+
// fill the whole page otherwise
|
|
22933
|
+
const div = document.createElement("div");
|
|
22934
|
+
div.style.width = `${figure.width}px`;
|
|
22935
|
+
div.style.height = `${figure.height}px`;
|
|
22936
|
+
const canvas = document.createElement("canvas");
|
|
22937
|
+
div.append(canvas);
|
|
22938
|
+
canvas.setAttribute("width", figure.width.toString());
|
|
22939
|
+
canvas.setAttribute("height", figure.height.toString());
|
|
22940
|
+
// we have to add the canvas to the DOM otherwise it won't be rendered
|
|
22941
|
+
document.body.append(div);
|
|
22942
|
+
if ("chartJsConfig" in runtime) {
|
|
22943
|
+
const config = deepCopy(runtime.chartJsConfig);
|
|
22944
|
+
config.plugins = [backgroundColorChartJSPlugin];
|
|
22945
|
+
const Chart = getChartJSConstructor();
|
|
22946
|
+
const chart = new Chart(canvas, config);
|
|
22947
|
+
const imgContent = chart.toBase64Image();
|
|
22948
|
+
chart.destroy();
|
|
22949
|
+
div.remove();
|
|
22950
|
+
return imgContent;
|
|
22951
|
+
}
|
|
22952
|
+
else if (type === "scorecard") {
|
|
22953
|
+
const design = getScorecardConfiguration(figure, runtime);
|
|
22954
|
+
drawScoreChart(design, canvas);
|
|
22955
|
+
const imgContent = canvas.toDataURL();
|
|
22956
|
+
div.remove();
|
|
22957
|
+
return imgContent;
|
|
22958
|
+
}
|
|
22959
|
+
else if (type === "gauge") {
|
|
22960
|
+
drawGaugeChart(canvas, runtime);
|
|
22961
|
+
const imgContent = canvas.toDataURL();
|
|
22962
|
+
div.remove();
|
|
22963
|
+
return imgContent;
|
|
22964
|
+
}
|
|
22965
|
+
return undefined;
|
|
22966
|
+
}
|
|
22967
|
+
/**
|
|
22968
|
+
* Custom chart.js plugin to set the background color of the canvas
|
|
22969
|
+
* https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md
|
|
22970
|
+
*/
|
|
22971
|
+
const backgroundColorChartJSPlugin = {
|
|
22972
|
+
id: "customCanvasBackgroundColor",
|
|
22973
|
+
beforeDraw: (chart) => {
|
|
22974
|
+
const { ctx } = chart;
|
|
22975
|
+
ctx.save();
|
|
22976
|
+
ctx.globalCompositeOperation = "destination-over";
|
|
22977
|
+
ctx.fillStyle = "#ffffff";
|
|
22978
|
+
ctx.fillRect(0, 0, chart.width, chart.height);
|
|
22979
|
+
ctx.restore();
|
|
22980
|
+
},
|
|
22981
|
+
};
|
|
22982
|
+
|
|
22965
22983
|
/**
|
|
22966
22984
|
* Represent a raw XML string
|
|
22967
22985
|
*/
|
|
@@ -34232,7 +34250,6 @@ var CHART_HELPERS = /*#__PURE__*/Object.freeze({
|
|
|
34232
34250
|
duplicateLabelRangeInDuplicatedSheet: duplicateLabelRangeInDuplicatedSheet,
|
|
34233
34251
|
formatChartDatasetValue: formatChartDatasetValue,
|
|
34234
34252
|
formatTickValue: formatTickValue,
|
|
34235
|
-
getChartJSConstructor: getChartJSConstructor,
|
|
34236
34253
|
getChartPositionAtCenterOfViewport: getChartPositionAtCenterOfViewport,
|
|
34237
34254
|
getDefinedAxis: getDefinedAxis,
|
|
34238
34255
|
getPieColors: getPieColors,
|
|
@@ -45080,7 +45097,8 @@ css /* scss */ `
|
|
|
45080
45097
|
&.pivot-dimension-invalid {
|
|
45081
45098
|
background-color: #ffdddd;
|
|
45082
45099
|
border-color: red !important;
|
|
45083
|
-
select
|
|
45100
|
+
select,
|
|
45101
|
+
input {
|
|
45084
45102
|
background-color: #ffdddd;
|
|
45085
45103
|
}
|
|
45086
45104
|
}
|
|
@@ -46941,7 +46959,7 @@ class PivotSidePanelStore extends SpreadsheetStore {
|
|
|
46941
46959
|
this.notification.notifyUser({
|
|
46942
46960
|
type: "info",
|
|
46943
46961
|
text: _t("Pivot updates only work with dynamic pivot tables. Use %s or re-insert the static pivot from the Data menu.", pivotExample),
|
|
46944
|
-
sticky:
|
|
46962
|
+
sticky: true,
|
|
46945
46963
|
});
|
|
46946
46964
|
}
|
|
46947
46965
|
}
|
|
@@ -75888,6 +75906,7 @@ const registries = {
|
|
|
75888
75906
|
supportedPivotPositionalFormulaRegistry,
|
|
75889
75907
|
pivotToFunctionValueRegistry,
|
|
75890
75908
|
migrationStepRegistry,
|
|
75909
|
+
chartJsExtensionRegistry,
|
|
75891
75910
|
};
|
|
75892
75911
|
const helpers = {
|
|
75893
75912
|
arg,
|
|
@@ -76043,6 +76062,6 @@ const chartHelpers = { ...CHART_HELPERS, ...CHART_RUNTIME_HELPERS };
|
|
|
76043
76062
|
export { AbstractCellClipboardHandler, AbstractChart, AbstractFigureClipboardHandler, CellErrorType, CommandResult, CorePlugin, CoreViewPlugin, DispatchResult, EvaluationError, Model, PivotRuntimeDefinition, Registry, Revision, SPREADSHEET_DIMENSIONS, Spreadsheet, SpreadsheetPivotTable, UIPlugin, __info__, addFunction, addRenderingLayer, astToFormula, chartHelpers, compile, compileTokens, components, constants, convertAstNodes, coreTypes, findCellInNewZone, functionCache, helpers, hooks, invalidateCFEvaluationCommands, invalidateDependenciesCommands, invalidateEvaluationCommands, iterateAstNodes, links, load, parse, parseTokens, readonlyAllowedCommands, registries, setDefaultSheetViewSize, setTranslationMethod, stores, tokenColors, tokenize };
|
|
76044
76063
|
|
|
76045
76064
|
|
|
76046
|
-
__info__.version = "18.2.
|
|
76047
|
-
__info__.date = "2025-
|
|
76048
|
-
__info__.hash = "
|
|
76065
|
+
__info__.version = "18.2.6";
|
|
76066
|
+
__info__.date = "2025-04-04T08:41:26.115Z";
|
|
76067
|
+
__info__.hash = "faa00e2";
|