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