@odoo/o-spreadsheet 18.2.5 → 18.2.7
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 +809 -709
- package/dist/o-spreadsheet.d.ts +35 -17
- package/dist/o-spreadsheet.esm.js +809 -709
- package/dist/o-spreadsheet.iife.js +809 -709
- package/dist/o-spreadsheet.iife.min.js +380 -379
- package/dist/o_spreadsheet.xml +3 -3
- package/package.json +1 -1
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* This file is generated by o-spreadsheet build tools. Do not edit it.
|
|
4
4
|
* @see https://github.com/odoo/o-spreadsheet
|
|
5
|
-
* @version 18.2.
|
|
6
|
-
* @date 2025-
|
|
7
|
-
* @hash
|
|
5
|
+
* @version 18.2.7
|
|
6
|
+
* @date 2025-04-14T17:19:31.011Z
|
|
7
|
+
* @hash e187958
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
'use strict';
|
|
@@ -806,8 +806,7 @@ function removeFalsyAttributes(obj) {
|
|
|
806
806
|
*
|
|
807
807
|
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Character_Classes
|
|
808
808
|
*/
|
|
809
|
-
const
|
|
810
|
-
" ",
|
|
809
|
+
const specialWhiteSpaceSpecialCharacters = [
|
|
811
810
|
"\t",
|
|
812
811
|
"\f",
|
|
813
812
|
"\v",
|
|
@@ -822,7 +821,7 @@ const whiteSpaceSpecialCharacters = [
|
|
|
822
821
|
String.fromCharCode(parseInt("3000", 16)),
|
|
823
822
|
String.fromCharCode(parseInt("feff", 16)),
|
|
824
823
|
];
|
|
825
|
-
const
|
|
824
|
+
const specialWhiteSpaceRegexp = new RegExp(specialWhiteSpaceSpecialCharacters.join("|"), "g");
|
|
826
825
|
const newLineRegexp = /(\r\n|\r)/g;
|
|
827
826
|
/**
|
|
828
827
|
* Replace all different newlines characters by \n
|
|
@@ -3576,6 +3575,7 @@ const coreTypes = new Set([
|
|
|
3576
3575
|
"CLEAR_FORMATTING",
|
|
3577
3576
|
"SET_BORDER",
|
|
3578
3577
|
"SET_ZONE_BORDERS",
|
|
3578
|
+
"SET_BORDERS_ON_TARGET",
|
|
3579
3579
|
/** CHART */
|
|
3580
3580
|
"CREATE_CHART",
|
|
3581
3581
|
"UPDATE_CHART",
|
|
@@ -6725,6 +6725,7 @@ class AbstractCellClipboardHandler extends ClipboardHandler {
|
|
|
6725
6725
|
}
|
|
6726
6726
|
|
|
6727
6727
|
class BorderClipboardHandler extends AbstractCellClipboardHandler {
|
|
6728
|
+
queuedBordersToAdd = {};
|
|
6728
6729
|
copy(data) {
|
|
6729
6730
|
const sheetId = data.sheetId;
|
|
6730
6731
|
if (data.zones.length === 0) {
|
|
@@ -6755,6 +6756,7 @@ class BorderClipboardHandler extends AbstractCellClipboardHandler {
|
|
|
6755
6756
|
const { left, top } = zones[0];
|
|
6756
6757
|
this.pasteZone(sheetId, left, top, content.borders);
|
|
6757
6758
|
}
|
|
6759
|
+
this.executeQueuedChanges(sheetId);
|
|
6758
6760
|
}
|
|
6759
6761
|
pasteZone(sheetId, col, row, borders) {
|
|
6760
6762
|
for (const [r, rowBorders] of borders.entries()) {
|
|
@@ -6773,7 +6775,20 @@ class BorderClipboardHandler extends AbstractCellClipboardHandler {
|
|
|
6773
6775
|
...targetBorders,
|
|
6774
6776
|
...originBorders,
|
|
6775
6777
|
};
|
|
6776
|
-
|
|
6778
|
+
const borderKey = JSON.stringify(border);
|
|
6779
|
+
if (!this.queuedBordersToAdd[borderKey]) {
|
|
6780
|
+
this.queuedBordersToAdd[borderKey] = [];
|
|
6781
|
+
}
|
|
6782
|
+
this.queuedBordersToAdd[borderKey].push(positionToZone(target));
|
|
6783
|
+
}
|
|
6784
|
+
executeQueuedChanges(pasteSheetTarget) {
|
|
6785
|
+
for (const borderKey in this.queuedBordersToAdd) {
|
|
6786
|
+
const zones = this.queuedBordersToAdd[borderKey];
|
|
6787
|
+
const border = JSON.parse(borderKey);
|
|
6788
|
+
const target = recomputeZones(zones, []);
|
|
6789
|
+
this.dispatch("SET_BORDERS_ON_TARGET", { sheetId: pasteSheetTarget, target, border });
|
|
6790
|
+
}
|
|
6791
|
+
this.queuedBordersToAdd = {};
|
|
6777
6792
|
}
|
|
6778
6793
|
}
|
|
6779
6794
|
|
|
@@ -6799,8 +6814,12 @@ function tokenize(str, locale = DEFAULT_LOCALE) {
|
|
|
6799
6814
|
str = replaceNewLines(str);
|
|
6800
6815
|
const chars = new TokenizingChars(str);
|
|
6801
6816
|
const result = [];
|
|
6817
|
+
const tokenizeSpace = specialWhiteSpaceRegexp.test(str)
|
|
6818
|
+
? tokenizeSpecialCharacterSpace
|
|
6819
|
+
: tokenizeSimpleSpace;
|
|
6802
6820
|
while (!chars.isOver()) {
|
|
6803
|
-
let token =
|
|
6821
|
+
let token = tokenizeNewLine(chars) ||
|
|
6822
|
+
tokenizeSpace(chars) ||
|
|
6804
6823
|
tokenizeArgsSeparator(chars, locale) ||
|
|
6805
6824
|
tokenizeParenthesis(chars) ||
|
|
6806
6825
|
tokenizeOperator(chars) ||
|
|
@@ -6934,17 +6953,19 @@ function tokenizeSymbol(chars) {
|
|
|
6934
6953
|
}
|
|
6935
6954
|
return null;
|
|
6936
6955
|
}
|
|
6937
|
-
function
|
|
6938
|
-
let
|
|
6939
|
-
while (chars.current ===
|
|
6940
|
-
|
|
6941
|
-
chars.shift();
|
|
6956
|
+
function tokenizeSpecialCharacterSpace(chars) {
|
|
6957
|
+
let spaces = "";
|
|
6958
|
+
while (chars.current === " " || (chars.current && chars.current.match(specialWhiteSpaceRegexp))) {
|
|
6959
|
+
spaces += chars.shift();
|
|
6942
6960
|
}
|
|
6943
|
-
if (
|
|
6944
|
-
return { type: "SPACE", value:
|
|
6961
|
+
if (spaces) {
|
|
6962
|
+
return { type: "SPACE", value: spaces };
|
|
6945
6963
|
}
|
|
6964
|
+
return null;
|
|
6965
|
+
}
|
|
6966
|
+
function tokenizeSimpleSpace(chars) {
|
|
6946
6967
|
let spaces = "";
|
|
6947
|
-
while (chars.current
|
|
6968
|
+
while (chars.current === " ") {
|
|
6948
6969
|
spaces += chars.shift();
|
|
6949
6970
|
}
|
|
6950
6971
|
if (spaces) {
|
|
@@ -6952,6 +6973,17 @@ function tokenizeSpace(chars) {
|
|
|
6952
6973
|
}
|
|
6953
6974
|
return null;
|
|
6954
6975
|
}
|
|
6976
|
+
function tokenizeNewLine(chars) {
|
|
6977
|
+
let length = 0;
|
|
6978
|
+
while (chars.current === NEWLINE) {
|
|
6979
|
+
length++;
|
|
6980
|
+
chars.shift();
|
|
6981
|
+
}
|
|
6982
|
+
if (length) {
|
|
6983
|
+
return { type: "SPACE", value: NEWLINE.repeat(length) };
|
|
6984
|
+
}
|
|
6985
|
+
return null;
|
|
6986
|
+
}
|
|
6955
6987
|
function tokenizeInvalidRange(chars) {
|
|
6956
6988
|
if (chars.currentStartsWith(CellErrorType.InvalidReference)) {
|
|
6957
6989
|
chars.advanceBy(CellErrorType.InvalidReference.length);
|
|
@@ -8696,12 +8728,13 @@ class ConditionalFormatClipboardHandler extends AbstractCellClipboardHandler {
|
|
|
8696
8728
|
}
|
|
8697
8729
|
pasteCf(origin, target, isCutOperation) {
|
|
8698
8730
|
if (origin?.rules && origin.rules.length > 0) {
|
|
8731
|
+
const originZone = positionToZone(origin.position);
|
|
8699
8732
|
const zone = positionToZone(target);
|
|
8700
8733
|
for (const rule of origin.rules) {
|
|
8701
8734
|
const toRemoveZones = [];
|
|
8702
8735
|
if (isCutOperation) {
|
|
8703
8736
|
//remove from current rule
|
|
8704
|
-
toRemoveZones.push(
|
|
8737
|
+
toRemoveZones.push(originZone);
|
|
8705
8738
|
}
|
|
8706
8739
|
if (origin.position.sheetId === target.sheetId) {
|
|
8707
8740
|
this.adaptCFRules(origin.position.sheetId, rule, [zone], toRemoveZones);
|
|
@@ -8815,6 +8848,7 @@ class DataValidationClipboardHandler extends AbstractCellClipboardHandler {
|
|
|
8815
8848
|
pasteDataValidation(origin, target, isCutOperation) {
|
|
8816
8849
|
if (origin) {
|
|
8817
8850
|
const zone = positionToZone(target);
|
|
8851
|
+
const originZone = positionToZone(origin.position);
|
|
8818
8852
|
const rule = origin.rule;
|
|
8819
8853
|
if (!rule) {
|
|
8820
8854
|
const targetRule = this.getters.getValidationRuleForCell(target);
|
|
@@ -8826,7 +8860,7 @@ class DataValidationClipboardHandler extends AbstractCellClipboardHandler {
|
|
|
8826
8860
|
}
|
|
8827
8861
|
const toRemoveZone = [];
|
|
8828
8862
|
if (isCutOperation) {
|
|
8829
|
-
toRemoveZone.push(
|
|
8863
|
+
toRemoveZone.push(originZone);
|
|
8830
8864
|
}
|
|
8831
8865
|
if (origin.position.sheetId === target.sheetId) {
|
|
8832
8866
|
const copyToRule = this.getDataValidationRuleToCopyTo(target.sheetId, rule, false);
|
|
@@ -8886,7 +8920,7 @@ class DataValidationClipboardHandler extends AbstractCellClipboardHandler {
|
|
|
8886
8920
|
continue;
|
|
8887
8921
|
}
|
|
8888
8922
|
this.dispatch("ADD_DATA_VALIDATION_RULE", {
|
|
8889
|
-
rule: dv,
|
|
8923
|
+
rule: { id: dv.id, criterion: dv.criterion, isBlocking: dv.isBlocking },
|
|
8890
8924
|
ranges: newDvZones.map((zone) => this.getters.getRangeDataFromZone(sheetId, zone)),
|
|
8891
8925
|
sheetId,
|
|
8892
8926
|
});
|
|
@@ -9578,6 +9612,159 @@ class ComposerFocusStore extends SpreadsheetStore {
|
|
|
9578
9612
|
}
|
|
9579
9613
|
}
|
|
9580
9614
|
|
|
9615
|
+
/**
|
|
9616
|
+
* This file is largely inspired by owl 1.
|
|
9617
|
+
* `css` tag has been removed from owl 2 without workaround to manage css.
|
|
9618
|
+
* So, the solution was to import the behavior of owl 1 directly in our
|
|
9619
|
+
* codebase, with one difference: the css is added to the sheet as soon as the
|
|
9620
|
+
* css tag is executed. In owl 1, the css was added as soon as a Component was
|
|
9621
|
+
* created for the first time.
|
|
9622
|
+
*/
|
|
9623
|
+
const STYLESHEETS = {};
|
|
9624
|
+
let nextId = 0;
|
|
9625
|
+
/**
|
|
9626
|
+
* CSS tag helper for defining inline stylesheets. With this, one can simply define
|
|
9627
|
+
* an inline stylesheet with just the following code:
|
|
9628
|
+
* ```js
|
|
9629
|
+
* css`.component-a { color: red; }`;
|
|
9630
|
+
* ```
|
|
9631
|
+
*/
|
|
9632
|
+
function css(strings, ...args) {
|
|
9633
|
+
const name = `__sheet__${nextId++}`;
|
|
9634
|
+
const value = String.raw(strings, ...args);
|
|
9635
|
+
registerSheet(name, value);
|
|
9636
|
+
activateSheet(name);
|
|
9637
|
+
return name;
|
|
9638
|
+
}
|
|
9639
|
+
function processSheet(str) {
|
|
9640
|
+
const tokens = str.split(/(\{|\}|;)/).map((s) => s.trim());
|
|
9641
|
+
const selectorStack = [];
|
|
9642
|
+
const parts = [];
|
|
9643
|
+
let rules = [];
|
|
9644
|
+
function generateSelector(stackIndex, parentSelector) {
|
|
9645
|
+
const parts = [];
|
|
9646
|
+
for (const selector of selectorStack[stackIndex]) {
|
|
9647
|
+
let part = (parentSelector && parentSelector + " " + selector) || selector;
|
|
9648
|
+
if (part.includes("&")) {
|
|
9649
|
+
part = selector.replace(/&/g, parentSelector || "");
|
|
9650
|
+
}
|
|
9651
|
+
if (stackIndex < selectorStack.length - 1) {
|
|
9652
|
+
part = generateSelector(stackIndex + 1, part);
|
|
9653
|
+
}
|
|
9654
|
+
parts.push(part);
|
|
9655
|
+
}
|
|
9656
|
+
return parts.join(", ");
|
|
9657
|
+
}
|
|
9658
|
+
function generateRules() {
|
|
9659
|
+
if (rules.length) {
|
|
9660
|
+
parts.push(generateSelector(0) + " {");
|
|
9661
|
+
parts.push(...rules);
|
|
9662
|
+
parts.push("}");
|
|
9663
|
+
rules = [];
|
|
9664
|
+
}
|
|
9665
|
+
}
|
|
9666
|
+
while (tokens.length) {
|
|
9667
|
+
let token = tokens.shift();
|
|
9668
|
+
if (token === "}") {
|
|
9669
|
+
generateRules();
|
|
9670
|
+
selectorStack.pop();
|
|
9671
|
+
}
|
|
9672
|
+
else {
|
|
9673
|
+
if (tokens[0] === "{") {
|
|
9674
|
+
generateRules();
|
|
9675
|
+
selectorStack.push(token.split(/\s*,\s*/));
|
|
9676
|
+
tokens.shift();
|
|
9677
|
+
}
|
|
9678
|
+
if (tokens[0] === ";") {
|
|
9679
|
+
rules.push(" " + token + ";");
|
|
9680
|
+
}
|
|
9681
|
+
}
|
|
9682
|
+
}
|
|
9683
|
+
return parts.join("\n");
|
|
9684
|
+
}
|
|
9685
|
+
function registerSheet(id, css) {
|
|
9686
|
+
const sheet = document.createElement("style");
|
|
9687
|
+
sheet.textContent = processSheet(css);
|
|
9688
|
+
STYLESHEETS[id] = sheet;
|
|
9689
|
+
}
|
|
9690
|
+
function activateSheet(id) {
|
|
9691
|
+
const sheet = STYLESHEETS[id];
|
|
9692
|
+
sheet.setAttribute("component", id);
|
|
9693
|
+
document.head.appendChild(sheet);
|
|
9694
|
+
}
|
|
9695
|
+
function getTextDecoration({ strikethrough, underline, }) {
|
|
9696
|
+
if (!strikethrough && !underline) {
|
|
9697
|
+
return "none";
|
|
9698
|
+
}
|
|
9699
|
+
return `${strikethrough ? "line-through" : ""} ${underline ? "underline" : ""}`;
|
|
9700
|
+
}
|
|
9701
|
+
/**
|
|
9702
|
+
* Convert the cell style to CSS properties.
|
|
9703
|
+
*/
|
|
9704
|
+
function cellStyleToCss(style) {
|
|
9705
|
+
const attributes = cellTextStyleToCss(style);
|
|
9706
|
+
if (!style)
|
|
9707
|
+
return attributes;
|
|
9708
|
+
if (style.fillColor) {
|
|
9709
|
+
attributes["background"] = style.fillColor;
|
|
9710
|
+
}
|
|
9711
|
+
return attributes;
|
|
9712
|
+
}
|
|
9713
|
+
/**
|
|
9714
|
+
* Convert the cell text style to CSS properties.
|
|
9715
|
+
*/
|
|
9716
|
+
function cellTextStyleToCss(style) {
|
|
9717
|
+
const attributes = {};
|
|
9718
|
+
if (!style)
|
|
9719
|
+
return attributes;
|
|
9720
|
+
if (style.bold) {
|
|
9721
|
+
attributes["font-weight"] = "bold";
|
|
9722
|
+
}
|
|
9723
|
+
if (style.italic) {
|
|
9724
|
+
attributes["font-style"] = "italic";
|
|
9725
|
+
}
|
|
9726
|
+
if (style.strikethrough || style.underline) {
|
|
9727
|
+
let decoration = style.strikethrough ? "line-through" : "";
|
|
9728
|
+
decoration = style.underline ? decoration + " underline" : decoration;
|
|
9729
|
+
attributes["text-decoration"] = decoration;
|
|
9730
|
+
}
|
|
9731
|
+
if (style.textColor) {
|
|
9732
|
+
attributes["color"] = style.textColor;
|
|
9733
|
+
}
|
|
9734
|
+
return attributes;
|
|
9735
|
+
}
|
|
9736
|
+
/**
|
|
9737
|
+
* Transform CSS properties into a CSS string.
|
|
9738
|
+
*/
|
|
9739
|
+
function cssPropertiesToCss(attributes) {
|
|
9740
|
+
let styleStr = "";
|
|
9741
|
+
for (const attName in attributes) {
|
|
9742
|
+
if (!attributes[attName]) {
|
|
9743
|
+
continue;
|
|
9744
|
+
}
|
|
9745
|
+
styleStr += `${attName}:${attributes[attName]}; `;
|
|
9746
|
+
}
|
|
9747
|
+
return styleStr;
|
|
9748
|
+
}
|
|
9749
|
+
function getElementMargins(el) {
|
|
9750
|
+
const style = window.getComputedStyle(el);
|
|
9751
|
+
return {
|
|
9752
|
+
top: parseInt(style.marginTop, 10) || 0,
|
|
9753
|
+
bottom: parseInt(style.marginBottom, 10) || 0,
|
|
9754
|
+
left: parseInt(style.marginLeft, 10) || 0,
|
|
9755
|
+
right: parseInt(style.marginRight, 10) || 0,
|
|
9756
|
+
};
|
|
9757
|
+
}
|
|
9758
|
+
|
|
9759
|
+
const chartJsExtensionRegistry = new Registry();
|
|
9760
|
+
/** Return window.Chart, making sure all our extensions are loaded in ChartJS */
|
|
9761
|
+
function getChartJSConstructor() {
|
|
9762
|
+
if (window.Chart && !window.Chart?.registry.plugins.get("chartShowValuesPlugin")) {
|
|
9763
|
+
window.Chart.register(...chartJsExtensionRegistry.getAll());
|
|
9764
|
+
}
|
|
9765
|
+
return window.Chart;
|
|
9766
|
+
}
|
|
9767
|
+
|
|
9581
9768
|
const TREND_LINE_XAXIS_ID = "x1";
|
|
9582
9769
|
const MOVING_AVERAGE_TREND_LINE_XAXIS_ID = "xMovingAverage";
|
|
9583
9770
|
/**
|
|
@@ -10126,341 +10313,79 @@ function getNextNonEmptyBar(bars, startIndex) {
|
|
|
10126
10313
|
return bars.find((bar, i) => i > startIndex && bar.height !== 0);
|
|
10127
10314
|
}
|
|
10128
10315
|
|
|
10129
|
-
|
|
10130
|
-
|
|
10131
|
-
|
|
10132
|
-
|
|
10133
|
-
|
|
10134
|
-
|
|
10135
|
-
const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
|
|
10136
|
-
const GAUGE_TITLE_SECTION_HEIGHT = 25;
|
|
10137
|
-
function drawGaugeChart(canvas, runtime) {
|
|
10138
|
-
const canvasBoundingRect = canvas.getBoundingClientRect();
|
|
10139
|
-
canvas.width = canvasBoundingRect.width;
|
|
10140
|
-
canvas.height = canvasBoundingRect.height;
|
|
10141
|
-
const ctx = canvas.getContext("2d");
|
|
10142
|
-
const config = getGaugeRenderingConfig(canvasBoundingRect, runtime, ctx);
|
|
10143
|
-
drawBackground(ctx, config);
|
|
10144
|
-
drawGauge(ctx, config);
|
|
10145
|
-
drawInflectionValues(ctx, config);
|
|
10146
|
-
drawLabels(ctx, config);
|
|
10147
|
-
drawTitle(ctx, config);
|
|
10148
|
-
}
|
|
10149
|
-
function drawGauge(ctx, config) {
|
|
10150
|
-
ctx.save();
|
|
10151
|
-
const gauge = config.gauge;
|
|
10152
|
-
const arcCenterX = gauge.rect.x + gauge.rect.width / 2;
|
|
10153
|
-
const arcCenterY = gauge.rect.y + gauge.rect.height;
|
|
10154
|
-
const arcRadius = gauge.rect.height - gauge.arcWidth / 2;
|
|
10155
|
-
if (arcRadius < 0) {
|
|
10156
|
-
return;
|
|
10157
|
-
}
|
|
10158
|
-
const gaugeAngle = gauge.percentage === 1 ? 0 : Math.PI * (1 + gauge.percentage);
|
|
10159
|
-
// Gauge background
|
|
10160
|
-
ctx.strokeStyle = GAUGE_BACKGROUND_COLOR;
|
|
10161
|
-
ctx.beginPath();
|
|
10162
|
-
ctx.lineWidth = gauge.arcWidth;
|
|
10163
|
-
ctx.arc(arcCenterX, arcCenterY, arcRadius, gaugeAngle, 0);
|
|
10164
|
-
ctx.stroke();
|
|
10165
|
-
// Gauge value
|
|
10166
|
-
ctx.strokeStyle = gauge.color;
|
|
10167
|
-
ctx.beginPath();
|
|
10168
|
-
ctx.arc(arcCenterX, arcCenterY, arcRadius, Math.PI, gaugeAngle);
|
|
10169
|
-
ctx.stroke();
|
|
10170
|
-
ctx.restore();
|
|
10171
|
-
}
|
|
10172
|
-
function drawBackground(ctx, config) {
|
|
10173
|
-
ctx.save();
|
|
10174
|
-
ctx.fillStyle = config.backgroundColor;
|
|
10175
|
-
ctx.fillRect(0, 0, config.width, config.height);
|
|
10176
|
-
ctx.restore();
|
|
10177
|
-
}
|
|
10178
|
-
function drawLabels(ctx, config) {
|
|
10179
|
-
for (const label of [config.minLabel, config.maxLabel, config.gaugeValue]) {
|
|
10180
|
-
ctx.save();
|
|
10181
|
-
ctx.textAlign = "center";
|
|
10182
|
-
ctx.fillStyle = label.color;
|
|
10183
|
-
ctx.font = `${label.fontSize}px ${DEFAULT_FONT}`;
|
|
10184
|
-
ctx.fillText(label.label, label.textPosition.x, label.textPosition.y);
|
|
10185
|
-
ctx.restore();
|
|
10186
|
-
}
|
|
10187
|
-
}
|
|
10188
|
-
function drawInflectionValues(ctx, config) {
|
|
10189
|
-
const { x: rectX, y: rectY, width, height } = config.gauge.rect;
|
|
10190
|
-
for (const inflectionValue of config.inflectionValues) {
|
|
10191
|
-
ctx.save();
|
|
10192
|
-
ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment
|
|
10193
|
-
ctx.rotate(Math.PI / 2 - inflectionValue.rotation);
|
|
10194
|
-
ctx.lineWidth = 2;
|
|
10195
|
-
ctx.strokeStyle = chartMutedFontColor(config.backgroundColor) + "aa";
|
|
10196
|
-
ctx.beginPath();
|
|
10197
|
-
ctx.moveTo(0, -(height - config.gauge.arcWidth));
|
|
10198
|
-
ctx.lineTo(0, -height - 3);
|
|
10199
|
-
ctx.stroke();
|
|
10200
|
-
ctx.textAlign = "center";
|
|
10201
|
-
ctx.font = `${inflectionValue.fontSize}px ${DEFAULT_FONT}`;
|
|
10202
|
-
ctx.fillStyle = inflectionValue.color;
|
|
10203
|
-
const textY = -height - GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN - inflectionValue.offset;
|
|
10204
|
-
ctx.fillText(inflectionValue.label, 0, textY);
|
|
10205
|
-
ctx.restore();
|
|
10206
|
-
}
|
|
10207
|
-
}
|
|
10208
|
-
function drawTitle(ctx, config) {
|
|
10209
|
-
ctx.save();
|
|
10210
|
-
const title = config.title;
|
|
10211
|
-
ctx.font = getDefaultContextFont(title.fontSize, title.bold, title.italic);
|
|
10212
|
-
ctx.textBaseline = "middle";
|
|
10213
|
-
ctx.fillStyle = title.color;
|
|
10214
|
-
ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
|
|
10215
|
-
ctx.restore();
|
|
10216
|
-
}
|
|
10217
|
-
function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
|
|
10218
|
-
const maxValue = runtime.maxValue;
|
|
10219
|
-
const minValue = runtime.minValue;
|
|
10220
|
-
const gaugeValue = runtime.gaugeValue;
|
|
10221
|
-
const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
|
|
10222
|
-
const gaugeArcWidth = gaugeRect.width / 6;
|
|
10223
|
-
const gaugePercentage = gaugeValue
|
|
10224
|
-
? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
|
|
10225
|
-
: 0;
|
|
10226
|
-
const gaugeValuePosition = {
|
|
10227
|
-
x: boundingRect.width / 2,
|
|
10228
|
-
y: gaugeRect.y + gaugeRect.height - gaugeRect.height / 12,
|
|
10229
|
-
};
|
|
10230
|
-
let gaugeValueFontSize = GAUGE_DEFAULT_VALUE_FONT_SIZE;
|
|
10231
|
-
// Scale down the font size if the gaugeRect is too small
|
|
10232
|
-
if (gaugeRect.height < 300) {
|
|
10233
|
-
gaugeValueFontSize = gaugeValueFontSize * (gaugeRect.height / 300);
|
|
10234
|
-
}
|
|
10235
|
-
// Scale down the font size if the text is too long
|
|
10236
|
-
const maxTextWidth = gaugeRect.width / 2;
|
|
10237
|
-
const gaugeLabel = gaugeValue?.label || "-";
|
|
10238
|
-
if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
|
|
10239
|
-
gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
|
|
10316
|
+
css /* scss */ `
|
|
10317
|
+
.o-spreadsheet {
|
|
10318
|
+
.o-chart-custom-tooltip {
|
|
10319
|
+
font-size: 12px;
|
|
10320
|
+
background-color: #fff;
|
|
10321
|
+
z-index: ${ComponentsImportance.FigureTooltip};
|
|
10240
10322
|
}
|
|
10241
|
-
|
|
10242
|
-
|
|
10243
|
-
|
|
10244
|
-
|
|
10245
|
-
|
|
10246
|
-
|
|
10247
|
-
|
|
10323
|
+
}
|
|
10324
|
+
`;
|
|
10325
|
+
chartJsExtensionRegistry.add("chartShowValuesPlugin", chartShowValuesPlugin);
|
|
10326
|
+
chartJsExtensionRegistry.add("waterfallLinesPlugin", waterfallLinesPlugin);
|
|
10327
|
+
class ChartJsComponent extends owl.Component {
|
|
10328
|
+
static template = "o-spreadsheet-ChartJsComponent";
|
|
10329
|
+
static props = {
|
|
10330
|
+
figure: Object,
|
|
10248
10331
|
};
|
|
10249
|
-
|
|
10250
|
-
|
|
10251
|
-
|
|
10252
|
-
|
|
10253
|
-
|
|
10254
|
-
}
|
|
10255
|
-
switch (runtime.title.align) {
|
|
10256
|
-
case "right":
|
|
10257
|
-
x = boundingRect.width - titleWidth - CHART_PADDING$1;
|
|
10258
|
-
break;
|
|
10259
|
-
case "center":
|
|
10260
|
-
x = (boundingRect.width - titleWidth) / 2;
|
|
10261
|
-
break;
|
|
10262
|
-
case "left":
|
|
10263
|
-
default:
|
|
10264
|
-
x = CHART_PADDING$1;
|
|
10265
|
-
break;
|
|
10332
|
+
canvas = owl.useRef("graphContainer");
|
|
10333
|
+
chart;
|
|
10334
|
+
currentRuntime;
|
|
10335
|
+
get background() {
|
|
10336
|
+
return this.chartRuntime.background;
|
|
10266
10337
|
}
|
|
10267
|
-
|
|
10268
|
-
|
|
10269
|
-
height: boundingRect.height,
|
|
10270
|
-
title: {
|
|
10271
|
-
label: runtime.title.text ?? "",
|
|
10272
|
-
fontSize: runtime.title.fontSize ?? CHART_TITLE_FONT_SIZE,
|
|
10273
|
-
textPosition: {
|
|
10274
|
-
x,
|
|
10275
|
-
y: CHART_PADDING_TOP + titleHeight / 2,
|
|
10276
|
-
},
|
|
10277
|
-
color: runtime.title.color ?? textColor,
|
|
10278
|
-
bold: runtime.title.bold,
|
|
10279
|
-
italic: runtime.title.italic,
|
|
10280
|
-
},
|
|
10281
|
-
backgroundColor: runtime.background,
|
|
10282
|
-
gauge: {
|
|
10283
|
-
rect: gaugeRect,
|
|
10284
|
-
arcWidth: gaugeArcWidth,
|
|
10285
|
-
percentage: clip(gaugePercentage, 0, 1),
|
|
10286
|
-
color: getGaugeColor(runtime),
|
|
10287
|
-
},
|
|
10288
|
-
inflectionValues,
|
|
10289
|
-
gaugeValue: {
|
|
10290
|
-
label: gaugeLabel,
|
|
10291
|
-
textPosition: gaugeValuePosition,
|
|
10292
|
-
fontSize: gaugeValueFontSize,
|
|
10293
|
-
color: textColor,
|
|
10294
|
-
},
|
|
10295
|
-
minLabel: {
|
|
10296
|
-
label: runtime.minValue.label,
|
|
10297
|
-
textPosition: minLabelPosition,
|
|
10298
|
-
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
10299
|
-
color: textColor,
|
|
10300
|
-
},
|
|
10301
|
-
maxLabel: {
|
|
10302
|
-
label: runtime.maxValue.label,
|
|
10303
|
-
textPosition: maxLabelPosition,
|
|
10304
|
-
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
10305
|
-
color: textColor,
|
|
10306
|
-
},
|
|
10307
|
-
};
|
|
10308
|
-
}
|
|
10309
|
-
/**
|
|
10310
|
-
* Get the rectangle in which the gauge will be drawn, based on the bounding rectangle of the canvas and leaving
|
|
10311
|
-
* space for the title and labels.
|
|
10312
|
-
*/
|
|
10313
|
-
function getGaugeRect(boundingRect, title) {
|
|
10314
|
-
const titleHeight = title ? GAUGE_TITLE_SECTION_HEIGHT : 0;
|
|
10315
|
-
const drawHeight = boundingRect.height - GAUGE_PADDING_BOTTOM - titleHeight - GAUGE_PADDING_TOP;
|
|
10316
|
-
const drawWidth = boundingRect.width - GAUGE_PADDING_SIDE * 2;
|
|
10317
|
-
let gaugeWidth;
|
|
10318
|
-
let gaugeHeight;
|
|
10319
|
-
if (drawWidth > 2 * drawHeight) {
|
|
10320
|
-
gaugeWidth = 2 * drawHeight;
|
|
10321
|
-
gaugeHeight = drawHeight;
|
|
10338
|
+
get canvasStyle() {
|
|
10339
|
+
return `background-color: ${this.background}`;
|
|
10322
10340
|
}
|
|
10323
|
-
|
|
10324
|
-
|
|
10325
|
-
|
|
10341
|
+
get chartRuntime() {
|
|
10342
|
+
const runtime = this.env.model.getters.getChartRuntime(this.props.figure.id);
|
|
10343
|
+
if (!("chartJsConfig" in runtime)) {
|
|
10344
|
+
throw new Error("Unsupported chart runtime");
|
|
10345
|
+
}
|
|
10346
|
+
return runtime;
|
|
10326
10347
|
}
|
|
10327
|
-
|
|
10328
|
-
|
|
10329
|
-
|
|
10330
|
-
|
|
10331
|
-
|
|
10332
|
-
|
|
10333
|
-
|
|
10334
|
-
|
|
10335
|
-
|
|
10336
|
-
|
|
10337
|
-
|
|
10338
|
-
|
|
10339
|
-
|
|
10340
|
-
|
|
10341
|
-
|
|
10342
|
-
|
|
10343
|
-
|
|
10344
|
-
|
|
10345
|
-
|
|
10346
|
-
|
|
10347
|
-
};
|
|
10348
|
-
const textStyle = { fontSize: GAUGE_LABELS_FONT_SIZE };
|
|
10349
|
-
const inflectionValues = [];
|
|
10350
|
-
const inflectionValuesTextRects = [];
|
|
10351
|
-
for (const inflectionValue of runtime.inflectionValues) {
|
|
10352
|
-
const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
|
|
10353
|
-
const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
|
|
10354
|
-
const angle = Math.PI - Math.PI * percentage;
|
|
10355
|
-
const textRect = getRectangleTangentToCircle(angle, // angle between X axis and the point where the rectangle is tangent to the circle
|
|
10356
|
-
gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, // radius of the gauge circle + margin below text
|
|
10357
|
-
gaugeCircleCenter.x, // center of the gauge circle
|
|
10358
|
-
gaugeCircleCenter.y, // center of the gauge circle
|
|
10359
|
-
labelWidth + 2, // width of the text + some margin
|
|
10360
|
-
GAUGE_LABELS_FONT_SIZE // height of the text
|
|
10361
|
-
);
|
|
10362
|
-
let offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect))
|
|
10363
|
-
? GAUGE_LABELS_FONT_SIZE
|
|
10364
|
-
: 0;
|
|
10365
|
-
inflectionValuesTextRects.push(textRect);
|
|
10366
|
-
inflectionValues.push({
|
|
10367
|
-
rotation: angle,
|
|
10368
|
-
label: inflectionValue.label,
|
|
10369
|
-
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
10370
|
-
color: textColor,
|
|
10371
|
-
offset,
|
|
10348
|
+
setup() {
|
|
10349
|
+
owl.onMounted(() => {
|
|
10350
|
+
const runtime = this.chartRuntime;
|
|
10351
|
+
this.currentRuntime = runtime;
|
|
10352
|
+
// Note: chartJS modify the runtime in place, so it's important to give it a copy
|
|
10353
|
+
this.createChart(deepCopy(runtime.chartJsConfig));
|
|
10354
|
+
});
|
|
10355
|
+
owl.onWillUnmount(() => this.chart?.destroy());
|
|
10356
|
+
owl.useEffect(() => {
|
|
10357
|
+
const runtime = this.chartRuntime;
|
|
10358
|
+
if (runtime !== this.currentRuntime) {
|
|
10359
|
+
if (runtime.chartJsConfig.type !== this.currentRuntime.chartJsConfig.type) {
|
|
10360
|
+
this.chart?.destroy();
|
|
10361
|
+
this.createChart(deepCopy(runtime.chartJsConfig));
|
|
10362
|
+
}
|
|
10363
|
+
else {
|
|
10364
|
+
this.updateChartJs(deepCopy(runtime.chartJsConfig));
|
|
10365
|
+
}
|
|
10366
|
+
this.currentRuntime = runtime;
|
|
10367
|
+
}
|
|
10372
10368
|
});
|
|
10373
10369
|
}
|
|
10374
|
-
|
|
10375
|
-
|
|
10376
|
-
|
|
10377
|
-
|
|
10378
|
-
|
|
10379
|
-
return GAUGE_BACKGROUND_COLOR;
|
|
10380
|
-
}
|
|
10381
|
-
for (let i = 0; i < runtime.inflectionValues.length; i++) {
|
|
10382
|
-
const inflectionValue = runtime.inflectionValues[i];
|
|
10383
|
-
if (inflectionValue.operator === "<" && gaugeValue < inflectionValue.value) {
|
|
10384
|
-
return runtime.colors[i];
|
|
10385
|
-
}
|
|
10386
|
-
else if (inflectionValue.operator === "<=" && gaugeValue <= inflectionValue.value) {
|
|
10387
|
-
return runtime.colors[i];
|
|
10388
|
-
}
|
|
10389
|
-
}
|
|
10390
|
-
return runtime.colors.at(-1);
|
|
10391
|
-
}
|
|
10392
|
-
function getSegmentsOfRectangle(rectangle) {
|
|
10393
|
-
return [
|
|
10394
|
-
{ start: rectangle.topLeft, end: rectangle.topRight },
|
|
10395
|
-
{ start: rectangle.topRight, end: rectangle.bottomRight },
|
|
10396
|
-
{ start: rectangle.bottomRight, end: rectangle.bottomLeft },
|
|
10397
|
-
{ start: rectangle.bottomLeft, end: rectangle.topLeft },
|
|
10398
|
-
];
|
|
10399
|
-
}
|
|
10400
|
-
/**
|
|
10401
|
-
* Check if two segment intersect. The case where the segments are colinear (both segments on the same line)
|
|
10402
|
-
* is not handled.
|
|
10403
|
-
*/
|
|
10404
|
-
function doSegmentIntersect(segment1, segment2) {
|
|
10405
|
-
const A = segment1.start;
|
|
10406
|
-
const B = segment1.end;
|
|
10407
|
-
const C = segment2.start;
|
|
10408
|
-
const D = segment2.end;
|
|
10409
|
-
/**
|
|
10410
|
-
* Line segment intersection algorithm
|
|
10411
|
-
* https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
|
|
10412
|
-
*/
|
|
10413
|
-
function ccw(a, b, c) {
|
|
10414
|
-
return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
|
|
10370
|
+
createChart(chartData) {
|
|
10371
|
+
const canvas = this.canvas.el;
|
|
10372
|
+
const ctx = canvas.getContext("2d");
|
|
10373
|
+
const Chart = getChartJSConstructor();
|
|
10374
|
+
this.chart = new Chart(ctx, chartData);
|
|
10415
10375
|
}
|
|
10416
|
-
|
|
10417
|
-
|
|
10418
|
-
|
|
10419
|
-
|
|
10420
|
-
|
|
10421
|
-
for (const segment1 of segments1) {
|
|
10422
|
-
for (const segment2 of segments2) {
|
|
10423
|
-
if (doSegmentIntersect(segment1, segment2)) {
|
|
10424
|
-
return true;
|
|
10376
|
+
updateChartJs(chartData) {
|
|
10377
|
+
if (chartData.data && chartData.data.datasets) {
|
|
10378
|
+
this.chart.data = chartData.data;
|
|
10379
|
+
if (chartData.options?.plugins?.title) {
|
|
10380
|
+
this.chart.config.options.plugins.title = chartData.options.plugins.title;
|
|
10425
10381
|
}
|
|
10426
10382
|
}
|
|
10383
|
+
else {
|
|
10384
|
+
this.chart.data.datasets = [];
|
|
10385
|
+
}
|
|
10386
|
+
this.chart.config.options = chartData.options;
|
|
10387
|
+
this.chart.update();
|
|
10427
10388
|
}
|
|
10428
|
-
return false;
|
|
10429
|
-
}
|
|
10430
|
-
/**
|
|
10431
|
-
* Get the rectangle that is tangent to a circle at a given angle.
|
|
10432
|
-
*
|
|
10433
|
-
* @param angle angle between X axis and the point where the rectangle is tangent to the circle
|
|
10434
|
-
*/
|
|
10435
|
-
function getRectangleTangentToCircle(angle, radius, circleCenterX, circleCenterY, rectWidth, rectHeight) {
|
|
10436
|
-
const cos = Math.cos(angle);
|
|
10437
|
-
const sin = Math.sin(angle);
|
|
10438
|
-
// x, y are the distance from the center of the circle to the point where the rectangle is tangent to the circle
|
|
10439
|
-
const x = cos * radius;
|
|
10440
|
-
const y = sin * radius;
|
|
10441
|
-
// x2, y2 are the distance from the point the rectangle is tangent to the circle to the bottom left corner of the rectangle
|
|
10442
|
-
const x2 = sin * (rectWidth / 2); // cos(angle + 90°) = sin(angle)
|
|
10443
|
-
const y2 = cos * (rectWidth / 2);
|
|
10444
|
-
const bottomRight = {
|
|
10445
|
-
x: x + x2 + circleCenterX,
|
|
10446
|
-
y: circleCenterY - (y - y2),
|
|
10447
|
-
};
|
|
10448
|
-
const bottomLeft = {
|
|
10449
|
-
x: x - x2 + circleCenterX,
|
|
10450
|
-
y: circleCenterY - (y + y2),
|
|
10451
|
-
};
|
|
10452
|
-
// Same as above but for the top corners of the rectangle (radius + rectangle height instead of radius)
|
|
10453
|
-
const xp = cos * (radius + rectHeight);
|
|
10454
|
-
const yp = sin * (radius + rectHeight);
|
|
10455
|
-
const topLeft = {
|
|
10456
|
-
x: xp - x2 + circleCenterX,
|
|
10457
|
-
y: circleCenterY - (yp + y2),
|
|
10458
|
-
};
|
|
10459
|
-
const topRight = {
|
|
10460
|
-
x: xp + x2 + circleCenterX,
|
|
10461
|
-
y: circleCenterY - (yp - y2),
|
|
10462
|
-
};
|
|
10463
|
-
return { bottomLeft, bottomRight, topRight, topLeft };
|
|
10464
10389
|
}
|
|
10465
10390
|
|
|
10466
10391
|
/**
|
|
@@ -11042,299 +10967,6 @@ class ScorecardChartConfigBuilder {
|
|
|
11042
10967
|
}
|
|
11043
10968
|
}
|
|
11044
10969
|
|
|
11045
|
-
const CHART_COMMON_OPTIONS = {
|
|
11046
|
-
// https://www.chartjs.org/docs/latest/general/responsive.html
|
|
11047
|
-
responsive: true, // will resize when its container is resized
|
|
11048
|
-
maintainAspectRatio: false, // doesn't maintain the aspect ratio (width/height =2 by default) so the user has the choice of the exact layout
|
|
11049
|
-
elements: {
|
|
11050
|
-
line: {
|
|
11051
|
-
fill: false, // do not fill the area under line charts
|
|
11052
|
-
},
|
|
11053
|
-
point: {
|
|
11054
|
-
hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
|
|
11055
|
-
},
|
|
11056
|
-
},
|
|
11057
|
-
animation: false,
|
|
11058
|
-
};
|
|
11059
|
-
function chartToImage(runtime, figure, type) {
|
|
11060
|
-
// wrap the canvas in a div with a fixed size because chart.js would
|
|
11061
|
-
// fill the whole page otherwise
|
|
11062
|
-
const div = document.createElement("div");
|
|
11063
|
-
div.style.width = `${figure.width}px`;
|
|
11064
|
-
div.style.height = `${figure.height}px`;
|
|
11065
|
-
const canvas = document.createElement("canvas");
|
|
11066
|
-
div.append(canvas);
|
|
11067
|
-
canvas.setAttribute("width", figure.width.toString());
|
|
11068
|
-
canvas.setAttribute("height", figure.height.toString());
|
|
11069
|
-
// we have to add the canvas to the DOM otherwise it won't be rendered
|
|
11070
|
-
document.body.append(div);
|
|
11071
|
-
if ("chartJsConfig" in runtime) {
|
|
11072
|
-
const config = deepCopy(runtime.chartJsConfig);
|
|
11073
|
-
config.plugins = [backgroundColorChartJSPlugin];
|
|
11074
|
-
const Chart = getChartJSConstructor();
|
|
11075
|
-
const chart = new Chart(canvas, config);
|
|
11076
|
-
const imgContent = chart.toBase64Image();
|
|
11077
|
-
chart.destroy();
|
|
11078
|
-
div.remove();
|
|
11079
|
-
return imgContent;
|
|
11080
|
-
}
|
|
11081
|
-
else if (type === "scorecard") {
|
|
11082
|
-
const design = getScorecardConfiguration(figure, runtime);
|
|
11083
|
-
drawScoreChart(design, canvas);
|
|
11084
|
-
const imgContent = canvas.toDataURL();
|
|
11085
|
-
div.remove();
|
|
11086
|
-
return imgContent;
|
|
11087
|
-
}
|
|
11088
|
-
else if (type === "gauge") {
|
|
11089
|
-
drawGaugeChart(canvas, runtime);
|
|
11090
|
-
const imgContent = canvas.toDataURL();
|
|
11091
|
-
div.remove();
|
|
11092
|
-
return imgContent;
|
|
11093
|
-
}
|
|
11094
|
-
return undefined;
|
|
11095
|
-
}
|
|
11096
|
-
/**
|
|
11097
|
-
* Custom chart.js plugin to set the background color of the canvas
|
|
11098
|
-
* https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md
|
|
11099
|
-
*/
|
|
11100
|
-
const backgroundColorChartJSPlugin = {
|
|
11101
|
-
id: "customCanvasBackgroundColor",
|
|
11102
|
-
beforeDraw: (chart) => {
|
|
11103
|
-
const { ctx } = chart;
|
|
11104
|
-
ctx.save();
|
|
11105
|
-
ctx.globalCompositeOperation = "destination-over";
|
|
11106
|
-
ctx.fillStyle = "#ffffff";
|
|
11107
|
-
ctx.fillRect(0, 0, chart.width, chart.height);
|
|
11108
|
-
ctx.restore();
|
|
11109
|
-
},
|
|
11110
|
-
};
|
|
11111
|
-
/** Return window.Chart, making sure all our extensions are loaded in ChartJS */
|
|
11112
|
-
function getChartJSConstructor() {
|
|
11113
|
-
if (window.Chart && !window.Chart?.registry.plugins.get("chartShowValuesPlugin")) {
|
|
11114
|
-
window.Chart.register(chartShowValuesPlugin);
|
|
11115
|
-
window.Chart.register(waterfallLinesPlugin);
|
|
11116
|
-
}
|
|
11117
|
-
return window.Chart;
|
|
11118
|
-
}
|
|
11119
|
-
|
|
11120
|
-
/**
|
|
11121
|
-
* This file is largely inspired by owl 1.
|
|
11122
|
-
* `css` tag has been removed from owl 2 without workaround to manage css.
|
|
11123
|
-
* So, the solution was to import the behavior of owl 1 directly in our
|
|
11124
|
-
* codebase, with one difference: the css is added to the sheet as soon as the
|
|
11125
|
-
* css tag is executed. In owl 1, the css was added as soon as a Component was
|
|
11126
|
-
* created for the first time.
|
|
11127
|
-
*/
|
|
11128
|
-
const STYLESHEETS = {};
|
|
11129
|
-
let nextId = 0;
|
|
11130
|
-
/**
|
|
11131
|
-
* CSS tag helper for defining inline stylesheets. With this, one can simply define
|
|
11132
|
-
* an inline stylesheet with just the following code:
|
|
11133
|
-
* ```js
|
|
11134
|
-
* css`.component-a { color: red; }`;
|
|
11135
|
-
* ```
|
|
11136
|
-
*/
|
|
11137
|
-
function css(strings, ...args) {
|
|
11138
|
-
const name = `__sheet__${nextId++}`;
|
|
11139
|
-
const value = String.raw(strings, ...args);
|
|
11140
|
-
registerSheet(name, value);
|
|
11141
|
-
activateSheet(name);
|
|
11142
|
-
return name;
|
|
11143
|
-
}
|
|
11144
|
-
function processSheet(str) {
|
|
11145
|
-
const tokens = str.split(/(\{|\}|;)/).map((s) => s.trim());
|
|
11146
|
-
const selectorStack = [];
|
|
11147
|
-
const parts = [];
|
|
11148
|
-
let rules = [];
|
|
11149
|
-
function generateSelector(stackIndex, parentSelector) {
|
|
11150
|
-
const parts = [];
|
|
11151
|
-
for (const selector of selectorStack[stackIndex]) {
|
|
11152
|
-
let part = (parentSelector && parentSelector + " " + selector) || selector;
|
|
11153
|
-
if (part.includes("&")) {
|
|
11154
|
-
part = selector.replace(/&/g, parentSelector || "");
|
|
11155
|
-
}
|
|
11156
|
-
if (stackIndex < selectorStack.length - 1) {
|
|
11157
|
-
part = generateSelector(stackIndex + 1, part);
|
|
11158
|
-
}
|
|
11159
|
-
parts.push(part);
|
|
11160
|
-
}
|
|
11161
|
-
return parts.join(", ");
|
|
11162
|
-
}
|
|
11163
|
-
function generateRules() {
|
|
11164
|
-
if (rules.length) {
|
|
11165
|
-
parts.push(generateSelector(0) + " {");
|
|
11166
|
-
parts.push(...rules);
|
|
11167
|
-
parts.push("}");
|
|
11168
|
-
rules = [];
|
|
11169
|
-
}
|
|
11170
|
-
}
|
|
11171
|
-
while (tokens.length) {
|
|
11172
|
-
let token = tokens.shift();
|
|
11173
|
-
if (token === "}") {
|
|
11174
|
-
generateRules();
|
|
11175
|
-
selectorStack.pop();
|
|
11176
|
-
}
|
|
11177
|
-
else {
|
|
11178
|
-
if (tokens[0] === "{") {
|
|
11179
|
-
generateRules();
|
|
11180
|
-
selectorStack.push(token.split(/\s*,\s*/));
|
|
11181
|
-
tokens.shift();
|
|
11182
|
-
}
|
|
11183
|
-
if (tokens[0] === ";") {
|
|
11184
|
-
rules.push(" " + token + ";");
|
|
11185
|
-
}
|
|
11186
|
-
}
|
|
11187
|
-
}
|
|
11188
|
-
return parts.join("\n");
|
|
11189
|
-
}
|
|
11190
|
-
function registerSheet(id, css) {
|
|
11191
|
-
const sheet = document.createElement("style");
|
|
11192
|
-
sheet.textContent = processSheet(css);
|
|
11193
|
-
STYLESHEETS[id] = sheet;
|
|
11194
|
-
}
|
|
11195
|
-
function activateSheet(id) {
|
|
11196
|
-
const sheet = STYLESHEETS[id];
|
|
11197
|
-
sheet.setAttribute("component", id);
|
|
11198
|
-
document.head.appendChild(sheet);
|
|
11199
|
-
}
|
|
11200
|
-
function getTextDecoration({ strikethrough, underline, }) {
|
|
11201
|
-
if (!strikethrough && !underline) {
|
|
11202
|
-
return "none";
|
|
11203
|
-
}
|
|
11204
|
-
return `${strikethrough ? "line-through" : ""} ${underline ? "underline" : ""}`;
|
|
11205
|
-
}
|
|
11206
|
-
/**
|
|
11207
|
-
* Convert the cell style to CSS properties.
|
|
11208
|
-
*/
|
|
11209
|
-
function cellStyleToCss(style) {
|
|
11210
|
-
const attributes = cellTextStyleToCss(style);
|
|
11211
|
-
if (!style)
|
|
11212
|
-
return attributes;
|
|
11213
|
-
if (style.fillColor) {
|
|
11214
|
-
attributes["background"] = style.fillColor;
|
|
11215
|
-
}
|
|
11216
|
-
return attributes;
|
|
11217
|
-
}
|
|
11218
|
-
/**
|
|
11219
|
-
* Convert the cell text style to CSS properties.
|
|
11220
|
-
*/
|
|
11221
|
-
function cellTextStyleToCss(style) {
|
|
11222
|
-
const attributes = {};
|
|
11223
|
-
if (!style)
|
|
11224
|
-
return attributes;
|
|
11225
|
-
if (style.bold) {
|
|
11226
|
-
attributes["font-weight"] = "bold";
|
|
11227
|
-
}
|
|
11228
|
-
if (style.italic) {
|
|
11229
|
-
attributes["font-style"] = "italic";
|
|
11230
|
-
}
|
|
11231
|
-
if (style.strikethrough || style.underline) {
|
|
11232
|
-
let decoration = style.strikethrough ? "line-through" : "";
|
|
11233
|
-
decoration = style.underline ? decoration + " underline" : decoration;
|
|
11234
|
-
attributes["text-decoration"] = decoration;
|
|
11235
|
-
}
|
|
11236
|
-
if (style.textColor) {
|
|
11237
|
-
attributes["color"] = style.textColor;
|
|
11238
|
-
}
|
|
11239
|
-
return attributes;
|
|
11240
|
-
}
|
|
11241
|
-
/**
|
|
11242
|
-
* Transform CSS properties into a CSS string.
|
|
11243
|
-
*/
|
|
11244
|
-
function cssPropertiesToCss(attributes) {
|
|
11245
|
-
let styleStr = "";
|
|
11246
|
-
for (const attName in attributes) {
|
|
11247
|
-
if (!attributes[attName]) {
|
|
11248
|
-
continue;
|
|
11249
|
-
}
|
|
11250
|
-
styleStr += `${attName}:${attributes[attName]}; `;
|
|
11251
|
-
}
|
|
11252
|
-
return styleStr;
|
|
11253
|
-
}
|
|
11254
|
-
function getElementMargins(el) {
|
|
11255
|
-
const style = window.getComputedStyle(el);
|
|
11256
|
-
return {
|
|
11257
|
-
top: parseInt(style.marginTop, 10) || 0,
|
|
11258
|
-
bottom: parseInt(style.marginBottom, 10) || 0,
|
|
11259
|
-
left: parseInt(style.marginLeft, 10) || 0,
|
|
11260
|
-
right: parseInt(style.marginRight, 10) || 0,
|
|
11261
|
-
};
|
|
11262
|
-
}
|
|
11263
|
-
|
|
11264
|
-
css /* scss */ `
|
|
11265
|
-
.o-spreadsheet {
|
|
11266
|
-
.o-chart-custom-tooltip {
|
|
11267
|
-
font-size: 12px;
|
|
11268
|
-
background-color: #fff;
|
|
11269
|
-
z-index: ${ComponentsImportance.FigureTooltip};
|
|
11270
|
-
}
|
|
11271
|
-
}
|
|
11272
|
-
`;
|
|
11273
|
-
class ChartJsComponent extends owl.Component {
|
|
11274
|
-
static template = "o-spreadsheet-ChartJsComponent";
|
|
11275
|
-
static props = {
|
|
11276
|
-
figure: Object,
|
|
11277
|
-
};
|
|
11278
|
-
canvas = owl.useRef("graphContainer");
|
|
11279
|
-
chart;
|
|
11280
|
-
currentRuntime;
|
|
11281
|
-
get background() {
|
|
11282
|
-
return this.chartRuntime.background;
|
|
11283
|
-
}
|
|
11284
|
-
get canvasStyle() {
|
|
11285
|
-
return `background-color: ${this.background}`;
|
|
11286
|
-
}
|
|
11287
|
-
get chartRuntime() {
|
|
11288
|
-
const runtime = this.env.model.getters.getChartRuntime(this.props.figure.id);
|
|
11289
|
-
if (!("chartJsConfig" in runtime)) {
|
|
11290
|
-
throw new Error("Unsupported chart runtime");
|
|
11291
|
-
}
|
|
11292
|
-
return runtime;
|
|
11293
|
-
}
|
|
11294
|
-
setup() {
|
|
11295
|
-
owl.onMounted(() => {
|
|
11296
|
-
const runtime = this.chartRuntime;
|
|
11297
|
-
this.currentRuntime = runtime;
|
|
11298
|
-
// Note: chartJS modify the runtime in place, so it's important to give it a copy
|
|
11299
|
-
this.createChart(deepCopy(runtime.chartJsConfig));
|
|
11300
|
-
});
|
|
11301
|
-
owl.onWillUnmount(() => this.chart?.destroy());
|
|
11302
|
-
owl.useEffect(() => {
|
|
11303
|
-
const runtime = this.chartRuntime;
|
|
11304
|
-
if (runtime !== this.currentRuntime) {
|
|
11305
|
-
if (runtime.chartJsConfig.type !== this.currentRuntime.chartJsConfig.type) {
|
|
11306
|
-
this.chart?.destroy();
|
|
11307
|
-
this.createChart(deepCopy(runtime.chartJsConfig));
|
|
11308
|
-
}
|
|
11309
|
-
else {
|
|
11310
|
-
this.updateChartJs(deepCopy(runtime));
|
|
11311
|
-
}
|
|
11312
|
-
this.currentRuntime = runtime;
|
|
11313
|
-
}
|
|
11314
|
-
});
|
|
11315
|
-
}
|
|
11316
|
-
createChart(chartData) {
|
|
11317
|
-
const canvas = this.canvas.el;
|
|
11318
|
-
const ctx = canvas.getContext("2d");
|
|
11319
|
-
const Chart = getChartJSConstructor();
|
|
11320
|
-
this.chart = new Chart(ctx, chartData);
|
|
11321
|
-
}
|
|
11322
|
-
updateChartJs(chartRuntime) {
|
|
11323
|
-
const chartData = chartRuntime.chartJsConfig;
|
|
11324
|
-
if (chartData.data && chartData.data.datasets) {
|
|
11325
|
-
this.chart.data = chartData.data;
|
|
11326
|
-
if (chartData.options?.plugins?.title) {
|
|
11327
|
-
this.chart.config.options.plugins.title = chartData.options.plugins.title;
|
|
11328
|
-
}
|
|
11329
|
-
}
|
|
11330
|
-
else {
|
|
11331
|
-
this.chart.data.datasets = [];
|
|
11332
|
-
}
|
|
11333
|
-
this.chart.config.options = chartData.options;
|
|
11334
|
-
this.chart.update();
|
|
11335
|
-
}
|
|
11336
|
-
}
|
|
11337
|
-
|
|
11338
10970
|
class ScorecardChart extends owl.Component {
|
|
11339
10971
|
static template = "o-spreadsheet-ScorecardChart";
|
|
11340
10972
|
static props = {
|
|
@@ -22932,6 +22564,343 @@ function getDateIntervals(dates) {
|
|
|
22932
22564
|
|
|
22933
22565
|
const cellPopoverRegistry = new Registry();
|
|
22934
22566
|
|
|
22567
|
+
const GAUGE_PADDING_SIDE = 30;
|
|
22568
|
+
const GAUGE_PADDING_TOP = 10;
|
|
22569
|
+
const GAUGE_PADDING_BOTTOM = 20;
|
|
22570
|
+
const GAUGE_LABELS_FONT_SIZE = 12;
|
|
22571
|
+
const GAUGE_DEFAULT_VALUE_FONT_SIZE = 80;
|
|
22572
|
+
const GAUGE_BACKGROUND_COLOR = "#F3F2F1";
|
|
22573
|
+
const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
|
|
22574
|
+
const GAUGE_TITLE_SECTION_HEIGHT = 25;
|
|
22575
|
+
function drawGaugeChart(canvas, runtime) {
|
|
22576
|
+
const canvasBoundingRect = canvas.getBoundingClientRect();
|
|
22577
|
+
canvas.width = canvasBoundingRect.width;
|
|
22578
|
+
canvas.height = canvasBoundingRect.height;
|
|
22579
|
+
const ctx = canvas.getContext("2d");
|
|
22580
|
+
const config = getGaugeRenderingConfig(canvasBoundingRect, runtime, ctx);
|
|
22581
|
+
drawBackground(ctx, config);
|
|
22582
|
+
drawGauge(ctx, config);
|
|
22583
|
+
drawInflectionValues(ctx, config);
|
|
22584
|
+
drawLabels(ctx, config);
|
|
22585
|
+
drawTitle(ctx, config);
|
|
22586
|
+
}
|
|
22587
|
+
function drawGauge(ctx, config) {
|
|
22588
|
+
ctx.save();
|
|
22589
|
+
const gauge = config.gauge;
|
|
22590
|
+
const arcCenterX = gauge.rect.x + gauge.rect.width / 2;
|
|
22591
|
+
const arcCenterY = gauge.rect.y + gauge.rect.height;
|
|
22592
|
+
const arcRadius = gauge.rect.height - gauge.arcWidth / 2;
|
|
22593
|
+
if (arcRadius < 0) {
|
|
22594
|
+
return;
|
|
22595
|
+
}
|
|
22596
|
+
const gaugeAngle = gauge.percentage === 1 ? 0 : Math.PI * (1 + gauge.percentage);
|
|
22597
|
+
// Gauge background
|
|
22598
|
+
ctx.strokeStyle = GAUGE_BACKGROUND_COLOR;
|
|
22599
|
+
ctx.beginPath();
|
|
22600
|
+
ctx.lineWidth = gauge.arcWidth;
|
|
22601
|
+
ctx.arc(arcCenterX, arcCenterY, arcRadius, gaugeAngle, 0);
|
|
22602
|
+
ctx.stroke();
|
|
22603
|
+
// Gauge value
|
|
22604
|
+
ctx.strokeStyle = gauge.color;
|
|
22605
|
+
ctx.beginPath();
|
|
22606
|
+
ctx.arc(arcCenterX, arcCenterY, arcRadius, Math.PI, gaugeAngle);
|
|
22607
|
+
ctx.stroke();
|
|
22608
|
+
ctx.restore();
|
|
22609
|
+
}
|
|
22610
|
+
function drawBackground(ctx, config) {
|
|
22611
|
+
ctx.save();
|
|
22612
|
+
ctx.fillStyle = config.backgroundColor;
|
|
22613
|
+
ctx.fillRect(0, 0, config.width, config.height);
|
|
22614
|
+
ctx.restore();
|
|
22615
|
+
}
|
|
22616
|
+
function drawLabels(ctx, config) {
|
|
22617
|
+
for (const label of [config.minLabel, config.maxLabel, config.gaugeValue]) {
|
|
22618
|
+
ctx.save();
|
|
22619
|
+
ctx.textAlign = "center";
|
|
22620
|
+
ctx.fillStyle = label.color;
|
|
22621
|
+
ctx.font = `${label.fontSize}px ${DEFAULT_FONT}`;
|
|
22622
|
+
ctx.fillText(label.label, label.textPosition.x, label.textPosition.y);
|
|
22623
|
+
ctx.restore();
|
|
22624
|
+
}
|
|
22625
|
+
}
|
|
22626
|
+
function drawInflectionValues(ctx, config) {
|
|
22627
|
+
const { x: rectX, y: rectY, width, height } = config.gauge.rect;
|
|
22628
|
+
for (const inflectionValue of config.inflectionValues) {
|
|
22629
|
+
ctx.save();
|
|
22630
|
+
ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment
|
|
22631
|
+
ctx.rotate(Math.PI / 2 - inflectionValue.rotation);
|
|
22632
|
+
ctx.lineWidth = 2;
|
|
22633
|
+
ctx.strokeStyle = chartMutedFontColor(config.backgroundColor) + "aa";
|
|
22634
|
+
ctx.beginPath();
|
|
22635
|
+
ctx.moveTo(0, -(height - config.gauge.arcWidth));
|
|
22636
|
+
ctx.lineTo(0, -height - 3);
|
|
22637
|
+
ctx.stroke();
|
|
22638
|
+
ctx.textAlign = "center";
|
|
22639
|
+
ctx.font = `${inflectionValue.fontSize}px ${DEFAULT_FONT}`;
|
|
22640
|
+
ctx.fillStyle = inflectionValue.color;
|
|
22641
|
+
const textY = -height - GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN - inflectionValue.offset;
|
|
22642
|
+
ctx.fillText(inflectionValue.label, 0, textY);
|
|
22643
|
+
ctx.restore();
|
|
22644
|
+
}
|
|
22645
|
+
}
|
|
22646
|
+
function drawTitle(ctx, config) {
|
|
22647
|
+
ctx.save();
|
|
22648
|
+
const title = config.title;
|
|
22649
|
+
ctx.font = getDefaultContextFont(title.fontSize, title.bold, title.italic);
|
|
22650
|
+
ctx.textBaseline = "middle";
|
|
22651
|
+
ctx.fillStyle = title.color;
|
|
22652
|
+
ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
|
|
22653
|
+
ctx.restore();
|
|
22654
|
+
}
|
|
22655
|
+
function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
|
|
22656
|
+
const maxValue = runtime.maxValue;
|
|
22657
|
+
const minValue = runtime.minValue;
|
|
22658
|
+
const gaugeValue = runtime.gaugeValue;
|
|
22659
|
+
const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
|
|
22660
|
+
const gaugeArcWidth = gaugeRect.width / 6;
|
|
22661
|
+
const gaugePercentage = gaugeValue
|
|
22662
|
+
? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
|
|
22663
|
+
: 0;
|
|
22664
|
+
const gaugeValuePosition = {
|
|
22665
|
+
x: boundingRect.width / 2,
|
|
22666
|
+
y: gaugeRect.y + gaugeRect.height - gaugeRect.height / 12,
|
|
22667
|
+
};
|
|
22668
|
+
let gaugeValueFontSize = GAUGE_DEFAULT_VALUE_FONT_SIZE;
|
|
22669
|
+
// Scale down the font size if the gaugeRect is too small
|
|
22670
|
+
if (gaugeRect.height < 300) {
|
|
22671
|
+
gaugeValueFontSize = gaugeValueFontSize * (gaugeRect.height / 300);
|
|
22672
|
+
}
|
|
22673
|
+
// Scale down the font size if the text is too long
|
|
22674
|
+
const maxTextWidth = gaugeRect.width / 2;
|
|
22675
|
+
const gaugeLabel = gaugeValue?.label || "-";
|
|
22676
|
+
if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
|
|
22677
|
+
gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
|
|
22678
|
+
}
|
|
22679
|
+
const minLabelPosition = {
|
|
22680
|
+
x: gaugeRect.x + gaugeArcWidth / 2,
|
|
22681
|
+
y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
|
|
22682
|
+
};
|
|
22683
|
+
const maxLabelPosition = {
|
|
22684
|
+
x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
|
|
22685
|
+
y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
|
|
22686
|
+
};
|
|
22687
|
+
const textColor = chartMutedFontColor(runtime.background);
|
|
22688
|
+
const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);
|
|
22689
|
+
let x = 0, titleWidth = 0, titleHeight = 0;
|
|
22690
|
+
if (runtime.title.text) {
|
|
22691
|
+
({ width: titleWidth, height: titleHeight } = computeTextDimension(ctx, runtime.title.text, { fontSize: CHART_TITLE_FONT_SIZE, ...runtime.title }, "px"));
|
|
22692
|
+
}
|
|
22693
|
+
switch (runtime.title.align) {
|
|
22694
|
+
case "right":
|
|
22695
|
+
x = boundingRect.width - titleWidth - CHART_PADDING$1;
|
|
22696
|
+
break;
|
|
22697
|
+
case "center":
|
|
22698
|
+
x = (boundingRect.width - titleWidth) / 2;
|
|
22699
|
+
break;
|
|
22700
|
+
case "left":
|
|
22701
|
+
default:
|
|
22702
|
+
x = CHART_PADDING$1;
|
|
22703
|
+
break;
|
|
22704
|
+
}
|
|
22705
|
+
return {
|
|
22706
|
+
width: boundingRect.width,
|
|
22707
|
+
height: boundingRect.height,
|
|
22708
|
+
title: {
|
|
22709
|
+
label: runtime.title.text ?? "",
|
|
22710
|
+
fontSize: runtime.title.fontSize ?? CHART_TITLE_FONT_SIZE,
|
|
22711
|
+
textPosition: {
|
|
22712
|
+
x,
|
|
22713
|
+
y: CHART_PADDING_TOP + titleHeight / 2,
|
|
22714
|
+
},
|
|
22715
|
+
color: runtime.title.color ?? textColor,
|
|
22716
|
+
bold: runtime.title.bold,
|
|
22717
|
+
italic: runtime.title.italic,
|
|
22718
|
+
},
|
|
22719
|
+
backgroundColor: runtime.background,
|
|
22720
|
+
gauge: {
|
|
22721
|
+
rect: gaugeRect,
|
|
22722
|
+
arcWidth: gaugeArcWidth,
|
|
22723
|
+
percentage: clip(gaugePercentage, 0, 1),
|
|
22724
|
+
color: getGaugeColor(runtime),
|
|
22725
|
+
},
|
|
22726
|
+
inflectionValues,
|
|
22727
|
+
gaugeValue: {
|
|
22728
|
+
label: gaugeLabel,
|
|
22729
|
+
textPosition: gaugeValuePosition,
|
|
22730
|
+
fontSize: gaugeValueFontSize,
|
|
22731
|
+
color: textColor,
|
|
22732
|
+
},
|
|
22733
|
+
minLabel: {
|
|
22734
|
+
label: runtime.minValue.label,
|
|
22735
|
+
textPosition: minLabelPosition,
|
|
22736
|
+
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
22737
|
+
color: textColor,
|
|
22738
|
+
},
|
|
22739
|
+
maxLabel: {
|
|
22740
|
+
label: runtime.maxValue.label,
|
|
22741
|
+
textPosition: maxLabelPosition,
|
|
22742
|
+
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
22743
|
+
color: textColor,
|
|
22744
|
+
},
|
|
22745
|
+
};
|
|
22746
|
+
}
|
|
22747
|
+
/**
|
|
22748
|
+
* Get the rectangle in which the gauge will be drawn, based on the bounding rectangle of the canvas and leaving
|
|
22749
|
+
* space for the title and labels.
|
|
22750
|
+
*/
|
|
22751
|
+
function getGaugeRect(boundingRect, title) {
|
|
22752
|
+
const titleHeight = title ? GAUGE_TITLE_SECTION_HEIGHT : 0;
|
|
22753
|
+
const drawHeight = boundingRect.height - GAUGE_PADDING_BOTTOM - titleHeight - GAUGE_PADDING_TOP;
|
|
22754
|
+
const drawWidth = boundingRect.width - GAUGE_PADDING_SIDE * 2;
|
|
22755
|
+
let gaugeWidth;
|
|
22756
|
+
let gaugeHeight;
|
|
22757
|
+
if (drawWidth > 2 * drawHeight) {
|
|
22758
|
+
gaugeWidth = 2 * drawHeight;
|
|
22759
|
+
gaugeHeight = drawHeight;
|
|
22760
|
+
}
|
|
22761
|
+
else {
|
|
22762
|
+
gaugeWidth = drawWidth;
|
|
22763
|
+
gaugeHeight = drawWidth / 2;
|
|
22764
|
+
}
|
|
22765
|
+
const gaugeX = GAUGE_PADDING_SIDE + (drawWidth - gaugeWidth) / 2;
|
|
22766
|
+
const gaugeY = titleHeight + GAUGE_PADDING_TOP + (drawHeight - gaugeHeight) / 2;
|
|
22767
|
+
return {
|
|
22768
|
+
x: gaugeX,
|
|
22769
|
+
y: gaugeY,
|
|
22770
|
+
width: gaugeWidth,
|
|
22771
|
+
height: gaugeHeight,
|
|
22772
|
+
};
|
|
22773
|
+
}
|
|
22774
|
+
/**
|
|
22775
|
+
* 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).
|
|
22776
|
+
*
|
|
22777
|
+
* Also compute an offset for the text so that it doesn't overlap with other text.
|
|
22778
|
+
*/
|
|
22779
|
+
function getInflectionValues(runtime, gaugeRect, textColor, ctx) {
|
|
22780
|
+
const maxValue = runtime.maxValue;
|
|
22781
|
+
const minValue = runtime.minValue;
|
|
22782
|
+
const gaugeCircleCenter = {
|
|
22783
|
+
x: gaugeRect.x + gaugeRect.width / 2,
|
|
22784
|
+
y: gaugeRect.y + gaugeRect.height,
|
|
22785
|
+
};
|
|
22786
|
+
const textStyle = { fontSize: GAUGE_LABELS_FONT_SIZE };
|
|
22787
|
+
const inflectionValues = [];
|
|
22788
|
+
const inflectionValuesTextRects = [];
|
|
22789
|
+
for (const inflectionValue of runtime.inflectionValues) {
|
|
22790
|
+
const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
|
|
22791
|
+
const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
|
|
22792
|
+
const angle = Math.PI - Math.PI * percentage;
|
|
22793
|
+
const textRect = getRectangleTangentToCircle(angle, // angle between X axis and the point where the rectangle is tangent to the circle
|
|
22794
|
+
gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, // radius of the gauge circle + margin below text
|
|
22795
|
+
gaugeCircleCenter.x, // center of the gauge circle
|
|
22796
|
+
gaugeCircleCenter.y, // center of the gauge circle
|
|
22797
|
+
labelWidth + 2, // width of the text + some margin
|
|
22798
|
+
GAUGE_LABELS_FONT_SIZE // height of the text
|
|
22799
|
+
);
|
|
22800
|
+
let offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect))
|
|
22801
|
+
? GAUGE_LABELS_FONT_SIZE
|
|
22802
|
+
: 0;
|
|
22803
|
+
inflectionValuesTextRects.push(textRect);
|
|
22804
|
+
inflectionValues.push({
|
|
22805
|
+
rotation: angle,
|
|
22806
|
+
label: inflectionValue.label,
|
|
22807
|
+
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
22808
|
+
color: textColor,
|
|
22809
|
+
offset,
|
|
22810
|
+
});
|
|
22811
|
+
}
|
|
22812
|
+
return inflectionValues;
|
|
22813
|
+
}
|
|
22814
|
+
function getGaugeColor(runtime) {
|
|
22815
|
+
const gaugeValue = runtime.gaugeValue?.value;
|
|
22816
|
+
if (gaugeValue === undefined) {
|
|
22817
|
+
return GAUGE_BACKGROUND_COLOR;
|
|
22818
|
+
}
|
|
22819
|
+
for (let i = 0; i < runtime.inflectionValues.length; i++) {
|
|
22820
|
+
const inflectionValue = runtime.inflectionValues[i];
|
|
22821
|
+
if (inflectionValue.operator === "<" && gaugeValue < inflectionValue.value) {
|
|
22822
|
+
return runtime.colors[i];
|
|
22823
|
+
}
|
|
22824
|
+
else if (inflectionValue.operator === "<=" && gaugeValue <= inflectionValue.value) {
|
|
22825
|
+
return runtime.colors[i];
|
|
22826
|
+
}
|
|
22827
|
+
}
|
|
22828
|
+
return runtime.colors.at(-1);
|
|
22829
|
+
}
|
|
22830
|
+
function getSegmentsOfRectangle(rectangle) {
|
|
22831
|
+
return [
|
|
22832
|
+
{ start: rectangle.topLeft, end: rectangle.topRight },
|
|
22833
|
+
{ start: rectangle.topRight, end: rectangle.bottomRight },
|
|
22834
|
+
{ start: rectangle.bottomRight, end: rectangle.bottomLeft },
|
|
22835
|
+
{ start: rectangle.bottomLeft, end: rectangle.topLeft },
|
|
22836
|
+
];
|
|
22837
|
+
}
|
|
22838
|
+
/**
|
|
22839
|
+
* Check if two segment intersect. The case where the segments are colinear (both segments on the same line)
|
|
22840
|
+
* is not handled.
|
|
22841
|
+
*/
|
|
22842
|
+
function doSegmentIntersect(segment1, segment2) {
|
|
22843
|
+
const A = segment1.start;
|
|
22844
|
+
const B = segment1.end;
|
|
22845
|
+
const C = segment2.start;
|
|
22846
|
+
const D = segment2.end;
|
|
22847
|
+
/**
|
|
22848
|
+
* Line segment intersection algorithm
|
|
22849
|
+
* https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
|
|
22850
|
+
*/
|
|
22851
|
+
function ccw(a, b, c) {
|
|
22852
|
+
return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
|
|
22853
|
+
}
|
|
22854
|
+
return ccw(A, C, D) !== ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D);
|
|
22855
|
+
}
|
|
22856
|
+
function doRectanglesIntersect(rect1, rect2) {
|
|
22857
|
+
const segments1 = getSegmentsOfRectangle(rect1);
|
|
22858
|
+
const segments2 = getSegmentsOfRectangle(rect2);
|
|
22859
|
+
for (const segment1 of segments1) {
|
|
22860
|
+
for (const segment2 of segments2) {
|
|
22861
|
+
if (doSegmentIntersect(segment1, segment2)) {
|
|
22862
|
+
return true;
|
|
22863
|
+
}
|
|
22864
|
+
}
|
|
22865
|
+
}
|
|
22866
|
+
return false;
|
|
22867
|
+
}
|
|
22868
|
+
/**
|
|
22869
|
+
* Get the rectangle that is tangent to a circle at a given angle.
|
|
22870
|
+
*
|
|
22871
|
+
* @param angle angle between X axis and the point where the rectangle is tangent to the circle
|
|
22872
|
+
*/
|
|
22873
|
+
function getRectangleTangentToCircle(angle, radius, circleCenterX, circleCenterY, rectWidth, rectHeight) {
|
|
22874
|
+
const cos = Math.cos(angle);
|
|
22875
|
+
const sin = Math.sin(angle);
|
|
22876
|
+
// x, y are the distance from the center of the circle to the point where the rectangle is tangent to the circle
|
|
22877
|
+
const x = cos * radius;
|
|
22878
|
+
const y = sin * radius;
|
|
22879
|
+
// x2, y2 are the distance from the point the rectangle is tangent to the circle to the bottom left corner of the rectangle
|
|
22880
|
+
const x2 = sin * (rectWidth / 2); // cos(angle + 90°) = sin(angle)
|
|
22881
|
+
const y2 = cos * (rectWidth / 2);
|
|
22882
|
+
const bottomRight = {
|
|
22883
|
+
x: x + x2 + circleCenterX,
|
|
22884
|
+
y: circleCenterY - (y - y2),
|
|
22885
|
+
};
|
|
22886
|
+
const bottomLeft = {
|
|
22887
|
+
x: x - x2 + circleCenterX,
|
|
22888
|
+
y: circleCenterY - (y + y2),
|
|
22889
|
+
};
|
|
22890
|
+
// Same as above but for the top corners of the rectangle (radius + rectangle height instead of radius)
|
|
22891
|
+
const xp = cos * (radius + rectHeight);
|
|
22892
|
+
const yp = sin * (radius + rectHeight);
|
|
22893
|
+
const topLeft = {
|
|
22894
|
+
x: xp - x2 + circleCenterX,
|
|
22895
|
+
y: circleCenterY - (yp + y2),
|
|
22896
|
+
};
|
|
22897
|
+
const topRight = {
|
|
22898
|
+
x: xp + x2 + circleCenterX,
|
|
22899
|
+
y: circleCenterY - (yp - y2),
|
|
22900
|
+
};
|
|
22901
|
+
return { bottomLeft, bottomRight, topRight, topLeft };
|
|
22902
|
+
}
|
|
22903
|
+
|
|
22935
22904
|
class GaugeChartComponent extends owl.Component {
|
|
22936
22905
|
static template = "o-spreadsheet-GaugeChartComponent";
|
|
22937
22906
|
canvas = owl.useRef("chartContainer");
|
|
@@ -22964,6 +22933,73 @@ function toXlsxHexColor(color) {
|
|
|
22964
22933
|
return color;
|
|
22965
22934
|
}
|
|
22966
22935
|
|
|
22936
|
+
const CHART_COMMON_OPTIONS = {
|
|
22937
|
+
// https://www.chartjs.org/docs/latest/general/responsive.html
|
|
22938
|
+
responsive: true, // will resize when its container is resized
|
|
22939
|
+
maintainAspectRatio: false, // doesn't maintain the aspect ratio (width/height =2 by default) so the user has the choice of the exact layout
|
|
22940
|
+
elements: {
|
|
22941
|
+
line: {
|
|
22942
|
+
fill: false, // do not fill the area under line charts
|
|
22943
|
+
},
|
|
22944
|
+
point: {
|
|
22945
|
+
hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
|
|
22946
|
+
},
|
|
22947
|
+
},
|
|
22948
|
+
animation: false,
|
|
22949
|
+
};
|
|
22950
|
+
function chartToImage(runtime, figure, type) {
|
|
22951
|
+
// wrap the canvas in a div with a fixed size because chart.js would
|
|
22952
|
+
// fill the whole page otherwise
|
|
22953
|
+
const div = document.createElement("div");
|
|
22954
|
+
div.style.width = `${figure.width}px`;
|
|
22955
|
+
div.style.height = `${figure.height}px`;
|
|
22956
|
+
const canvas = document.createElement("canvas");
|
|
22957
|
+
div.append(canvas);
|
|
22958
|
+
canvas.setAttribute("width", figure.width.toString());
|
|
22959
|
+
canvas.setAttribute("height", figure.height.toString());
|
|
22960
|
+
// we have to add the canvas to the DOM otherwise it won't be rendered
|
|
22961
|
+
document.body.append(div);
|
|
22962
|
+
if ("chartJsConfig" in runtime) {
|
|
22963
|
+
const config = deepCopy(runtime.chartJsConfig);
|
|
22964
|
+
config.plugins = [backgroundColorChartJSPlugin];
|
|
22965
|
+
const Chart = getChartJSConstructor();
|
|
22966
|
+
const chart = new Chart(canvas, config);
|
|
22967
|
+
const imgContent = chart.toBase64Image();
|
|
22968
|
+
chart.destroy();
|
|
22969
|
+
div.remove();
|
|
22970
|
+
return imgContent;
|
|
22971
|
+
}
|
|
22972
|
+
else if (type === "scorecard") {
|
|
22973
|
+
const design = getScorecardConfiguration(figure, runtime);
|
|
22974
|
+
drawScoreChart(design, canvas);
|
|
22975
|
+
const imgContent = canvas.toDataURL();
|
|
22976
|
+
div.remove();
|
|
22977
|
+
return imgContent;
|
|
22978
|
+
}
|
|
22979
|
+
else if (type === "gauge") {
|
|
22980
|
+
drawGaugeChart(canvas, runtime);
|
|
22981
|
+
const imgContent = canvas.toDataURL();
|
|
22982
|
+
div.remove();
|
|
22983
|
+
return imgContent;
|
|
22984
|
+
}
|
|
22985
|
+
return undefined;
|
|
22986
|
+
}
|
|
22987
|
+
/**
|
|
22988
|
+
* Custom chart.js plugin to set the background color of the canvas
|
|
22989
|
+
* https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md
|
|
22990
|
+
*/
|
|
22991
|
+
const backgroundColorChartJSPlugin = {
|
|
22992
|
+
id: "customCanvasBackgroundColor",
|
|
22993
|
+
beforeDraw: (chart) => {
|
|
22994
|
+
const { ctx } = chart;
|
|
22995
|
+
ctx.save();
|
|
22996
|
+
ctx.globalCompositeOperation = "destination-over";
|
|
22997
|
+
ctx.fillStyle = "#ffffff";
|
|
22998
|
+
ctx.fillRect(0, 0, chart.width, chart.height);
|
|
22999
|
+
ctx.restore();
|
|
23000
|
+
},
|
|
23001
|
+
};
|
|
23002
|
+
|
|
22967
23003
|
/**
|
|
22968
23004
|
* Represent a raw XML string
|
|
22969
23005
|
*/
|
|
@@ -34234,7 +34270,6 @@ var CHART_HELPERS = /*#__PURE__*/Object.freeze({
|
|
|
34234
34270
|
duplicateLabelRangeInDuplicatedSheet: duplicateLabelRangeInDuplicatedSheet,
|
|
34235
34271
|
formatChartDatasetValue: formatChartDatasetValue,
|
|
34236
34272
|
formatTickValue: formatTickValue,
|
|
34237
|
-
getChartJSConstructor: getChartJSConstructor,
|
|
34238
34273
|
getChartPositionAtCenterOfViewport: getChartPositionAtCenterOfViewport,
|
|
34239
34274
|
getDefinedAxis: getDefinedAxis,
|
|
34240
34275
|
getPieColors: getPieColors,
|
|
@@ -45082,7 +45117,8 @@ css /* scss */ `
|
|
|
45082
45117
|
&.pivot-dimension-invalid {
|
|
45083
45118
|
background-color: #ffdddd;
|
|
45084
45119
|
border-color: red !important;
|
|
45085
|
-
select
|
|
45120
|
+
select,
|
|
45121
|
+
input {
|
|
45086
45122
|
background-color: #ffdddd;
|
|
45087
45123
|
}
|
|
45088
45124
|
}
|
|
@@ -46943,7 +46979,7 @@ class PivotSidePanelStore extends SpreadsheetStore {
|
|
|
46943
46979
|
this.notification.notifyUser({
|
|
46944
46980
|
type: "info",
|
|
46945
46981
|
text: _t("Pivot updates only work with dynamic pivot tables. Use %s or re-insert the static pivot from the Data menu.", pivotExample),
|
|
46946
|
-
sticky:
|
|
46982
|
+
sticky: true,
|
|
46947
46983
|
});
|
|
46948
46984
|
}
|
|
46949
46985
|
}
|
|
@@ -53295,6 +53331,15 @@ class BordersPlugin extends CorePlugin {
|
|
|
53295
53331
|
case "SET_BORDER":
|
|
53296
53332
|
this.setBorder(cmd.sheetId, cmd.col, cmd.row, cmd.border);
|
|
53297
53333
|
break;
|
|
53334
|
+
case "SET_BORDERS_ON_TARGET":
|
|
53335
|
+
for (const zone of cmd.target) {
|
|
53336
|
+
for (let row = zone.top; row <= zone.bottom; row++) {
|
|
53337
|
+
for (let col = zone.left; col <= zone.right; col++) {
|
|
53338
|
+
this.setBorder(cmd.sheetId, col, row, cmd.border);
|
|
53339
|
+
}
|
|
53340
|
+
}
|
|
53341
|
+
}
|
|
53342
|
+
break;
|
|
53298
53343
|
case "SET_ZONE_BORDERS":
|
|
53299
53344
|
if (cmd.border) {
|
|
53300
53345
|
const target = cmd.target.map((zone) => this.getters.expandZone(cmd.sheetId, zone));
|
|
@@ -63057,25 +63102,6 @@ class AutofillPlugin extends UIPlugin {
|
|
|
63057
63102
|
case "AUTOFILL_AUTO":
|
|
63058
63103
|
this.autofillAuto();
|
|
63059
63104
|
break;
|
|
63060
|
-
case "AUTOFILL_CELL":
|
|
63061
|
-
this.autoFillMerge(cmd.originCol, cmd.originRow, cmd.col, cmd.row);
|
|
63062
|
-
const sheetId = this.getters.getActiveSheetId();
|
|
63063
|
-
this.dispatch("UPDATE_CELL", {
|
|
63064
|
-
sheetId,
|
|
63065
|
-
col: cmd.col,
|
|
63066
|
-
row: cmd.row,
|
|
63067
|
-
style: cmd.style || null,
|
|
63068
|
-
content: cmd.content || "",
|
|
63069
|
-
format: cmd.format || "",
|
|
63070
|
-
});
|
|
63071
|
-
this.dispatch("SET_BORDER", {
|
|
63072
|
-
sheetId,
|
|
63073
|
-
col: cmd.col,
|
|
63074
|
-
row: cmd.row,
|
|
63075
|
-
border: cmd.border,
|
|
63076
|
-
});
|
|
63077
|
-
this.autofillCF(cmd.originCol, cmd.originRow, cmd.col, cmd.row);
|
|
63078
|
-
this.autofillDV(cmd.originCol, cmd.originRow, cmd.col, cmd.row);
|
|
63079
63105
|
}
|
|
63080
63106
|
}
|
|
63081
63107
|
// ---------------------------------------------------------------------------
|
|
@@ -63099,6 +63125,7 @@ class AutofillPlugin extends UIPlugin {
|
|
|
63099
63125
|
}
|
|
63100
63126
|
const source = this.getters.getSelectedZone();
|
|
63101
63127
|
const target = this.autofillZone;
|
|
63128
|
+
const autofillCellsData = [];
|
|
63102
63129
|
switch (this.direction) {
|
|
63103
63130
|
case "down" /* DIRECTION.DOWN */:
|
|
63104
63131
|
for (let col = source.left; col <= source.right; col++) {
|
|
@@ -63108,7 +63135,7 @@ class AutofillPlugin extends UIPlugin {
|
|
|
63108
63135
|
}
|
|
63109
63136
|
const generator = this.createGenerator(xcs);
|
|
63110
63137
|
for (let row = target.top; row <= target.bottom; row++) {
|
|
63111
|
-
this.computeNewCell(generator, col, row
|
|
63138
|
+
autofillCellsData.push(this.computeNewCell(generator, col, row));
|
|
63112
63139
|
}
|
|
63113
63140
|
}
|
|
63114
63141
|
break;
|
|
@@ -63120,7 +63147,7 @@ class AutofillPlugin extends UIPlugin {
|
|
|
63120
63147
|
}
|
|
63121
63148
|
const generator = this.createGenerator(xcs);
|
|
63122
63149
|
for (let row = target.bottom; row >= target.top; row--) {
|
|
63123
|
-
this.computeNewCell(generator, col, row
|
|
63150
|
+
autofillCellsData.push(this.computeNewCell(generator, col, row));
|
|
63124
63151
|
}
|
|
63125
63152
|
}
|
|
63126
63153
|
break;
|
|
@@ -63132,7 +63159,7 @@ class AutofillPlugin extends UIPlugin {
|
|
|
63132
63159
|
}
|
|
63133
63160
|
const generator = this.createGenerator(xcs);
|
|
63134
63161
|
for (let col = target.right; col >= target.left; col--) {
|
|
63135
|
-
this.computeNewCell(generator, col, row
|
|
63162
|
+
autofillCellsData.push(this.computeNewCell(generator, col, row));
|
|
63136
63163
|
}
|
|
63137
63164
|
}
|
|
63138
63165
|
break;
|
|
@@ -63144,12 +63171,26 @@ class AutofillPlugin extends UIPlugin {
|
|
|
63144
63171
|
}
|
|
63145
63172
|
const generator = this.createGenerator(xcs);
|
|
63146
63173
|
for (let col = target.left; col <= target.right; col++) {
|
|
63147
|
-
this.computeNewCell(generator, col, row
|
|
63174
|
+
autofillCellsData.push(this.computeNewCell(generator, col, row));
|
|
63148
63175
|
}
|
|
63149
63176
|
}
|
|
63150
63177
|
break;
|
|
63151
63178
|
}
|
|
63152
63179
|
if (apply) {
|
|
63180
|
+
const bordersZones = {};
|
|
63181
|
+
const cfNewRanges = {};
|
|
63182
|
+
const dvNewZones = {};
|
|
63183
|
+
const sheetId = this.getters.getActiveSheetId();
|
|
63184
|
+
for (const data of autofillCellsData) {
|
|
63185
|
+
this.collectBordersData(data, bordersZones);
|
|
63186
|
+
this.autofillMerge(sheetId, data);
|
|
63187
|
+
this.autofillCell(sheetId, data);
|
|
63188
|
+
this.collectConditionalFormatsData(sheetId, data, cfNewRanges);
|
|
63189
|
+
this.collectDataValidationsData(sheetId, data, dvNewZones);
|
|
63190
|
+
}
|
|
63191
|
+
this.autofillBorders(sheetId, bordersZones);
|
|
63192
|
+
this.autofillConditionalFormats(sheetId, cfNewRanges);
|
|
63193
|
+
this.autofillDataValidations(sheetId, dvNewZones);
|
|
63153
63194
|
this.autofillZone = undefined;
|
|
63154
63195
|
this.selection.resizeAnchorZone(this.direction, this.steps);
|
|
63155
63196
|
this.lastCellSelected = {};
|
|
@@ -63158,6 +63199,95 @@ class AutofillPlugin extends UIPlugin {
|
|
|
63158
63199
|
this.tooltip = undefined;
|
|
63159
63200
|
}
|
|
63160
63201
|
}
|
|
63202
|
+
collectBordersData(data, bordersPositions) {
|
|
63203
|
+
const key = JSON.stringify(data.border);
|
|
63204
|
+
if (!(key in bordersPositions)) {
|
|
63205
|
+
bordersPositions[key] = [];
|
|
63206
|
+
}
|
|
63207
|
+
bordersPositions[key].push(positionToZone({ col: data.col, row: data.row }));
|
|
63208
|
+
}
|
|
63209
|
+
collectConditionalFormatsData(sheetId, data, cfNewRanges) {
|
|
63210
|
+
const { originCol, originRow, col, row } = data;
|
|
63211
|
+
const cfsAtOrigin = this.getters.getRulesByCell(sheetId, originCol, originRow);
|
|
63212
|
+
const xc = toXC(col, row);
|
|
63213
|
+
for (const cf of cfsAtOrigin) {
|
|
63214
|
+
if (!(cf.id in cfNewRanges)) {
|
|
63215
|
+
cfNewRanges[cf.id] = [];
|
|
63216
|
+
}
|
|
63217
|
+
cfNewRanges[cf.id].push(xc);
|
|
63218
|
+
}
|
|
63219
|
+
}
|
|
63220
|
+
collectDataValidationsData(sheetId, data, dvNewZones) {
|
|
63221
|
+
const { originCol, originRow, col, row } = data;
|
|
63222
|
+
const cellPosition = { sheetId, col: originCol, row: originRow };
|
|
63223
|
+
const dvsAtOrigin = this.getters.getValidationRuleForCell(cellPosition);
|
|
63224
|
+
if (!dvsAtOrigin) {
|
|
63225
|
+
return;
|
|
63226
|
+
}
|
|
63227
|
+
if (!(dvsAtOrigin.id in dvNewZones)) {
|
|
63228
|
+
dvNewZones[dvsAtOrigin.id] = [];
|
|
63229
|
+
}
|
|
63230
|
+
dvNewZones[dvsAtOrigin.id].push(positionToZone({ col, row }));
|
|
63231
|
+
}
|
|
63232
|
+
autofillCell(sheetId, data) {
|
|
63233
|
+
this.dispatch("UPDATE_CELL", {
|
|
63234
|
+
sheetId,
|
|
63235
|
+
col: data.col,
|
|
63236
|
+
row: data.row,
|
|
63237
|
+
content: data.content || "",
|
|
63238
|
+
style: data.style || null,
|
|
63239
|
+
format: data.format || "",
|
|
63240
|
+
});
|
|
63241
|
+
// Still usefull in odoo ATM to autofill field sync
|
|
63242
|
+
this.dispatch("AUTOFILL_CELL", data);
|
|
63243
|
+
}
|
|
63244
|
+
autofillBorders(sheetId, bordersPositions) {
|
|
63245
|
+
for (const stringifiedBorder in bordersPositions) {
|
|
63246
|
+
const border = stringifiedBorder === "undefined" ? undefined : JSON.parse(stringifiedBorder);
|
|
63247
|
+
this.dispatch("SET_BORDERS_ON_TARGET", {
|
|
63248
|
+
sheetId,
|
|
63249
|
+
border,
|
|
63250
|
+
target: recomputeZones(bordersPositions[stringifiedBorder]),
|
|
63251
|
+
});
|
|
63252
|
+
}
|
|
63253
|
+
}
|
|
63254
|
+
autofillConditionalFormats(sheetId, cfNewRanges) {
|
|
63255
|
+
for (const cfId in cfNewRanges) {
|
|
63256
|
+
const changes = cfNewRanges[cfId];
|
|
63257
|
+
const cf = this.getters.getConditionalFormats(sheetId).find((cf) => cf.id === cfId);
|
|
63258
|
+
if (!cf) {
|
|
63259
|
+
continue;
|
|
63260
|
+
}
|
|
63261
|
+
const newCfRanges = this.getters.getAdaptedCfRanges(sheetId, cf, changes.map(toZone), []);
|
|
63262
|
+
if (newCfRanges) {
|
|
63263
|
+
this.dispatch("ADD_CONDITIONAL_FORMAT", {
|
|
63264
|
+
cf: {
|
|
63265
|
+
id: cf.id,
|
|
63266
|
+
rule: cf.rule,
|
|
63267
|
+
stopIfTrue: cf.stopIfTrue,
|
|
63268
|
+
},
|
|
63269
|
+
ranges: newCfRanges,
|
|
63270
|
+
sheetId,
|
|
63271
|
+
});
|
|
63272
|
+
}
|
|
63273
|
+
}
|
|
63274
|
+
}
|
|
63275
|
+
autofillDataValidations(sheetId, dvNewZones) {
|
|
63276
|
+
for (const dvId in dvNewZones) {
|
|
63277
|
+
const changes = dvNewZones[dvId];
|
|
63278
|
+
const dvOrigin = this.getters.getDataValidationRule(sheetId, dvId);
|
|
63279
|
+
if (!dvOrigin) {
|
|
63280
|
+
continue;
|
|
63281
|
+
}
|
|
63282
|
+
const dvRangesXcs = dvOrigin.ranges.map((range) => range.zone);
|
|
63283
|
+
const newDvRanges = recomputeZones(dvRangesXcs.concat(changes), []);
|
|
63284
|
+
this.dispatch("ADD_DATA_VALIDATION_RULE", {
|
|
63285
|
+
rule: dvOrigin,
|
|
63286
|
+
ranges: newDvRanges.map((zone) => this.getters.getRangeDataFromZone(sheetId, zone)),
|
|
63287
|
+
sheetId,
|
|
63288
|
+
});
|
|
63289
|
+
}
|
|
63290
|
+
}
|
|
63161
63291
|
/**
|
|
63162
63292
|
* Select a cell which becomes the last cell of the autofillZone
|
|
63163
63293
|
*/
|
|
@@ -63236,22 +63366,20 @@ class AutofillPlugin extends UIPlugin {
|
|
|
63236
63366
|
/**
|
|
63237
63367
|
* Generate the next cell
|
|
63238
63368
|
*/
|
|
63239
|
-
computeNewCell(generator, col, row
|
|
63369
|
+
computeNewCell(generator, col, row) {
|
|
63240
63370
|
const { cellData, tooltip, origin } = generator.next();
|
|
63241
63371
|
const { content, style, border, format } = cellData;
|
|
63242
63372
|
this.tooltip = tooltip;
|
|
63243
|
-
|
|
63244
|
-
|
|
63245
|
-
|
|
63246
|
-
|
|
63247
|
-
|
|
63248
|
-
|
|
63249
|
-
|
|
63250
|
-
|
|
63251
|
-
|
|
63252
|
-
|
|
63253
|
-
});
|
|
63254
|
-
}
|
|
63373
|
+
return {
|
|
63374
|
+
originCol: origin.col,
|
|
63375
|
+
originRow: origin.row,
|
|
63376
|
+
col,
|
|
63377
|
+
row,
|
|
63378
|
+
content,
|
|
63379
|
+
style,
|
|
63380
|
+
border,
|
|
63381
|
+
format,
|
|
63382
|
+
};
|
|
63255
63383
|
}
|
|
63256
63384
|
/**
|
|
63257
63385
|
* Get the rule associated to the current cell
|
|
@@ -63319,8 +63447,8 @@ class AutofillPlugin extends UIPlugin {
|
|
|
63319
63447
|
? position[first].value
|
|
63320
63448
|
: position[second].value;
|
|
63321
63449
|
}
|
|
63322
|
-
|
|
63323
|
-
const
|
|
63450
|
+
autofillMerge(sheetId, data) {
|
|
63451
|
+
const { originCol, originRow, col, row } = data;
|
|
63324
63452
|
const position = { sheetId, col, row };
|
|
63325
63453
|
const originPosition = { sheetId, col: originCol, row: originRow };
|
|
63326
63454
|
if (this.getters.isInMerge(position) && !this.getters.isInMerge(originPosition)) {
|
|
@@ -63347,35 +63475,6 @@ class AutofillPlugin extends UIPlugin {
|
|
|
63347
63475
|
});
|
|
63348
63476
|
}
|
|
63349
63477
|
}
|
|
63350
|
-
autofillCF(originCol, originRow, col, row) {
|
|
63351
|
-
const sheetId = this.getters.getActiveSheetId();
|
|
63352
|
-
const cfOrigin = this.getters.getRulesByCell(sheetId, originCol, originRow);
|
|
63353
|
-
for (const cf of cfOrigin) {
|
|
63354
|
-
const newCfRanges = this.getters.getAdaptedCfRanges(sheetId, cf, [positionToZone({ col, row })], []);
|
|
63355
|
-
if (newCfRanges) {
|
|
63356
|
-
this.dispatch("ADD_CONDITIONAL_FORMAT", {
|
|
63357
|
-
cf: deepCopy(cf),
|
|
63358
|
-
ranges: newCfRanges,
|
|
63359
|
-
sheetId,
|
|
63360
|
-
});
|
|
63361
|
-
}
|
|
63362
|
-
}
|
|
63363
|
-
}
|
|
63364
|
-
autofillDV(originCol, originRow, col, row) {
|
|
63365
|
-
const sheetId = this.getters.getActiveSheetId();
|
|
63366
|
-
const cellPosition = { sheetId, col: originCol, row: originRow };
|
|
63367
|
-
const dvOrigin = this.getters.getValidationRuleForCell(cellPosition);
|
|
63368
|
-
if (!dvOrigin) {
|
|
63369
|
-
return;
|
|
63370
|
-
}
|
|
63371
|
-
const dvRangesZones = dvOrigin.ranges.map((range) => range.zone);
|
|
63372
|
-
const newDvRanges = recomputeZones(dvRangesZones.concat(positionToZone({ col, row })), []);
|
|
63373
|
-
this.dispatch("ADD_DATA_VALIDATION_RULE", {
|
|
63374
|
-
rule: dvOrigin,
|
|
63375
|
-
ranges: newDvRanges.map((zone) => this.getters.getRangeDataFromZone(sheetId, zone)),
|
|
63376
|
-
sheetId,
|
|
63377
|
-
});
|
|
63378
|
-
}
|
|
63379
63478
|
// ---------------------------------------------------------------------------
|
|
63380
63479
|
// Grid rendering
|
|
63381
63480
|
// ---------------------------------------------------------------------------
|
|
@@ -75890,6 +75989,7 @@ const registries = {
|
|
|
75890
75989
|
supportedPivotPositionalFormulaRegistry,
|
|
75891
75990
|
pivotToFunctionValueRegistry,
|
|
75892
75991
|
migrationStepRegistry,
|
|
75992
|
+
chartJsExtensionRegistry,
|
|
75893
75993
|
};
|
|
75894
75994
|
const helpers = {
|
|
75895
75995
|
arg,
|
|
@@ -76090,6 +76190,6 @@ exports.tokenColors = tokenColors;
|
|
|
76090
76190
|
exports.tokenize = tokenize;
|
|
76091
76191
|
|
|
76092
76192
|
|
|
76093
|
-
__info__.version = "18.2.
|
|
76094
|
-
__info__.date = "2025-
|
|
76095
|
-
__info__.hash = "
|
|
76193
|
+
__info__.version = "18.2.7";
|
|
76194
|
+
__info__.date = "2025-04-14T17:19:31.011Z";
|
|
76195
|
+
__info__.hash = "e187958";
|