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