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