@odoo/o-spreadsheet 18.1.12 → 18.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/o-spreadsheet.cjs.js +640 -599
- package/dist/o-spreadsheet.d.ts +9 -6
- package/dist/o-spreadsheet.esm.js +640 -599
- package/dist/o-spreadsheet.iife.js +640 -599
- package/dist/o-spreadsheet.iife.min.js +382 -379
- 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.1.
|
|
6
|
-
* @date 2025-
|
|
7
|
-
* @hash
|
|
5
|
+
* @version 18.1.14
|
|
6
|
+
* @date 2025-04-04T08:42:40.149Z
|
|
7
|
+
* @hash 63b2fb7
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { useEnv, useSubEnv, onWillUnmount, useComponent, status, Component, useRef, onMounted, useEffect, useState, onPatched, onWillPatch, onWillUpdateProps, useExternalListener, onWillStart, xml, useChildSubEnv, markRaw, toRaw } from '@odoo/owl';
|
|
@@ -803,8 +803,7 @@ function removeFalsyAttributes(obj) {
|
|
|
803
803
|
*
|
|
804
804
|
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Character_Classes
|
|
805
805
|
*/
|
|
806
|
-
const
|
|
807
|
-
" ",
|
|
806
|
+
const specialWhiteSpaceSpecialCharacters = [
|
|
808
807
|
"\t",
|
|
809
808
|
"\f",
|
|
810
809
|
"\v",
|
|
@@ -819,7 +818,7 @@ const whiteSpaceSpecialCharacters = [
|
|
|
819
818
|
String.fromCharCode(parseInt("3000", 16)),
|
|
820
819
|
String.fromCharCode(parseInt("feff", 16)),
|
|
821
820
|
];
|
|
822
|
-
const
|
|
821
|
+
const specialWhiteSpaceRegexp = new RegExp(specialWhiteSpaceSpecialCharacters.join("|"), "g");
|
|
823
822
|
const newLineRegexp = /(\r\n|\r)/g;
|
|
824
823
|
/**
|
|
825
824
|
* Replace all different newlines characters by \n
|
|
@@ -1120,7 +1119,10 @@ function rgbaStringToHex(color) {
|
|
|
1120
1119
|
}
|
|
1121
1120
|
else if (stringVals.length === 4) {
|
|
1122
1121
|
const alpha = parseFloat(stringVals.pop() || "1");
|
|
1123
|
-
|
|
1122
|
+
if (isNaN(alpha)) {
|
|
1123
|
+
throw new Error("invalid alpha value");
|
|
1124
|
+
}
|
|
1125
|
+
alphaHex = Math.round(alpha * 255);
|
|
1124
1126
|
}
|
|
1125
1127
|
const vals = stringVals.map((val) => parseInt(val, 10));
|
|
1126
1128
|
if (alphaHex !== 255) {
|
|
@@ -6785,8 +6787,12 @@ function tokenize(str, locale = DEFAULT_LOCALE) {
|
|
|
6785
6787
|
str = replaceNewLines(str);
|
|
6786
6788
|
const chars = new TokenizingChars(str);
|
|
6787
6789
|
const result = [];
|
|
6790
|
+
const tokenizeSpace = specialWhiteSpaceRegexp.test(str)
|
|
6791
|
+
? tokenizeSpecialCharacterSpace
|
|
6792
|
+
: tokenizeSimpleSpace;
|
|
6788
6793
|
while (!chars.isOver()) {
|
|
6789
|
-
let token =
|
|
6794
|
+
let token = tokenizeNewLine(chars) ||
|
|
6795
|
+
tokenizeSpace(chars) ||
|
|
6790
6796
|
tokenizeArgsSeparator(chars, locale) ||
|
|
6791
6797
|
tokenizeParenthesis(chars) ||
|
|
6792
6798
|
tokenizeOperator(chars) ||
|
|
@@ -6920,17 +6926,19 @@ function tokenizeSymbol(chars) {
|
|
|
6920
6926
|
}
|
|
6921
6927
|
return null;
|
|
6922
6928
|
}
|
|
6923
|
-
function
|
|
6924
|
-
let
|
|
6925
|
-
while (chars.current ===
|
|
6926
|
-
|
|
6927
|
-
chars.shift();
|
|
6929
|
+
function tokenizeSpecialCharacterSpace(chars) {
|
|
6930
|
+
let spaces = "";
|
|
6931
|
+
while (chars.current === " " || (chars.current && chars.current.match(specialWhiteSpaceRegexp))) {
|
|
6932
|
+
spaces += chars.shift();
|
|
6928
6933
|
}
|
|
6929
|
-
if (
|
|
6930
|
-
return { type: "SPACE", value:
|
|
6934
|
+
if (spaces) {
|
|
6935
|
+
return { type: "SPACE", value: spaces };
|
|
6931
6936
|
}
|
|
6937
|
+
return null;
|
|
6938
|
+
}
|
|
6939
|
+
function tokenizeSimpleSpace(chars) {
|
|
6932
6940
|
let spaces = "";
|
|
6933
|
-
while (chars.current
|
|
6941
|
+
while (chars.current === " ") {
|
|
6934
6942
|
spaces += chars.shift();
|
|
6935
6943
|
}
|
|
6936
6944
|
if (spaces) {
|
|
@@ -6938,6 +6946,17 @@ function tokenizeSpace(chars) {
|
|
|
6938
6946
|
}
|
|
6939
6947
|
return null;
|
|
6940
6948
|
}
|
|
6949
|
+
function tokenizeNewLine(chars) {
|
|
6950
|
+
let length = 0;
|
|
6951
|
+
while (chars.current === NEWLINE) {
|
|
6952
|
+
length++;
|
|
6953
|
+
chars.shift();
|
|
6954
|
+
}
|
|
6955
|
+
if (length) {
|
|
6956
|
+
return { type: "SPACE", value: NEWLINE.repeat(length) };
|
|
6957
|
+
}
|
|
6958
|
+
return null;
|
|
6959
|
+
}
|
|
6941
6960
|
function tokenizeInvalidRange(chars) {
|
|
6942
6961
|
if (chars.currentStartsWith(CellErrorType.InvalidReference)) {
|
|
6943
6962
|
chars.advanceBy(CellErrorType.InvalidReference.length);
|
|
@@ -6990,7 +7009,7 @@ function isValidLocale(locale) {
|
|
|
6990
7009
|
*/
|
|
6991
7010
|
function canonicalizeNumberContent(content, locale) {
|
|
6992
7011
|
return content.startsWith("=")
|
|
6993
|
-
? canonicalizeFormula
|
|
7012
|
+
? canonicalizeFormula(content, locale)
|
|
6994
7013
|
: canonicalizeNumberLiteral(content, locale);
|
|
6995
7014
|
}
|
|
6996
7015
|
/**
|
|
@@ -7005,7 +7024,7 @@ function canonicalizeNumberContent(content, locale) {
|
|
|
7005
7024
|
*/
|
|
7006
7025
|
function canonicalizeContent(content, locale) {
|
|
7007
7026
|
return content.startsWith("=")
|
|
7008
|
-
? canonicalizeFormula
|
|
7027
|
+
? canonicalizeFormula(content, locale)
|
|
7009
7028
|
: canonicalizeLiteral(content, locale);
|
|
7010
7029
|
}
|
|
7011
7030
|
/**
|
|
@@ -7021,15 +7040,21 @@ function localizeContent(content, locale) {
|
|
|
7021
7040
|
? localizeFormula(content, locale)
|
|
7022
7041
|
: localizeLiteral(content, locale);
|
|
7023
7042
|
}
|
|
7043
|
+
/** Change a number string to its canonical form (en_US locale) */
|
|
7044
|
+
function canonicalizeNumberValue(content, locale) {
|
|
7045
|
+
return content.startsWith("=")
|
|
7046
|
+
? canonicalizeFormula(content, locale)
|
|
7047
|
+
: canonicalizeNumberLiteral(content, locale);
|
|
7048
|
+
}
|
|
7024
7049
|
/** Change a formula to its canonical form (en_US locale) */
|
|
7025
|
-
function canonicalizeFormula
|
|
7026
|
-
return _localizeFormula
|
|
7050
|
+
function canonicalizeFormula(formula, locale) {
|
|
7051
|
+
return _localizeFormula(formula, locale, DEFAULT_LOCALE);
|
|
7027
7052
|
}
|
|
7028
7053
|
/** Change a formula from the canonical form to the given locale */
|
|
7029
7054
|
function localizeFormula(formula, locale) {
|
|
7030
|
-
return _localizeFormula
|
|
7055
|
+
return _localizeFormula(formula, DEFAULT_LOCALE, locale);
|
|
7031
7056
|
}
|
|
7032
|
-
function _localizeFormula
|
|
7057
|
+
function _localizeFormula(formula, fromLocale, toLocale) {
|
|
7033
7058
|
if (fromLocale.formulaArgSeparator === toLocale.formulaArgSeparator &&
|
|
7034
7059
|
fromLocale.decimalSeparator === toLocale.decimalSeparator) {
|
|
7035
7060
|
return formula;
|
|
@@ -7184,37 +7209,6 @@ function getDateTimeFormat(locale) {
|
|
|
7184
7209
|
return locale.dateFormat + " " + locale.timeFormat;
|
|
7185
7210
|
}
|
|
7186
7211
|
|
|
7187
|
-
/** Change a number string to its canonical form (en_US locale) */
|
|
7188
|
-
function canonicalizeNumberValue(content, locale) {
|
|
7189
|
-
return content.startsWith("=")
|
|
7190
|
-
? canonicalizeFormula(content, locale)
|
|
7191
|
-
: canonicalizeNumberLiteral(content, locale);
|
|
7192
|
-
}
|
|
7193
|
-
/** Change a formula to its canonical form (en_US locale) */
|
|
7194
|
-
function canonicalizeFormula(formula, locale) {
|
|
7195
|
-
return _localizeFormula(formula, locale, DEFAULT_LOCALE);
|
|
7196
|
-
}
|
|
7197
|
-
function _localizeFormula(formula, fromLocale, toLocale) {
|
|
7198
|
-
if (fromLocale.formulaArgSeparator === toLocale.formulaArgSeparator &&
|
|
7199
|
-
fromLocale.decimalSeparator === toLocale.decimalSeparator) {
|
|
7200
|
-
return formula;
|
|
7201
|
-
}
|
|
7202
|
-
const tokens = tokenize(formula, fromLocale);
|
|
7203
|
-
let localizedFormula = "";
|
|
7204
|
-
for (const token of tokens) {
|
|
7205
|
-
if (token.type === "NUMBER") {
|
|
7206
|
-
localizedFormula += token.value.replace(fromLocale.decimalSeparator, toLocale.decimalSeparator);
|
|
7207
|
-
}
|
|
7208
|
-
else if (token.type === "ARG_SEPARATOR") {
|
|
7209
|
-
localizedFormula += toLocale.formulaArgSeparator;
|
|
7210
|
-
}
|
|
7211
|
-
else {
|
|
7212
|
-
localizedFormula += token.value;
|
|
7213
|
-
}
|
|
7214
|
-
}
|
|
7215
|
-
return localizedFormula;
|
|
7216
|
-
}
|
|
7217
|
-
|
|
7218
7212
|
function boolAnd(args) {
|
|
7219
7213
|
let foundBoolean = false;
|
|
7220
7214
|
let acc = true;
|
|
@@ -9588,7 +9582,17 @@ class ComposerFocusStore extends SpreadsheetStore {
|
|
|
9588
9582
|
}
|
|
9589
9583
|
}
|
|
9590
9584
|
|
|
9585
|
+
const chartJsExtensionRegistry = new Registry();
|
|
9586
|
+
/** Return window.Chart, making sure all our extensions are loaded in ChartJS */
|
|
9587
|
+
function getChartJSConstructor() {
|
|
9588
|
+
if (window.Chart && !window.Chart?.registry.plugins.get("chartShowValuesPlugin")) {
|
|
9589
|
+
window.Chart.register(...chartJsExtensionRegistry.getAll());
|
|
9590
|
+
}
|
|
9591
|
+
return window.Chart;
|
|
9592
|
+
}
|
|
9593
|
+
|
|
9591
9594
|
const TREND_LINE_XAXIS_ID = "x1";
|
|
9595
|
+
const MOVING_AVERAGE_TREND_LINE_XAXIS_ID = "xMovingAverage";
|
|
9592
9596
|
/**
|
|
9593
9597
|
* This file contains helpers that are common to different charts (mainly
|
|
9594
9598
|
* line, bar and pie charts)
|
|
@@ -9929,6 +9933,9 @@ function getPieColors(colors, dataSetsValues) {
|
|
|
9929
9933
|
}
|
|
9930
9934
|
return pieColors;
|
|
9931
9935
|
}
|
|
9936
|
+
function isTrendLineAxis(axisID) {
|
|
9937
|
+
return axisID === TREND_LINE_XAXIS_ID || axisID === MOVING_AVERAGE_TREND_LINE_XAXIS_ID;
|
|
9938
|
+
}
|
|
9932
9939
|
|
|
9933
9940
|
/** This is a chartJS plugin that will draw the values of each data next to the point/bar/pie slice */
|
|
9934
9941
|
const chartShowValuesPlugin = {
|
|
@@ -9973,7 +9980,7 @@ function drawLineOrBarOrRadarChartValues(chart, options, ctx) {
|
|
|
9973
9980
|
const yMin = chart.chartArea.top;
|
|
9974
9981
|
const textsPositions = {};
|
|
9975
9982
|
for (const dataset of chart._metasets) {
|
|
9976
|
-
if (dataset.
|
|
9983
|
+
if (isTrendLineAxis(dataset.axisID) || dataset.hidden) {
|
|
9977
9984
|
continue;
|
|
9978
9985
|
}
|
|
9979
9986
|
for (let i = 0; i < dataset._parsed.length; i++) {
|
|
@@ -10016,7 +10023,7 @@ function drawHorizontalBarChartValues(chart, options, ctx) {
|
|
|
10016
10023
|
const xMin = chart.chartArea.left;
|
|
10017
10024
|
const textsPositions = {};
|
|
10018
10025
|
for (const dataset of chart._metasets) {
|
|
10019
|
-
if (dataset.
|
|
10026
|
+
if (isTrendLineAxis(dataset.axisID)) {
|
|
10020
10027
|
return; // ignore trend lines
|
|
10021
10028
|
}
|
|
10022
10029
|
for (let i = 0; i < dataset._parsed.length; i++) {
|
|
@@ -10122,341 +10129,70 @@ function getNextNonEmptyBar(bars, startIndex) {
|
|
|
10122
10129
|
return bars.find((bar, i) => i > startIndex && bar.height !== 0);
|
|
10123
10130
|
}
|
|
10124
10131
|
|
|
10125
|
-
|
|
10126
|
-
|
|
10127
|
-
|
|
10128
|
-
|
|
10129
|
-
|
|
10130
|
-
|
|
10131
|
-
const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
|
|
10132
|
-
const GAUGE_TITLE_SECTION_HEIGHT = 25;
|
|
10133
|
-
function drawGaugeChart(canvas, runtime) {
|
|
10134
|
-
const canvasBoundingRect = canvas.getBoundingClientRect();
|
|
10135
|
-
canvas.width = canvasBoundingRect.width;
|
|
10136
|
-
canvas.height = canvasBoundingRect.height;
|
|
10137
|
-
const ctx = canvas.getContext("2d");
|
|
10138
|
-
const config = getGaugeRenderingConfig(canvasBoundingRect, runtime, ctx);
|
|
10139
|
-
drawBackground(ctx, config);
|
|
10140
|
-
drawGauge(ctx, config);
|
|
10141
|
-
drawInflectionValues(ctx, config);
|
|
10142
|
-
drawLabels(ctx, config);
|
|
10143
|
-
drawTitle(ctx, config);
|
|
10144
|
-
}
|
|
10145
|
-
function drawGauge(ctx, config) {
|
|
10146
|
-
ctx.save();
|
|
10147
|
-
const gauge = config.gauge;
|
|
10148
|
-
const arcCenterX = gauge.rect.x + gauge.rect.width / 2;
|
|
10149
|
-
const arcCenterY = gauge.rect.y + gauge.rect.height;
|
|
10150
|
-
const arcRadius = gauge.rect.height - gauge.arcWidth / 2;
|
|
10151
|
-
if (arcRadius < 0) {
|
|
10152
|
-
return;
|
|
10153
|
-
}
|
|
10154
|
-
const gaugeAngle = gauge.percentage === 1 ? 0 : Math.PI * (1 + gauge.percentage);
|
|
10155
|
-
// Gauge background
|
|
10156
|
-
ctx.strokeStyle = GAUGE_BACKGROUND_COLOR;
|
|
10157
|
-
ctx.beginPath();
|
|
10158
|
-
ctx.lineWidth = gauge.arcWidth;
|
|
10159
|
-
ctx.arc(arcCenterX, arcCenterY, arcRadius, gaugeAngle, 0);
|
|
10160
|
-
ctx.stroke();
|
|
10161
|
-
// Gauge value
|
|
10162
|
-
ctx.strokeStyle = gauge.color;
|
|
10163
|
-
ctx.beginPath();
|
|
10164
|
-
ctx.arc(arcCenterX, arcCenterY, arcRadius, Math.PI, gaugeAngle);
|
|
10165
|
-
ctx.stroke();
|
|
10166
|
-
ctx.restore();
|
|
10167
|
-
}
|
|
10168
|
-
function drawBackground(ctx, config) {
|
|
10169
|
-
ctx.save();
|
|
10170
|
-
ctx.fillStyle = config.backgroundColor;
|
|
10171
|
-
ctx.fillRect(0, 0, config.width, config.height);
|
|
10172
|
-
ctx.restore();
|
|
10173
|
-
}
|
|
10174
|
-
function drawLabels(ctx, config) {
|
|
10175
|
-
for (const label of [config.minLabel, config.maxLabel, config.gaugeValue]) {
|
|
10176
|
-
ctx.save();
|
|
10177
|
-
ctx.textAlign = "center";
|
|
10178
|
-
ctx.fillStyle = label.color;
|
|
10179
|
-
ctx.font = `${label.fontSize}px ${DEFAULT_FONT}`;
|
|
10180
|
-
ctx.fillText(label.label, label.textPosition.x, label.textPosition.y);
|
|
10181
|
-
ctx.restore();
|
|
10182
|
-
}
|
|
10183
|
-
}
|
|
10184
|
-
function drawInflectionValues(ctx, config) {
|
|
10185
|
-
const { x: rectX, y: rectY, width, height } = config.gauge.rect;
|
|
10186
|
-
for (const inflectionValue of config.inflectionValues) {
|
|
10187
|
-
ctx.save();
|
|
10188
|
-
ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment
|
|
10189
|
-
ctx.rotate(Math.PI / 2 - inflectionValue.rotation);
|
|
10190
|
-
ctx.lineWidth = 2;
|
|
10191
|
-
ctx.strokeStyle = chartMutedFontColor(config.backgroundColor) + "aa";
|
|
10192
|
-
ctx.beginPath();
|
|
10193
|
-
ctx.moveTo(0, -(height - config.gauge.arcWidth));
|
|
10194
|
-
ctx.lineTo(0, -height - 3);
|
|
10195
|
-
ctx.stroke();
|
|
10196
|
-
ctx.textAlign = "center";
|
|
10197
|
-
ctx.font = `${inflectionValue.fontSize}px ${DEFAULT_FONT}`;
|
|
10198
|
-
ctx.fillStyle = inflectionValue.color;
|
|
10199
|
-
const textY = -height - GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN - inflectionValue.offset;
|
|
10200
|
-
ctx.fillText(inflectionValue.label, 0, textY);
|
|
10201
|
-
ctx.restore();
|
|
10202
|
-
}
|
|
10203
|
-
}
|
|
10204
|
-
function drawTitle(ctx, config) {
|
|
10205
|
-
ctx.save();
|
|
10206
|
-
const title = config.title;
|
|
10207
|
-
ctx.font = getDefaultContextFont(title.fontSize, title.bold, title.italic);
|
|
10208
|
-
ctx.textBaseline = "middle";
|
|
10209
|
-
ctx.fillStyle = title.color;
|
|
10210
|
-
ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
|
|
10211
|
-
ctx.restore();
|
|
10212
|
-
}
|
|
10213
|
-
function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
|
|
10214
|
-
const maxValue = runtime.maxValue;
|
|
10215
|
-
const minValue = runtime.minValue;
|
|
10216
|
-
const gaugeValue = runtime.gaugeValue;
|
|
10217
|
-
const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
|
|
10218
|
-
const gaugeArcWidth = gaugeRect.width / 6;
|
|
10219
|
-
const gaugePercentage = gaugeValue
|
|
10220
|
-
? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
|
|
10221
|
-
: 0;
|
|
10222
|
-
const gaugeValuePosition = {
|
|
10223
|
-
x: boundingRect.width / 2,
|
|
10224
|
-
y: gaugeRect.y + gaugeRect.height - gaugeRect.height / 12,
|
|
10225
|
-
};
|
|
10226
|
-
let gaugeValueFontSize = GAUGE_DEFAULT_VALUE_FONT_SIZE;
|
|
10227
|
-
// Scale down the font size if the gaugeRect is too small
|
|
10228
|
-
if (gaugeRect.height < 300) {
|
|
10229
|
-
gaugeValueFontSize = gaugeValueFontSize * (gaugeRect.height / 300);
|
|
10230
|
-
}
|
|
10231
|
-
// Scale down the font size if the text is too long
|
|
10232
|
-
const maxTextWidth = gaugeRect.width / 2;
|
|
10233
|
-
const gaugeLabel = gaugeValue?.label || "-";
|
|
10234
|
-
if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
|
|
10235
|
-
gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
|
|
10236
|
-
}
|
|
10237
|
-
const minLabelPosition = {
|
|
10238
|
-
x: gaugeRect.x + gaugeArcWidth / 2,
|
|
10239
|
-
y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
|
|
10240
|
-
};
|
|
10241
|
-
const maxLabelPosition = {
|
|
10242
|
-
x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
|
|
10243
|
-
y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
|
|
10132
|
+
chartJsExtensionRegistry.add("chartShowValuesPlugin", chartShowValuesPlugin);
|
|
10133
|
+
chartJsExtensionRegistry.add("waterfallLinesPlugin", waterfallLinesPlugin);
|
|
10134
|
+
class ChartJsComponent extends Component {
|
|
10135
|
+
static template = "o-spreadsheet-ChartJsComponent";
|
|
10136
|
+
static props = {
|
|
10137
|
+
figure: Object,
|
|
10244
10138
|
};
|
|
10245
|
-
|
|
10246
|
-
|
|
10247
|
-
|
|
10248
|
-
|
|
10249
|
-
|
|
10250
|
-
}
|
|
10251
|
-
switch (runtime.title.align) {
|
|
10252
|
-
case "right":
|
|
10253
|
-
x = boundingRect.width - titleWidth - CHART_PADDING$1;
|
|
10254
|
-
break;
|
|
10255
|
-
case "center":
|
|
10256
|
-
x = (boundingRect.width - titleWidth) / 2;
|
|
10257
|
-
break;
|
|
10258
|
-
case "left":
|
|
10259
|
-
default:
|
|
10260
|
-
x = CHART_PADDING$1;
|
|
10261
|
-
break;
|
|
10139
|
+
canvas = useRef("graphContainer");
|
|
10140
|
+
chart;
|
|
10141
|
+
currentRuntime;
|
|
10142
|
+
get background() {
|
|
10143
|
+
return this.chartRuntime.background;
|
|
10262
10144
|
}
|
|
10263
|
-
|
|
10264
|
-
|
|
10265
|
-
height: boundingRect.height,
|
|
10266
|
-
title: {
|
|
10267
|
-
label: runtime.title.text ?? "",
|
|
10268
|
-
fontSize: runtime.title.fontSize ?? CHART_TITLE_FONT_SIZE,
|
|
10269
|
-
textPosition: {
|
|
10270
|
-
x,
|
|
10271
|
-
y: CHART_PADDING_TOP + titleHeight / 2,
|
|
10272
|
-
},
|
|
10273
|
-
color: runtime.title.color ?? textColor,
|
|
10274
|
-
bold: runtime.title.bold,
|
|
10275
|
-
italic: runtime.title.italic,
|
|
10276
|
-
},
|
|
10277
|
-
backgroundColor: runtime.background,
|
|
10278
|
-
gauge: {
|
|
10279
|
-
rect: gaugeRect,
|
|
10280
|
-
arcWidth: gaugeArcWidth,
|
|
10281
|
-
percentage: clip(gaugePercentage, 0, 1),
|
|
10282
|
-
color: getGaugeColor(runtime),
|
|
10283
|
-
},
|
|
10284
|
-
inflectionValues,
|
|
10285
|
-
gaugeValue: {
|
|
10286
|
-
label: gaugeLabel,
|
|
10287
|
-
textPosition: gaugeValuePosition,
|
|
10288
|
-
fontSize: gaugeValueFontSize,
|
|
10289
|
-
color: textColor,
|
|
10290
|
-
},
|
|
10291
|
-
minLabel: {
|
|
10292
|
-
label: runtime.minValue.label,
|
|
10293
|
-
textPosition: minLabelPosition,
|
|
10294
|
-
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
10295
|
-
color: textColor,
|
|
10296
|
-
},
|
|
10297
|
-
maxLabel: {
|
|
10298
|
-
label: runtime.maxValue.label,
|
|
10299
|
-
textPosition: maxLabelPosition,
|
|
10300
|
-
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
10301
|
-
color: textColor,
|
|
10302
|
-
},
|
|
10303
|
-
};
|
|
10304
|
-
}
|
|
10305
|
-
/**
|
|
10306
|
-
* Get the rectangle in which the gauge will be drawn, based on the bounding rectangle of the canvas and leaving
|
|
10307
|
-
* space for the title and labels.
|
|
10308
|
-
*/
|
|
10309
|
-
function getGaugeRect(boundingRect, title) {
|
|
10310
|
-
const titleHeight = title ? GAUGE_TITLE_SECTION_HEIGHT : 0;
|
|
10311
|
-
const drawHeight = boundingRect.height - GAUGE_PADDING_BOTTOM - titleHeight - GAUGE_PADDING_TOP;
|
|
10312
|
-
const drawWidth = boundingRect.width - GAUGE_PADDING_SIDE * 2;
|
|
10313
|
-
let gaugeWidth;
|
|
10314
|
-
let gaugeHeight;
|
|
10315
|
-
if (drawWidth > 2 * drawHeight) {
|
|
10316
|
-
gaugeWidth = 2 * drawHeight;
|
|
10317
|
-
gaugeHeight = drawHeight;
|
|
10145
|
+
get canvasStyle() {
|
|
10146
|
+
return `background-color: ${this.background}`;
|
|
10318
10147
|
}
|
|
10319
|
-
|
|
10320
|
-
|
|
10321
|
-
|
|
10148
|
+
get chartRuntime() {
|
|
10149
|
+
const runtime = this.env.model.getters.getChartRuntime(this.props.figure.id);
|
|
10150
|
+
if (!("chartJsConfig" in runtime)) {
|
|
10151
|
+
throw new Error("Unsupported chart runtime");
|
|
10152
|
+
}
|
|
10153
|
+
return runtime;
|
|
10322
10154
|
}
|
|
10323
|
-
|
|
10324
|
-
|
|
10325
|
-
|
|
10326
|
-
|
|
10327
|
-
|
|
10328
|
-
|
|
10329
|
-
|
|
10330
|
-
|
|
10331
|
-
|
|
10332
|
-
|
|
10333
|
-
|
|
10334
|
-
|
|
10335
|
-
|
|
10336
|
-
|
|
10337
|
-
|
|
10338
|
-
|
|
10339
|
-
|
|
10340
|
-
|
|
10341
|
-
|
|
10342
|
-
|
|
10343
|
-
};
|
|
10344
|
-
const textStyle = { fontSize: GAUGE_LABELS_FONT_SIZE };
|
|
10345
|
-
const inflectionValues = [];
|
|
10346
|
-
const inflectionValuesTextRects = [];
|
|
10347
|
-
for (const inflectionValue of runtime.inflectionValues) {
|
|
10348
|
-
const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
|
|
10349
|
-
const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
|
|
10350
|
-
const angle = Math.PI - Math.PI * percentage;
|
|
10351
|
-
const textRect = getRectangleTangentToCircle(angle, // angle between X axis and the point where the rectangle is tangent to the circle
|
|
10352
|
-
gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, // radius of the gauge circle + margin below text
|
|
10353
|
-
gaugeCircleCenter.x, // center of the gauge circle
|
|
10354
|
-
gaugeCircleCenter.y, // center of the gauge circle
|
|
10355
|
-
labelWidth + 2, // width of the text + some margin
|
|
10356
|
-
GAUGE_LABELS_FONT_SIZE // height of the text
|
|
10357
|
-
);
|
|
10358
|
-
let offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect))
|
|
10359
|
-
? GAUGE_LABELS_FONT_SIZE
|
|
10360
|
-
: 0;
|
|
10361
|
-
inflectionValuesTextRects.push(textRect);
|
|
10362
|
-
inflectionValues.push({
|
|
10363
|
-
rotation: angle,
|
|
10364
|
-
label: inflectionValue.label,
|
|
10365
|
-
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
10366
|
-
color: textColor,
|
|
10367
|
-
offset,
|
|
10155
|
+
setup() {
|
|
10156
|
+
onMounted(() => {
|
|
10157
|
+
const runtime = this.chartRuntime;
|
|
10158
|
+
this.currentRuntime = runtime;
|
|
10159
|
+
// Note: chartJS modify the runtime in place, so it's important to give it a copy
|
|
10160
|
+
this.createChart(deepCopy(runtime.chartJsConfig));
|
|
10161
|
+
});
|
|
10162
|
+
onWillUnmount(() => this.chart?.destroy());
|
|
10163
|
+
useEffect(() => {
|
|
10164
|
+
const runtime = this.chartRuntime;
|
|
10165
|
+
if (runtime !== this.currentRuntime) {
|
|
10166
|
+
if (runtime.chartJsConfig.type !== this.currentRuntime.chartJsConfig.type) {
|
|
10167
|
+
this.chart?.destroy();
|
|
10168
|
+
this.createChart(deepCopy(runtime.chartJsConfig));
|
|
10169
|
+
}
|
|
10170
|
+
else {
|
|
10171
|
+
this.updateChartJs(deepCopy(runtime.chartJsConfig));
|
|
10172
|
+
}
|
|
10173
|
+
this.currentRuntime = runtime;
|
|
10174
|
+
}
|
|
10368
10175
|
});
|
|
10369
10176
|
}
|
|
10370
|
-
|
|
10371
|
-
|
|
10372
|
-
|
|
10373
|
-
|
|
10374
|
-
|
|
10375
|
-
return GAUGE_BACKGROUND_COLOR;
|
|
10376
|
-
}
|
|
10377
|
-
for (let i = 0; i < runtime.inflectionValues.length; i++) {
|
|
10378
|
-
const inflectionValue = runtime.inflectionValues[i];
|
|
10379
|
-
if (inflectionValue.operator === "<" && gaugeValue < inflectionValue.value) {
|
|
10380
|
-
return runtime.colors[i];
|
|
10381
|
-
}
|
|
10382
|
-
else if (inflectionValue.operator === "<=" && gaugeValue <= inflectionValue.value) {
|
|
10383
|
-
return runtime.colors[i];
|
|
10384
|
-
}
|
|
10385
|
-
}
|
|
10386
|
-
return runtime.colors.at(-1);
|
|
10387
|
-
}
|
|
10388
|
-
function getSegmentsOfRectangle(rectangle) {
|
|
10389
|
-
return [
|
|
10390
|
-
{ start: rectangle.topLeft, end: rectangle.topRight },
|
|
10391
|
-
{ start: rectangle.topRight, end: rectangle.bottomRight },
|
|
10392
|
-
{ start: rectangle.bottomRight, end: rectangle.bottomLeft },
|
|
10393
|
-
{ start: rectangle.bottomLeft, end: rectangle.topLeft },
|
|
10394
|
-
];
|
|
10395
|
-
}
|
|
10396
|
-
/**
|
|
10397
|
-
* Check if two segment intersect. The case where the segments are colinear (both segments on the same line)
|
|
10398
|
-
* is not handled.
|
|
10399
|
-
*/
|
|
10400
|
-
function doSegmentIntersect(segment1, segment2) {
|
|
10401
|
-
const A = segment1.start;
|
|
10402
|
-
const B = segment1.end;
|
|
10403
|
-
const C = segment2.start;
|
|
10404
|
-
const D = segment2.end;
|
|
10405
|
-
/**
|
|
10406
|
-
* Line segment intersection algorithm
|
|
10407
|
-
* https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
|
|
10408
|
-
*/
|
|
10409
|
-
function ccw(a, b, c) {
|
|
10410
|
-
return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
|
|
10177
|
+
createChart(chartData) {
|
|
10178
|
+
const canvas = this.canvas.el;
|
|
10179
|
+
const ctx = canvas.getContext("2d");
|
|
10180
|
+
const Chart = getChartJSConstructor();
|
|
10181
|
+
this.chart = new Chart(ctx, chartData);
|
|
10411
10182
|
}
|
|
10412
|
-
|
|
10413
|
-
|
|
10414
|
-
|
|
10415
|
-
|
|
10416
|
-
|
|
10417
|
-
for (const segment1 of segments1) {
|
|
10418
|
-
for (const segment2 of segments2) {
|
|
10419
|
-
if (doSegmentIntersect(segment1, segment2)) {
|
|
10420
|
-
return true;
|
|
10183
|
+
updateChartJs(chartData) {
|
|
10184
|
+
if (chartData.data && chartData.data.datasets) {
|
|
10185
|
+
this.chart.data = chartData.data;
|
|
10186
|
+
if (chartData.options?.plugins?.title) {
|
|
10187
|
+
this.chart.config.options.plugins.title = chartData.options.plugins.title;
|
|
10421
10188
|
}
|
|
10422
10189
|
}
|
|
10190
|
+
else {
|
|
10191
|
+
this.chart.data.datasets = [];
|
|
10192
|
+
}
|
|
10193
|
+
this.chart.config.options = chartData.options;
|
|
10194
|
+
this.chart.update();
|
|
10423
10195
|
}
|
|
10424
|
-
return false;
|
|
10425
|
-
}
|
|
10426
|
-
/**
|
|
10427
|
-
* Get the rectangle that is tangent to a circle at a given angle.
|
|
10428
|
-
*
|
|
10429
|
-
* @param angle angle between X axis and the point where the rectangle is tangent to the circle
|
|
10430
|
-
*/
|
|
10431
|
-
function getRectangleTangentToCircle(angle, radius, circleCenterX, circleCenterY, rectWidth, rectHeight) {
|
|
10432
|
-
const cos = Math.cos(angle);
|
|
10433
|
-
const sin = Math.sin(angle);
|
|
10434
|
-
// x, y are the distance from the center of the circle to the point where the rectangle is tangent to the circle
|
|
10435
|
-
const x = cos * radius;
|
|
10436
|
-
const y = sin * radius;
|
|
10437
|
-
// x2, y2 are the distance from the point the rectangle is tangent to the circle to the bottom left corner of the rectangle
|
|
10438
|
-
const x2 = sin * (rectWidth / 2); // cos(angle + 90°) = sin(angle)
|
|
10439
|
-
const y2 = cos * (rectWidth / 2);
|
|
10440
|
-
const bottomRight = {
|
|
10441
|
-
x: x + x2 + circleCenterX,
|
|
10442
|
-
y: circleCenterY - (y - y2),
|
|
10443
|
-
};
|
|
10444
|
-
const bottomLeft = {
|
|
10445
|
-
x: x - x2 + circleCenterX,
|
|
10446
|
-
y: circleCenterY - (y + y2),
|
|
10447
|
-
};
|
|
10448
|
-
// Same as above but for the top corners of the rectangle (radius + rectangle height instead of radius)
|
|
10449
|
-
const xp = cos * (radius + rectHeight);
|
|
10450
|
-
const yp = sin * (radius + rectHeight);
|
|
10451
|
-
const topLeft = {
|
|
10452
|
-
x: xp - x2 + circleCenterX,
|
|
10453
|
-
y: circleCenterY - (yp + y2),
|
|
10454
|
-
};
|
|
10455
|
-
const topRight = {
|
|
10456
|
-
x: xp + x2 + circleCenterX,
|
|
10457
|
-
y: circleCenterY - (yp - y2),
|
|
10458
|
-
};
|
|
10459
|
-
return { bottomLeft, bottomRight, topRight, topLeft };
|
|
10460
10196
|
}
|
|
10461
10197
|
|
|
10462
10198
|
/**
|
|
@@ -11038,155 +10774,6 @@ class ScorecardChartConfigBuilder {
|
|
|
11038
10774
|
}
|
|
11039
10775
|
}
|
|
11040
10776
|
|
|
11041
|
-
const CHART_COMMON_OPTIONS = {
|
|
11042
|
-
// https://www.chartjs.org/docs/latest/general/responsive.html
|
|
11043
|
-
responsive: true, // will resize when its container is resized
|
|
11044
|
-
maintainAspectRatio: false, // doesn't maintain the aspect ratio (width/height =2 by default) so the user has the choice of the exact layout
|
|
11045
|
-
elements: {
|
|
11046
|
-
line: {
|
|
11047
|
-
fill: false, // do not fill the area under line charts
|
|
11048
|
-
},
|
|
11049
|
-
point: {
|
|
11050
|
-
hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
|
|
11051
|
-
},
|
|
11052
|
-
},
|
|
11053
|
-
animation: false,
|
|
11054
|
-
};
|
|
11055
|
-
function truncateLabel(label) {
|
|
11056
|
-
if (!label) {
|
|
11057
|
-
return "";
|
|
11058
|
-
}
|
|
11059
|
-
if (label.length > MAX_CHAR_LABEL) {
|
|
11060
|
-
return label.substring(0, MAX_CHAR_LABEL) + "…";
|
|
11061
|
-
}
|
|
11062
|
-
return label;
|
|
11063
|
-
}
|
|
11064
|
-
function chartToImage(runtime, figure, type) {
|
|
11065
|
-
// wrap the canvas in a div with a fixed size because chart.js would
|
|
11066
|
-
// fill the whole page otherwise
|
|
11067
|
-
const div = document.createElement("div");
|
|
11068
|
-
div.style.width = `${figure.width}px`;
|
|
11069
|
-
div.style.height = `${figure.height}px`;
|
|
11070
|
-
const canvas = document.createElement("canvas");
|
|
11071
|
-
div.append(canvas);
|
|
11072
|
-
canvas.setAttribute("width", figure.width.toString());
|
|
11073
|
-
canvas.setAttribute("height", figure.height.toString());
|
|
11074
|
-
// we have to add the canvas to the DOM otherwise it won't be rendered
|
|
11075
|
-
document.body.append(div);
|
|
11076
|
-
if ("chartJsConfig" in runtime) {
|
|
11077
|
-
const config = deepCopy(runtime.chartJsConfig);
|
|
11078
|
-
config.plugins = [backgroundColorChartJSPlugin];
|
|
11079
|
-
const Chart = getChartJSConstructor();
|
|
11080
|
-
const chart = new Chart(canvas, config);
|
|
11081
|
-
const imgContent = chart.toBase64Image();
|
|
11082
|
-
chart.destroy();
|
|
11083
|
-
div.remove();
|
|
11084
|
-
return imgContent;
|
|
11085
|
-
}
|
|
11086
|
-
else if (type === "scorecard") {
|
|
11087
|
-
const design = getScorecardConfiguration(figure, runtime);
|
|
11088
|
-
drawScoreChart(design, canvas);
|
|
11089
|
-
const imgContent = canvas.toDataURL();
|
|
11090
|
-
div.remove();
|
|
11091
|
-
return imgContent;
|
|
11092
|
-
}
|
|
11093
|
-
else if (type === "gauge") {
|
|
11094
|
-
drawGaugeChart(canvas, runtime);
|
|
11095
|
-
const imgContent = canvas.toDataURL();
|
|
11096
|
-
div.remove();
|
|
11097
|
-
return imgContent;
|
|
11098
|
-
}
|
|
11099
|
-
return undefined;
|
|
11100
|
-
}
|
|
11101
|
-
/**
|
|
11102
|
-
* Custom chart.js plugin to set the background color of the canvas
|
|
11103
|
-
* https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md
|
|
11104
|
-
*/
|
|
11105
|
-
const backgroundColorChartJSPlugin = {
|
|
11106
|
-
id: "customCanvasBackgroundColor",
|
|
11107
|
-
beforeDraw: (chart) => {
|
|
11108
|
-
const { ctx } = chart;
|
|
11109
|
-
ctx.save();
|
|
11110
|
-
ctx.globalCompositeOperation = "destination-over";
|
|
11111
|
-
ctx.fillStyle = "#ffffff";
|
|
11112
|
-
ctx.fillRect(0, 0, chart.width, chart.height);
|
|
11113
|
-
ctx.restore();
|
|
11114
|
-
},
|
|
11115
|
-
};
|
|
11116
|
-
/** Return window.Chart, making sure all our extensions are loaded in ChartJS */
|
|
11117
|
-
function getChartJSConstructor() {
|
|
11118
|
-
if (window.Chart && !window.Chart?.registry.plugins.get("chartShowValuesPlugin")) {
|
|
11119
|
-
window.Chart.register(chartShowValuesPlugin);
|
|
11120
|
-
window.Chart.register(waterfallLinesPlugin);
|
|
11121
|
-
}
|
|
11122
|
-
return window.Chart;
|
|
11123
|
-
}
|
|
11124
|
-
|
|
11125
|
-
class ChartJsComponent extends Component {
|
|
11126
|
-
static template = "o-spreadsheet-ChartJsComponent";
|
|
11127
|
-
static props = {
|
|
11128
|
-
figure: Object,
|
|
11129
|
-
};
|
|
11130
|
-
canvas = useRef("graphContainer");
|
|
11131
|
-
chart;
|
|
11132
|
-
currentRuntime;
|
|
11133
|
-
get background() {
|
|
11134
|
-
return this.chartRuntime.background;
|
|
11135
|
-
}
|
|
11136
|
-
get canvasStyle() {
|
|
11137
|
-
return `background-color: ${this.background}`;
|
|
11138
|
-
}
|
|
11139
|
-
get chartRuntime() {
|
|
11140
|
-
const runtime = this.env.model.getters.getChartRuntime(this.props.figure.id);
|
|
11141
|
-
if (!("chartJsConfig" in runtime)) {
|
|
11142
|
-
throw new Error("Unsupported chart runtime");
|
|
11143
|
-
}
|
|
11144
|
-
return runtime;
|
|
11145
|
-
}
|
|
11146
|
-
setup() {
|
|
11147
|
-
onMounted(() => {
|
|
11148
|
-
const runtime = this.chartRuntime;
|
|
11149
|
-
this.currentRuntime = runtime;
|
|
11150
|
-
// Note: chartJS modify the runtime in place, so it's important to give it a copy
|
|
11151
|
-
this.createChart(deepCopy(runtime.chartJsConfig));
|
|
11152
|
-
});
|
|
11153
|
-
onWillUnmount(() => this.chart?.destroy());
|
|
11154
|
-
useEffect(() => {
|
|
11155
|
-
const runtime = this.chartRuntime;
|
|
11156
|
-
if (runtime !== this.currentRuntime) {
|
|
11157
|
-
if (runtime.chartJsConfig.type !== this.currentRuntime.chartJsConfig.type) {
|
|
11158
|
-
this.chart?.destroy();
|
|
11159
|
-
this.createChart(deepCopy(runtime.chartJsConfig));
|
|
11160
|
-
}
|
|
11161
|
-
else {
|
|
11162
|
-
this.updateChartJs(deepCopy(runtime));
|
|
11163
|
-
}
|
|
11164
|
-
this.currentRuntime = runtime;
|
|
11165
|
-
}
|
|
11166
|
-
});
|
|
11167
|
-
}
|
|
11168
|
-
createChart(chartData) {
|
|
11169
|
-
const canvas = this.canvas.el;
|
|
11170
|
-
const ctx = canvas.getContext("2d");
|
|
11171
|
-
const Chart = getChartJSConstructor();
|
|
11172
|
-
this.chart = new Chart(ctx, chartData);
|
|
11173
|
-
}
|
|
11174
|
-
updateChartJs(chartRuntime) {
|
|
11175
|
-
const chartData = chartRuntime.chartJsConfig;
|
|
11176
|
-
if (chartData.data && chartData.data.datasets) {
|
|
11177
|
-
this.chart.data = chartData.data;
|
|
11178
|
-
if (chartData.options?.plugins?.title) {
|
|
11179
|
-
this.chart.config.options.plugins.title = chartData.options.plugins.title;
|
|
11180
|
-
}
|
|
11181
|
-
}
|
|
11182
|
-
else {
|
|
11183
|
-
this.chart.data.datasets = [];
|
|
11184
|
-
}
|
|
11185
|
-
this.chart.config.options = chartData.options;
|
|
11186
|
-
this.chart.update();
|
|
11187
|
-
}
|
|
11188
|
-
}
|
|
11189
|
-
|
|
11190
10777
|
class ScorecardChart extends Component {
|
|
11191
10778
|
static template = "o-spreadsheet-ScorecardChart";
|
|
11192
10779
|
static props = {
|
|
@@ -20380,11 +19967,26 @@ const SEARCH = {
|
|
|
20380
19967
|
const _searchFor = toString(searchFor).toLowerCase();
|
|
20381
19968
|
const _textToSearch = toString(textToSearch).toLowerCase();
|
|
20382
19969
|
const _startingAt = toNumber(startingAt, this.locale);
|
|
20383
|
-
|
|
20384
|
-
|
|
19970
|
+
if (_textToSearch === "") {
|
|
19971
|
+
return {
|
|
19972
|
+
value: CellErrorType.GenericError,
|
|
19973
|
+
message: _t("The text_to_search must be non-empty."),
|
|
19974
|
+
};
|
|
19975
|
+
}
|
|
19976
|
+
if (_startingAt < 1) {
|
|
19977
|
+
return {
|
|
19978
|
+
value: CellErrorType.GenericError,
|
|
19979
|
+
message: _t("The starting_at (%s) must be greater than or equal to 1.", _startingAt),
|
|
19980
|
+
};
|
|
19981
|
+
}
|
|
20385
19982
|
const result = _textToSearch.indexOf(_searchFor, _startingAt - 1);
|
|
20386
|
-
|
|
20387
|
-
|
|
19983
|
+
if (result === -1) {
|
|
19984
|
+
return {
|
|
19985
|
+
value: CellErrorType.GenericError,
|
|
19986
|
+
message: _t("In [[FUNCTION_NAME]] evaluation, cannot find '%s' within '%s'.", _searchFor, _textToSearch),
|
|
19987
|
+
};
|
|
19988
|
+
}
|
|
19989
|
+
return { value: result + 1 };
|
|
20388
19990
|
},
|
|
20389
19991
|
isExported: true,
|
|
20390
19992
|
};
|
|
@@ -21719,11 +21321,14 @@ function compileTokens(tokens) {
|
|
|
21719
21321
|
}
|
|
21720
21322
|
}
|
|
21721
21323
|
function compileTokensOrThrow(tokens) {
|
|
21722
|
-
const { dependencies,
|
|
21723
|
-
const cacheKey = compilationCacheKey(tokens
|
|
21324
|
+
const { dependencies, literalValues, symbols } = formulaArguments(tokens);
|
|
21325
|
+
const cacheKey = compilationCacheKey(tokens);
|
|
21724
21326
|
if (!functionCache[cacheKey]) {
|
|
21725
21327
|
const ast = parseTokens([...tokens]);
|
|
21726
21328
|
const scope = new Scope();
|
|
21329
|
+
let stringCount = 0;
|
|
21330
|
+
let numberCount = 0;
|
|
21331
|
+
let dependencyCount = 0;
|
|
21727
21332
|
if (ast.type === "BIN_OPERATION" && ast.value === ":") {
|
|
21728
21333
|
throw new BadExpressionError(_t("Invalid formula"));
|
|
21729
21334
|
}
|
|
@@ -21797,16 +21402,15 @@ function compileTokensOrThrow(tokens) {
|
|
|
21797
21402
|
case "BOOLEAN":
|
|
21798
21403
|
return code.return(`{ value: ${ast.value} }`);
|
|
21799
21404
|
case "NUMBER":
|
|
21800
|
-
return code.return(`
|
|
21405
|
+
return code.return(`this.literalValues.numbers[${numberCount++}]`);
|
|
21801
21406
|
case "STRING":
|
|
21802
|
-
return code.return(`
|
|
21407
|
+
return code.return(`this.literalValues.strings[${stringCount++}]`);
|
|
21803
21408
|
case "REFERENCE":
|
|
21804
|
-
const referenceIndex = dependencies.indexOf(ast.value);
|
|
21805
21409
|
if ((!isMeta && ast.value.includes(":")) || hasRange) {
|
|
21806
|
-
return code.return(`range(deps[${
|
|
21410
|
+
return code.return(`range(deps[${dependencyCount++}])`);
|
|
21807
21411
|
}
|
|
21808
21412
|
else {
|
|
21809
|
-
return code.return(`ref(deps[${
|
|
21413
|
+
return code.return(`ref(deps[${dependencyCount++}], ${isMeta ? "true" : "false"})`);
|
|
21810
21414
|
}
|
|
21811
21415
|
case "FUNCALL":
|
|
21812
21416
|
const args = compileFunctionArgs(ast).map((arg) => arg.assignResultToVariable());
|
|
@@ -21838,7 +21442,7 @@ function compileTokensOrThrow(tokens) {
|
|
|
21838
21442
|
const compiledFormula = {
|
|
21839
21443
|
execute: functionCache[cacheKey],
|
|
21840
21444
|
dependencies,
|
|
21841
|
-
|
|
21445
|
+
literalValues,
|
|
21842
21446
|
symbols,
|
|
21843
21447
|
tokens,
|
|
21844
21448
|
isBadExpression: false,
|
|
@@ -21851,33 +21455,31 @@ function compileTokensOrThrow(tokens) {
|
|
|
21851
21455
|
* References, numbers and strings are replaced with placeholders because
|
|
21852
21456
|
* the compiled formula does not depend on their actual value.
|
|
21853
21457
|
* Both `=A1+1+"2"` and `=A2+2+"3"` are compiled to the exact same function.
|
|
21854
|
-
*
|
|
21855
21458
|
* Spaces are also ignored to compute the cache key.
|
|
21856
21459
|
*
|
|
21857
|
-
* A formula `=A1+A2+SUM(2, 2, "2")` have the cache key `=|
|
|
21460
|
+
* A formula `=A1+A2+SUM(2, 2, "2")` have the cache key `=|C|+|C|+SUM(|N|,|N|,|S|)`
|
|
21858
21461
|
*/
|
|
21859
|
-
function compilationCacheKey(tokens
|
|
21462
|
+
function compilationCacheKey(tokens) {
|
|
21860
21463
|
let cacheKey = "";
|
|
21861
21464
|
for (const token of tokens) {
|
|
21862
21465
|
switch (token.type) {
|
|
21863
21466
|
case "STRING":
|
|
21864
|
-
|
|
21865
|
-
cacheKey += `|S${constantValues.strings.indexOf(value)}|`;
|
|
21467
|
+
cacheKey += "|S|";
|
|
21866
21468
|
break;
|
|
21867
21469
|
case "NUMBER":
|
|
21868
|
-
cacheKey +=
|
|
21470
|
+
cacheKey += "|N|";
|
|
21869
21471
|
break;
|
|
21870
21472
|
case "REFERENCE":
|
|
21871
21473
|
case "INVALID_REFERENCE":
|
|
21872
21474
|
if (token.value.includes(":")) {
|
|
21873
|
-
cacheKey +=
|
|
21475
|
+
cacheKey += "|R|";
|
|
21874
21476
|
}
|
|
21875
21477
|
else {
|
|
21876
|
-
cacheKey +=
|
|
21478
|
+
cacheKey += "|C|";
|
|
21877
21479
|
}
|
|
21878
21480
|
break;
|
|
21879
21481
|
case "SPACE":
|
|
21880
|
-
|
|
21482
|
+
// ignore spaces
|
|
21881
21483
|
break;
|
|
21882
21484
|
default:
|
|
21883
21485
|
cacheKey += token.value;
|
|
@@ -21890,7 +21492,7 @@ function compilationCacheKey(tokens, dependencies, constantValues, symbols) {
|
|
|
21890
21492
|
* Return formula arguments which are references, strings and numbers.
|
|
21891
21493
|
*/
|
|
21892
21494
|
function formulaArguments(tokens) {
|
|
21893
|
-
const
|
|
21495
|
+
const literalValues = {
|
|
21894
21496
|
numbers: [],
|
|
21895
21497
|
strings: [],
|
|
21896
21498
|
};
|
|
@@ -21904,15 +21506,11 @@ function formulaArguments(tokens) {
|
|
|
21904
21506
|
break;
|
|
21905
21507
|
case "STRING":
|
|
21906
21508
|
const value = removeStringQuotes(token.value);
|
|
21907
|
-
|
|
21908
|
-
constantValues.strings.push(value);
|
|
21909
|
-
}
|
|
21509
|
+
literalValues.strings.push({ value });
|
|
21910
21510
|
break;
|
|
21911
21511
|
case "NUMBER": {
|
|
21912
21512
|
const value = parseNumber(token.value, DEFAULT_LOCALE);
|
|
21913
|
-
|
|
21914
|
-
constantValues.numbers.push(value);
|
|
21915
|
-
}
|
|
21513
|
+
literalValues.numbers.push({ value });
|
|
21916
21514
|
break;
|
|
21917
21515
|
}
|
|
21918
21516
|
case "SYMBOL": {
|
|
@@ -21923,7 +21521,7 @@ function formulaArguments(tokens) {
|
|
|
21923
21521
|
}
|
|
21924
21522
|
return {
|
|
21925
21523
|
dependencies,
|
|
21926
|
-
|
|
21524
|
+
literalValues,
|
|
21927
21525
|
symbols,
|
|
21928
21526
|
};
|
|
21929
21527
|
}
|
|
@@ -22773,6 +22371,343 @@ function getDateIntervals(dates) {
|
|
|
22773
22371
|
|
|
22774
22372
|
const cellPopoverRegistry = new Registry();
|
|
22775
22373
|
|
|
22374
|
+
const GAUGE_PADDING_SIDE = 30;
|
|
22375
|
+
const GAUGE_PADDING_TOP = 10;
|
|
22376
|
+
const GAUGE_PADDING_BOTTOM = 20;
|
|
22377
|
+
const GAUGE_LABELS_FONT_SIZE = 12;
|
|
22378
|
+
const GAUGE_DEFAULT_VALUE_FONT_SIZE = 80;
|
|
22379
|
+
const GAUGE_BACKGROUND_COLOR = "#F3F2F1";
|
|
22380
|
+
const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
|
|
22381
|
+
const GAUGE_TITLE_SECTION_HEIGHT = 25;
|
|
22382
|
+
function drawGaugeChart(canvas, runtime) {
|
|
22383
|
+
const canvasBoundingRect = canvas.getBoundingClientRect();
|
|
22384
|
+
canvas.width = canvasBoundingRect.width;
|
|
22385
|
+
canvas.height = canvasBoundingRect.height;
|
|
22386
|
+
const ctx = canvas.getContext("2d");
|
|
22387
|
+
const config = getGaugeRenderingConfig(canvasBoundingRect, runtime, ctx);
|
|
22388
|
+
drawBackground(ctx, config);
|
|
22389
|
+
drawGauge(ctx, config);
|
|
22390
|
+
drawInflectionValues(ctx, config);
|
|
22391
|
+
drawLabels(ctx, config);
|
|
22392
|
+
drawTitle(ctx, config);
|
|
22393
|
+
}
|
|
22394
|
+
function drawGauge(ctx, config) {
|
|
22395
|
+
ctx.save();
|
|
22396
|
+
const gauge = config.gauge;
|
|
22397
|
+
const arcCenterX = gauge.rect.x + gauge.rect.width / 2;
|
|
22398
|
+
const arcCenterY = gauge.rect.y + gauge.rect.height;
|
|
22399
|
+
const arcRadius = gauge.rect.height - gauge.arcWidth / 2;
|
|
22400
|
+
if (arcRadius < 0) {
|
|
22401
|
+
return;
|
|
22402
|
+
}
|
|
22403
|
+
const gaugeAngle = gauge.percentage === 1 ? 0 : Math.PI * (1 + gauge.percentage);
|
|
22404
|
+
// Gauge background
|
|
22405
|
+
ctx.strokeStyle = GAUGE_BACKGROUND_COLOR;
|
|
22406
|
+
ctx.beginPath();
|
|
22407
|
+
ctx.lineWidth = gauge.arcWidth;
|
|
22408
|
+
ctx.arc(arcCenterX, arcCenterY, arcRadius, gaugeAngle, 0);
|
|
22409
|
+
ctx.stroke();
|
|
22410
|
+
// Gauge value
|
|
22411
|
+
ctx.strokeStyle = gauge.color;
|
|
22412
|
+
ctx.beginPath();
|
|
22413
|
+
ctx.arc(arcCenterX, arcCenterY, arcRadius, Math.PI, gaugeAngle);
|
|
22414
|
+
ctx.stroke();
|
|
22415
|
+
ctx.restore();
|
|
22416
|
+
}
|
|
22417
|
+
function drawBackground(ctx, config) {
|
|
22418
|
+
ctx.save();
|
|
22419
|
+
ctx.fillStyle = config.backgroundColor;
|
|
22420
|
+
ctx.fillRect(0, 0, config.width, config.height);
|
|
22421
|
+
ctx.restore();
|
|
22422
|
+
}
|
|
22423
|
+
function drawLabels(ctx, config) {
|
|
22424
|
+
for (const label of [config.minLabel, config.maxLabel, config.gaugeValue]) {
|
|
22425
|
+
ctx.save();
|
|
22426
|
+
ctx.textAlign = "center";
|
|
22427
|
+
ctx.fillStyle = label.color;
|
|
22428
|
+
ctx.font = `${label.fontSize}px ${DEFAULT_FONT}`;
|
|
22429
|
+
ctx.fillText(label.label, label.textPosition.x, label.textPosition.y);
|
|
22430
|
+
ctx.restore();
|
|
22431
|
+
}
|
|
22432
|
+
}
|
|
22433
|
+
function drawInflectionValues(ctx, config) {
|
|
22434
|
+
const { x: rectX, y: rectY, width, height } = config.gauge.rect;
|
|
22435
|
+
for (const inflectionValue of config.inflectionValues) {
|
|
22436
|
+
ctx.save();
|
|
22437
|
+
ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment
|
|
22438
|
+
ctx.rotate(Math.PI / 2 - inflectionValue.rotation);
|
|
22439
|
+
ctx.lineWidth = 2;
|
|
22440
|
+
ctx.strokeStyle = chartMutedFontColor(config.backgroundColor) + "aa";
|
|
22441
|
+
ctx.beginPath();
|
|
22442
|
+
ctx.moveTo(0, -(height - config.gauge.arcWidth));
|
|
22443
|
+
ctx.lineTo(0, -height - 3);
|
|
22444
|
+
ctx.stroke();
|
|
22445
|
+
ctx.textAlign = "center";
|
|
22446
|
+
ctx.font = `${inflectionValue.fontSize}px ${DEFAULT_FONT}`;
|
|
22447
|
+
ctx.fillStyle = inflectionValue.color;
|
|
22448
|
+
const textY = -height - GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN - inflectionValue.offset;
|
|
22449
|
+
ctx.fillText(inflectionValue.label, 0, textY);
|
|
22450
|
+
ctx.restore();
|
|
22451
|
+
}
|
|
22452
|
+
}
|
|
22453
|
+
function drawTitle(ctx, config) {
|
|
22454
|
+
ctx.save();
|
|
22455
|
+
const title = config.title;
|
|
22456
|
+
ctx.font = getDefaultContextFont(title.fontSize, title.bold, title.italic);
|
|
22457
|
+
ctx.textBaseline = "middle";
|
|
22458
|
+
ctx.fillStyle = title.color;
|
|
22459
|
+
ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
|
|
22460
|
+
ctx.restore();
|
|
22461
|
+
}
|
|
22462
|
+
function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
|
|
22463
|
+
const maxValue = runtime.maxValue;
|
|
22464
|
+
const minValue = runtime.minValue;
|
|
22465
|
+
const gaugeValue = runtime.gaugeValue;
|
|
22466
|
+
const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
|
|
22467
|
+
const gaugeArcWidth = gaugeRect.width / 6;
|
|
22468
|
+
const gaugePercentage = gaugeValue
|
|
22469
|
+
? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
|
|
22470
|
+
: 0;
|
|
22471
|
+
const gaugeValuePosition = {
|
|
22472
|
+
x: boundingRect.width / 2,
|
|
22473
|
+
y: gaugeRect.y + gaugeRect.height - gaugeRect.height / 12,
|
|
22474
|
+
};
|
|
22475
|
+
let gaugeValueFontSize = GAUGE_DEFAULT_VALUE_FONT_SIZE;
|
|
22476
|
+
// Scale down the font size if the gaugeRect is too small
|
|
22477
|
+
if (gaugeRect.height < 300) {
|
|
22478
|
+
gaugeValueFontSize = gaugeValueFontSize * (gaugeRect.height / 300);
|
|
22479
|
+
}
|
|
22480
|
+
// Scale down the font size if the text is too long
|
|
22481
|
+
const maxTextWidth = gaugeRect.width / 2;
|
|
22482
|
+
const gaugeLabel = gaugeValue?.label || "-";
|
|
22483
|
+
if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
|
|
22484
|
+
gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
|
|
22485
|
+
}
|
|
22486
|
+
const minLabelPosition = {
|
|
22487
|
+
x: gaugeRect.x + gaugeArcWidth / 2,
|
|
22488
|
+
y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
|
|
22489
|
+
};
|
|
22490
|
+
const maxLabelPosition = {
|
|
22491
|
+
x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
|
|
22492
|
+
y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
|
|
22493
|
+
};
|
|
22494
|
+
const textColor = chartMutedFontColor(runtime.background);
|
|
22495
|
+
const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);
|
|
22496
|
+
let x = 0, titleWidth = 0, titleHeight = 0;
|
|
22497
|
+
if (runtime.title.text) {
|
|
22498
|
+
({ width: titleWidth, height: titleHeight } = computeTextDimension(ctx, runtime.title.text, { fontSize: CHART_TITLE_FONT_SIZE, ...runtime.title }, "px"));
|
|
22499
|
+
}
|
|
22500
|
+
switch (runtime.title.align) {
|
|
22501
|
+
case "right":
|
|
22502
|
+
x = boundingRect.width - titleWidth - CHART_PADDING$1;
|
|
22503
|
+
break;
|
|
22504
|
+
case "center":
|
|
22505
|
+
x = (boundingRect.width - titleWidth) / 2;
|
|
22506
|
+
break;
|
|
22507
|
+
case "left":
|
|
22508
|
+
default:
|
|
22509
|
+
x = CHART_PADDING$1;
|
|
22510
|
+
break;
|
|
22511
|
+
}
|
|
22512
|
+
return {
|
|
22513
|
+
width: boundingRect.width,
|
|
22514
|
+
height: boundingRect.height,
|
|
22515
|
+
title: {
|
|
22516
|
+
label: runtime.title.text ?? "",
|
|
22517
|
+
fontSize: runtime.title.fontSize ?? CHART_TITLE_FONT_SIZE,
|
|
22518
|
+
textPosition: {
|
|
22519
|
+
x,
|
|
22520
|
+
y: CHART_PADDING_TOP + titleHeight / 2,
|
|
22521
|
+
},
|
|
22522
|
+
color: runtime.title.color ?? textColor,
|
|
22523
|
+
bold: runtime.title.bold,
|
|
22524
|
+
italic: runtime.title.italic,
|
|
22525
|
+
},
|
|
22526
|
+
backgroundColor: runtime.background,
|
|
22527
|
+
gauge: {
|
|
22528
|
+
rect: gaugeRect,
|
|
22529
|
+
arcWidth: gaugeArcWidth,
|
|
22530
|
+
percentage: clip(gaugePercentage, 0, 1),
|
|
22531
|
+
color: getGaugeColor(runtime),
|
|
22532
|
+
},
|
|
22533
|
+
inflectionValues,
|
|
22534
|
+
gaugeValue: {
|
|
22535
|
+
label: gaugeLabel,
|
|
22536
|
+
textPosition: gaugeValuePosition,
|
|
22537
|
+
fontSize: gaugeValueFontSize,
|
|
22538
|
+
color: textColor,
|
|
22539
|
+
},
|
|
22540
|
+
minLabel: {
|
|
22541
|
+
label: runtime.minValue.label,
|
|
22542
|
+
textPosition: minLabelPosition,
|
|
22543
|
+
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
22544
|
+
color: textColor,
|
|
22545
|
+
},
|
|
22546
|
+
maxLabel: {
|
|
22547
|
+
label: runtime.maxValue.label,
|
|
22548
|
+
textPosition: maxLabelPosition,
|
|
22549
|
+
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
22550
|
+
color: textColor,
|
|
22551
|
+
},
|
|
22552
|
+
};
|
|
22553
|
+
}
|
|
22554
|
+
/**
|
|
22555
|
+
* Get the rectangle in which the gauge will be drawn, based on the bounding rectangle of the canvas and leaving
|
|
22556
|
+
* space for the title and labels.
|
|
22557
|
+
*/
|
|
22558
|
+
function getGaugeRect(boundingRect, title) {
|
|
22559
|
+
const titleHeight = title ? GAUGE_TITLE_SECTION_HEIGHT : 0;
|
|
22560
|
+
const drawHeight = boundingRect.height - GAUGE_PADDING_BOTTOM - titleHeight - GAUGE_PADDING_TOP;
|
|
22561
|
+
const drawWidth = boundingRect.width - GAUGE_PADDING_SIDE * 2;
|
|
22562
|
+
let gaugeWidth;
|
|
22563
|
+
let gaugeHeight;
|
|
22564
|
+
if (drawWidth > 2 * drawHeight) {
|
|
22565
|
+
gaugeWidth = 2 * drawHeight;
|
|
22566
|
+
gaugeHeight = drawHeight;
|
|
22567
|
+
}
|
|
22568
|
+
else {
|
|
22569
|
+
gaugeWidth = drawWidth;
|
|
22570
|
+
gaugeHeight = drawWidth / 2;
|
|
22571
|
+
}
|
|
22572
|
+
const gaugeX = GAUGE_PADDING_SIDE + (drawWidth - gaugeWidth) / 2;
|
|
22573
|
+
const gaugeY = titleHeight + GAUGE_PADDING_TOP + (drawHeight - gaugeHeight) / 2;
|
|
22574
|
+
return {
|
|
22575
|
+
x: gaugeX,
|
|
22576
|
+
y: gaugeY,
|
|
22577
|
+
width: gaugeWidth,
|
|
22578
|
+
height: gaugeHeight,
|
|
22579
|
+
};
|
|
22580
|
+
}
|
|
22581
|
+
/**
|
|
22582
|
+
* 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).
|
|
22583
|
+
*
|
|
22584
|
+
* Also compute an offset for the text so that it doesn't overlap with other text.
|
|
22585
|
+
*/
|
|
22586
|
+
function getInflectionValues(runtime, gaugeRect, textColor, ctx) {
|
|
22587
|
+
const maxValue = runtime.maxValue;
|
|
22588
|
+
const minValue = runtime.minValue;
|
|
22589
|
+
const gaugeCircleCenter = {
|
|
22590
|
+
x: gaugeRect.x + gaugeRect.width / 2,
|
|
22591
|
+
y: gaugeRect.y + gaugeRect.height,
|
|
22592
|
+
};
|
|
22593
|
+
const textStyle = { fontSize: GAUGE_LABELS_FONT_SIZE };
|
|
22594
|
+
const inflectionValues = [];
|
|
22595
|
+
const inflectionValuesTextRects = [];
|
|
22596
|
+
for (const inflectionValue of runtime.inflectionValues) {
|
|
22597
|
+
const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
|
|
22598
|
+
const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
|
|
22599
|
+
const angle = Math.PI - Math.PI * percentage;
|
|
22600
|
+
const textRect = getRectangleTangentToCircle(angle, // angle between X axis and the point where the rectangle is tangent to the circle
|
|
22601
|
+
gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, // radius of the gauge circle + margin below text
|
|
22602
|
+
gaugeCircleCenter.x, // center of the gauge circle
|
|
22603
|
+
gaugeCircleCenter.y, // center of the gauge circle
|
|
22604
|
+
labelWidth + 2, // width of the text + some margin
|
|
22605
|
+
GAUGE_LABELS_FONT_SIZE // height of the text
|
|
22606
|
+
);
|
|
22607
|
+
let offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect))
|
|
22608
|
+
? GAUGE_LABELS_FONT_SIZE
|
|
22609
|
+
: 0;
|
|
22610
|
+
inflectionValuesTextRects.push(textRect);
|
|
22611
|
+
inflectionValues.push({
|
|
22612
|
+
rotation: angle,
|
|
22613
|
+
label: inflectionValue.label,
|
|
22614
|
+
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
22615
|
+
color: textColor,
|
|
22616
|
+
offset,
|
|
22617
|
+
});
|
|
22618
|
+
}
|
|
22619
|
+
return inflectionValues;
|
|
22620
|
+
}
|
|
22621
|
+
function getGaugeColor(runtime) {
|
|
22622
|
+
const gaugeValue = runtime.gaugeValue?.value;
|
|
22623
|
+
if (gaugeValue === undefined) {
|
|
22624
|
+
return GAUGE_BACKGROUND_COLOR;
|
|
22625
|
+
}
|
|
22626
|
+
for (let i = 0; i < runtime.inflectionValues.length; i++) {
|
|
22627
|
+
const inflectionValue = runtime.inflectionValues[i];
|
|
22628
|
+
if (inflectionValue.operator === "<" && gaugeValue < inflectionValue.value) {
|
|
22629
|
+
return runtime.colors[i];
|
|
22630
|
+
}
|
|
22631
|
+
else if (inflectionValue.operator === "<=" && gaugeValue <= inflectionValue.value) {
|
|
22632
|
+
return runtime.colors[i];
|
|
22633
|
+
}
|
|
22634
|
+
}
|
|
22635
|
+
return runtime.colors.at(-1);
|
|
22636
|
+
}
|
|
22637
|
+
function getSegmentsOfRectangle(rectangle) {
|
|
22638
|
+
return [
|
|
22639
|
+
{ start: rectangle.topLeft, end: rectangle.topRight },
|
|
22640
|
+
{ start: rectangle.topRight, end: rectangle.bottomRight },
|
|
22641
|
+
{ start: rectangle.bottomRight, end: rectangle.bottomLeft },
|
|
22642
|
+
{ start: rectangle.bottomLeft, end: rectangle.topLeft },
|
|
22643
|
+
];
|
|
22644
|
+
}
|
|
22645
|
+
/**
|
|
22646
|
+
* Check if two segment intersect. The case where the segments are colinear (both segments on the same line)
|
|
22647
|
+
* is not handled.
|
|
22648
|
+
*/
|
|
22649
|
+
function doSegmentIntersect(segment1, segment2) {
|
|
22650
|
+
const A = segment1.start;
|
|
22651
|
+
const B = segment1.end;
|
|
22652
|
+
const C = segment2.start;
|
|
22653
|
+
const D = segment2.end;
|
|
22654
|
+
/**
|
|
22655
|
+
* Line segment intersection algorithm
|
|
22656
|
+
* https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
|
|
22657
|
+
*/
|
|
22658
|
+
function ccw(a, b, c) {
|
|
22659
|
+
return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
|
|
22660
|
+
}
|
|
22661
|
+
return ccw(A, C, D) !== ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D);
|
|
22662
|
+
}
|
|
22663
|
+
function doRectanglesIntersect(rect1, rect2) {
|
|
22664
|
+
const segments1 = getSegmentsOfRectangle(rect1);
|
|
22665
|
+
const segments2 = getSegmentsOfRectangle(rect2);
|
|
22666
|
+
for (const segment1 of segments1) {
|
|
22667
|
+
for (const segment2 of segments2) {
|
|
22668
|
+
if (doSegmentIntersect(segment1, segment2)) {
|
|
22669
|
+
return true;
|
|
22670
|
+
}
|
|
22671
|
+
}
|
|
22672
|
+
}
|
|
22673
|
+
return false;
|
|
22674
|
+
}
|
|
22675
|
+
/**
|
|
22676
|
+
* Get the rectangle that is tangent to a circle at a given angle.
|
|
22677
|
+
*
|
|
22678
|
+
* @param angle angle between X axis and the point where the rectangle is tangent to the circle
|
|
22679
|
+
*/
|
|
22680
|
+
function getRectangleTangentToCircle(angle, radius, circleCenterX, circleCenterY, rectWidth, rectHeight) {
|
|
22681
|
+
const cos = Math.cos(angle);
|
|
22682
|
+
const sin = Math.sin(angle);
|
|
22683
|
+
// x, y are the distance from the center of the circle to the point where the rectangle is tangent to the circle
|
|
22684
|
+
const x = cos * radius;
|
|
22685
|
+
const y = sin * radius;
|
|
22686
|
+
// x2, y2 are the distance from the point the rectangle is tangent to the circle to the bottom left corner of the rectangle
|
|
22687
|
+
const x2 = sin * (rectWidth / 2); // cos(angle + 90°) = sin(angle)
|
|
22688
|
+
const y2 = cos * (rectWidth / 2);
|
|
22689
|
+
const bottomRight = {
|
|
22690
|
+
x: x + x2 + circleCenterX,
|
|
22691
|
+
y: circleCenterY - (y - y2),
|
|
22692
|
+
};
|
|
22693
|
+
const bottomLeft = {
|
|
22694
|
+
x: x - x2 + circleCenterX,
|
|
22695
|
+
y: circleCenterY - (y + y2),
|
|
22696
|
+
};
|
|
22697
|
+
// Same as above but for the top corners of the rectangle (radius + rectangle height instead of radius)
|
|
22698
|
+
const xp = cos * (radius + rectHeight);
|
|
22699
|
+
const yp = sin * (radius + rectHeight);
|
|
22700
|
+
const topLeft = {
|
|
22701
|
+
x: xp - x2 + circleCenterX,
|
|
22702
|
+
y: circleCenterY - (yp + y2),
|
|
22703
|
+
};
|
|
22704
|
+
const topRight = {
|
|
22705
|
+
x: xp + x2 + circleCenterX,
|
|
22706
|
+
y: circleCenterY - (yp - y2),
|
|
22707
|
+
};
|
|
22708
|
+
return { bottomLeft, bottomRight, topRight, topLeft };
|
|
22709
|
+
}
|
|
22710
|
+
|
|
22776
22711
|
class GaugeChartComponent extends Component {
|
|
22777
22712
|
static template = "o-spreadsheet-GaugeChartComponent";
|
|
22778
22713
|
canvas = useRef("chartContainer");
|
|
@@ -22805,6 +22740,82 @@ function toXlsxHexColor(color) {
|
|
|
22805
22740
|
return color;
|
|
22806
22741
|
}
|
|
22807
22742
|
|
|
22743
|
+
const CHART_COMMON_OPTIONS = {
|
|
22744
|
+
// https://www.chartjs.org/docs/latest/general/responsive.html
|
|
22745
|
+
responsive: true, // will resize when its container is resized
|
|
22746
|
+
maintainAspectRatio: false, // doesn't maintain the aspect ratio (width/height =2 by default) so the user has the choice of the exact layout
|
|
22747
|
+
elements: {
|
|
22748
|
+
line: {
|
|
22749
|
+
fill: false, // do not fill the area under line charts
|
|
22750
|
+
},
|
|
22751
|
+
point: {
|
|
22752
|
+
hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
|
|
22753
|
+
},
|
|
22754
|
+
},
|
|
22755
|
+
animation: false,
|
|
22756
|
+
};
|
|
22757
|
+
function truncateLabel(label) {
|
|
22758
|
+
if (!label) {
|
|
22759
|
+
return "";
|
|
22760
|
+
}
|
|
22761
|
+
if (label.length > MAX_CHAR_LABEL) {
|
|
22762
|
+
return label.substring(0, MAX_CHAR_LABEL) + "…";
|
|
22763
|
+
}
|
|
22764
|
+
return label;
|
|
22765
|
+
}
|
|
22766
|
+
function chartToImage(runtime, figure, type) {
|
|
22767
|
+
// wrap the canvas in a div with a fixed size because chart.js would
|
|
22768
|
+
// fill the whole page otherwise
|
|
22769
|
+
const div = document.createElement("div");
|
|
22770
|
+
div.style.width = `${figure.width}px`;
|
|
22771
|
+
div.style.height = `${figure.height}px`;
|
|
22772
|
+
const canvas = document.createElement("canvas");
|
|
22773
|
+
div.append(canvas);
|
|
22774
|
+
canvas.setAttribute("width", figure.width.toString());
|
|
22775
|
+
canvas.setAttribute("height", figure.height.toString());
|
|
22776
|
+
// we have to add the canvas to the DOM otherwise it won't be rendered
|
|
22777
|
+
document.body.append(div);
|
|
22778
|
+
if ("chartJsConfig" in runtime) {
|
|
22779
|
+
const config = deepCopy(runtime.chartJsConfig);
|
|
22780
|
+
config.plugins = [backgroundColorChartJSPlugin];
|
|
22781
|
+
const Chart = getChartJSConstructor();
|
|
22782
|
+
const chart = new Chart(canvas, config);
|
|
22783
|
+
const imgContent = chart.toBase64Image();
|
|
22784
|
+
chart.destroy();
|
|
22785
|
+
div.remove();
|
|
22786
|
+
return imgContent;
|
|
22787
|
+
}
|
|
22788
|
+
else if (type === "scorecard") {
|
|
22789
|
+
const design = getScorecardConfiguration(figure, runtime);
|
|
22790
|
+
drawScoreChart(design, canvas);
|
|
22791
|
+
const imgContent = canvas.toDataURL();
|
|
22792
|
+
div.remove();
|
|
22793
|
+
return imgContent;
|
|
22794
|
+
}
|
|
22795
|
+
else if (type === "gauge") {
|
|
22796
|
+
drawGaugeChart(canvas, runtime);
|
|
22797
|
+
const imgContent = canvas.toDataURL();
|
|
22798
|
+
div.remove();
|
|
22799
|
+
return imgContent;
|
|
22800
|
+
}
|
|
22801
|
+
return undefined;
|
|
22802
|
+
}
|
|
22803
|
+
/**
|
|
22804
|
+
* Custom chart.js plugin to set the background color of the canvas
|
|
22805
|
+
* https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md
|
|
22806
|
+
*/
|
|
22807
|
+
const backgroundColorChartJSPlugin = {
|
|
22808
|
+
id: "customCanvasBackgroundColor",
|
|
22809
|
+
beforeDraw: (chart) => {
|
|
22810
|
+
const { ctx } = chart;
|
|
22811
|
+
ctx.save();
|
|
22812
|
+
ctx.globalCompositeOperation = "destination-over";
|
|
22813
|
+
ctx.fillStyle = "#ffffff";
|
|
22814
|
+
ctx.fillRect(0, 0, chart.width, chart.height);
|
|
22815
|
+
ctx.restore();
|
|
22816
|
+
},
|
|
22817
|
+
};
|
|
22818
|
+
|
|
22808
22819
|
/**
|
|
22809
22820
|
* Represent a raw XML string
|
|
22810
22821
|
*/
|
|
@@ -22844,9 +22855,9 @@ const XLSX_CHART_TYPES = [
|
|
|
22844
22855
|
/** In XLSX color format (no #) */
|
|
22845
22856
|
const AUTO_COLOR = "000000";
|
|
22846
22857
|
const XLSX_ICONSET_MAP = {
|
|
22847
|
-
|
|
22858
|
+
arrows: "3Arrows",
|
|
22848
22859
|
smiley: "3Symbols",
|
|
22849
|
-
|
|
22860
|
+
dots: "3TrafficLights1",
|
|
22850
22861
|
};
|
|
22851
22862
|
const NAMESPACE = {
|
|
22852
22863
|
styleSheet: "http://schemas.openxmlformats.org/spreadsheetml/2006/main",
|
|
@@ -23517,6 +23528,7 @@ const ICON_SET_CONVERSION_MAP = {
|
|
|
23517
23528
|
};
|
|
23518
23529
|
/** Map between legend position in XLSX file and human readable position */
|
|
23519
23530
|
const DRAWING_LEGEND_POSITION_CONVERSION_MAP = {
|
|
23531
|
+
none: "none",
|
|
23520
23532
|
b: "bottom",
|
|
23521
23533
|
t: "top",
|
|
23522
23534
|
l: "left",
|
|
@@ -26279,7 +26291,7 @@ class XlsxChartExtractor extends XlsxBaseExtractor {
|
|
|
26279
26291
|
default: "ffffff",
|
|
26280
26292
|
}).asString(),
|
|
26281
26293
|
legendPosition: DRAWING_LEGEND_POSITION_CONVERSION_MAP[this.extractChildAttr(rootChartElement, "c:legendPos", "val", {
|
|
26282
|
-
default: "
|
|
26294
|
+
default: "none",
|
|
26283
26295
|
}).asString()],
|
|
26284
26296
|
stacked: barChartGrouping === "stacked",
|
|
26285
26297
|
fontColor: "000000",
|
|
@@ -26313,7 +26325,7 @@ class XlsxChartExtractor extends XlsxBaseExtractor {
|
|
|
26313
26325
|
default: "ffffff",
|
|
26314
26326
|
}).asString(),
|
|
26315
26327
|
legendPosition: DRAWING_LEGEND_POSITION_CONVERSION_MAP[this.extractChildAttr(chartElement, "c:legendPos", "val", {
|
|
26316
|
-
default: "
|
|
26328
|
+
default: "none",
|
|
26317
26329
|
}).asString()],
|
|
26318
26330
|
stacked: barChartGrouping === "stacked",
|
|
26319
26331
|
fontColor: "000000",
|
|
@@ -28925,7 +28937,8 @@ function getChartLabelValues(getters, dataSets, labelRange) {
|
|
|
28925
28937
|
}
|
|
28926
28938
|
}
|
|
28927
28939
|
else if (dataSets.length === 1) {
|
|
28928
|
-
|
|
28940
|
+
const dataLength = getData(getters, dataSets[0]).length;
|
|
28941
|
+
for (let i = 0; i < dataLength; i++) {
|
|
28929
28942
|
labels.formattedValues.push("");
|
|
28930
28943
|
labels.values.push("");
|
|
28931
28944
|
}
|
|
@@ -29108,7 +29121,7 @@ function getLineChartDatasets(definition, args) {
|
|
|
29108
29121
|
function getScatterChartDatasets(definition, args) {
|
|
29109
29122
|
const dataSets = getLineChartDatasets(definition, args);
|
|
29110
29123
|
for (const dataSet of dataSets) {
|
|
29111
|
-
if (dataSet.xAxisID
|
|
29124
|
+
if (!isTrendLineAxis(dataSet.xAxisID)) {
|
|
29112
29125
|
dataSet.showLine = false;
|
|
29113
29126
|
}
|
|
29114
29127
|
}
|
|
@@ -29235,7 +29248,9 @@ function getTrendingLineDataSet(dataset, config, data) {
|
|
|
29235
29248
|
const borderColor = config.color || lightenColor(rgbaToHex(defaultBorderColor), 0.5);
|
|
29236
29249
|
return {
|
|
29237
29250
|
type: "line",
|
|
29238
|
-
xAxisID:
|
|
29251
|
+
xAxisID: config.type === "trailingMovingAverage"
|
|
29252
|
+
? MOVING_AVERAGE_TREND_LINE_XAXIS_ID
|
|
29253
|
+
: TREND_LINE_XAXIS_ID,
|
|
29239
29254
|
yAxisID: dataset.yAxisID,
|
|
29240
29255
|
label: dataset.label ? _t("Trend line for %s", dataset.label) : "",
|
|
29241
29256
|
data,
|
|
@@ -29310,22 +29325,19 @@ function getPieChartLegend(definition, args) {
|
|
|
29310
29325
|
const { dataSetsValues } = args;
|
|
29311
29326
|
const dataSetsLength = Math.max(0, ...dataSetsValues.map((ds) => ds?.data?.length ?? 0));
|
|
29312
29327
|
const colors = getPieColors(new ColorGenerator(dataSetsLength), dataSetsValues);
|
|
29328
|
+
const fontColor = chartFontColor(definition.background);
|
|
29313
29329
|
return {
|
|
29314
29330
|
...getLegendDisplayOptions(definition),
|
|
29315
29331
|
labels: {
|
|
29316
|
-
color: chartFontColor(definition.background),
|
|
29317
29332
|
usePointStyle: true,
|
|
29318
|
-
|
|
29319
|
-
|
|
29320
|
-
//@ts-ignore
|
|
29321
|
-
c.data.labels.map((label, index) => ({
|
|
29322
|
-
text: label,
|
|
29333
|
+
generateLabels: (c) => c.data.labels?.map((label, index) => ({
|
|
29334
|
+
text: String(label),
|
|
29323
29335
|
strokeStyle: colors[index],
|
|
29324
29336
|
fillStyle: colors[index],
|
|
29325
29337
|
pointStyle: "rect",
|
|
29326
|
-
hidden: false,
|
|
29327
29338
|
lineWidth: 2,
|
|
29328
|
-
|
|
29339
|
+
fontColor,
|
|
29340
|
+
})) || [],
|
|
29329
29341
|
filter: (legendItem, data) => {
|
|
29330
29342
|
return "datasetIndex" in legendItem
|
|
29331
29343
|
? !data.datasets[legendItem.datasetIndex].hidden
|
|
@@ -29458,7 +29470,7 @@ function getCustomLegendLabels(fontColor, legendLabelConfig) {
|
|
|
29458
29470
|
color: fontColor,
|
|
29459
29471
|
usePointStyle: true,
|
|
29460
29472
|
generateLabels: (chart) => chart.data.datasets.map((dataset, index) => {
|
|
29461
|
-
if (dataset["xAxisID"]
|
|
29473
|
+
if (isTrendLineAxis(dataset["xAxisID"])) {
|
|
29462
29474
|
return {
|
|
29463
29475
|
text: dataset.label ?? "",
|
|
29464
29476
|
fontColor,
|
|
@@ -29516,6 +29528,11 @@ function getBarChartScales(definition, args) {
|
|
|
29516
29528
|
offset: false,
|
|
29517
29529
|
display: false,
|
|
29518
29530
|
};
|
|
29531
|
+
scales[MOVING_AVERAGE_TREND_LINE_XAXIS_ID] = {
|
|
29532
|
+
...scales.x,
|
|
29533
|
+
offset: false,
|
|
29534
|
+
display: false,
|
|
29535
|
+
};
|
|
29519
29536
|
}
|
|
29520
29537
|
return scales;
|
|
29521
29538
|
}
|
|
@@ -29549,6 +29566,10 @@ function getLineChartScales(definition, args) {
|
|
|
29549
29566
|
...scales.x,
|
|
29550
29567
|
display: false,
|
|
29551
29568
|
};
|
|
29569
|
+
scales[MOVING_AVERAGE_TREND_LINE_XAXIS_ID] = {
|
|
29570
|
+
...scales.x,
|
|
29571
|
+
display: false,
|
|
29572
|
+
};
|
|
29552
29573
|
if (axisType === "category" || axisType === "time") {
|
|
29553
29574
|
/* We add a second x axis here to draw the trend lines, with the labels length being
|
|
29554
29575
|
* set so that the second axis points match the classical x axis
|
|
@@ -29557,6 +29578,8 @@ function getLineChartScales(definition, args) {
|
|
|
29557
29578
|
scales[TREND_LINE_XAXIS_ID]["type"] = "category";
|
|
29558
29579
|
scales[TREND_LINE_XAXIS_ID]["labels"] = range(0, maxLength).map((x) => x.toString());
|
|
29559
29580
|
scales[TREND_LINE_XAXIS_ID]["offset"] = false;
|
|
29581
|
+
scales[MOVING_AVERAGE_TREND_LINE_XAXIS_ID]["type"] = "category";
|
|
29582
|
+
scales[MOVING_AVERAGE_TREND_LINE_XAXIS_ID]["offset"] = false;
|
|
29560
29583
|
}
|
|
29561
29584
|
}
|
|
29562
29585
|
return scales;
|
|
@@ -29802,9 +29825,7 @@ function getBarChartTooltip(definition, args) {
|
|
|
29802
29825
|
return {
|
|
29803
29826
|
callbacks: {
|
|
29804
29827
|
title: function (tooltipItems) {
|
|
29805
|
-
return tooltipItems.some((item) => item.dataset.xAxisID
|
|
29806
|
-
? undefined
|
|
29807
|
-
: "";
|
|
29828
|
+
return tooltipItems.some((item) => !isTrendLineAxis(item.dataset.xAxisID)) ? undefined : "";
|
|
29808
29829
|
},
|
|
29809
29830
|
label: function (tooltipItem) {
|
|
29810
29831
|
const xLabel = tooltipItem.dataset?.label || tooltipItem.label;
|
|
@@ -29827,7 +29848,7 @@ function getLineChartTooltip(definition, args) {
|
|
|
29827
29848
|
if (axisType === "linear") {
|
|
29828
29849
|
tooltip.callbacks.label = (tooltipItem) => {
|
|
29829
29850
|
const dataSetPoint = tooltipItem.parsed.y;
|
|
29830
|
-
let label = tooltipItem.dataset.xAxisID
|
|
29851
|
+
let label = isTrendLineAxis(tooltipItem.dataset.xAxisID)
|
|
29831
29852
|
? ""
|
|
29832
29853
|
: tooltipItem.parsed.x;
|
|
29833
29854
|
if (typeof label === "string" && isNumber(label, locale)) {
|
|
@@ -29852,8 +29873,7 @@ function getLineChartTooltip(definition, args) {
|
|
|
29852
29873
|
};
|
|
29853
29874
|
}
|
|
29854
29875
|
tooltip.callbacks.title = function (tooltipItems) {
|
|
29855
|
-
const displayTooltipTitle = axisType !== "linear" &&
|
|
29856
|
-
tooltipItems.some((item) => item.dataset.xAxisID !== TREND_LINE_XAXIS_ID);
|
|
29876
|
+
const displayTooltipTitle = axisType !== "linear" && tooltipItems.some((item) => !isTrendLineAxis(item.dataset.xAxisID));
|
|
29857
29877
|
return displayTooltipTitle ? undefined : "";
|
|
29858
29878
|
};
|
|
29859
29879
|
return tooltip;
|
|
@@ -34010,6 +34030,7 @@ var CHART_HELPERS = /*#__PURE__*/Object.freeze({
|
|
|
34010
34030
|
CHART_COMMON_OPTIONS: CHART_COMMON_OPTIONS,
|
|
34011
34031
|
GaugeChart: GaugeChart,
|
|
34012
34032
|
LineChart: LineChart,
|
|
34033
|
+
MOVING_AVERAGE_TREND_LINE_XAXIS_ID: MOVING_AVERAGE_TREND_LINE_XAXIS_ID,
|
|
34013
34034
|
PieChart: PieChart,
|
|
34014
34035
|
ScorecardChart: ScorecardChart$1,
|
|
34015
34036
|
TREND_LINE_XAXIS_ID: TREND_LINE_XAXIS_ID,
|
|
@@ -34034,11 +34055,11 @@ var CHART_HELPERS = /*#__PURE__*/Object.freeze({
|
|
|
34034
34055
|
drawScoreChart: drawScoreChart,
|
|
34035
34056
|
formatChartDatasetValue: formatChartDatasetValue,
|
|
34036
34057
|
formatTickValue: formatTickValue,
|
|
34037
|
-
getChartJSConstructor: getChartJSConstructor,
|
|
34038
34058
|
getChartPositionAtCenterOfViewport: getChartPositionAtCenterOfViewport,
|
|
34039
34059
|
getDefinedAxis: getDefinedAxis,
|
|
34040
34060
|
getPieColors: getPieColors,
|
|
34041
34061
|
getSmartChartDefinition: getSmartChartDefinition,
|
|
34062
|
+
isTrendLineAxis: isTrendLineAxis,
|
|
34042
34063
|
shouldRemoveFirstLabel: shouldRemoveFirstLabel,
|
|
34043
34064
|
toExcelDataset: toExcelDataset,
|
|
34044
34065
|
toExcelLabelRange: toExcelLabelRange,
|
|
@@ -36086,9 +36107,7 @@ class FormulaFingerprintStore extends SpreadsheetStore {
|
|
|
36086
36107
|
}
|
|
36087
36108
|
}
|
|
36088
36109
|
}
|
|
36089
|
-
|
|
36090
|
-
// =|N0|+|N1|+|N0| -> =|N|+|N|+|N|
|
|
36091
|
-
const normalizedFormula = cell.compiledFormula.normalizedFormula.replace(/(|\w)(\d)(|)/g, "$1$3");
|
|
36110
|
+
const normalizedFormula = cell.compiledFormula.normalizedFormula;
|
|
36092
36111
|
return hash(fingerprintVector) + normalizedFormula;
|
|
36093
36112
|
}
|
|
36094
36113
|
getLiteralFingerprint(position) {
|
|
@@ -39079,9 +39098,11 @@ class SeriesDesignEditor extends Component {
|
|
|
39079
39098
|
if (!runtime || !("chartJsConfig" in runtime)) {
|
|
39080
39099
|
return [];
|
|
39081
39100
|
}
|
|
39082
|
-
return runtime.chartJsConfig.data.datasets
|
|
39101
|
+
return runtime.chartJsConfig.data.datasets
|
|
39102
|
+
.filter((d) => !isTrendLineAxis(d["xAxisID"] ?? ""))
|
|
39103
|
+
.map((d) => d.label);
|
|
39083
39104
|
}
|
|
39084
|
-
|
|
39105
|
+
updateEditedSeries(ev) {
|
|
39085
39106
|
this.state.index = ev.target.selectedIndex;
|
|
39086
39107
|
}
|
|
39087
39108
|
updateDataSeriesColor(color) {
|
|
@@ -39094,7 +39115,7 @@ class SeriesDesignEditor extends Component {
|
|
|
39094
39115
|
};
|
|
39095
39116
|
this.props.updateChart(this.props.figureId, { dataSets });
|
|
39096
39117
|
}
|
|
39097
|
-
|
|
39118
|
+
getDataSeriesColor() {
|
|
39098
39119
|
const dataSets = this.props.definition.dataSets;
|
|
39099
39120
|
if (!dataSets?.[this.state.index])
|
|
39100
39121
|
return "";
|
|
@@ -39114,7 +39135,7 @@ class SeriesDesignEditor extends Component {
|
|
|
39114
39135
|
};
|
|
39115
39136
|
this.props.updateChart(this.props.figureId, { dataSets });
|
|
39116
39137
|
}
|
|
39117
|
-
|
|
39138
|
+
getDataSeriesLabel() {
|
|
39118
39139
|
const dataSets = this.props.definition.dataSets;
|
|
39119
39140
|
return dataSets[this.state.index]?.label || this.getDataSeries()[this.state.index];
|
|
39120
39141
|
}
|
|
@@ -39227,7 +39248,7 @@ class SeriesWithAxisDesignEditor extends Component {
|
|
|
39227
39248
|
}
|
|
39228
39249
|
this.updateTrendLineValue(index, { window });
|
|
39229
39250
|
}
|
|
39230
|
-
|
|
39251
|
+
getDataSeriesColor(index) {
|
|
39231
39252
|
const dataSets = this.props.definition.dataSets;
|
|
39232
39253
|
if (!dataSets?.[index])
|
|
39233
39254
|
return "";
|
|
@@ -39238,7 +39259,7 @@ class SeriesWithAxisDesignEditor extends Component {
|
|
|
39238
39259
|
}
|
|
39239
39260
|
getTrendLineColor(index) {
|
|
39240
39261
|
return (this.getTrendLineConfiguration(index)?.color ??
|
|
39241
|
-
setColorAlpha(this.
|
|
39262
|
+
setColorAlpha(this.getDataSeriesColor(index), 0.5));
|
|
39242
39263
|
}
|
|
39243
39264
|
updateTrendLineColor(index, color) {
|
|
39244
39265
|
this.updateTrendLineValue(index, { color });
|
|
@@ -44743,7 +44764,8 @@ css /* scss */ `
|
|
|
44743
44764
|
&.pivot-dimension-invalid {
|
|
44744
44765
|
background-color: #ffdddd;
|
|
44745
44766
|
border-color: red !important;
|
|
44746
|
-
select
|
|
44767
|
+
select,
|
|
44768
|
+
input {
|
|
44747
44769
|
background-color: #ffdddd;
|
|
44748
44770
|
}
|
|
44749
44771
|
}
|
|
@@ -46606,7 +46628,7 @@ class PivotSidePanelStore extends SpreadsheetStore {
|
|
|
46606
46628
|
this.notification.notifyUser({
|
|
46607
46629
|
type: "info",
|
|
46608
46630
|
text: _t("Pivot updates only work with dynamic pivot tables. Use %s or re-insert the static pivot from the Data menu.", pivotExample),
|
|
46609
|
-
sticky:
|
|
46631
|
+
sticky: true,
|
|
46610
46632
|
});
|
|
46611
46633
|
}
|
|
46612
46634
|
}
|
|
@@ -70321,7 +70343,9 @@ css /* scss */ `
|
|
|
70321
70343
|
border: 1px solid;
|
|
70322
70344
|
font-family: ${DEFAULT_FONT};
|
|
70323
70345
|
|
|
70324
|
-
|
|
70346
|
+
/* In readonly we always show the fx icon if the composer is empty, not matter the focus */
|
|
70347
|
+
.o-composer:empty:not(:focus):not(.active)::before,
|
|
70348
|
+
&.o-topbar-composer-readonly .o-composer:empty::before {
|
|
70325
70349
|
content: url("data:image/svg+xml,${encodeURIComponent(FX_SVG)}");
|
|
70326
70350
|
position: relative;
|
|
70327
70351
|
top: 20%;
|
|
@@ -73662,10 +73686,14 @@ function addIconSetRule(cf, rule) {
|
|
|
73662
73686
|
continue;
|
|
73663
73687
|
}
|
|
73664
73688
|
const cfValueObjectNodes = cfValueObject.map((attrs) => escapeXml /*xml*/ `<cfvo ${formatAttributes(attrs)} />`);
|
|
73689
|
+
const iconSetAttrs = [["iconSet", getIconSet(rule.icons)]];
|
|
73690
|
+
if (isIconSetReversed(rule.icons)) {
|
|
73691
|
+
iconSetAttrs.push(["reverse", "1"]);
|
|
73692
|
+
}
|
|
73665
73693
|
conditionalFormats.push(escapeXml /*xml*/ `
|
|
73666
73694
|
<conditionalFormatting sqref="${range}">
|
|
73667
73695
|
<cfRule ${formatAttributes(ruleAttributes)}>
|
|
73668
|
-
<iconSet
|
|
73696
|
+
<iconSet ${formatAttributes(iconSetAttrs)}>
|
|
73669
73697
|
${joinXmlNodes(cfValueObjectNodes)}
|
|
73670
73698
|
</iconSet>
|
|
73671
73699
|
</cfRule>
|
|
@@ -73683,9 +73711,21 @@ function commonCfAttributes(cf) {
|
|
|
73683
73711
|
["stopIfTrue", cf.stopIfTrue ? 1 : 0],
|
|
73684
73712
|
];
|
|
73685
73713
|
}
|
|
73714
|
+
function isIconSetReversed(iconSet) {
|
|
73715
|
+
const defaultIconSet = ICON_SETS[detectIconsType(iconSet)];
|
|
73716
|
+
return iconSet.upper === defaultIconSet.bad && iconSet.lower === defaultIconSet.good;
|
|
73717
|
+
}
|
|
73686
73718
|
function getIconSet(iconSet) {
|
|
73687
|
-
return XLSX_ICONSET_MAP[
|
|
73688
|
-
|
|
73719
|
+
return XLSX_ICONSET_MAP[detectIconsType(iconSet)];
|
|
73720
|
+
}
|
|
73721
|
+
/**
|
|
73722
|
+
* Partial detection based on "upper" point only.
|
|
73723
|
+
* We support any arbitrary icon in the set, while excel doesn't allow
|
|
73724
|
+
* mixing icons from different types.
|
|
73725
|
+
*/
|
|
73726
|
+
function detectIconsType(iconSet) {
|
|
73727
|
+
const type = Object.keys(ICON_SETS).find((type) => Object.values(ICON_SETS[type]).includes(iconSet.upper)) || "dots";
|
|
73728
|
+
return type;
|
|
73689
73729
|
}
|
|
73690
73730
|
function thresholdAttributes(threshold, position) {
|
|
73691
73731
|
const type = getExcelThresholdType(threshold.type, position);
|
|
@@ -75404,6 +75444,7 @@ const registries = {
|
|
|
75404
75444
|
supportedPivotPositionalFormulaRegistry,
|
|
75405
75445
|
pivotToFunctionValueRegistry,
|
|
75406
75446
|
migrationStepRegistry,
|
|
75447
|
+
chartJsExtensionRegistry,
|
|
75407
75448
|
};
|
|
75408
75449
|
const helpers = {
|
|
75409
75450
|
arg,
|
|
@@ -75558,6 +75599,6 @@ const chartHelpers = { ...CHART_HELPERS, ...CHART_RUNTIME_HELPERS };
|
|
|
75558
75599
|
export { AbstractCellClipboardHandler, AbstractChart, AbstractFigureClipboardHandler, CellErrorType, CommandResult, CorePlugin, 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 };
|
|
75559
75600
|
|
|
75560
75601
|
|
|
75561
|
-
__info__.version = "18.1.
|
|
75562
|
-
__info__.date = "2025-
|
|
75563
|
-
__info__.hash = "
|
|
75602
|
+
__info__.version = "18.1.14";
|
|
75603
|
+
__info__.date = "2025-04-04T08:42:40.149Z";
|
|
75604
|
+
__info__.hash = "63b2fb7";
|