@odoo/o-spreadsheet 18.2.4 → 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 +782 -741
- package/dist/o-spreadsheet.d.ts +9 -6
- package/dist/o-spreadsheet.esm.js +782 -741
- package/dist/o-spreadsheet.iife.js +782 -741
- package/dist/o-spreadsheet.iife.min.js +387 -384
- package/dist/o_spreadsheet.xml +9 -6
- 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
|
(function (exports, owl) {
|
|
@@ -805,8 +805,7 @@
|
|
|
805
805
|
*
|
|
806
806
|
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Character_Classes
|
|
807
807
|
*/
|
|
808
|
-
const
|
|
809
|
-
" ",
|
|
808
|
+
const specialWhiteSpaceSpecialCharacters = [
|
|
810
809
|
"\t",
|
|
811
810
|
"\f",
|
|
812
811
|
"\v",
|
|
@@ -821,7 +820,7 @@
|
|
|
821
820
|
String.fromCharCode(parseInt("3000", 16)),
|
|
822
821
|
String.fromCharCode(parseInt("feff", 16)),
|
|
823
822
|
];
|
|
824
|
-
const
|
|
823
|
+
const specialWhiteSpaceRegexp = new RegExp(specialWhiteSpaceSpecialCharacters.join("|"), "g");
|
|
825
824
|
const newLineRegexp = /(\r\n|\r)/g;
|
|
826
825
|
/**
|
|
827
826
|
* Replace all different newlines characters by \n
|
|
@@ -1132,7 +1131,10 @@
|
|
|
1132
1131
|
}
|
|
1133
1132
|
else if (stringVals.length === 4) {
|
|
1134
1133
|
const alpha = parseFloat(stringVals.pop() || "1");
|
|
1135
|
-
|
|
1134
|
+
if (isNaN(alpha)) {
|
|
1135
|
+
throw new Error("invalid alpha value");
|
|
1136
|
+
}
|
|
1137
|
+
alphaHex = Math.round(alpha * 255);
|
|
1136
1138
|
}
|
|
1137
1139
|
const vals = stringVals.map((val) => parseInt(val, 10));
|
|
1138
1140
|
if (alphaHex !== 255) {
|
|
@@ -6795,8 +6797,12 @@
|
|
|
6795
6797
|
str = replaceNewLines(str);
|
|
6796
6798
|
const chars = new TokenizingChars(str);
|
|
6797
6799
|
const result = [];
|
|
6800
|
+
const tokenizeSpace = specialWhiteSpaceRegexp.test(str)
|
|
6801
|
+
? tokenizeSpecialCharacterSpace
|
|
6802
|
+
: tokenizeSimpleSpace;
|
|
6798
6803
|
while (!chars.isOver()) {
|
|
6799
|
-
let token =
|
|
6804
|
+
let token = tokenizeNewLine(chars) ||
|
|
6805
|
+
tokenizeSpace(chars) ||
|
|
6800
6806
|
tokenizeArgsSeparator(chars, locale) ||
|
|
6801
6807
|
tokenizeParenthesis(chars) ||
|
|
6802
6808
|
tokenizeOperator(chars) ||
|
|
@@ -6930,17 +6936,19 @@
|
|
|
6930
6936
|
}
|
|
6931
6937
|
return null;
|
|
6932
6938
|
}
|
|
6933
|
-
function
|
|
6934
|
-
let
|
|
6935
|
-
while (chars.current ===
|
|
6936
|
-
|
|
6937
|
-
chars.shift();
|
|
6939
|
+
function tokenizeSpecialCharacterSpace(chars) {
|
|
6940
|
+
let spaces = "";
|
|
6941
|
+
while (chars.current === " " || (chars.current && chars.current.match(specialWhiteSpaceRegexp))) {
|
|
6942
|
+
spaces += chars.shift();
|
|
6938
6943
|
}
|
|
6939
|
-
if (
|
|
6940
|
-
return { type: "SPACE", value:
|
|
6944
|
+
if (spaces) {
|
|
6945
|
+
return { type: "SPACE", value: spaces };
|
|
6941
6946
|
}
|
|
6947
|
+
return null;
|
|
6948
|
+
}
|
|
6949
|
+
function tokenizeSimpleSpace(chars) {
|
|
6942
6950
|
let spaces = "";
|
|
6943
|
-
while (chars.current
|
|
6951
|
+
while (chars.current === " ") {
|
|
6944
6952
|
spaces += chars.shift();
|
|
6945
6953
|
}
|
|
6946
6954
|
if (spaces) {
|
|
@@ -6948,6 +6956,17 @@
|
|
|
6948
6956
|
}
|
|
6949
6957
|
return null;
|
|
6950
6958
|
}
|
|
6959
|
+
function tokenizeNewLine(chars) {
|
|
6960
|
+
let length = 0;
|
|
6961
|
+
while (chars.current === NEWLINE) {
|
|
6962
|
+
length++;
|
|
6963
|
+
chars.shift();
|
|
6964
|
+
}
|
|
6965
|
+
if (length) {
|
|
6966
|
+
return { type: "SPACE", value: NEWLINE.repeat(length) };
|
|
6967
|
+
}
|
|
6968
|
+
return null;
|
|
6969
|
+
}
|
|
6951
6970
|
function tokenizeInvalidRange(chars) {
|
|
6952
6971
|
if (chars.currentStartsWith(CellErrorType.InvalidReference)) {
|
|
6953
6972
|
chars.advanceBy(CellErrorType.InvalidReference.length);
|
|
@@ -7000,7 +7019,7 @@
|
|
|
7000
7019
|
*/
|
|
7001
7020
|
function canonicalizeNumberContent(content, locale) {
|
|
7002
7021
|
return content.startsWith("=")
|
|
7003
|
-
? canonicalizeFormula
|
|
7022
|
+
? canonicalizeFormula(content, locale)
|
|
7004
7023
|
: canonicalizeNumberLiteral(content, locale);
|
|
7005
7024
|
}
|
|
7006
7025
|
/**
|
|
@@ -7015,7 +7034,7 @@
|
|
|
7015
7034
|
*/
|
|
7016
7035
|
function canonicalizeContent(content, locale) {
|
|
7017
7036
|
return content.startsWith("=")
|
|
7018
|
-
? canonicalizeFormula
|
|
7037
|
+
? canonicalizeFormula(content, locale)
|
|
7019
7038
|
: canonicalizeLiteral(content, locale);
|
|
7020
7039
|
}
|
|
7021
7040
|
/**
|
|
@@ -7031,15 +7050,21 @@
|
|
|
7031
7050
|
? localizeFormula(content, locale)
|
|
7032
7051
|
: localizeLiteral(content, locale);
|
|
7033
7052
|
}
|
|
7053
|
+
/** Change a number string to its canonical form (en_US locale) */
|
|
7054
|
+
function canonicalizeNumberValue(content, locale) {
|
|
7055
|
+
return content.startsWith("=")
|
|
7056
|
+
? canonicalizeFormula(content, locale)
|
|
7057
|
+
: canonicalizeNumberLiteral(content, locale);
|
|
7058
|
+
}
|
|
7034
7059
|
/** Change a formula to its canonical form (en_US locale) */
|
|
7035
|
-
function canonicalizeFormula
|
|
7036
|
-
return _localizeFormula
|
|
7060
|
+
function canonicalizeFormula(formula, locale) {
|
|
7061
|
+
return _localizeFormula(formula, locale, DEFAULT_LOCALE);
|
|
7037
7062
|
}
|
|
7038
7063
|
/** Change a formula from the canonical form to the given locale */
|
|
7039
7064
|
function localizeFormula(formula, locale) {
|
|
7040
|
-
return _localizeFormula
|
|
7065
|
+
return _localizeFormula(formula, DEFAULT_LOCALE, locale);
|
|
7041
7066
|
}
|
|
7042
|
-
function _localizeFormula
|
|
7067
|
+
function _localizeFormula(formula, fromLocale, toLocale) {
|
|
7043
7068
|
if (fromLocale.formulaArgSeparator === toLocale.formulaArgSeparator &&
|
|
7044
7069
|
fromLocale.decimalSeparator === toLocale.decimalSeparator) {
|
|
7045
7070
|
return formula;
|
|
@@ -7194,37 +7219,6 @@
|
|
|
7194
7219
|
return locale.dateFormat + " " + locale.timeFormat;
|
|
7195
7220
|
}
|
|
7196
7221
|
|
|
7197
|
-
/** Change a number string to its canonical form (en_US locale) */
|
|
7198
|
-
function canonicalizeNumberValue(content, locale) {
|
|
7199
|
-
return content.startsWith("=")
|
|
7200
|
-
? canonicalizeFormula(content, locale)
|
|
7201
|
-
: canonicalizeNumberLiteral(content, locale);
|
|
7202
|
-
}
|
|
7203
|
-
/** Change a formula to its canonical form (en_US locale) */
|
|
7204
|
-
function canonicalizeFormula(formula, locale) {
|
|
7205
|
-
return _localizeFormula(formula, locale, DEFAULT_LOCALE);
|
|
7206
|
-
}
|
|
7207
|
-
function _localizeFormula(formula, fromLocale, toLocale) {
|
|
7208
|
-
if (fromLocale.formulaArgSeparator === toLocale.formulaArgSeparator &&
|
|
7209
|
-
fromLocale.decimalSeparator === toLocale.decimalSeparator) {
|
|
7210
|
-
return formula;
|
|
7211
|
-
}
|
|
7212
|
-
const tokens = tokenize(formula, fromLocale);
|
|
7213
|
-
let localizedFormula = "";
|
|
7214
|
-
for (const token of tokens) {
|
|
7215
|
-
if (token.type === "NUMBER") {
|
|
7216
|
-
localizedFormula += token.value.replace(fromLocale.decimalSeparator, toLocale.decimalSeparator);
|
|
7217
|
-
}
|
|
7218
|
-
else if (token.type === "ARG_SEPARATOR") {
|
|
7219
|
-
localizedFormula += toLocale.formulaArgSeparator;
|
|
7220
|
-
}
|
|
7221
|
-
else {
|
|
7222
|
-
localizedFormula += token.value;
|
|
7223
|
-
}
|
|
7224
|
-
}
|
|
7225
|
-
return localizedFormula;
|
|
7226
|
-
}
|
|
7227
|
-
|
|
7228
7222
|
function boolAnd(args) {
|
|
7229
7223
|
let foundBoolean = false;
|
|
7230
7224
|
let acc = true;
|
|
@@ -9599,7 +9593,161 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
9599
9593
|
}
|
|
9600
9594
|
}
|
|
9601
9595
|
|
|
9596
|
+
/**
|
|
9597
|
+
* This file is largely inspired by owl 1.
|
|
9598
|
+
* `css` tag has been removed from owl 2 without workaround to manage css.
|
|
9599
|
+
* So, the solution was to import the behavior of owl 1 directly in our
|
|
9600
|
+
* codebase, with one difference: the css is added to the sheet as soon as the
|
|
9601
|
+
* css tag is executed. In owl 1, the css was added as soon as a Component was
|
|
9602
|
+
* created for the first time.
|
|
9603
|
+
*/
|
|
9604
|
+
const STYLESHEETS = {};
|
|
9605
|
+
let nextId = 0;
|
|
9606
|
+
/**
|
|
9607
|
+
* CSS tag helper for defining inline stylesheets. With this, one can simply define
|
|
9608
|
+
* an inline stylesheet with just the following code:
|
|
9609
|
+
* ```js
|
|
9610
|
+
* css`.component-a { color: red; }`;
|
|
9611
|
+
* ```
|
|
9612
|
+
*/
|
|
9613
|
+
function css(strings, ...args) {
|
|
9614
|
+
const name = `__sheet__${nextId++}`;
|
|
9615
|
+
const value = String.raw(strings, ...args);
|
|
9616
|
+
registerSheet(name, value);
|
|
9617
|
+
activateSheet(name);
|
|
9618
|
+
return name;
|
|
9619
|
+
}
|
|
9620
|
+
function processSheet(str) {
|
|
9621
|
+
const tokens = str.split(/(\{|\}|;)/).map((s) => s.trim());
|
|
9622
|
+
const selectorStack = [];
|
|
9623
|
+
const parts = [];
|
|
9624
|
+
let rules = [];
|
|
9625
|
+
function generateSelector(stackIndex, parentSelector) {
|
|
9626
|
+
const parts = [];
|
|
9627
|
+
for (const selector of selectorStack[stackIndex]) {
|
|
9628
|
+
let part = (parentSelector && parentSelector + " " + selector) || selector;
|
|
9629
|
+
if (part.includes("&")) {
|
|
9630
|
+
part = selector.replace(/&/g, parentSelector || "");
|
|
9631
|
+
}
|
|
9632
|
+
if (stackIndex < selectorStack.length - 1) {
|
|
9633
|
+
part = generateSelector(stackIndex + 1, part);
|
|
9634
|
+
}
|
|
9635
|
+
parts.push(part);
|
|
9636
|
+
}
|
|
9637
|
+
return parts.join(", ");
|
|
9638
|
+
}
|
|
9639
|
+
function generateRules() {
|
|
9640
|
+
if (rules.length) {
|
|
9641
|
+
parts.push(generateSelector(0) + " {");
|
|
9642
|
+
parts.push(...rules);
|
|
9643
|
+
parts.push("}");
|
|
9644
|
+
rules = [];
|
|
9645
|
+
}
|
|
9646
|
+
}
|
|
9647
|
+
while (tokens.length) {
|
|
9648
|
+
let token = tokens.shift();
|
|
9649
|
+
if (token === "}") {
|
|
9650
|
+
generateRules();
|
|
9651
|
+
selectorStack.pop();
|
|
9652
|
+
}
|
|
9653
|
+
else {
|
|
9654
|
+
if (tokens[0] === "{") {
|
|
9655
|
+
generateRules();
|
|
9656
|
+
selectorStack.push(token.split(/\s*,\s*/));
|
|
9657
|
+
tokens.shift();
|
|
9658
|
+
}
|
|
9659
|
+
if (tokens[0] === ";") {
|
|
9660
|
+
rules.push(" " + token + ";");
|
|
9661
|
+
}
|
|
9662
|
+
}
|
|
9663
|
+
}
|
|
9664
|
+
return parts.join("\n");
|
|
9665
|
+
}
|
|
9666
|
+
function registerSheet(id, css) {
|
|
9667
|
+
const sheet = document.createElement("style");
|
|
9668
|
+
sheet.textContent = processSheet(css);
|
|
9669
|
+
STYLESHEETS[id] = sheet;
|
|
9670
|
+
}
|
|
9671
|
+
function activateSheet(id) {
|
|
9672
|
+
const sheet = STYLESHEETS[id];
|
|
9673
|
+
sheet.setAttribute("component", id);
|
|
9674
|
+
document.head.appendChild(sheet);
|
|
9675
|
+
}
|
|
9676
|
+
function getTextDecoration({ strikethrough, underline, }) {
|
|
9677
|
+
if (!strikethrough && !underline) {
|
|
9678
|
+
return "none";
|
|
9679
|
+
}
|
|
9680
|
+
return `${strikethrough ? "line-through" : ""} ${underline ? "underline" : ""}`;
|
|
9681
|
+
}
|
|
9682
|
+
/**
|
|
9683
|
+
* Convert the cell style to CSS properties.
|
|
9684
|
+
*/
|
|
9685
|
+
function cellStyleToCss(style) {
|
|
9686
|
+
const attributes = cellTextStyleToCss(style);
|
|
9687
|
+
if (!style)
|
|
9688
|
+
return attributes;
|
|
9689
|
+
if (style.fillColor) {
|
|
9690
|
+
attributes["background"] = style.fillColor;
|
|
9691
|
+
}
|
|
9692
|
+
return attributes;
|
|
9693
|
+
}
|
|
9694
|
+
/**
|
|
9695
|
+
* Convert the cell text style to CSS properties.
|
|
9696
|
+
*/
|
|
9697
|
+
function cellTextStyleToCss(style) {
|
|
9698
|
+
const attributes = {};
|
|
9699
|
+
if (!style)
|
|
9700
|
+
return attributes;
|
|
9701
|
+
if (style.bold) {
|
|
9702
|
+
attributes["font-weight"] = "bold";
|
|
9703
|
+
}
|
|
9704
|
+
if (style.italic) {
|
|
9705
|
+
attributes["font-style"] = "italic";
|
|
9706
|
+
}
|
|
9707
|
+
if (style.strikethrough || style.underline) {
|
|
9708
|
+
let decoration = style.strikethrough ? "line-through" : "";
|
|
9709
|
+
decoration = style.underline ? decoration + " underline" : decoration;
|
|
9710
|
+
attributes["text-decoration"] = decoration;
|
|
9711
|
+
}
|
|
9712
|
+
if (style.textColor) {
|
|
9713
|
+
attributes["color"] = style.textColor;
|
|
9714
|
+
}
|
|
9715
|
+
return attributes;
|
|
9716
|
+
}
|
|
9717
|
+
/**
|
|
9718
|
+
* Transform CSS properties into a CSS string.
|
|
9719
|
+
*/
|
|
9720
|
+
function cssPropertiesToCss(attributes) {
|
|
9721
|
+
let styleStr = "";
|
|
9722
|
+
for (const attName in attributes) {
|
|
9723
|
+
if (!attributes[attName]) {
|
|
9724
|
+
continue;
|
|
9725
|
+
}
|
|
9726
|
+
styleStr += `${attName}:${attributes[attName]}; `;
|
|
9727
|
+
}
|
|
9728
|
+
return styleStr;
|
|
9729
|
+
}
|
|
9730
|
+
function getElementMargins(el) {
|
|
9731
|
+
const style = window.getComputedStyle(el);
|
|
9732
|
+
return {
|
|
9733
|
+
top: parseInt(style.marginTop, 10) || 0,
|
|
9734
|
+
bottom: parseInt(style.marginBottom, 10) || 0,
|
|
9735
|
+
left: parseInt(style.marginLeft, 10) || 0,
|
|
9736
|
+
right: parseInt(style.marginRight, 10) || 0,
|
|
9737
|
+
};
|
|
9738
|
+
}
|
|
9739
|
+
|
|
9740
|
+
const chartJsExtensionRegistry = new Registry();
|
|
9741
|
+
/** Return window.Chart, making sure all our extensions are loaded in ChartJS */
|
|
9742
|
+
function getChartJSConstructor() {
|
|
9743
|
+
if (window.Chart && !window.Chart?.registry.plugins.get("chartShowValuesPlugin")) {
|
|
9744
|
+
window.Chart.register(...chartJsExtensionRegistry.getAll());
|
|
9745
|
+
}
|
|
9746
|
+
return window.Chart;
|
|
9747
|
+
}
|
|
9748
|
+
|
|
9602
9749
|
const TREND_LINE_XAXIS_ID = "x1";
|
|
9750
|
+
const MOVING_AVERAGE_TREND_LINE_XAXIS_ID = "xMovingAverage";
|
|
9603
9751
|
/**
|
|
9604
9752
|
* This file contains helpers that are common to different charts (mainly
|
|
9605
9753
|
* line, bar and pie charts)
|
|
@@ -9950,6 +10098,9 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
9950
10098
|
}
|
|
9951
10099
|
return label;
|
|
9952
10100
|
}
|
|
10101
|
+
function isTrendLineAxis(axisID) {
|
|
10102
|
+
return axisID === TREND_LINE_XAXIS_ID || axisID === MOVING_AVERAGE_TREND_LINE_XAXIS_ID;
|
|
10103
|
+
}
|
|
9953
10104
|
|
|
9954
10105
|
/** This is a chartJS plugin that will draw the values of each data next to the point/bar/pie slice */
|
|
9955
10106
|
const chartShowValuesPlugin = {
|
|
@@ -9994,7 +10145,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
9994
10145
|
const yMin = chart.chartArea.top;
|
|
9995
10146
|
const textsPositions = {};
|
|
9996
10147
|
for (const dataset of chart._metasets) {
|
|
9997
|
-
if (dataset.
|
|
10148
|
+
if (isTrendLineAxis(dataset.axisID) || dataset.hidden) {
|
|
9998
10149
|
continue;
|
|
9999
10150
|
}
|
|
10000
10151
|
for (let i = 0; i < dataset._parsed.length; i++) {
|
|
@@ -10037,7 +10188,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
10037
10188
|
const xMin = chart.chartArea.left;
|
|
10038
10189
|
const textsPositions = {};
|
|
10039
10190
|
for (const dataset of chart._metasets) {
|
|
10040
|
-
if (dataset.
|
|
10191
|
+
if (isTrendLineAxis(dataset.axisID)) {
|
|
10041
10192
|
return; // ignore trend lines
|
|
10042
10193
|
}
|
|
10043
10194
|
for (let i = 0; i < dataset._parsed.length; i++) {
|
|
@@ -10143,341 +10294,79 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
10143
10294
|
return bars.find((bar, i) => i > startIndex && bar.height !== 0);
|
|
10144
10295
|
}
|
|
10145
10296
|
|
|
10146
|
-
|
|
10147
|
-
|
|
10148
|
-
|
|
10149
|
-
|
|
10150
|
-
|
|
10151
|
-
|
|
10152
|
-
|
|
10153
|
-
const GAUGE_TITLE_SECTION_HEIGHT = 25;
|
|
10154
|
-
function drawGaugeChart(canvas, runtime) {
|
|
10155
|
-
const canvasBoundingRect = canvas.getBoundingClientRect();
|
|
10156
|
-
canvas.width = canvasBoundingRect.width;
|
|
10157
|
-
canvas.height = canvasBoundingRect.height;
|
|
10158
|
-
const ctx = canvas.getContext("2d");
|
|
10159
|
-
const config = getGaugeRenderingConfig(canvasBoundingRect, runtime, ctx);
|
|
10160
|
-
drawBackground(ctx, config);
|
|
10161
|
-
drawGauge(ctx, config);
|
|
10162
|
-
drawInflectionValues(ctx, config);
|
|
10163
|
-
drawLabels(ctx, config);
|
|
10164
|
-
drawTitle(ctx, config);
|
|
10165
|
-
}
|
|
10166
|
-
function drawGauge(ctx, config) {
|
|
10167
|
-
ctx.save();
|
|
10168
|
-
const gauge = config.gauge;
|
|
10169
|
-
const arcCenterX = gauge.rect.x + gauge.rect.width / 2;
|
|
10170
|
-
const arcCenterY = gauge.rect.y + gauge.rect.height;
|
|
10171
|
-
const arcRadius = gauge.rect.height - gauge.arcWidth / 2;
|
|
10172
|
-
if (arcRadius < 0) {
|
|
10173
|
-
return;
|
|
10174
|
-
}
|
|
10175
|
-
const gaugeAngle = gauge.percentage === 1 ? 0 : Math.PI * (1 + gauge.percentage);
|
|
10176
|
-
// Gauge background
|
|
10177
|
-
ctx.strokeStyle = GAUGE_BACKGROUND_COLOR;
|
|
10178
|
-
ctx.beginPath();
|
|
10179
|
-
ctx.lineWidth = gauge.arcWidth;
|
|
10180
|
-
ctx.arc(arcCenterX, arcCenterY, arcRadius, gaugeAngle, 0);
|
|
10181
|
-
ctx.stroke();
|
|
10182
|
-
// Gauge value
|
|
10183
|
-
ctx.strokeStyle = gauge.color;
|
|
10184
|
-
ctx.beginPath();
|
|
10185
|
-
ctx.arc(arcCenterX, arcCenterY, arcRadius, Math.PI, gaugeAngle);
|
|
10186
|
-
ctx.stroke();
|
|
10187
|
-
ctx.restore();
|
|
10188
|
-
}
|
|
10189
|
-
function drawBackground(ctx, config) {
|
|
10190
|
-
ctx.save();
|
|
10191
|
-
ctx.fillStyle = config.backgroundColor;
|
|
10192
|
-
ctx.fillRect(0, 0, config.width, config.height);
|
|
10193
|
-
ctx.restore();
|
|
10194
|
-
}
|
|
10195
|
-
function drawLabels(ctx, config) {
|
|
10196
|
-
for (const label of [config.minLabel, config.maxLabel, config.gaugeValue]) {
|
|
10197
|
-
ctx.save();
|
|
10198
|
-
ctx.textAlign = "center";
|
|
10199
|
-
ctx.fillStyle = label.color;
|
|
10200
|
-
ctx.font = `${label.fontSize}px ${DEFAULT_FONT}`;
|
|
10201
|
-
ctx.fillText(label.label, label.textPosition.x, label.textPosition.y);
|
|
10202
|
-
ctx.restore();
|
|
10203
|
-
}
|
|
10204
|
-
}
|
|
10205
|
-
function drawInflectionValues(ctx, config) {
|
|
10206
|
-
const { x: rectX, y: rectY, width, height } = config.gauge.rect;
|
|
10207
|
-
for (const inflectionValue of config.inflectionValues) {
|
|
10208
|
-
ctx.save();
|
|
10209
|
-
ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment
|
|
10210
|
-
ctx.rotate(Math.PI / 2 - inflectionValue.rotation);
|
|
10211
|
-
ctx.lineWidth = 2;
|
|
10212
|
-
ctx.strokeStyle = chartMutedFontColor(config.backgroundColor) + "aa";
|
|
10213
|
-
ctx.beginPath();
|
|
10214
|
-
ctx.moveTo(0, -(height - config.gauge.arcWidth));
|
|
10215
|
-
ctx.lineTo(0, -height - 3);
|
|
10216
|
-
ctx.stroke();
|
|
10217
|
-
ctx.textAlign = "center";
|
|
10218
|
-
ctx.font = `${inflectionValue.fontSize}px ${DEFAULT_FONT}`;
|
|
10219
|
-
ctx.fillStyle = inflectionValue.color;
|
|
10220
|
-
const textY = -height - GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN - inflectionValue.offset;
|
|
10221
|
-
ctx.fillText(inflectionValue.label, 0, textY);
|
|
10222
|
-
ctx.restore();
|
|
10223
|
-
}
|
|
10224
|
-
}
|
|
10225
|
-
function drawTitle(ctx, config) {
|
|
10226
|
-
ctx.save();
|
|
10227
|
-
const title = config.title;
|
|
10228
|
-
ctx.font = getDefaultContextFont(title.fontSize, title.bold, title.italic);
|
|
10229
|
-
ctx.textBaseline = "middle";
|
|
10230
|
-
ctx.fillStyle = title.color;
|
|
10231
|
-
ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
|
|
10232
|
-
ctx.restore();
|
|
10297
|
+
css /* scss */ `
|
|
10298
|
+
.o-spreadsheet {
|
|
10299
|
+
.o-chart-custom-tooltip {
|
|
10300
|
+
font-size: 12px;
|
|
10301
|
+
background-color: #fff;
|
|
10302
|
+
z-index: ${ComponentsImportance.FigureTooltip};
|
|
10303
|
+
}
|
|
10233
10304
|
}
|
|
10234
|
-
|
|
10235
|
-
|
|
10236
|
-
|
|
10237
|
-
|
|
10238
|
-
|
|
10239
|
-
|
|
10240
|
-
|
|
10241
|
-
? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
|
|
10242
|
-
: 0;
|
|
10243
|
-
const gaugeValuePosition = {
|
|
10244
|
-
x: boundingRect.width / 2,
|
|
10245
|
-
y: gaugeRect.y + gaugeRect.height - gaugeRect.height / 12,
|
|
10246
|
-
};
|
|
10247
|
-
let gaugeValueFontSize = GAUGE_DEFAULT_VALUE_FONT_SIZE;
|
|
10248
|
-
// Scale down the font size if the gaugeRect is too small
|
|
10249
|
-
if (gaugeRect.height < 300) {
|
|
10250
|
-
gaugeValueFontSize = gaugeValueFontSize * (gaugeRect.height / 300);
|
|
10251
|
-
}
|
|
10252
|
-
// Scale down the font size if the text is too long
|
|
10253
|
-
const maxTextWidth = gaugeRect.width / 2;
|
|
10254
|
-
const gaugeLabel = gaugeValue?.label || "-";
|
|
10255
|
-
if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
|
|
10256
|
-
gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
|
|
10257
|
-
}
|
|
10258
|
-
const minLabelPosition = {
|
|
10259
|
-
x: gaugeRect.x + gaugeArcWidth / 2,
|
|
10260
|
-
y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
|
|
10261
|
-
};
|
|
10262
|
-
const maxLabelPosition = {
|
|
10263
|
-
x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
|
|
10264
|
-
y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
|
|
10305
|
+
`;
|
|
10306
|
+
chartJsExtensionRegistry.add("chartShowValuesPlugin", chartShowValuesPlugin);
|
|
10307
|
+
chartJsExtensionRegistry.add("waterfallLinesPlugin", waterfallLinesPlugin);
|
|
10308
|
+
class ChartJsComponent extends owl.Component {
|
|
10309
|
+
static template = "o-spreadsheet-ChartJsComponent";
|
|
10310
|
+
static props = {
|
|
10311
|
+
figure: Object,
|
|
10265
10312
|
};
|
|
10266
|
-
|
|
10267
|
-
|
|
10268
|
-
|
|
10269
|
-
|
|
10270
|
-
|
|
10271
|
-
}
|
|
10272
|
-
switch (runtime.title.align) {
|
|
10273
|
-
case "right":
|
|
10274
|
-
x = boundingRect.width - titleWidth - CHART_PADDING$1;
|
|
10275
|
-
break;
|
|
10276
|
-
case "center":
|
|
10277
|
-
x = (boundingRect.width - titleWidth) / 2;
|
|
10278
|
-
break;
|
|
10279
|
-
case "left":
|
|
10280
|
-
default:
|
|
10281
|
-
x = CHART_PADDING$1;
|
|
10282
|
-
break;
|
|
10313
|
+
canvas = owl.useRef("graphContainer");
|
|
10314
|
+
chart;
|
|
10315
|
+
currentRuntime;
|
|
10316
|
+
get background() {
|
|
10317
|
+
return this.chartRuntime.background;
|
|
10283
10318
|
}
|
|
10284
|
-
|
|
10285
|
-
|
|
10286
|
-
height: boundingRect.height,
|
|
10287
|
-
title: {
|
|
10288
|
-
label: runtime.title.text ?? "",
|
|
10289
|
-
fontSize: runtime.title.fontSize ?? CHART_TITLE_FONT_SIZE,
|
|
10290
|
-
textPosition: {
|
|
10291
|
-
x,
|
|
10292
|
-
y: CHART_PADDING_TOP + titleHeight / 2,
|
|
10293
|
-
},
|
|
10294
|
-
color: runtime.title.color ?? textColor,
|
|
10295
|
-
bold: runtime.title.bold,
|
|
10296
|
-
italic: runtime.title.italic,
|
|
10297
|
-
},
|
|
10298
|
-
backgroundColor: runtime.background,
|
|
10299
|
-
gauge: {
|
|
10300
|
-
rect: gaugeRect,
|
|
10301
|
-
arcWidth: gaugeArcWidth,
|
|
10302
|
-
percentage: clip(gaugePercentage, 0, 1),
|
|
10303
|
-
color: getGaugeColor(runtime),
|
|
10304
|
-
},
|
|
10305
|
-
inflectionValues,
|
|
10306
|
-
gaugeValue: {
|
|
10307
|
-
label: gaugeLabel,
|
|
10308
|
-
textPosition: gaugeValuePosition,
|
|
10309
|
-
fontSize: gaugeValueFontSize,
|
|
10310
|
-
color: textColor,
|
|
10311
|
-
},
|
|
10312
|
-
minLabel: {
|
|
10313
|
-
label: runtime.minValue.label,
|
|
10314
|
-
textPosition: minLabelPosition,
|
|
10315
|
-
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
10316
|
-
color: textColor,
|
|
10317
|
-
},
|
|
10318
|
-
maxLabel: {
|
|
10319
|
-
label: runtime.maxValue.label,
|
|
10320
|
-
textPosition: maxLabelPosition,
|
|
10321
|
-
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
10322
|
-
color: textColor,
|
|
10323
|
-
},
|
|
10324
|
-
};
|
|
10325
|
-
}
|
|
10326
|
-
/**
|
|
10327
|
-
* Get the rectangle in which the gauge will be drawn, based on the bounding rectangle of the canvas and leaving
|
|
10328
|
-
* space for the title and labels.
|
|
10329
|
-
*/
|
|
10330
|
-
function getGaugeRect(boundingRect, title) {
|
|
10331
|
-
const titleHeight = title ? GAUGE_TITLE_SECTION_HEIGHT : 0;
|
|
10332
|
-
const drawHeight = boundingRect.height - GAUGE_PADDING_BOTTOM - titleHeight - GAUGE_PADDING_TOP;
|
|
10333
|
-
const drawWidth = boundingRect.width - GAUGE_PADDING_SIDE * 2;
|
|
10334
|
-
let gaugeWidth;
|
|
10335
|
-
let gaugeHeight;
|
|
10336
|
-
if (drawWidth > 2 * drawHeight) {
|
|
10337
|
-
gaugeWidth = 2 * drawHeight;
|
|
10338
|
-
gaugeHeight = drawHeight;
|
|
10319
|
+
get canvasStyle() {
|
|
10320
|
+
return `background-color: ${this.background}`;
|
|
10339
10321
|
}
|
|
10340
|
-
|
|
10341
|
-
|
|
10342
|
-
|
|
10322
|
+
get chartRuntime() {
|
|
10323
|
+
const runtime = this.env.model.getters.getChartRuntime(this.props.figure.id);
|
|
10324
|
+
if (!("chartJsConfig" in runtime)) {
|
|
10325
|
+
throw new Error("Unsupported chart runtime");
|
|
10326
|
+
}
|
|
10327
|
+
return runtime;
|
|
10343
10328
|
}
|
|
10344
|
-
|
|
10345
|
-
|
|
10346
|
-
|
|
10347
|
-
|
|
10348
|
-
|
|
10349
|
-
|
|
10350
|
-
|
|
10351
|
-
|
|
10352
|
-
|
|
10353
|
-
|
|
10354
|
-
|
|
10355
|
-
|
|
10356
|
-
|
|
10357
|
-
|
|
10358
|
-
|
|
10359
|
-
|
|
10360
|
-
|
|
10361
|
-
|
|
10362
|
-
|
|
10363
|
-
|
|
10364
|
-
};
|
|
10365
|
-
const textStyle = { fontSize: GAUGE_LABELS_FONT_SIZE };
|
|
10366
|
-
const inflectionValues = [];
|
|
10367
|
-
const inflectionValuesTextRects = [];
|
|
10368
|
-
for (const inflectionValue of runtime.inflectionValues) {
|
|
10369
|
-
const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
|
|
10370
|
-
const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
|
|
10371
|
-
const angle = Math.PI - Math.PI * percentage;
|
|
10372
|
-
const textRect = getRectangleTangentToCircle(angle, // angle between X axis and the point where the rectangle is tangent to the circle
|
|
10373
|
-
gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, // radius of the gauge circle + margin below text
|
|
10374
|
-
gaugeCircleCenter.x, // center of the gauge circle
|
|
10375
|
-
gaugeCircleCenter.y, // center of the gauge circle
|
|
10376
|
-
labelWidth + 2, // width of the text + some margin
|
|
10377
|
-
GAUGE_LABELS_FONT_SIZE // height of the text
|
|
10378
|
-
);
|
|
10379
|
-
let offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect))
|
|
10380
|
-
? GAUGE_LABELS_FONT_SIZE
|
|
10381
|
-
: 0;
|
|
10382
|
-
inflectionValuesTextRects.push(textRect);
|
|
10383
|
-
inflectionValues.push({
|
|
10384
|
-
rotation: angle,
|
|
10385
|
-
label: inflectionValue.label,
|
|
10386
|
-
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
10387
|
-
color: textColor,
|
|
10388
|
-
offset,
|
|
10329
|
+
setup() {
|
|
10330
|
+
owl.onMounted(() => {
|
|
10331
|
+
const runtime = this.chartRuntime;
|
|
10332
|
+
this.currentRuntime = runtime;
|
|
10333
|
+
// Note: chartJS modify the runtime in place, so it's important to give it a copy
|
|
10334
|
+
this.createChart(deepCopy(runtime.chartJsConfig));
|
|
10335
|
+
});
|
|
10336
|
+
owl.onWillUnmount(() => this.chart?.destroy());
|
|
10337
|
+
owl.useEffect(() => {
|
|
10338
|
+
const runtime = this.chartRuntime;
|
|
10339
|
+
if (runtime !== this.currentRuntime) {
|
|
10340
|
+
if (runtime.chartJsConfig.type !== this.currentRuntime.chartJsConfig.type) {
|
|
10341
|
+
this.chart?.destroy();
|
|
10342
|
+
this.createChart(deepCopy(runtime.chartJsConfig));
|
|
10343
|
+
}
|
|
10344
|
+
else {
|
|
10345
|
+
this.updateChartJs(deepCopy(runtime.chartJsConfig));
|
|
10346
|
+
}
|
|
10347
|
+
this.currentRuntime = runtime;
|
|
10348
|
+
}
|
|
10389
10349
|
});
|
|
10390
10350
|
}
|
|
10391
|
-
|
|
10392
|
-
|
|
10393
|
-
|
|
10394
|
-
|
|
10395
|
-
|
|
10396
|
-
return GAUGE_BACKGROUND_COLOR;
|
|
10397
|
-
}
|
|
10398
|
-
for (let i = 0; i < runtime.inflectionValues.length; i++) {
|
|
10399
|
-
const inflectionValue = runtime.inflectionValues[i];
|
|
10400
|
-
if (inflectionValue.operator === "<" && gaugeValue < inflectionValue.value) {
|
|
10401
|
-
return runtime.colors[i];
|
|
10402
|
-
}
|
|
10403
|
-
else if (inflectionValue.operator === "<=" && gaugeValue <= inflectionValue.value) {
|
|
10404
|
-
return runtime.colors[i];
|
|
10405
|
-
}
|
|
10406
|
-
}
|
|
10407
|
-
return runtime.colors.at(-1);
|
|
10408
|
-
}
|
|
10409
|
-
function getSegmentsOfRectangle(rectangle) {
|
|
10410
|
-
return [
|
|
10411
|
-
{ start: rectangle.topLeft, end: rectangle.topRight },
|
|
10412
|
-
{ start: rectangle.topRight, end: rectangle.bottomRight },
|
|
10413
|
-
{ start: rectangle.bottomRight, end: rectangle.bottomLeft },
|
|
10414
|
-
{ start: rectangle.bottomLeft, end: rectangle.topLeft },
|
|
10415
|
-
];
|
|
10416
|
-
}
|
|
10417
|
-
/**
|
|
10418
|
-
* Check if two segment intersect. The case where the segments are colinear (both segments on the same line)
|
|
10419
|
-
* is not handled.
|
|
10420
|
-
*/
|
|
10421
|
-
function doSegmentIntersect(segment1, segment2) {
|
|
10422
|
-
const A = segment1.start;
|
|
10423
|
-
const B = segment1.end;
|
|
10424
|
-
const C = segment2.start;
|
|
10425
|
-
const D = segment2.end;
|
|
10426
|
-
/**
|
|
10427
|
-
* Line segment intersection algorithm
|
|
10428
|
-
* https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
|
|
10429
|
-
*/
|
|
10430
|
-
function ccw(a, b, c) {
|
|
10431
|
-
return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
|
|
10351
|
+
createChart(chartData) {
|
|
10352
|
+
const canvas = this.canvas.el;
|
|
10353
|
+
const ctx = canvas.getContext("2d");
|
|
10354
|
+
const Chart = getChartJSConstructor();
|
|
10355
|
+
this.chart = new Chart(ctx, chartData);
|
|
10432
10356
|
}
|
|
10433
|
-
|
|
10434
|
-
|
|
10435
|
-
|
|
10436
|
-
|
|
10437
|
-
|
|
10438
|
-
for (const segment1 of segments1) {
|
|
10439
|
-
for (const segment2 of segments2) {
|
|
10440
|
-
if (doSegmentIntersect(segment1, segment2)) {
|
|
10441
|
-
return true;
|
|
10357
|
+
updateChartJs(chartData) {
|
|
10358
|
+
if (chartData.data && chartData.data.datasets) {
|
|
10359
|
+
this.chart.data = chartData.data;
|
|
10360
|
+
if (chartData.options?.plugins?.title) {
|
|
10361
|
+
this.chart.config.options.plugins.title = chartData.options.plugins.title;
|
|
10442
10362
|
}
|
|
10443
10363
|
}
|
|
10364
|
+
else {
|
|
10365
|
+
this.chart.data.datasets = [];
|
|
10366
|
+
}
|
|
10367
|
+
this.chart.config.options = chartData.options;
|
|
10368
|
+
this.chart.update();
|
|
10444
10369
|
}
|
|
10445
|
-
return false;
|
|
10446
|
-
}
|
|
10447
|
-
/**
|
|
10448
|
-
* Get the rectangle that is tangent to a circle at a given angle.
|
|
10449
|
-
*
|
|
10450
|
-
* @param angle angle between X axis and the point where the rectangle is tangent to the circle
|
|
10451
|
-
*/
|
|
10452
|
-
function getRectangleTangentToCircle(angle, radius, circleCenterX, circleCenterY, rectWidth, rectHeight) {
|
|
10453
|
-
const cos = Math.cos(angle);
|
|
10454
|
-
const sin = Math.sin(angle);
|
|
10455
|
-
// x, y are the distance from the center of the circle to the point where the rectangle is tangent to the circle
|
|
10456
|
-
const x = cos * radius;
|
|
10457
|
-
const y = sin * radius;
|
|
10458
|
-
// x2, y2 are the distance from the point the rectangle is tangent to the circle to the bottom left corner of the rectangle
|
|
10459
|
-
const x2 = sin * (rectWidth / 2); // cos(angle + 90°) = sin(angle)
|
|
10460
|
-
const y2 = cos * (rectWidth / 2);
|
|
10461
|
-
const bottomRight = {
|
|
10462
|
-
x: x + x2 + circleCenterX,
|
|
10463
|
-
y: circleCenterY - (y - y2),
|
|
10464
|
-
};
|
|
10465
|
-
const bottomLeft = {
|
|
10466
|
-
x: x - x2 + circleCenterX,
|
|
10467
|
-
y: circleCenterY - (y + y2),
|
|
10468
|
-
};
|
|
10469
|
-
// Same as above but for the top corners of the rectangle (radius + rectangle height instead of radius)
|
|
10470
|
-
const xp = cos * (radius + rectHeight);
|
|
10471
|
-
const yp = sin * (radius + rectHeight);
|
|
10472
|
-
const topLeft = {
|
|
10473
|
-
x: xp - x2 + circleCenterX,
|
|
10474
|
-
y: circleCenterY - (yp + y2),
|
|
10475
|
-
};
|
|
10476
|
-
const topRight = {
|
|
10477
|
-
x: xp + x2 + circleCenterX,
|
|
10478
|
-
y: circleCenterY - (yp - y2),
|
|
10479
|
-
};
|
|
10480
|
-
return { bottomLeft, bottomRight, topRight, topLeft };
|
|
10481
10370
|
}
|
|
10482
10371
|
|
|
10483
10372
|
/**
|
|
@@ -11059,299 +10948,6 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
11059
10948
|
}
|
|
11060
10949
|
}
|
|
11061
10950
|
|
|
11062
|
-
const CHART_COMMON_OPTIONS = {
|
|
11063
|
-
// https://www.chartjs.org/docs/latest/general/responsive.html
|
|
11064
|
-
responsive: true, // will resize when its container is resized
|
|
11065
|
-
maintainAspectRatio: false, // doesn't maintain the aspect ratio (width/height =2 by default) so the user has the choice of the exact layout
|
|
11066
|
-
elements: {
|
|
11067
|
-
line: {
|
|
11068
|
-
fill: false, // do not fill the area under line charts
|
|
11069
|
-
},
|
|
11070
|
-
point: {
|
|
11071
|
-
hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
|
|
11072
|
-
},
|
|
11073
|
-
},
|
|
11074
|
-
animation: false,
|
|
11075
|
-
};
|
|
11076
|
-
function chartToImage(runtime, figure, type) {
|
|
11077
|
-
// wrap the canvas in a div with a fixed size because chart.js would
|
|
11078
|
-
// fill the whole page otherwise
|
|
11079
|
-
const div = document.createElement("div");
|
|
11080
|
-
div.style.width = `${figure.width}px`;
|
|
11081
|
-
div.style.height = `${figure.height}px`;
|
|
11082
|
-
const canvas = document.createElement("canvas");
|
|
11083
|
-
div.append(canvas);
|
|
11084
|
-
canvas.setAttribute("width", figure.width.toString());
|
|
11085
|
-
canvas.setAttribute("height", figure.height.toString());
|
|
11086
|
-
// we have to add the canvas to the DOM otherwise it won't be rendered
|
|
11087
|
-
document.body.append(div);
|
|
11088
|
-
if ("chartJsConfig" in runtime) {
|
|
11089
|
-
const config = deepCopy(runtime.chartJsConfig);
|
|
11090
|
-
config.plugins = [backgroundColorChartJSPlugin];
|
|
11091
|
-
const Chart = getChartJSConstructor();
|
|
11092
|
-
const chart = new Chart(canvas, config);
|
|
11093
|
-
const imgContent = chart.toBase64Image();
|
|
11094
|
-
chart.destroy();
|
|
11095
|
-
div.remove();
|
|
11096
|
-
return imgContent;
|
|
11097
|
-
}
|
|
11098
|
-
else if (type === "scorecard") {
|
|
11099
|
-
const design = getScorecardConfiguration(figure, runtime);
|
|
11100
|
-
drawScoreChart(design, canvas);
|
|
11101
|
-
const imgContent = canvas.toDataURL();
|
|
11102
|
-
div.remove();
|
|
11103
|
-
return imgContent;
|
|
11104
|
-
}
|
|
11105
|
-
else if (type === "gauge") {
|
|
11106
|
-
drawGaugeChart(canvas, runtime);
|
|
11107
|
-
const imgContent = canvas.toDataURL();
|
|
11108
|
-
div.remove();
|
|
11109
|
-
return imgContent;
|
|
11110
|
-
}
|
|
11111
|
-
return undefined;
|
|
11112
|
-
}
|
|
11113
|
-
/**
|
|
11114
|
-
* Custom chart.js plugin to set the background color of the canvas
|
|
11115
|
-
* https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md
|
|
11116
|
-
*/
|
|
11117
|
-
const backgroundColorChartJSPlugin = {
|
|
11118
|
-
id: "customCanvasBackgroundColor",
|
|
11119
|
-
beforeDraw: (chart) => {
|
|
11120
|
-
const { ctx } = chart;
|
|
11121
|
-
ctx.save();
|
|
11122
|
-
ctx.globalCompositeOperation = "destination-over";
|
|
11123
|
-
ctx.fillStyle = "#ffffff";
|
|
11124
|
-
ctx.fillRect(0, 0, chart.width, chart.height);
|
|
11125
|
-
ctx.restore();
|
|
11126
|
-
},
|
|
11127
|
-
};
|
|
11128
|
-
/** Return window.Chart, making sure all our extensions are loaded in ChartJS */
|
|
11129
|
-
function getChartJSConstructor() {
|
|
11130
|
-
if (window.Chart && !window.Chart?.registry.plugins.get("chartShowValuesPlugin")) {
|
|
11131
|
-
window.Chart.register(chartShowValuesPlugin);
|
|
11132
|
-
window.Chart.register(waterfallLinesPlugin);
|
|
11133
|
-
}
|
|
11134
|
-
return window.Chart;
|
|
11135
|
-
}
|
|
11136
|
-
|
|
11137
|
-
/**
|
|
11138
|
-
* This file is largely inspired by owl 1.
|
|
11139
|
-
* `css` tag has been removed from owl 2 without workaround to manage css.
|
|
11140
|
-
* So, the solution was to import the behavior of owl 1 directly in our
|
|
11141
|
-
* codebase, with one difference: the css is added to the sheet as soon as the
|
|
11142
|
-
* css tag is executed. In owl 1, the css was added as soon as a Component was
|
|
11143
|
-
* created for the first time.
|
|
11144
|
-
*/
|
|
11145
|
-
const STYLESHEETS = {};
|
|
11146
|
-
let nextId = 0;
|
|
11147
|
-
/**
|
|
11148
|
-
* CSS tag helper for defining inline stylesheets. With this, one can simply define
|
|
11149
|
-
* an inline stylesheet with just the following code:
|
|
11150
|
-
* ```js
|
|
11151
|
-
* css`.component-a { color: red; }`;
|
|
11152
|
-
* ```
|
|
11153
|
-
*/
|
|
11154
|
-
function css(strings, ...args) {
|
|
11155
|
-
const name = `__sheet__${nextId++}`;
|
|
11156
|
-
const value = String.raw(strings, ...args);
|
|
11157
|
-
registerSheet(name, value);
|
|
11158
|
-
activateSheet(name);
|
|
11159
|
-
return name;
|
|
11160
|
-
}
|
|
11161
|
-
function processSheet(str) {
|
|
11162
|
-
const tokens = str.split(/(\{|\}|;)/).map((s) => s.trim());
|
|
11163
|
-
const selectorStack = [];
|
|
11164
|
-
const parts = [];
|
|
11165
|
-
let rules = [];
|
|
11166
|
-
function generateSelector(stackIndex, parentSelector) {
|
|
11167
|
-
const parts = [];
|
|
11168
|
-
for (const selector of selectorStack[stackIndex]) {
|
|
11169
|
-
let part = (parentSelector && parentSelector + " " + selector) || selector;
|
|
11170
|
-
if (part.includes("&")) {
|
|
11171
|
-
part = selector.replace(/&/g, parentSelector || "");
|
|
11172
|
-
}
|
|
11173
|
-
if (stackIndex < selectorStack.length - 1) {
|
|
11174
|
-
part = generateSelector(stackIndex + 1, part);
|
|
11175
|
-
}
|
|
11176
|
-
parts.push(part);
|
|
11177
|
-
}
|
|
11178
|
-
return parts.join(", ");
|
|
11179
|
-
}
|
|
11180
|
-
function generateRules() {
|
|
11181
|
-
if (rules.length) {
|
|
11182
|
-
parts.push(generateSelector(0) + " {");
|
|
11183
|
-
parts.push(...rules);
|
|
11184
|
-
parts.push("}");
|
|
11185
|
-
rules = [];
|
|
11186
|
-
}
|
|
11187
|
-
}
|
|
11188
|
-
while (tokens.length) {
|
|
11189
|
-
let token = tokens.shift();
|
|
11190
|
-
if (token === "}") {
|
|
11191
|
-
generateRules();
|
|
11192
|
-
selectorStack.pop();
|
|
11193
|
-
}
|
|
11194
|
-
else {
|
|
11195
|
-
if (tokens[0] === "{") {
|
|
11196
|
-
generateRules();
|
|
11197
|
-
selectorStack.push(token.split(/\s*,\s*/));
|
|
11198
|
-
tokens.shift();
|
|
11199
|
-
}
|
|
11200
|
-
if (tokens[0] === ";") {
|
|
11201
|
-
rules.push(" " + token + ";");
|
|
11202
|
-
}
|
|
11203
|
-
}
|
|
11204
|
-
}
|
|
11205
|
-
return parts.join("\n");
|
|
11206
|
-
}
|
|
11207
|
-
function registerSheet(id, css) {
|
|
11208
|
-
const sheet = document.createElement("style");
|
|
11209
|
-
sheet.textContent = processSheet(css);
|
|
11210
|
-
STYLESHEETS[id] = sheet;
|
|
11211
|
-
}
|
|
11212
|
-
function activateSheet(id) {
|
|
11213
|
-
const sheet = STYLESHEETS[id];
|
|
11214
|
-
sheet.setAttribute("component", id);
|
|
11215
|
-
document.head.appendChild(sheet);
|
|
11216
|
-
}
|
|
11217
|
-
function getTextDecoration({ strikethrough, underline, }) {
|
|
11218
|
-
if (!strikethrough && !underline) {
|
|
11219
|
-
return "none";
|
|
11220
|
-
}
|
|
11221
|
-
return `${strikethrough ? "line-through" : ""} ${underline ? "underline" : ""}`;
|
|
11222
|
-
}
|
|
11223
|
-
/**
|
|
11224
|
-
* Convert the cell style to CSS properties.
|
|
11225
|
-
*/
|
|
11226
|
-
function cellStyleToCss(style) {
|
|
11227
|
-
const attributes = cellTextStyleToCss(style);
|
|
11228
|
-
if (!style)
|
|
11229
|
-
return attributes;
|
|
11230
|
-
if (style.fillColor) {
|
|
11231
|
-
attributes["background"] = style.fillColor;
|
|
11232
|
-
}
|
|
11233
|
-
return attributes;
|
|
11234
|
-
}
|
|
11235
|
-
/**
|
|
11236
|
-
* Convert the cell text style to CSS properties.
|
|
11237
|
-
*/
|
|
11238
|
-
function cellTextStyleToCss(style) {
|
|
11239
|
-
const attributes = {};
|
|
11240
|
-
if (!style)
|
|
11241
|
-
return attributes;
|
|
11242
|
-
if (style.bold) {
|
|
11243
|
-
attributes["font-weight"] = "bold";
|
|
11244
|
-
}
|
|
11245
|
-
if (style.italic) {
|
|
11246
|
-
attributes["font-style"] = "italic";
|
|
11247
|
-
}
|
|
11248
|
-
if (style.strikethrough || style.underline) {
|
|
11249
|
-
let decoration = style.strikethrough ? "line-through" : "";
|
|
11250
|
-
decoration = style.underline ? decoration + " underline" : decoration;
|
|
11251
|
-
attributes["text-decoration"] = decoration;
|
|
11252
|
-
}
|
|
11253
|
-
if (style.textColor) {
|
|
11254
|
-
attributes["color"] = style.textColor;
|
|
11255
|
-
}
|
|
11256
|
-
return attributes;
|
|
11257
|
-
}
|
|
11258
|
-
/**
|
|
11259
|
-
* Transform CSS properties into a CSS string.
|
|
11260
|
-
*/
|
|
11261
|
-
function cssPropertiesToCss(attributes) {
|
|
11262
|
-
let styleStr = "";
|
|
11263
|
-
for (const attName in attributes) {
|
|
11264
|
-
if (!attributes[attName]) {
|
|
11265
|
-
continue;
|
|
11266
|
-
}
|
|
11267
|
-
styleStr += `${attName}:${attributes[attName]}; `;
|
|
11268
|
-
}
|
|
11269
|
-
return styleStr;
|
|
11270
|
-
}
|
|
11271
|
-
function getElementMargins(el) {
|
|
11272
|
-
const style = window.getComputedStyle(el);
|
|
11273
|
-
return {
|
|
11274
|
-
top: parseInt(style.marginTop, 10) || 0,
|
|
11275
|
-
bottom: parseInt(style.marginBottom, 10) || 0,
|
|
11276
|
-
left: parseInt(style.marginLeft, 10) || 0,
|
|
11277
|
-
right: parseInt(style.marginRight, 10) || 0,
|
|
11278
|
-
};
|
|
11279
|
-
}
|
|
11280
|
-
|
|
11281
|
-
css /* scss */ `
|
|
11282
|
-
.o-spreadsheet {
|
|
11283
|
-
.o-chart-custom-tooltip {
|
|
11284
|
-
font-size: 12px;
|
|
11285
|
-
background-color: #fff;
|
|
11286
|
-
z-index: ${ComponentsImportance.FigureTooltip};
|
|
11287
|
-
}
|
|
11288
|
-
}
|
|
11289
|
-
`;
|
|
11290
|
-
class ChartJsComponent extends owl.Component {
|
|
11291
|
-
static template = "o-spreadsheet-ChartJsComponent";
|
|
11292
|
-
static props = {
|
|
11293
|
-
figure: Object,
|
|
11294
|
-
};
|
|
11295
|
-
canvas = owl.useRef("graphContainer");
|
|
11296
|
-
chart;
|
|
11297
|
-
currentRuntime;
|
|
11298
|
-
get background() {
|
|
11299
|
-
return this.chartRuntime.background;
|
|
11300
|
-
}
|
|
11301
|
-
get canvasStyle() {
|
|
11302
|
-
return `background-color: ${this.background}`;
|
|
11303
|
-
}
|
|
11304
|
-
get chartRuntime() {
|
|
11305
|
-
const runtime = this.env.model.getters.getChartRuntime(this.props.figure.id);
|
|
11306
|
-
if (!("chartJsConfig" in runtime)) {
|
|
11307
|
-
throw new Error("Unsupported chart runtime");
|
|
11308
|
-
}
|
|
11309
|
-
return runtime;
|
|
11310
|
-
}
|
|
11311
|
-
setup() {
|
|
11312
|
-
owl.onMounted(() => {
|
|
11313
|
-
const runtime = this.chartRuntime;
|
|
11314
|
-
this.currentRuntime = runtime;
|
|
11315
|
-
// Note: chartJS modify the runtime in place, so it's important to give it a copy
|
|
11316
|
-
this.createChart(deepCopy(runtime.chartJsConfig));
|
|
11317
|
-
});
|
|
11318
|
-
owl.onWillUnmount(() => this.chart?.destroy());
|
|
11319
|
-
owl.useEffect(() => {
|
|
11320
|
-
const runtime = this.chartRuntime;
|
|
11321
|
-
if (runtime !== this.currentRuntime) {
|
|
11322
|
-
if (runtime.chartJsConfig.type !== this.currentRuntime.chartJsConfig.type) {
|
|
11323
|
-
this.chart?.destroy();
|
|
11324
|
-
this.createChart(deepCopy(runtime.chartJsConfig));
|
|
11325
|
-
}
|
|
11326
|
-
else {
|
|
11327
|
-
this.updateChartJs(deepCopy(runtime));
|
|
11328
|
-
}
|
|
11329
|
-
this.currentRuntime = runtime;
|
|
11330
|
-
}
|
|
11331
|
-
});
|
|
11332
|
-
}
|
|
11333
|
-
createChart(chartData) {
|
|
11334
|
-
const canvas = this.canvas.el;
|
|
11335
|
-
const ctx = canvas.getContext("2d");
|
|
11336
|
-
const Chart = getChartJSConstructor();
|
|
11337
|
-
this.chart = new Chart(ctx, chartData);
|
|
11338
|
-
}
|
|
11339
|
-
updateChartJs(chartRuntime) {
|
|
11340
|
-
const chartData = chartRuntime.chartJsConfig;
|
|
11341
|
-
if (chartData.data && chartData.data.datasets) {
|
|
11342
|
-
this.chart.data = chartData.data;
|
|
11343
|
-
if (chartData.options?.plugins?.title) {
|
|
11344
|
-
this.chart.config.options.plugins.title = chartData.options.plugins.title;
|
|
11345
|
-
}
|
|
11346
|
-
}
|
|
11347
|
-
else {
|
|
11348
|
-
this.chart.data.datasets = [];
|
|
11349
|
-
}
|
|
11350
|
-
this.chart.config.options = chartData.options;
|
|
11351
|
-
this.chart.update();
|
|
11352
|
-
}
|
|
11353
|
-
}
|
|
11354
|
-
|
|
11355
10951
|
class ScorecardChart extends owl.Component {
|
|
11356
10952
|
static template = "o-spreadsheet-ScorecardChart";
|
|
11357
10953
|
static props = {
|
|
@@ -20545,11 +20141,26 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
20545
20141
|
const _searchFor = toString(searchFor).toLowerCase();
|
|
20546
20142
|
const _textToSearch = toString(textToSearch).toLowerCase();
|
|
20547
20143
|
const _startingAt = toNumber(startingAt, this.locale);
|
|
20548
|
-
|
|
20549
|
-
|
|
20144
|
+
if (_textToSearch === "") {
|
|
20145
|
+
return {
|
|
20146
|
+
value: CellErrorType.GenericError,
|
|
20147
|
+
message: _t("The text_to_search must be non-empty."),
|
|
20148
|
+
};
|
|
20149
|
+
}
|
|
20150
|
+
if (_startingAt < 1) {
|
|
20151
|
+
return {
|
|
20152
|
+
value: CellErrorType.GenericError,
|
|
20153
|
+
message: _t("The starting_at (%s) must be greater than or equal to 1.", _startingAt),
|
|
20154
|
+
};
|
|
20155
|
+
}
|
|
20550
20156
|
const result = _textToSearch.indexOf(_searchFor, _startingAt - 1);
|
|
20551
|
-
|
|
20552
|
-
|
|
20157
|
+
if (result === -1) {
|
|
20158
|
+
return {
|
|
20159
|
+
value: CellErrorType.GenericError,
|
|
20160
|
+
message: _t("In [[FUNCTION_NAME]] evaluation, cannot find '%s' within '%s'.", _searchFor, _textToSearch),
|
|
20161
|
+
};
|
|
20162
|
+
}
|
|
20163
|
+
return { value: result + 1 };
|
|
20553
20164
|
},
|
|
20554
20165
|
isExported: true,
|
|
20555
20166
|
};
|
|
@@ -21884,11 +21495,14 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
21884
21495
|
}
|
|
21885
21496
|
}
|
|
21886
21497
|
function compileTokensOrThrow(tokens) {
|
|
21887
|
-
const { dependencies,
|
|
21888
|
-
const cacheKey = compilationCacheKey(tokens
|
|
21498
|
+
const { dependencies, literalValues, symbols } = formulaArguments(tokens);
|
|
21499
|
+
const cacheKey = compilationCacheKey(tokens);
|
|
21889
21500
|
if (!functionCache[cacheKey]) {
|
|
21890
21501
|
const ast = parseTokens([...tokens]);
|
|
21891
21502
|
const scope = new Scope();
|
|
21503
|
+
let stringCount = 0;
|
|
21504
|
+
let numberCount = 0;
|
|
21505
|
+
let dependencyCount = 0;
|
|
21892
21506
|
if (ast.type === "BIN_OPERATION" && ast.value === ":") {
|
|
21893
21507
|
throw new BadExpressionError(_t("Invalid formula"));
|
|
21894
21508
|
}
|
|
@@ -21962,16 +21576,15 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
21962
21576
|
case "BOOLEAN":
|
|
21963
21577
|
return code.return(`{ value: ${ast.value} }`);
|
|
21964
21578
|
case "NUMBER":
|
|
21965
|
-
return code.return(`
|
|
21579
|
+
return code.return(`this.literalValues.numbers[${numberCount++}]`);
|
|
21966
21580
|
case "STRING":
|
|
21967
|
-
return code.return(`
|
|
21581
|
+
return code.return(`this.literalValues.strings[${stringCount++}]`);
|
|
21968
21582
|
case "REFERENCE":
|
|
21969
|
-
const referenceIndex = dependencies.indexOf(ast.value);
|
|
21970
21583
|
if ((!isMeta && ast.value.includes(":")) || hasRange) {
|
|
21971
|
-
return code.return(`range(deps[${
|
|
21584
|
+
return code.return(`range(deps[${dependencyCount++}])`);
|
|
21972
21585
|
}
|
|
21973
21586
|
else {
|
|
21974
|
-
return code.return(`ref(deps[${
|
|
21587
|
+
return code.return(`ref(deps[${dependencyCount++}], ${isMeta ? "true" : "false"})`);
|
|
21975
21588
|
}
|
|
21976
21589
|
case "FUNCALL":
|
|
21977
21590
|
const args = compileFunctionArgs(ast).map((arg) => arg.assignResultToVariable());
|
|
@@ -22003,7 +21616,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
22003
21616
|
const compiledFormula = {
|
|
22004
21617
|
execute: functionCache[cacheKey],
|
|
22005
21618
|
dependencies,
|
|
22006
|
-
|
|
21619
|
+
literalValues,
|
|
22007
21620
|
symbols,
|
|
22008
21621
|
tokens,
|
|
22009
21622
|
isBadExpression: false,
|
|
@@ -22016,33 +21629,31 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
22016
21629
|
* References, numbers and strings are replaced with placeholders because
|
|
22017
21630
|
* the compiled formula does not depend on their actual value.
|
|
22018
21631
|
* Both `=A1+1+"2"` and `=A2+2+"3"` are compiled to the exact same function.
|
|
22019
|
-
*
|
|
22020
21632
|
* Spaces are also ignored to compute the cache key.
|
|
22021
21633
|
*
|
|
22022
|
-
* A formula `=A1+A2+SUM(2, 2, "2")` have the cache key `=|
|
|
21634
|
+
* A formula `=A1+A2+SUM(2, 2, "2")` have the cache key `=|C|+|C|+SUM(|N|,|N|,|S|)`
|
|
22023
21635
|
*/
|
|
22024
|
-
function compilationCacheKey(tokens
|
|
21636
|
+
function compilationCacheKey(tokens) {
|
|
22025
21637
|
let cacheKey = "";
|
|
22026
21638
|
for (const token of tokens) {
|
|
22027
21639
|
switch (token.type) {
|
|
22028
21640
|
case "STRING":
|
|
22029
|
-
|
|
22030
|
-
cacheKey += `|S${constantValues.strings.indexOf(value)}|`;
|
|
21641
|
+
cacheKey += "|S|";
|
|
22031
21642
|
break;
|
|
22032
21643
|
case "NUMBER":
|
|
22033
|
-
cacheKey +=
|
|
21644
|
+
cacheKey += "|N|";
|
|
22034
21645
|
break;
|
|
22035
21646
|
case "REFERENCE":
|
|
22036
21647
|
case "INVALID_REFERENCE":
|
|
22037
21648
|
if (token.value.includes(":")) {
|
|
22038
|
-
cacheKey +=
|
|
21649
|
+
cacheKey += "|R|";
|
|
22039
21650
|
}
|
|
22040
21651
|
else {
|
|
22041
|
-
cacheKey +=
|
|
21652
|
+
cacheKey += "|C|";
|
|
22042
21653
|
}
|
|
22043
21654
|
break;
|
|
22044
21655
|
case "SPACE":
|
|
22045
|
-
|
|
21656
|
+
// ignore spaces
|
|
22046
21657
|
break;
|
|
22047
21658
|
default:
|
|
22048
21659
|
cacheKey += token.value;
|
|
@@ -22055,7 +21666,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
22055
21666
|
* Return formula arguments which are references, strings and numbers.
|
|
22056
21667
|
*/
|
|
22057
21668
|
function formulaArguments(tokens) {
|
|
22058
|
-
const
|
|
21669
|
+
const literalValues = {
|
|
22059
21670
|
numbers: [],
|
|
22060
21671
|
strings: [],
|
|
22061
21672
|
};
|
|
@@ -22069,15 +21680,11 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
22069
21680
|
break;
|
|
22070
21681
|
case "STRING":
|
|
22071
21682
|
const value = removeStringQuotes(token.value);
|
|
22072
|
-
|
|
22073
|
-
constantValues.strings.push(value);
|
|
22074
|
-
}
|
|
21683
|
+
literalValues.strings.push({ value });
|
|
22075
21684
|
break;
|
|
22076
21685
|
case "NUMBER": {
|
|
22077
21686
|
const value = parseNumber(token.value, DEFAULT_LOCALE);
|
|
22078
|
-
|
|
22079
|
-
constantValues.numbers.push(value);
|
|
22080
|
-
}
|
|
21687
|
+
literalValues.numbers.push({ value });
|
|
22081
21688
|
break;
|
|
22082
21689
|
}
|
|
22083
21690
|
case "SYMBOL": {
|
|
@@ -22088,7 +21695,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
22088
21695
|
}
|
|
22089
21696
|
return {
|
|
22090
21697
|
dependencies,
|
|
22091
|
-
|
|
21698
|
+
literalValues,
|
|
22092
21699
|
symbols,
|
|
22093
21700
|
};
|
|
22094
21701
|
}
|
|
@@ -22938,6 +22545,343 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
22938
22545
|
|
|
22939
22546
|
const cellPopoverRegistry = new Registry();
|
|
22940
22547
|
|
|
22548
|
+
const GAUGE_PADDING_SIDE = 30;
|
|
22549
|
+
const GAUGE_PADDING_TOP = 10;
|
|
22550
|
+
const GAUGE_PADDING_BOTTOM = 20;
|
|
22551
|
+
const GAUGE_LABELS_FONT_SIZE = 12;
|
|
22552
|
+
const GAUGE_DEFAULT_VALUE_FONT_SIZE = 80;
|
|
22553
|
+
const GAUGE_BACKGROUND_COLOR = "#F3F2F1";
|
|
22554
|
+
const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
|
|
22555
|
+
const GAUGE_TITLE_SECTION_HEIGHT = 25;
|
|
22556
|
+
function drawGaugeChart(canvas, runtime) {
|
|
22557
|
+
const canvasBoundingRect = canvas.getBoundingClientRect();
|
|
22558
|
+
canvas.width = canvasBoundingRect.width;
|
|
22559
|
+
canvas.height = canvasBoundingRect.height;
|
|
22560
|
+
const ctx = canvas.getContext("2d");
|
|
22561
|
+
const config = getGaugeRenderingConfig(canvasBoundingRect, runtime, ctx);
|
|
22562
|
+
drawBackground(ctx, config);
|
|
22563
|
+
drawGauge(ctx, config);
|
|
22564
|
+
drawInflectionValues(ctx, config);
|
|
22565
|
+
drawLabels(ctx, config);
|
|
22566
|
+
drawTitle(ctx, config);
|
|
22567
|
+
}
|
|
22568
|
+
function drawGauge(ctx, config) {
|
|
22569
|
+
ctx.save();
|
|
22570
|
+
const gauge = config.gauge;
|
|
22571
|
+
const arcCenterX = gauge.rect.x + gauge.rect.width / 2;
|
|
22572
|
+
const arcCenterY = gauge.rect.y + gauge.rect.height;
|
|
22573
|
+
const arcRadius = gauge.rect.height - gauge.arcWidth / 2;
|
|
22574
|
+
if (arcRadius < 0) {
|
|
22575
|
+
return;
|
|
22576
|
+
}
|
|
22577
|
+
const gaugeAngle = gauge.percentage === 1 ? 0 : Math.PI * (1 + gauge.percentage);
|
|
22578
|
+
// Gauge background
|
|
22579
|
+
ctx.strokeStyle = GAUGE_BACKGROUND_COLOR;
|
|
22580
|
+
ctx.beginPath();
|
|
22581
|
+
ctx.lineWidth = gauge.arcWidth;
|
|
22582
|
+
ctx.arc(arcCenterX, arcCenterY, arcRadius, gaugeAngle, 0);
|
|
22583
|
+
ctx.stroke();
|
|
22584
|
+
// Gauge value
|
|
22585
|
+
ctx.strokeStyle = gauge.color;
|
|
22586
|
+
ctx.beginPath();
|
|
22587
|
+
ctx.arc(arcCenterX, arcCenterY, arcRadius, Math.PI, gaugeAngle);
|
|
22588
|
+
ctx.stroke();
|
|
22589
|
+
ctx.restore();
|
|
22590
|
+
}
|
|
22591
|
+
function drawBackground(ctx, config) {
|
|
22592
|
+
ctx.save();
|
|
22593
|
+
ctx.fillStyle = config.backgroundColor;
|
|
22594
|
+
ctx.fillRect(0, 0, config.width, config.height);
|
|
22595
|
+
ctx.restore();
|
|
22596
|
+
}
|
|
22597
|
+
function drawLabels(ctx, config) {
|
|
22598
|
+
for (const label of [config.minLabel, config.maxLabel, config.gaugeValue]) {
|
|
22599
|
+
ctx.save();
|
|
22600
|
+
ctx.textAlign = "center";
|
|
22601
|
+
ctx.fillStyle = label.color;
|
|
22602
|
+
ctx.font = `${label.fontSize}px ${DEFAULT_FONT}`;
|
|
22603
|
+
ctx.fillText(label.label, label.textPosition.x, label.textPosition.y);
|
|
22604
|
+
ctx.restore();
|
|
22605
|
+
}
|
|
22606
|
+
}
|
|
22607
|
+
function drawInflectionValues(ctx, config) {
|
|
22608
|
+
const { x: rectX, y: rectY, width, height } = config.gauge.rect;
|
|
22609
|
+
for (const inflectionValue of config.inflectionValues) {
|
|
22610
|
+
ctx.save();
|
|
22611
|
+
ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment
|
|
22612
|
+
ctx.rotate(Math.PI / 2 - inflectionValue.rotation);
|
|
22613
|
+
ctx.lineWidth = 2;
|
|
22614
|
+
ctx.strokeStyle = chartMutedFontColor(config.backgroundColor) + "aa";
|
|
22615
|
+
ctx.beginPath();
|
|
22616
|
+
ctx.moveTo(0, -(height - config.gauge.arcWidth));
|
|
22617
|
+
ctx.lineTo(0, -height - 3);
|
|
22618
|
+
ctx.stroke();
|
|
22619
|
+
ctx.textAlign = "center";
|
|
22620
|
+
ctx.font = `${inflectionValue.fontSize}px ${DEFAULT_FONT}`;
|
|
22621
|
+
ctx.fillStyle = inflectionValue.color;
|
|
22622
|
+
const textY = -height - GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN - inflectionValue.offset;
|
|
22623
|
+
ctx.fillText(inflectionValue.label, 0, textY);
|
|
22624
|
+
ctx.restore();
|
|
22625
|
+
}
|
|
22626
|
+
}
|
|
22627
|
+
function drawTitle(ctx, config) {
|
|
22628
|
+
ctx.save();
|
|
22629
|
+
const title = config.title;
|
|
22630
|
+
ctx.font = getDefaultContextFont(title.fontSize, title.bold, title.italic);
|
|
22631
|
+
ctx.textBaseline = "middle";
|
|
22632
|
+
ctx.fillStyle = title.color;
|
|
22633
|
+
ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
|
|
22634
|
+
ctx.restore();
|
|
22635
|
+
}
|
|
22636
|
+
function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
|
|
22637
|
+
const maxValue = runtime.maxValue;
|
|
22638
|
+
const minValue = runtime.minValue;
|
|
22639
|
+
const gaugeValue = runtime.gaugeValue;
|
|
22640
|
+
const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
|
|
22641
|
+
const gaugeArcWidth = gaugeRect.width / 6;
|
|
22642
|
+
const gaugePercentage = gaugeValue
|
|
22643
|
+
? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
|
|
22644
|
+
: 0;
|
|
22645
|
+
const gaugeValuePosition = {
|
|
22646
|
+
x: boundingRect.width / 2,
|
|
22647
|
+
y: gaugeRect.y + gaugeRect.height - gaugeRect.height / 12,
|
|
22648
|
+
};
|
|
22649
|
+
let gaugeValueFontSize = GAUGE_DEFAULT_VALUE_FONT_SIZE;
|
|
22650
|
+
// Scale down the font size if the gaugeRect is too small
|
|
22651
|
+
if (gaugeRect.height < 300) {
|
|
22652
|
+
gaugeValueFontSize = gaugeValueFontSize * (gaugeRect.height / 300);
|
|
22653
|
+
}
|
|
22654
|
+
// Scale down the font size if the text is too long
|
|
22655
|
+
const maxTextWidth = gaugeRect.width / 2;
|
|
22656
|
+
const gaugeLabel = gaugeValue?.label || "-";
|
|
22657
|
+
if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
|
|
22658
|
+
gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
|
|
22659
|
+
}
|
|
22660
|
+
const minLabelPosition = {
|
|
22661
|
+
x: gaugeRect.x + gaugeArcWidth / 2,
|
|
22662
|
+
y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
|
|
22663
|
+
};
|
|
22664
|
+
const maxLabelPosition = {
|
|
22665
|
+
x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
|
|
22666
|
+
y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
|
|
22667
|
+
};
|
|
22668
|
+
const textColor = chartMutedFontColor(runtime.background);
|
|
22669
|
+
const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);
|
|
22670
|
+
let x = 0, titleWidth = 0, titleHeight = 0;
|
|
22671
|
+
if (runtime.title.text) {
|
|
22672
|
+
({ width: titleWidth, height: titleHeight } = computeTextDimension(ctx, runtime.title.text, { fontSize: CHART_TITLE_FONT_SIZE, ...runtime.title }, "px"));
|
|
22673
|
+
}
|
|
22674
|
+
switch (runtime.title.align) {
|
|
22675
|
+
case "right":
|
|
22676
|
+
x = boundingRect.width - titleWidth - CHART_PADDING$1;
|
|
22677
|
+
break;
|
|
22678
|
+
case "center":
|
|
22679
|
+
x = (boundingRect.width - titleWidth) / 2;
|
|
22680
|
+
break;
|
|
22681
|
+
case "left":
|
|
22682
|
+
default:
|
|
22683
|
+
x = CHART_PADDING$1;
|
|
22684
|
+
break;
|
|
22685
|
+
}
|
|
22686
|
+
return {
|
|
22687
|
+
width: boundingRect.width,
|
|
22688
|
+
height: boundingRect.height,
|
|
22689
|
+
title: {
|
|
22690
|
+
label: runtime.title.text ?? "",
|
|
22691
|
+
fontSize: runtime.title.fontSize ?? CHART_TITLE_FONT_SIZE,
|
|
22692
|
+
textPosition: {
|
|
22693
|
+
x,
|
|
22694
|
+
y: CHART_PADDING_TOP + titleHeight / 2,
|
|
22695
|
+
},
|
|
22696
|
+
color: runtime.title.color ?? textColor,
|
|
22697
|
+
bold: runtime.title.bold,
|
|
22698
|
+
italic: runtime.title.italic,
|
|
22699
|
+
},
|
|
22700
|
+
backgroundColor: runtime.background,
|
|
22701
|
+
gauge: {
|
|
22702
|
+
rect: gaugeRect,
|
|
22703
|
+
arcWidth: gaugeArcWidth,
|
|
22704
|
+
percentage: clip(gaugePercentage, 0, 1),
|
|
22705
|
+
color: getGaugeColor(runtime),
|
|
22706
|
+
},
|
|
22707
|
+
inflectionValues,
|
|
22708
|
+
gaugeValue: {
|
|
22709
|
+
label: gaugeLabel,
|
|
22710
|
+
textPosition: gaugeValuePosition,
|
|
22711
|
+
fontSize: gaugeValueFontSize,
|
|
22712
|
+
color: textColor,
|
|
22713
|
+
},
|
|
22714
|
+
minLabel: {
|
|
22715
|
+
label: runtime.minValue.label,
|
|
22716
|
+
textPosition: minLabelPosition,
|
|
22717
|
+
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
22718
|
+
color: textColor,
|
|
22719
|
+
},
|
|
22720
|
+
maxLabel: {
|
|
22721
|
+
label: runtime.maxValue.label,
|
|
22722
|
+
textPosition: maxLabelPosition,
|
|
22723
|
+
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
22724
|
+
color: textColor,
|
|
22725
|
+
},
|
|
22726
|
+
};
|
|
22727
|
+
}
|
|
22728
|
+
/**
|
|
22729
|
+
* Get the rectangle in which the gauge will be drawn, based on the bounding rectangle of the canvas and leaving
|
|
22730
|
+
* space for the title and labels.
|
|
22731
|
+
*/
|
|
22732
|
+
function getGaugeRect(boundingRect, title) {
|
|
22733
|
+
const titleHeight = title ? GAUGE_TITLE_SECTION_HEIGHT : 0;
|
|
22734
|
+
const drawHeight = boundingRect.height - GAUGE_PADDING_BOTTOM - titleHeight - GAUGE_PADDING_TOP;
|
|
22735
|
+
const drawWidth = boundingRect.width - GAUGE_PADDING_SIDE * 2;
|
|
22736
|
+
let gaugeWidth;
|
|
22737
|
+
let gaugeHeight;
|
|
22738
|
+
if (drawWidth > 2 * drawHeight) {
|
|
22739
|
+
gaugeWidth = 2 * drawHeight;
|
|
22740
|
+
gaugeHeight = drawHeight;
|
|
22741
|
+
}
|
|
22742
|
+
else {
|
|
22743
|
+
gaugeWidth = drawWidth;
|
|
22744
|
+
gaugeHeight = drawWidth / 2;
|
|
22745
|
+
}
|
|
22746
|
+
const gaugeX = GAUGE_PADDING_SIDE + (drawWidth - gaugeWidth) / 2;
|
|
22747
|
+
const gaugeY = titleHeight + GAUGE_PADDING_TOP + (drawHeight - gaugeHeight) / 2;
|
|
22748
|
+
return {
|
|
22749
|
+
x: gaugeX,
|
|
22750
|
+
y: gaugeY,
|
|
22751
|
+
width: gaugeWidth,
|
|
22752
|
+
height: gaugeHeight,
|
|
22753
|
+
};
|
|
22754
|
+
}
|
|
22755
|
+
/**
|
|
22756
|
+
* 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).
|
|
22757
|
+
*
|
|
22758
|
+
* Also compute an offset for the text so that it doesn't overlap with other text.
|
|
22759
|
+
*/
|
|
22760
|
+
function getInflectionValues(runtime, gaugeRect, textColor, ctx) {
|
|
22761
|
+
const maxValue = runtime.maxValue;
|
|
22762
|
+
const minValue = runtime.minValue;
|
|
22763
|
+
const gaugeCircleCenter = {
|
|
22764
|
+
x: gaugeRect.x + gaugeRect.width / 2,
|
|
22765
|
+
y: gaugeRect.y + gaugeRect.height,
|
|
22766
|
+
};
|
|
22767
|
+
const textStyle = { fontSize: GAUGE_LABELS_FONT_SIZE };
|
|
22768
|
+
const inflectionValues = [];
|
|
22769
|
+
const inflectionValuesTextRects = [];
|
|
22770
|
+
for (const inflectionValue of runtime.inflectionValues) {
|
|
22771
|
+
const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
|
|
22772
|
+
const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
|
|
22773
|
+
const angle = Math.PI - Math.PI * percentage;
|
|
22774
|
+
const textRect = getRectangleTangentToCircle(angle, // angle between X axis and the point where the rectangle is tangent to the circle
|
|
22775
|
+
gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, // radius of the gauge circle + margin below text
|
|
22776
|
+
gaugeCircleCenter.x, // center of the gauge circle
|
|
22777
|
+
gaugeCircleCenter.y, // center of the gauge circle
|
|
22778
|
+
labelWidth + 2, // width of the text + some margin
|
|
22779
|
+
GAUGE_LABELS_FONT_SIZE // height of the text
|
|
22780
|
+
);
|
|
22781
|
+
let offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect))
|
|
22782
|
+
? GAUGE_LABELS_FONT_SIZE
|
|
22783
|
+
: 0;
|
|
22784
|
+
inflectionValuesTextRects.push(textRect);
|
|
22785
|
+
inflectionValues.push({
|
|
22786
|
+
rotation: angle,
|
|
22787
|
+
label: inflectionValue.label,
|
|
22788
|
+
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
22789
|
+
color: textColor,
|
|
22790
|
+
offset,
|
|
22791
|
+
});
|
|
22792
|
+
}
|
|
22793
|
+
return inflectionValues;
|
|
22794
|
+
}
|
|
22795
|
+
function getGaugeColor(runtime) {
|
|
22796
|
+
const gaugeValue = runtime.gaugeValue?.value;
|
|
22797
|
+
if (gaugeValue === undefined) {
|
|
22798
|
+
return GAUGE_BACKGROUND_COLOR;
|
|
22799
|
+
}
|
|
22800
|
+
for (let i = 0; i < runtime.inflectionValues.length; i++) {
|
|
22801
|
+
const inflectionValue = runtime.inflectionValues[i];
|
|
22802
|
+
if (inflectionValue.operator === "<" && gaugeValue < inflectionValue.value) {
|
|
22803
|
+
return runtime.colors[i];
|
|
22804
|
+
}
|
|
22805
|
+
else if (inflectionValue.operator === "<=" && gaugeValue <= inflectionValue.value) {
|
|
22806
|
+
return runtime.colors[i];
|
|
22807
|
+
}
|
|
22808
|
+
}
|
|
22809
|
+
return runtime.colors.at(-1);
|
|
22810
|
+
}
|
|
22811
|
+
function getSegmentsOfRectangle(rectangle) {
|
|
22812
|
+
return [
|
|
22813
|
+
{ start: rectangle.topLeft, end: rectangle.topRight },
|
|
22814
|
+
{ start: rectangle.topRight, end: rectangle.bottomRight },
|
|
22815
|
+
{ start: rectangle.bottomRight, end: rectangle.bottomLeft },
|
|
22816
|
+
{ start: rectangle.bottomLeft, end: rectangle.topLeft },
|
|
22817
|
+
];
|
|
22818
|
+
}
|
|
22819
|
+
/**
|
|
22820
|
+
* Check if two segment intersect. The case where the segments are colinear (both segments on the same line)
|
|
22821
|
+
* is not handled.
|
|
22822
|
+
*/
|
|
22823
|
+
function doSegmentIntersect(segment1, segment2) {
|
|
22824
|
+
const A = segment1.start;
|
|
22825
|
+
const B = segment1.end;
|
|
22826
|
+
const C = segment2.start;
|
|
22827
|
+
const D = segment2.end;
|
|
22828
|
+
/**
|
|
22829
|
+
* Line segment intersection algorithm
|
|
22830
|
+
* https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
|
|
22831
|
+
*/
|
|
22832
|
+
function ccw(a, b, c) {
|
|
22833
|
+
return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
|
|
22834
|
+
}
|
|
22835
|
+
return ccw(A, C, D) !== ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D);
|
|
22836
|
+
}
|
|
22837
|
+
function doRectanglesIntersect(rect1, rect2) {
|
|
22838
|
+
const segments1 = getSegmentsOfRectangle(rect1);
|
|
22839
|
+
const segments2 = getSegmentsOfRectangle(rect2);
|
|
22840
|
+
for (const segment1 of segments1) {
|
|
22841
|
+
for (const segment2 of segments2) {
|
|
22842
|
+
if (doSegmentIntersect(segment1, segment2)) {
|
|
22843
|
+
return true;
|
|
22844
|
+
}
|
|
22845
|
+
}
|
|
22846
|
+
}
|
|
22847
|
+
return false;
|
|
22848
|
+
}
|
|
22849
|
+
/**
|
|
22850
|
+
* Get the rectangle that is tangent to a circle at a given angle.
|
|
22851
|
+
*
|
|
22852
|
+
* @param angle angle between X axis and the point where the rectangle is tangent to the circle
|
|
22853
|
+
*/
|
|
22854
|
+
function getRectangleTangentToCircle(angle, radius, circleCenterX, circleCenterY, rectWidth, rectHeight) {
|
|
22855
|
+
const cos = Math.cos(angle);
|
|
22856
|
+
const sin = Math.sin(angle);
|
|
22857
|
+
// x, y are the distance from the center of the circle to the point where the rectangle is tangent to the circle
|
|
22858
|
+
const x = cos * radius;
|
|
22859
|
+
const y = sin * radius;
|
|
22860
|
+
// x2, y2 are the distance from the point the rectangle is tangent to the circle to the bottom left corner of the rectangle
|
|
22861
|
+
const x2 = sin * (rectWidth / 2); // cos(angle + 90°) = sin(angle)
|
|
22862
|
+
const y2 = cos * (rectWidth / 2);
|
|
22863
|
+
const bottomRight = {
|
|
22864
|
+
x: x + x2 + circleCenterX,
|
|
22865
|
+
y: circleCenterY - (y - y2),
|
|
22866
|
+
};
|
|
22867
|
+
const bottomLeft = {
|
|
22868
|
+
x: x - x2 + circleCenterX,
|
|
22869
|
+
y: circleCenterY - (y + y2),
|
|
22870
|
+
};
|
|
22871
|
+
// Same as above but for the top corners of the rectangle (radius + rectangle height instead of radius)
|
|
22872
|
+
const xp = cos * (radius + rectHeight);
|
|
22873
|
+
const yp = sin * (radius + rectHeight);
|
|
22874
|
+
const topLeft = {
|
|
22875
|
+
x: xp - x2 + circleCenterX,
|
|
22876
|
+
y: circleCenterY - (yp + y2),
|
|
22877
|
+
};
|
|
22878
|
+
const topRight = {
|
|
22879
|
+
x: xp + x2 + circleCenterX,
|
|
22880
|
+
y: circleCenterY - (yp - y2),
|
|
22881
|
+
};
|
|
22882
|
+
return { bottomLeft, bottomRight, topRight, topLeft };
|
|
22883
|
+
}
|
|
22884
|
+
|
|
22941
22885
|
class GaugeChartComponent extends owl.Component {
|
|
22942
22886
|
static template = "o-spreadsheet-GaugeChartComponent";
|
|
22943
22887
|
canvas = owl.useRef("chartContainer");
|
|
@@ -22970,6 +22914,73 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
22970
22914
|
return color;
|
|
22971
22915
|
}
|
|
22972
22916
|
|
|
22917
|
+
const CHART_COMMON_OPTIONS = {
|
|
22918
|
+
// https://www.chartjs.org/docs/latest/general/responsive.html
|
|
22919
|
+
responsive: true, // will resize when its container is resized
|
|
22920
|
+
maintainAspectRatio: false, // doesn't maintain the aspect ratio (width/height =2 by default) so the user has the choice of the exact layout
|
|
22921
|
+
elements: {
|
|
22922
|
+
line: {
|
|
22923
|
+
fill: false, // do not fill the area under line charts
|
|
22924
|
+
},
|
|
22925
|
+
point: {
|
|
22926
|
+
hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
|
|
22927
|
+
},
|
|
22928
|
+
},
|
|
22929
|
+
animation: false,
|
|
22930
|
+
};
|
|
22931
|
+
function chartToImage(runtime, figure, type) {
|
|
22932
|
+
// wrap the canvas in a div with a fixed size because chart.js would
|
|
22933
|
+
// fill the whole page otherwise
|
|
22934
|
+
const div = document.createElement("div");
|
|
22935
|
+
div.style.width = `${figure.width}px`;
|
|
22936
|
+
div.style.height = `${figure.height}px`;
|
|
22937
|
+
const canvas = document.createElement("canvas");
|
|
22938
|
+
div.append(canvas);
|
|
22939
|
+
canvas.setAttribute("width", figure.width.toString());
|
|
22940
|
+
canvas.setAttribute("height", figure.height.toString());
|
|
22941
|
+
// we have to add the canvas to the DOM otherwise it won't be rendered
|
|
22942
|
+
document.body.append(div);
|
|
22943
|
+
if ("chartJsConfig" in runtime) {
|
|
22944
|
+
const config = deepCopy(runtime.chartJsConfig);
|
|
22945
|
+
config.plugins = [backgroundColorChartJSPlugin];
|
|
22946
|
+
const Chart = getChartJSConstructor();
|
|
22947
|
+
const chart = new Chart(canvas, config);
|
|
22948
|
+
const imgContent = chart.toBase64Image();
|
|
22949
|
+
chart.destroy();
|
|
22950
|
+
div.remove();
|
|
22951
|
+
return imgContent;
|
|
22952
|
+
}
|
|
22953
|
+
else if (type === "scorecard") {
|
|
22954
|
+
const design = getScorecardConfiguration(figure, runtime);
|
|
22955
|
+
drawScoreChart(design, canvas);
|
|
22956
|
+
const imgContent = canvas.toDataURL();
|
|
22957
|
+
div.remove();
|
|
22958
|
+
return imgContent;
|
|
22959
|
+
}
|
|
22960
|
+
else if (type === "gauge") {
|
|
22961
|
+
drawGaugeChart(canvas, runtime);
|
|
22962
|
+
const imgContent = canvas.toDataURL();
|
|
22963
|
+
div.remove();
|
|
22964
|
+
return imgContent;
|
|
22965
|
+
}
|
|
22966
|
+
return undefined;
|
|
22967
|
+
}
|
|
22968
|
+
/**
|
|
22969
|
+
* Custom chart.js plugin to set the background color of the canvas
|
|
22970
|
+
* https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md
|
|
22971
|
+
*/
|
|
22972
|
+
const backgroundColorChartJSPlugin = {
|
|
22973
|
+
id: "customCanvasBackgroundColor",
|
|
22974
|
+
beforeDraw: (chart) => {
|
|
22975
|
+
const { ctx } = chart;
|
|
22976
|
+
ctx.save();
|
|
22977
|
+
ctx.globalCompositeOperation = "destination-over";
|
|
22978
|
+
ctx.fillStyle = "#ffffff";
|
|
22979
|
+
ctx.fillRect(0, 0, chart.width, chart.height);
|
|
22980
|
+
ctx.restore();
|
|
22981
|
+
},
|
|
22982
|
+
};
|
|
22983
|
+
|
|
22973
22984
|
/**
|
|
22974
22985
|
* Represent a raw XML string
|
|
22975
22986
|
*/
|
|
@@ -23009,9 +23020,9 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
23009
23020
|
/** In XLSX color format (no #) */
|
|
23010
23021
|
const AUTO_COLOR = "000000";
|
|
23011
23022
|
const XLSX_ICONSET_MAP = {
|
|
23012
|
-
|
|
23023
|
+
arrows: "3Arrows",
|
|
23013
23024
|
smiley: "3Symbols",
|
|
23014
|
-
|
|
23025
|
+
dots: "3TrafficLights1",
|
|
23015
23026
|
};
|
|
23016
23027
|
const NAMESPACE = {
|
|
23017
23028
|
styleSheet: "http://schemas.openxmlformats.org/spreadsheetml/2006/main",
|
|
@@ -23542,6 +23553,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
23542
23553
|
};
|
|
23543
23554
|
/** Map between legend position in XLSX file and human readable position */
|
|
23544
23555
|
const DRAWING_LEGEND_POSITION_CONVERSION_MAP = {
|
|
23556
|
+
none: "none",
|
|
23545
23557
|
b: "bottom",
|
|
23546
23558
|
t: "top",
|
|
23547
23559
|
l: "left",
|
|
@@ -26304,7 +26316,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
26304
26316
|
default: "ffffff",
|
|
26305
26317
|
}).asString(),
|
|
26306
26318
|
legendPosition: DRAWING_LEGEND_POSITION_CONVERSION_MAP[this.extractChildAttr(rootChartElement, "c:legendPos", "val", {
|
|
26307
|
-
default: "
|
|
26319
|
+
default: "none",
|
|
26308
26320
|
}).asString()],
|
|
26309
26321
|
stacked: barChartGrouping === "stacked",
|
|
26310
26322
|
fontColor: "000000",
|
|
@@ -26338,7 +26350,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
26338
26350
|
default: "ffffff",
|
|
26339
26351
|
}).asString(),
|
|
26340
26352
|
legendPosition: DRAWING_LEGEND_POSITION_CONVERSION_MAP[this.extractChildAttr(chartElement, "c:legendPos", "val", {
|
|
26341
|
-
default: "
|
|
26353
|
+
default: "none",
|
|
26342
26354
|
}).asString()],
|
|
26343
26355
|
stacked: barChartGrouping === "stacked",
|
|
26344
26356
|
fontColor: "000000",
|
|
@@ -28950,7 +28962,8 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
28950
28962
|
}
|
|
28951
28963
|
}
|
|
28952
28964
|
else if (dataSets.length === 1) {
|
|
28953
|
-
|
|
28965
|
+
const dataLength = getData(getters, dataSets[0]).length;
|
|
28966
|
+
for (let i = 0; i < dataLength; i++) {
|
|
28954
28967
|
labels.formattedValues.push("");
|
|
28955
28968
|
labels.values.push("");
|
|
28956
28969
|
}
|
|
@@ -29133,7 +29146,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
29133
29146
|
function getScatterChartDatasets(definition, args) {
|
|
29134
29147
|
const dataSets = getLineChartDatasets(definition, args);
|
|
29135
29148
|
for (const dataSet of dataSets) {
|
|
29136
|
-
if (dataSet.xAxisID
|
|
29149
|
+
if (!isTrendLineAxis(dataSet.xAxisID)) {
|
|
29137
29150
|
dataSet.showLine = false;
|
|
29138
29151
|
}
|
|
29139
29152
|
}
|
|
@@ -29260,7 +29273,9 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
29260
29273
|
const borderColor = config.color || lightenColor(rgbaToHex(defaultBorderColor), 0.5);
|
|
29261
29274
|
return {
|
|
29262
29275
|
type: "line",
|
|
29263
|
-
xAxisID:
|
|
29276
|
+
xAxisID: config.type === "trailingMovingAverage"
|
|
29277
|
+
? MOVING_AVERAGE_TREND_LINE_XAXIS_ID
|
|
29278
|
+
: TREND_LINE_XAXIS_ID,
|
|
29264
29279
|
yAxisID: dataset.yAxisID,
|
|
29265
29280
|
label: dataset.label ? _t("Trend line for %s", dataset.label) : "",
|
|
29266
29281
|
data,
|
|
@@ -29335,22 +29350,19 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
29335
29350
|
const { dataSetsValues } = args;
|
|
29336
29351
|
const dataSetsLength = Math.max(0, ...dataSetsValues.map((ds) => ds?.data?.length ?? 0));
|
|
29337
29352
|
const colors = getPieColors(new ColorGenerator(dataSetsLength), dataSetsValues);
|
|
29353
|
+
const fontColor = chartFontColor(definition.background);
|
|
29338
29354
|
return {
|
|
29339
29355
|
...getLegendDisplayOptions(definition),
|
|
29340
29356
|
labels: {
|
|
29341
|
-
color: chartFontColor(definition.background),
|
|
29342
29357
|
usePointStyle: true,
|
|
29343
|
-
|
|
29344
|
-
generateLabels: (c) =>
|
|
29345
|
-
//@ts-ignore
|
|
29346
|
-
c.data.labels.map((label, index) => ({
|
|
29358
|
+
generateLabels: (c) => c.data.labels?.map((label, index) => ({
|
|
29347
29359
|
text: truncateLabel(String(label)),
|
|
29348
29360
|
strokeStyle: colors[index],
|
|
29349
29361
|
fillStyle: colors[index],
|
|
29350
29362
|
pointStyle: "rect",
|
|
29351
|
-
hidden: false,
|
|
29352
29363
|
lineWidth: 2,
|
|
29353
|
-
|
|
29364
|
+
fontColor,
|
|
29365
|
+
})) || [],
|
|
29354
29366
|
filter: (legendItem, data) => {
|
|
29355
29367
|
return "datasetIndex" in legendItem
|
|
29356
29368
|
? !data.datasets[legendItem.datasetIndex].hidden
|
|
@@ -29483,7 +29495,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
29483
29495
|
color: fontColor,
|
|
29484
29496
|
usePointStyle: true,
|
|
29485
29497
|
generateLabels: (chart) => chart.data.datasets.map((dataset, index) => {
|
|
29486
|
-
if (dataset["xAxisID"]
|
|
29498
|
+
if (isTrendLineAxis(dataset["xAxisID"])) {
|
|
29487
29499
|
return {
|
|
29488
29500
|
text: truncateLabel(dataset.label),
|
|
29489
29501
|
fontColor,
|
|
@@ -29541,6 +29553,11 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
29541
29553
|
offset: false,
|
|
29542
29554
|
display: false,
|
|
29543
29555
|
};
|
|
29556
|
+
scales[MOVING_AVERAGE_TREND_LINE_XAXIS_ID] = {
|
|
29557
|
+
...scales.x,
|
|
29558
|
+
offset: false,
|
|
29559
|
+
display: false,
|
|
29560
|
+
};
|
|
29544
29561
|
}
|
|
29545
29562
|
return scales;
|
|
29546
29563
|
}
|
|
@@ -29574,6 +29591,10 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
29574
29591
|
...scales.x,
|
|
29575
29592
|
display: false,
|
|
29576
29593
|
};
|
|
29594
|
+
scales[MOVING_AVERAGE_TREND_LINE_XAXIS_ID] = {
|
|
29595
|
+
...scales.x,
|
|
29596
|
+
display: false,
|
|
29597
|
+
};
|
|
29577
29598
|
if (axisType === "category" || axisType === "time") {
|
|
29578
29599
|
/* We add a second x axis here to draw the trend lines, with the labels length being
|
|
29579
29600
|
* set so that the second axis points match the classical x axis
|
|
@@ -29582,6 +29603,8 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
29582
29603
|
scales[TREND_LINE_XAXIS_ID]["type"] = "category";
|
|
29583
29604
|
scales[TREND_LINE_XAXIS_ID]["labels"] = range(0, maxLength).map((x) => x.toString());
|
|
29584
29605
|
scales[TREND_LINE_XAXIS_ID]["offset"] = false;
|
|
29606
|
+
scales[MOVING_AVERAGE_TREND_LINE_XAXIS_ID]["type"] = "category";
|
|
29607
|
+
scales[MOVING_AVERAGE_TREND_LINE_XAXIS_ID]["offset"] = false;
|
|
29585
29608
|
}
|
|
29586
29609
|
}
|
|
29587
29610
|
return scales;
|
|
@@ -29900,9 +29923,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
29900
29923
|
external: customTooltipHandler,
|
|
29901
29924
|
callbacks: {
|
|
29902
29925
|
title: function (tooltipItems) {
|
|
29903
|
-
return tooltipItems.some((item) => item.dataset.xAxisID
|
|
29904
|
-
? undefined
|
|
29905
|
-
: "";
|
|
29926
|
+
return tooltipItems.some((item) => !isTrendLineAxis(item.dataset.xAxisID)) ? undefined : "";
|
|
29906
29927
|
},
|
|
29907
29928
|
beforeLabel: (tooltipItem) => tooltipItem.dataset?.label || tooltipItem.label,
|
|
29908
29929
|
label: function (tooltipItem) {
|
|
@@ -29929,7 +29950,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
29929
29950
|
if (axisType === "linear") {
|
|
29930
29951
|
tooltip.callbacks.label = (tooltipItem) => {
|
|
29931
29952
|
const dataSetPoint = tooltipItem.parsed.y;
|
|
29932
|
-
let label = tooltipItem.dataset.xAxisID
|
|
29953
|
+
let label = isTrendLineAxis(tooltipItem.dataset.xAxisID)
|
|
29933
29954
|
? ""
|
|
29934
29955
|
: tooltipItem.parsed.x;
|
|
29935
29956
|
if (typeof label === "string" && isNumber(label, locale)) {
|
|
@@ -29951,8 +29972,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
29951
29972
|
}
|
|
29952
29973
|
tooltip.callbacks.beforeLabel = (tooltipItem) => tooltipItem.dataset?.label || tooltipItem.label;
|
|
29953
29974
|
tooltip.callbacks.title = function (tooltipItems) {
|
|
29954
|
-
const displayTooltipTitle = axisType !== "linear" &&
|
|
29955
|
-
tooltipItems.some((item) => item.dataset.xAxisID !== TREND_LINE_XAXIS_ID);
|
|
29975
|
+
const displayTooltipTitle = axisType !== "linear" && tooltipItems.some((item) => !isTrendLineAxis(item.dataset.xAxisID));
|
|
29956
29976
|
return displayTooltipTitle ? undefined : "";
|
|
29957
29977
|
};
|
|
29958
29978
|
return tooltip;
|
|
@@ -34206,6 +34226,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
34206
34226
|
CHART_COMMON_OPTIONS: CHART_COMMON_OPTIONS,
|
|
34207
34227
|
GaugeChart: GaugeChart,
|
|
34208
34228
|
LineChart: LineChart,
|
|
34229
|
+
MOVING_AVERAGE_TREND_LINE_XAXIS_ID: MOVING_AVERAGE_TREND_LINE_XAXIS_ID,
|
|
34209
34230
|
PieChart: PieChart,
|
|
34210
34231
|
ScorecardChart: ScorecardChart$1,
|
|
34211
34232
|
TREND_LINE_XAXIS_ID: TREND_LINE_XAXIS_ID,
|
|
@@ -34230,11 +34251,11 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
34230
34251
|
duplicateLabelRangeInDuplicatedSheet: duplicateLabelRangeInDuplicatedSheet,
|
|
34231
34252
|
formatChartDatasetValue: formatChartDatasetValue,
|
|
34232
34253
|
formatTickValue: formatTickValue,
|
|
34233
|
-
getChartJSConstructor: getChartJSConstructor,
|
|
34234
34254
|
getChartPositionAtCenterOfViewport: getChartPositionAtCenterOfViewport,
|
|
34235
34255
|
getDefinedAxis: getDefinedAxis,
|
|
34236
34256
|
getPieColors: getPieColors,
|
|
34237
34257
|
getSmartChartDefinition: getSmartChartDefinition,
|
|
34258
|
+
isTrendLineAxis: isTrendLineAxis,
|
|
34238
34259
|
shouldRemoveFirstLabel: shouldRemoveFirstLabel,
|
|
34239
34260
|
toExcelDataset: toExcelDataset,
|
|
34240
34261
|
toExcelLabelRange: toExcelLabelRange,
|
|
@@ -36282,9 +36303,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
36282
36303
|
}
|
|
36283
36304
|
}
|
|
36284
36305
|
}
|
|
36285
|
-
|
|
36286
|
-
// =|N0|+|N1|+|N0| -> =|N|+|N|+|N|
|
|
36287
|
-
const normalizedFormula = cell.compiledFormula.normalizedFormula.replace(/(|\w)(\d)(|)/g, "$1$3");
|
|
36306
|
+
const normalizedFormula = cell.compiledFormula.normalizedFormula;
|
|
36288
36307
|
return hash(fingerprintVector) + normalizedFormula;
|
|
36289
36308
|
}
|
|
36290
36309
|
getLiteralFingerprint(position) {
|
|
@@ -39629,9 +39648,11 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
39629
39648
|
if (!runtime || !("chartJsConfig" in runtime)) {
|
|
39630
39649
|
return [];
|
|
39631
39650
|
}
|
|
39632
|
-
return runtime.chartJsConfig.data.datasets
|
|
39651
|
+
return runtime.chartJsConfig.data.datasets
|
|
39652
|
+
.filter((d) => !isTrendLineAxis(d["xAxisID"] ?? ""))
|
|
39653
|
+
.map((d) => d.label);
|
|
39633
39654
|
}
|
|
39634
|
-
|
|
39655
|
+
updateEditedSeries(ev) {
|
|
39635
39656
|
this.state.index = ev.target.selectedIndex;
|
|
39636
39657
|
}
|
|
39637
39658
|
updateDataSeriesColor(color) {
|
|
@@ -39644,7 +39665,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
39644
39665
|
};
|
|
39645
39666
|
this.props.updateChart(this.props.figureId, { dataSets });
|
|
39646
39667
|
}
|
|
39647
|
-
|
|
39668
|
+
getDataSeriesColor() {
|
|
39648
39669
|
const dataSets = this.props.definition.dataSets;
|
|
39649
39670
|
if (!dataSets?.[this.state.index])
|
|
39650
39671
|
return "";
|
|
@@ -39664,7 +39685,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
39664
39685
|
};
|
|
39665
39686
|
this.props.updateChart(this.props.figureId, { dataSets });
|
|
39666
39687
|
}
|
|
39667
|
-
|
|
39688
|
+
getDataSeriesLabel() {
|
|
39668
39689
|
const dataSets = this.props.definition.dataSets;
|
|
39669
39690
|
return dataSets[this.state.index]?.label || this.getDataSeries()[this.state.index];
|
|
39670
39691
|
}
|
|
@@ -39777,7 +39798,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
39777
39798
|
}
|
|
39778
39799
|
this.updateTrendLineValue(index, { window });
|
|
39779
39800
|
}
|
|
39780
|
-
|
|
39801
|
+
getDataSeriesColor(index) {
|
|
39781
39802
|
const dataSets = this.props.definition.dataSets;
|
|
39782
39803
|
if (!dataSets?.[index])
|
|
39783
39804
|
return "";
|
|
@@ -39788,7 +39809,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
39788
39809
|
}
|
|
39789
39810
|
getTrendLineColor(index) {
|
|
39790
39811
|
return (this.getTrendLineConfiguration(index)?.color ??
|
|
39791
|
-
setColorAlpha(this.
|
|
39812
|
+
setColorAlpha(this.getDataSeriesColor(index), 0.5));
|
|
39792
39813
|
}
|
|
39793
39814
|
updateTrendLineColor(index, color) {
|
|
39794
39815
|
this.updateTrendLineValue(index, { color });
|
|
@@ -45077,7 +45098,8 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
45077
45098
|
&.pivot-dimension-invalid {
|
|
45078
45099
|
background-color: #ffdddd;
|
|
45079
45100
|
border-color: red !important;
|
|
45080
|
-
select
|
|
45101
|
+
select,
|
|
45102
|
+
input {
|
|
45081
45103
|
background-color: #ffdddd;
|
|
45082
45104
|
}
|
|
45083
45105
|
}
|
|
@@ -46938,7 +46960,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
46938
46960
|
this.notification.notifyUser({
|
|
46939
46961
|
type: "info",
|
|
46940
46962
|
text: _t("Pivot updates only work with dynamic pivot tables. Use %s or re-insert the static pivot from the Data menu.", pivotExample),
|
|
46941
|
-
sticky:
|
|
46963
|
+
sticky: true,
|
|
46942
46964
|
});
|
|
46943
46965
|
}
|
|
46944
46966
|
}
|
|
@@ -70768,7 +70790,9 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
70768
70790
|
border: 1px solid;
|
|
70769
70791
|
font-family: ${DEFAULT_FONT};
|
|
70770
70792
|
|
|
70771
|
-
|
|
70793
|
+
/* In readonly we always show the fx icon if the composer is empty, not matter the focus */
|
|
70794
|
+
.o-composer:empty:not(:focus):not(.active)::before,
|
|
70795
|
+
&.o-topbar-composer-readonly .o-composer:empty::before {
|
|
70772
70796
|
content: url("data:image/svg+xml,${encodeURIComponent(FX_SVG)}");
|
|
70773
70797
|
position: relative;
|
|
70774
70798
|
top: 20%;
|
|
@@ -74094,10 +74118,14 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
74094
74118
|
continue;
|
|
74095
74119
|
}
|
|
74096
74120
|
const cfValueObjectNodes = cfValueObject.map((attrs) => escapeXml /*xml*/ `<cfvo ${formatAttributes(attrs)} />`);
|
|
74121
|
+
const iconSetAttrs = [["iconSet", getIconSet(rule.icons)]];
|
|
74122
|
+
if (isIconSetReversed(rule.icons)) {
|
|
74123
|
+
iconSetAttrs.push(["reverse", "1"]);
|
|
74124
|
+
}
|
|
74097
74125
|
conditionalFormats.push(escapeXml /*xml*/ `
|
|
74098
74126
|
<conditionalFormatting sqref="${range}">
|
|
74099
74127
|
<cfRule ${formatAttributes(ruleAttributes)}>
|
|
74100
|
-
<iconSet
|
|
74128
|
+
<iconSet ${formatAttributes(iconSetAttrs)}>
|
|
74101
74129
|
${joinXmlNodes(cfValueObjectNodes)}
|
|
74102
74130
|
</iconSet>
|
|
74103
74131
|
</cfRule>
|
|
@@ -74115,9 +74143,21 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
74115
74143
|
["stopIfTrue", cf.stopIfTrue ? 1 : 0],
|
|
74116
74144
|
];
|
|
74117
74145
|
}
|
|
74146
|
+
function isIconSetReversed(iconSet) {
|
|
74147
|
+
const defaultIconSet = ICON_SETS[detectIconsType(iconSet)];
|
|
74148
|
+
return iconSet.upper === defaultIconSet.bad && iconSet.lower === defaultIconSet.good;
|
|
74149
|
+
}
|
|
74118
74150
|
function getIconSet(iconSet) {
|
|
74119
|
-
return XLSX_ICONSET_MAP[
|
|
74120
|
-
|
|
74151
|
+
return XLSX_ICONSET_MAP[detectIconsType(iconSet)];
|
|
74152
|
+
}
|
|
74153
|
+
/**
|
|
74154
|
+
* Partial detection based on "upper" point only.
|
|
74155
|
+
* We support any arbitrary icon in the set, while excel doesn't allow
|
|
74156
|
+
* mixing icons from different types.
|
|
74157
|
+
*/
|
|
74158
|
+
function detectIconsType(iconSet) {
|
|
74159
|
+
const type = Object.keys(ICON_SETS).find((type) => Object.values(ICON_SETS[type]).includes(iconSet.upper)) || "dots";
|
|
74160
|
+
return type;
|
|
74121
74161
|
}
|
|
74122
74162
|
function thresholdAttributes(threshold, position) {
|
|
74123
74163
|
const type = getExcelThresholdType(threshold.type, position);
|
|
@@ -75867,6 +75907,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
75867
75907
|
supportedPivotPositionalFormulaRegistry,
|
|
75868
75908
|
pivotToFunctionValueRegistry,
|
|
75869
75909
|
migrationStepRegistry,
|
|
75910
|
+
chartJsExtensionRegistry,
|
|
75870
75911
|
};
|
|
75871
75912
|
const helpers = {
|
|
75872
75913
|
arg,
|
|
@@ -76067,9 +76108,9 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
76067
76108
|
exports.tokenize = tokenize;
|
|
76068
76109
|
|
|
76069
76110
|
|
|
76070
|
-
__info__.version = "18.2.
|
|
76071
|
-
__info__.date = "2025-
|
|
76072
|
-
__info__.hash = "
|
|
76111
|
+
__info__.version = "18.2.6";
|
|
76112
|
+
__info__.date = "2025-04-04T08:41:26.115Z";
|
|
76113
|
+
__info__.hash = "faa00e2";
|
|
76073
76114
|
|
|
76074
76115
|
|
|
76075
76116
|
})(this.o_spreadsheet = this.o_spreadsheet || {}, owl);
|