@odoo/o-spreadsheet 17.4.24 → 17.4.26
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 +934 -785
- package/dist/o-spreadsheet.d.ts +8 -2
- package/dist/o-spreadsheet.esm.js +934 -785
- package/dist/o-spreadsheet.iife.js +934 -785
- package/dist/o-spreadsheet.iife.min.js +368 -346
- package/dist/o_spreadsheet.xml +17 -11
- 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 17.4.
|
|
6
|
-
* @date 2025-
|
|
7
|
-
* @hash
|
|
5
|
+
* @version 17.4.26
|
|
6
|
+
* @date 2025-03-12T15:31:45.184Z
|
|
7
|
+
* @hash a18429e
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
'use strict';
|
|
@@ -5059,8 +5059,9 @@ function spreadRange(getters, dataSets) {
|
|
|
5059
5059
|
if (zone.bottom !== zone.top && zone.left != zone.right) {
|
|
5060
5060
|
if (zone.right) {
|
|
5061
5061
|
for (let j = zone.left; j <= zone.right; ++j) {
|
|
5062
|
+
const datasetOptions = j === zone.left ? dataSet : { yAxisId: dataSet.yAxisId };
|
|
5062
5063
|
postProcessedRanges.push({
|
|
5063
|
-
...
|
|
5064
|
+
...datasetOptions,
|
|
5064
5065
|
dataRange: `${sheetPrefix}${zoneToXc({
|
|
5065
5066
|
left: j,
|
|
5066
5067
|
right: j,
|
|
@@ -5072,8 +5073,9 @@ function spreadRange(getters, dataSets) {
|
|
|
5072
5073
|
}
|
|
5073
5074
|
else {
|
|
5074
5075
|
for (let j = zone.top; j <= zone.bottom; ++j) {
|
|
5076
|
+
const datasetOptions = j === zone.top ? dataSet : { yAxisId: dataSet.yAxisId };
|
|
5075
5077
|
postProcessedRanges.push({
|
|
5076
|
-
...
|
|
5078
|
+
...datasetOptions,
|
|
5077
5079
|
dataRange: `${sheetPrefix}${zoneToXc({
|
|
5078
5080
|
left: zone.left,
|
|
5079
5081
|
right: zone.right,
|
|
@@ -10185,6 +10187,15 @@ class ComposerStore extends SpreadsheetStore {
|
|
|
10185
10187
|
const exactMatch = proposals?.find((p) => p.text === tokenAtCursor.value);
|
|
10186
10188
|
// remove tokens that are likely to be other parts of the formula that slipped in the token if it's a string
|
|
10187
10189
|
const searchTerm = tokenAtCursor.value.replace(/[ ,\(\)]/g, "");
|
|
10190
|
+
if (this._currentContent === this.initialContent &&
|
|
10191
|
+
provider.displayAllOnInitialContent &&
|
|
10192
|
+
proposals?.length) {
|
|
10193
|
+
return {
|
|
10194
|
+
proposals,
|
|
10195
|
+
selectProposal: provider.selectProposal,
|
|
10196
|
+
autoSelectFirstProposal: provider.autoSelectFirstProposal ?? false,
|
|
10197
|
+
};
|
|
10198
|
+
}
|
|
10188
10199
|
if (exactMatch && this._currentContent !== this.initialContent) {
|
|
10189
10200
|
// this means the user has chosen a proposal
|
|
10190
10201
|
return;
|
|
@@ -10832,70 +10843,347 @@ function getNextNonEmptyBar(bars, startIndex) {
|
|
|
10832
10843
|
return bars.find((bar, i) => i > startIndex && bar.height !== 0);
|
|
10833
10844
|
}
|
|
10834
10845
|
|
|
10835
|
-
|
|
10836
|
-
|
|
10837
|
-
|
|
10838
|
-
|
|
10839
|
-
|
|
10840
|
-
|
|
10846
|
+
const GAUGE_PADDING_SIDE = 30;
|
|
10847
|
+
const GAUGE_PADDING_TOP = 10;
|
|
10848
|
+
const GAUGE_PADDING_BOTTOM = 20;
|
|
10849
|
+
const GAUGE_LABELS_FONT_SIZE = 12;
|
|
10850
|
+
const GAUGE_DEFAULT_VALUE_FONT_SIZE = 80;
|
|
10851
|
+
const GAUGE_BACKGROUND_COLOR = "#F3F2F1";
|
|
10852
|
+
const GAUGE_TEXT_COLOR = "#666666";
|
|
10853
|
+
const GAUGE_TEXT_COLOR_HIGH_CONTRAST = "#C8C8C8";
|
|
10854
|
+
const GAUGE_INFLECTION_MARKER_COLOR = "#666666aa";
|
|
10855
|
+
const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
|
|
10856
|
+
const GAUGE_TITLE_SECTION_HEIGHT = 25;
|
|
10857
|
+
const GAUGE_TITLE_FONT_SIZE = DEFAULT_CHART_FONT_SIZE;
|
|
10858
|
+
const GAUGE_TITLE_PADDING_LEFT = DEFAULT_CHART_PADDING;
|
|
10859
|
+
const GAUGE_TITLE_PADDING_TOP = DEFAULT_CHART_PADDING;
|
|
10860
|
+
function drawGaugeChart(canvas, runtime) {
|
|
10861
|
+
const canvasBoundingRect = canvas.getBoundingClientRect();
|
|
10862
|
+
canvas.width = canvasBoundingRect.width;
|
|
10863
|
+
canvas.height = canvasBoundingRect.height;
|
|
10864
|
+
const ctx = canvas.getContext("2d");
|
|
10865
|
+
const config = getGaugeRenderingConfig(canvasBoundingRect, runtime, ctx);
|
|
10866
|
+
drawBackground(ctx, config);
|
|
10867
|
+
drawGauge(ctx, config);
|
|
10868
|
+
drawInflectionValues(ctx, config);
|
|
10869
|
+
drawLabels(ctx, config);
|
|
10870
|
+
drawTitle(ctx, config);
|
|
10871
|
+
}
|
|
10872
|
+
function drawGauge(ctx, config) {
|
|
10873
|
+
ctx.save();
|
|
10874
|
+
const gauge = config.gauge;
|
|
10875
|
+
const arcCenterX = gauge.rect.x + gauge.rect.width / 2;
|
|
10876
|
+
const arcCenterY = gauge.rect.y + gauge.rect.height;
|
|
10877
|
+
const arcRadius = gauge.rect.height - gauge.arcWidth / 2;
|
|
10878
|
+
if (arcRadius < 0) {
|
|
10879
|
+
return;
|
|
10880
|
+
}
|
|
10881
|
+
const gaugeAngle = gauge.percentage === 1 ? 0 : Math.PI * (1 + gauge.percentage);
|
|
10882
|
+
// Gauge background
|
|
10883
|
+
ctx.strokeStyle = GAUGE_BACKGROUND_COLOR;
|
|
10884
|
+
ctx.beginPath();
|
|
10885
|
+
ctx.lineWidth = gauge.arcWidth;
|
|
10886
|
+
ctx.arc(arcCenterX, arcCenterY, arcRadius, gaugeAngle, 0);
|
|
10887
|
+
ctx.stroke();
|
|
10888
|
+
// Gauge value
|
|
10889
|
+
ctx.strokeStyle = gauge.color;
|
|
10890
|
+
ctx.beginPath();
|
|
10891
|
+
ctx.arc(arcCenterX, arcCenterY, arcRadius, Math.PI, gaugeAngle);
|
|
10892
|
+
ctx.stroke();
|
|
10893
|
+
ctx.restore();
|
|
10894
|
+
}
|
|
10895
|
+
function drawBackground(ctx, config) {
|
|
10896
|
+
ctx.save();
|
|
10897
|
+
ctx.fillStyle = config.backgroundColor;
|
|
10898
|
+
ctx.fillRect(0, 0, config.width, config.height);
|
|
10899
|
+
ctx.restore();
|
|
10900
|
+
}
|
|
10901
|
+
function drawLabels(ctx, config) {
|
|
10902
|
+
for (const label of [config.minLabel, config.maxLabel, config.gaugeValue]) {
|
|
10903
|
+
ctx.save();
|
|
10904
|
+
ctx.textAlign = "center";
|
|
10905
|
+
ctx.fillStyle = label.color;
|
|
10906
|
+
ctx.font = `${label.fontSize}px ${DEFAULT_FONT}`;
|
|
10907
|
+
ctx.fillText(label.label, label.textPosition.x, label.textPosition.y);
|
|
10908
|
+
ctx.restore();
|
|
10909
|
+
}
|
|
10910
|
+
}
|
|
10911
|
+
function drawInflectionValues(ctx, config) {
|
|
10912
|
+
const { x: rectX, y: rectY, width, height } = config.gauge.rect;
|
|
10913
|
+
for (const inflectionValue of config.inflectionValues) {
|
|
10914
|
+
ctx.save();
|
|
10915
|
+
ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment
|
|
10916
|
+
ctx.rotate(Math.PI / 2 - inflectionValue.rotation);
|
|
10917
|
+
ctx.lineWidth = 2;
|
|
10918
|
+
ctx.strokeStyle = GAUGE_INFLECTION_MARKER_COLOR;
|
|
10919
|
+
ctx.beginPath();
|
|
10920
|
+
ctx.moveTo(0, -(height - config.gauge.arcWidth));
|
|
10921
|
+
ctx.lineTo(0, -height - 3);
|
|
10922
|
+
ctx.stroke();
|
|
10923
|
+
ctx.textAlign = "center";
|
|
10924
|
+
ctx.font = `${inflectionValue.fontSize}px ${DEFAULT_FONT}`;
|
|
10925
|
+
ctx.fillStyle = inflectionValue.color;
|
|
10926
|
+
const textY = -height - GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN - inflectionValue.offset;
|
|
10927
|
+
ctx.fillText(inflectionValue.label, 0, textY);
|
|
10928
|
+
ctx.restore();
|
|
10929
|
+
}
|
|
10930
|
+
}
|
|
10931
|
+
function drawTitle(ctx, config) {
|
|
10932
|
+
ctx.save();
|
|
10933
|
+
const title = config.title;
|
|
10934
|
+
ctx.font = getDefaultContextFont(title.fontSize, title.bold, title.italic);
|
|
10935
|
+
ctx.textBaseline = "middle";
|
|
10936
|
+
ctx.fillStyle = title.color;
|
|
10937
|
+
ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
|
|
10938
|
+
ctx.restore();
|
|
10939
|
+
}
|
|
10940
|
+
function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
|
|
10941
|
+
const maxValue = runtime.maxValue;
|
|
10942
|
+
const minValue = runtime.minValue;
|
|
10943
|
+
const gaugeValue = runtime.gaugeValue;
|
|
10944
|
+
const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
|
|
10945
|
+
const gaugeArcWidth = gaugeRect.width / 6;
|
|
10946
|
+
const gaugePercentage = gaugeValue
|
|
10947
|
+
? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
|
|
10948
|
+
: 0;
|
|
10949
|
+
const gaugeValuePosition = {
|
|
10950
|
+
x: boundingRect.width / 2,
|
|
10951
|
+
y: gaugeRect.y + gaugeRect.height - gaugeRect.height / 12,
|
|
10841
10952
|
};
|
|
10842
|
-
|
|
10843
|
-
|
|
10844
|
-
|
|
10845
|
-
|
|
10846
|
-
return this.chartRuntime.background;
|
|
10953
|
+
let gaugeValueFontSize = GAUGE_DEFAULT_VALUE_FONT_SIZE;
|
|
10954
|
+
// Scale down the font size if the gaugeRect is too small
|
|
10955
|
+
if (gaugeRect.height < 300) {
|
|
10956
|
+
gaugeValueFontSize = gaugeValueFontSize * (gaugeRect.height / 300);
|
|
10847
10957
|
}
|
|
10848
|
-
|
|
10849
|
-
|
|
10958
|
+
// Scale down the font size if the text is too long
|
|
10959
|
+
const maxTextWidth = gaugeRect.width / 2;
|
|
10960
|
+
const gaugeLabel = gaugeValue?.label || "-";
|
|
10961
|
+
if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
|
|
10962
|
+
gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
|
|
10850
10963
|
}
|
|
10851
|
-
|
|
10852
|
-
|
|
10853
|
-
|
|
10854
|
-
|
|
10855
|
-
|
|
10856
|
-
|
|
10964
|
+
const minLabelPosition = {
|
|
10965
|
+
x: gaugeRect.x + gaugeArcWidth / 2,
|
|
10966
|
+
y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
|
|
10967
|
+
};
|
|
10968
|
+
const maxLabelPosition = {
|
|
10969
|
+
x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
|
|
10970
|
+
y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
|
|
10971
|
+
};
|
|
10972
|
+
const textColor = getContrastedTextColor(runtime.background);
|
|
10973
|
+
const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);
|
|
10974
|
+
let x = 0, titleWidth = 0, titleHeight = 0;
|
|
10975
|
+
if (runtime.title.text) {
|
|
10976
|
+
({ width: titleWidth, height: titleHeight } = computeTextDimension(ctx, runtime.title.text, { ...runtime.title, fontSize: GAUGE_TITLE_FONT_SIZE }, "px"));
|
|
10857
10977
|
}
|
|
10858
|
-
|
|
10859
|
-
|
|
10860
|
-
|
|
10861
|
-
|
|
10862
|
-
|
|
10863
|
-
|
|
10864
|
-
|
|
10865
|
-
|
|
10866
|
-
|
|
10867
|
-
|
|
10868
|
-
|
|
10869
|
-
|
|
10870
|
-
|
|
10871
|
-
|
|
10872
|
-
|
|
10873
|
-
|
|
10874
|
-
|
|
10875
|
-
|
|
10876
|
-
|
|
10877
|
-
|
|
10978
|
+
switch (runtime.title.align) {
|
|
10979
|
+
case "right":
|
|
10980
|
+
x = boundingRect.width - titleWidth - GAUGE_TITLE_PADDING_LEFT;
|
|
10981
|
+
break;
|
|
10982
|
+
case "center":
|
|
10983
|
+
x = (boundingRect.width - titleWidth) / 2;
|
|
10984
|
+
break;
|
|
10985
|
+
case "left":
|
|
10986
|
+
default:
|
|
10987
|
+
x = GAUGE_TITLE_PADDING_LEFT;
|
|
10988
|
+
break;
|
|
10989
|
+
}
|
|
10990
|
+
return {
|
|
10991
|
+
width: boundingRect.width,
|
|
10992
|
+
height: boundingRect.height,
|
|
10993
|
+
title: {
|
|
10994
|
+
label: runtime.title.text ?? "",
|
|
10995
|
+
fontSize: GAUGE_TITLE_FONT_SIZE,
|
|
10996
|
+
textPosition: {
|
|
10997
|
+
x,
|
|
10998
|
+
y: GAUGE_TITLE_PADDING_TOP + titleHeight / 2,
|
|
10999
|
+
},
|
|
11000
|
+
color: runtime.title.color ?? textColor,
|
|
11001
|
+
bold: runtime.title.bold,
|
|
11002
|
+
italic: runtime.title.italic,
|
|
11003
|
+
},
|
|
11004
|
+
backgroundColor: runtime.background,
|
|
11005
|
+
gauge: {
|
|
11006
|
+
rect: gaugeRect,
|
|
11007
|
+
arcWidth: gaugeArcWidth,
|
|
11008
|
+
percentage: clip(gaugePercentage, 0, 1),
|
|
11009
|
+
color: getGaugeColor(runtime),
|
|
11010
|
+
},
|
|
11011
|
+
inflectionValues,
|
|
11012
|
+
gaugeValue: {
|
|
11013
|
+
label: gaugeLabel,
|
|
11014
|
+
textPosition: gaugeValuePosition,
|
|
11015
|
+
fontSize: gaugeValueFontSize,
|
|
11016
|
+
color: textColor,
|
|
11017
|
+
},
|
|
11018
|
+
minLabel: {
|
|
11019
|
+
label: runtime.minValue.label,
|
|
11020
|
+
textPosition: minLabelPosition,
|
|
11021
|
+
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
11022
|
+
color: textColor,
|
|
11023
|
+
},
|
|
11024
|
+
maxLabel: {
|
|
11025
|
+
label: runtime.maxValue.label,
|
|
11026
|
+
textPosition: maxLabelPosition,
|
|
11027
|
+
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
11028
|
+
color: textColor,
|
|
11029
|
+
},
|
|
11030
|
+
};
|
|
11031
|
+
}
|
|
11032
|
+
/**
|
|
11033
|
+
* Get the rectangle in which the gauge will be drawn, based on the bounding rectangle of the canvas and leaving
|
|
11034
|
+
* space for the title and labels.
|
|
11035
|
+
*/
|
|
11036
|
+
function getGaugeRect(boundingRect, title) {
|
|
11037
|
+
const titleHeight = title ? GAUGE_TITLE_SECTION_HEIGHT : 0;
|
|
11038
|
+
const drawHeight = boundingRect.height - GAUGE_PADDING_BOTTOM - titleHeight - GAUGE_PADDING_TOP;
|
|
11039
|
+
const drawWidth = boundingRect.width - GAUGE_PADDING_SIDE * 2;
|
|
11040
|
+
let gaugeWidth;
|
|
11041
|
+
let gaugeHeight;
|
|
11042
|
+
if (drawWidth > 2 * drawHeight) {
|
|
11043
|
+
gaugeWidth = 2 * drawHeight;
|
|
11044
|
+
gaugeHeight = drawHeight;
|
|
11045
|
+
}
|
|
11046
|
+
else {
|
|
11047
|
+
gaugeWidth = drawWidth;
|
|
11048
|
+
gaugeHeight = drawWidth / 2;
|
|
11049
|
+
}
|
|
11050
|
+
const gaugeX = GAUGE_PADDING_SIDE + (drawWidth - gaugeWidth) / 2;
|
|
11051
|
+
const gaugeY = titleHeight + GAUGE_PADDING_TOP + (drawHeight - gaugeHeight) / 2;
|
|
11052
|
+
return {
|
|
11053
|
+
x: gaugeX,
|
|
11054
|
+
y: gaugeY,
|
|
11055
|
+
width: gaugeWidth,
|
|
11056
|
+
height: gaugeHeight,
|
|
11057
|
+
};
|
|
11058
|
+
}
|
|
11059
|
+
/**
|
|
11060
|
+
* 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).
|
|
11061
|
+
*
|
|
11062
|
+
* Also compute an offset for the text so that it doesn't overlap with other text.
|
|
11063
|
+
*/
|
|
11064
|
+
function getInflectionValues(runtime, gaugeRect, textColor, ctx) {
|
|
11065
|
+
const maxValue = runtime.maxValue;
|
|
11066
|
+
const minValue = runtime.minValue;
|
|
11067
|
+
const gaugeCircleCenter = {
|
|
11068
|
+
x: gaugeRect.x + gaugeRect.width / 2,
|
|
11069
|
+
y: gaugeRect.y + gaugeRect.height,
|
|
11070
|
+
};
|
|
11071
|
+
const textStyle = { fontSize: GAUGE_LABELS_FONT_SIZE };
|
|
11072
|
+
const inflectionValues = [];
|
|
11073
|
+
const inflectionValuesTextRects = [];
|
|
11074
|
+
for (const inflectionValue of runtime.inflectionValues) {
|
|
11075
|
+
const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
|
|
11076
|
+
const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
|
|
11077
|
+
const angle = Math.PI - Math.PI * percentage;
|
|
11078
|
+
const textRect = getRectangleTangentToCircle(angle, // angle between X axis and the point where the rectangle is tangent to the circle
|
|
11079
|
+
gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, // radius of the gauge circle + margin below text
|
|
11080
|
+
gaugeCircleCenter.x, // center of the gauge circle
|
|
11081
|
+
gaugeCircleCenter.y, // center of the gauge circle
|
|
11082
|
+
labelWidth + 2, // width of the text + some margin
|
|
11083
|
+
GAUGE_LABELS_FONT_SIZE // height of the text
|
|
11084
|
+
);
|
|
11085
|
+
let offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect))
|
|
11086
|
+
? GAUGE_LABELS_FONT_SIZE
|
|
11087
|
+
: 0;
|
|
11088
|
+
inflectionValuesTextRects.push(textRect);
|
|
11089
|
+
inflectionValues.push({
|
|
11090
|
+
rotation: angle,
|
|
11091
|
+
label: inflectionValue.label,
|
|
11092
|
+
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
11093
|
+
color: textColor,
|
|
11094
|
+
offset,
|
|
10878
11095
|
});
|
|
10879
11096
|
}
|
|
10880
|
-
|
|
10881
|
-
|
|
10882
|
-
|
|
10883
|
-
|
|
11097
|
+
return inflectionValues;
|
|
11098
|
+
}
|
|
11099
|
+
function getGaugeColor(runtime) {
|
|
11100
|
+
const gaugeValue = runtime.gaugeValue?.value;
|
|
11101
|
+
if (gaugeValue === undefined) {
|
|
11102
|
+
return GAUGE_BACKGROUND_COLOR;
|
|
10884
11103
|
}
|
|
10885
|
-
|
|
10886
|
-
|
|
10887
|
-
|
|
10888
|
-
|
|
10889
|
-
|
|
10890
|
-
|
|
11104
|
+
let colorIndex = 0;
|
|
11105
|
+
while (runtime.inflectionValues[colorIndex]?.value <= gaugeValue) {
|
|
11106
|
+
colorIndex++;
|
|
11107
|
+
}
|
|
11108
|
+
return runtime.colors[colorIndex];
|
|
11109
|
+
}
|
|
11110
|
+
function getContrastedTextColor(backgroundColor) {
|
|
11111
|
+
return relativeLuminance(backgroundColor) > 0.3
|
|
11112
|
+
? GAUGE_TEXT_COLOR
|
|
11113
|
+
: GAUGE_TEXT_COLOR_HIGH_CONTRAST;
|
|
11114
|
+
}
|
|
11115
|
+
function getSegmentsOfRectangle(rectangle) {
|
|
11116
|
+
return [
|
|
11117
|
+
{ start: rectangle.topLeft, end: rectangle.topRight },
|
|
11118
|
+
{ start: rectangle.topRight, end: rectangle.bottomRight },
|
|
11119
|
+
{ start: rectangle.bottomRight, end: rectangle.bottomLeft },
|
|
11120
|
+
{ start: rectangle.bottomLeft, end: rectangle.topLeft },
|
|
11121
|
+
];
|
|
11122
|
+
}
|
|
11123
|
+
/**
|
|
11124
|
+
* Check if two segment intersect. The case where the segments are colinear (both segments on the same line)
|
|
11125
|
+
* is not handled.
|
|
11126
|
+
*/
|
|
11127
|
+
function doSegmentIntersect(segment1, segment2) {
|
|
11128
|
+
const A = segment1.start;
|
|
11129
|
+
const B = segment1.end;
|
|
11130
|
+
const C = segment2.start;
|
|
11131
|
+
const D = segment2.end;
|
|
11132
|
+
/**
|
|
11133
|
+
* Line segment intersection algorithm
|
|
11134
|
+
* https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
|
|
11135
|
+
*/
|
|
11136
|
+
function ccw(a, b, c) {
|
|
11137
|
+
return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
|
|
11138
|
+
}
|
|
11139
|
+
return ccw(A, C, D) !== ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D);
|
|
11140
|
+
}
|
|
11141
|
+
function doRectanglesIntersect(rect1, rect2) {
|
|
11142
|
+
const segments1 = getSegmentsOfRectangle(rect1);
|
|
11143
|
+
const segments2 = getSegmentsOfRectangle(rect2);
|
|
11144
|
+
for (const segment1 of segments1) {
|
|
11145
|
+
for (const segment2 of segments2) {
|
|
11146
|
+
if (doSegmentIntersect(segment1, segment2)) {
|
|
11147
|
+
return true;
|
|
10891
11148
|
}
|
|
10892
11149
|
}
|
|
10893
|
-
else {
|
|
10894
|
-
this.chart.data.datasets = [];
|
|
10895
|
-
}
|
|
10896
|
-
this.chart.config.options = chartData.options;
|
|
10897
|
-
this.chart.update();
|
|
10898
11150
|
}
|
|
11151
|
+
return false;
|
|
11152
|
+
}
|
|
11153
|
+
/**
|
|
11154
|
+
* Get the rectangle that is tangent to a circle at a given angle.
|
|
11155
|
+
*
|
|
11156
|
+
* @param angle angle between X axis and the point where the rectangle is tangent to the circle
|
|
11157
|
+
*/
|
|
11158
|
+
function getRectangleTangentToCircle(angle, radius, circleCenterX, circleCenterY, rectWidth, rectHeight) {
|
|
11159
|
+
const cos = Math.cos(angle);
|
|
11160
|
+
const sin = Math.sin(angle);
|
|
11161
|
+
// x, y are the distance from the center of the circle to the point where the rectangle is tangent to the circle
|
|
11162
|
+
const x = cos * radius;
|
|
11163
|
+
const y = sin * radius;
|
|
11164
|
+
// x2, y2 are the distance from the point the rectangle is tangent to the circle to the bottom left corner of the rectangle
|
|
11165
|
+
const x2 = sin * (rectWidth / 2); // cos(angle + 90°) = sin(angle)
|
|
11166
|
+
const y2 = cos * (rectWidth / 2);
|
|
11167
|
+
const bottomRight = {
|
|
11168
|
+
x: x + x2 + circleCenterX,
|
|
11169
|
+
y: circleCenterY - (y - y2),
|
|
11170
|
+
};
|
|
11171
|
+
const bottomLeft = {
|
|
11172
|
+
x: x - x2 + circleCenterX,
|
|
11173
|
+
y: circleCenterY - (y + y2),
|
|
11174
|
+
};
|
|
11175
|
+
// Same as above but for the top corners of the rectangle (radius + rectangle height instead of radius)
|
|
11176
|
+
const xp = cos * (radius + rectHeight);
|
|
11177
|
+
const yp = sin * (radius + rectHeight);
|
|
11178
|
+
const topLeft = {
|
|
11179
|
+
x: xp - x2 + circleCenterX,
|
|
11180
|
+
y: circleCenterY - (yp + y2),
|
|
11181
|
+
};
|
|
11182
|
+
const topRight = {
|
|
11183
|
+
x: xp + x2 + circleCenterX,
|
|
11184
|
+
y: circleCenterY - (yp - y2),
|
|
11185
|
+
};
|
|
11186
|
+
return { bottomLeft, bottomRight, topRight, topLeft };
|
|
10899
11187
|
}
|
|
10900
11188
|
|
|
10901
11189
|
/**
|
|
@@ -11561,6 +11849,364 @@ class KeyValueElement extends ScorecardScalableElement {
|
|
|
11561
11849
|
}
|
|
11562
11850
|
}
|
|
11563
11851
|
|
|
11852
|
+
/**
|
|
11853
|
+
* This file contains helpers that are common to different runtime charts (mainly
|
|
11854
|
+
* line, bar and pie charts)
|
|
11855
|
+
*/
|
|
11856
|
+
/**
|
|
11857
|
+
* Get the data from a dataSet
|
|
11858
|
+
*/
|
|
11859
|
+
function getData(getters, ds) {
|
|
11860
|
+
if (ds.dataRange) {
|
|
11861
|
+
const labelCellZone = ds.labelCell ? [ds.labelCell.zone] : [];
|
|
11862
|
+
const dataZone = recomputeZones([ds.dataRange.zone], labelCellZone)[0];
|
|
11863
|
+
if (dataZone === undefined) {
|
|
11864
|
+
return [];
|
|
11865
|
+
}
|
|
11866
|
+
const dataRange = getters.getRangeFromZone(ds.dataRange.sheetId, dataZone);
|
|
11867
|
+
return getters.getRangeValues(dataRange).map((value) => (value === "" ? undefined : value));
|
|
11868
|
+
}
|
|
11869
|
+
return [];
|
|
11870
|
+
}
|
|
11871
|
+
function filterEmptyDataPoints(labels, datasets) {
|
|
11872
|
+
const numberOfDataPoints = Math.max(labels.length, ...datasets.map((dataset) => dataset.data?.length || 0));
|
|
11873
|
+
const dataPointsIndexes = range(0, numberOfDataPoints).filter((dataPointIndex) => {
|
|
11874
|
+
const label = labels[dataPointIndex];
|
|
11875
|
+
const values = datasets.map((dataset) => dataset.data?.[dataPointIndex]);
|
|
11876
|
+
return label || values.some((value) => value === 0 || Boolean(value));
|
|
11877
|
+
});
|
|
11878
|
+
return {
|
|
11879
|
+
labels: dataPointsIndexes.map((i) => labels[i] || ""),
|
|
11880
|
+
dataSetsValues: datasets.map((dataset) => ({
|
|
11881
|
+
...dataset,
|
|
11882
|
+
data: dataPointsIndexes.map((i) => dataset.data[i]),
|
|
11883
|
+
})),
|
|
11884
|
+
};
|
|
11885
|
+
}
|
|
11886
|
+
/**
|
|
11887
|
+
* Aggregates data based on labels
|
|
11888
|
+
*/
|
|
11889
|
+
function aggregateDataForLabels(labels, datasets) {
|
|
11890
|
+
const parseNumber = (value) => (typeof value === "number" ? value : 0);
|
|
11891
|
+
const labelSet = new Set(labels);
|
|
11892
|
+
const labelMap = {};
|
|
11893
|
+
labelSet.forEach((label) => {
|
|
11894
|
+
labelMap[label] = new Array(datasets.length).fill(0);
|
|
11895
|
+
});
|
|
11896
|
+
for (const indexOfLabel of range(0, labels.length)) {
|
|
11897
|
+
const label = labels[indexOfLabel];
|
|
11898
|
+
for (const indexOfDataset of range(0, datasets.length)) {
|
|
11899
|
+
labelMap[label][indexOfDataset] += parseNumber(datasets[indexOfDataset].data[indexOfLabel]);
|
|
11900
|
+
}
|
|
11901
|
+
}
|
|
11902
|
+
return {
|
|
11903
|
+
labels: Array.from(labelSet),
|
|
11904
|
+
dataSetsValues: datasets.map((dataset, indexOfDataset) => ({
|
|
11905
|
+
...dataset,
|
|
11906
|
+
data: Array.from(labelSet).map((label) => labelMap[label][indexOfDataset]),
|
|
11907
|
+
})),
|
|
11908
|
+
};
|
|
11909
|
+
}
|
|
11910
|
+
function truncateLabel(label) {
|
|
11911
|
+
if (!label) {
|
|
11912
|
+
return "";
|
|
11913
|
+
}
|
|
11914
|
+
if (label.length > MAX_CHAR_LABEL) {
|
|
11915
|
+
return label.substring(0, MAX_CHAR_LABEL) + "…";
|
|
11916
|
+
}
|
|
11917
|
+
return label;
|
|
11918
|
+
}
|
|
11919
|
+
/**
|
|
11920
|
+
* Get a default chart js configuration
|
|
11921
|
+
*/
|
|
11922
|
+
function getDefaultChartJsRuntime(chart, labels, fontColor, { format, locale, truncateLabels = true, horizontalChart, }) {
|
|
11923
|
+
const chartTitle = chart.title.text ? chart.title : { ...chart.title, content: "" };
|
|
11924
|
+
const options = {
|
|
11925
|
+
// https://www.chartjs.org/docs/latest/general/responsive.html
|
|
11926
|
+
responsive: true, // will resize when its container is resized
|
|
11927
|
+
maintainAspectRatio: false, // doesn't maintain the aspect ration (width/height =2 by default) so the user has the choice of the exact layout
|
|
11928
|
+
layout: {
|
|
11929
|
+
padding: {
|
|
11930
|
+
left: DEFAULT_CHART_PADDING,
|
|
11931
|
+
right: DEFAULT_CHART_PADDING,
|
|
11932
|
+
top: chartTitle.text ? DEFAULT_CHART_PADDING / 2 : DEFAULT_CHART_PADDING + 5,
|
|
11933
|
+
bottom: DEFAULT_CHART_PADDING,
|
|
11934
|
+
},
|
|
11935
|
+
},
|
|
11936
|
+
elements: {
|
|
11937
|
+
line: {
|
|
11938
|
+
fill: false, // do not fill the area under line charts
|
|
11939
|
+
},
|
|
11940
|
+
point: {
|
|
11941
|
+
hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
|
|
11942
|
+
},
|
|
11943
|
+
},
|
|
11944
|
+
animation: false,
|
|
11945
|
+
plugins: {
|
|
11946
|
+
title: {
|
|
11947
|
+
display: !!chartTitle.text,
|
|
11948
|
+
text: _t(chartTitle.text),
|
|
11949
|
+
color: chartTitle?.color ?? fontColor,
|
|
11950
|
+
align: chartTitle.align === "center" ? "center" : chartTitle.align === "right" ? "end" : "start",
|
|
11951
|
+
font: {
|
|
11952
|
+
size: DEFAULT_CHART_FONT_SIZE,
|
|
11953
|
+
weight: chartTitle.bold ? "bold" : "normal",
|
|
11954
|
+
style: chartTitle.italic ? "italic" : "normal",
|
|
11955
|
+
},
|
|
11956
|
+
},
|
|
11957
|
+
legend: {
|
|
11958
|
+
// Disable default legend onClick (show/hide dataset), to allow us to set a global onClick on the chart container.
|
|
11959
|
+
// If we want to re-enable this in the future, we need to override the default onClick to stop the event propagation
|
|
11960
|
+
onClick: () => { },
|
|
11961
|
+
},
|
|
11962
|
+
tooltip: {
|
|
11963
|
+
callbacks: {
|
|
11964
|
+
label: function (tooltipItem) {
|
|
11965
|
+
const xLabel = tooltipItem.dataset?.label || tooltipItem.label;
|
|
11966
|
+
// tooltipItem.parsed can be an object or a number for pie charts
|
|
11967
|
+
let yLabel = horizontalChart ? tooltipItem.parsed.x : tooltipItem.parsed.y;
|
|
11968
|
+
if (yLabel === undefined || yLabel === null) {
|
|
11969
|
+
yLabel = tooltipItem.parsed;
|
|
11970
|
+
}
|
|
11971
|
+
const toolTipFormat = !format && Math.abs(yLabel) >= 1000 ? "#,##" : format;
|
|
11972
|
+
const yLabelStr = formatValue(yLabel, { format: toolTipFormat, locale });
|
|
11973
|
+
return xLabel ? `${xLabel}: ${yLabelStr}` : yLabelStr;
|
|
11974
|
+
},
|
|
11975
|
+
},
|
|
11976
|
+
},
|
|
11977
|
+
},
|
|
11978
|
+
};
|
|
11979
|
+
return {
|
|
11980
|
+
type: chart.type,
|
|
11981
|
+
options,
|
|
11982
|
+
data: {
|
|
11983
|
+
labels: truncateLabels ? labels.map(truncateLabel) : labels,
|
|
11984
|
+
datasets: [],
|
|
11985
|
+
},
|
|
11986
|
+
platform: undefined, // This key is optional and will be set by chart.js
|
|
11987
|
+
plugins: [],
|
|
11988
|
+
};
|
|
11989
|
+
}
|
|
11990
|
+
function getChartLabelFormat(getters, range, shouldRemoveFirstLabel) {
|
|
11991
|
+
if (!range)
|
|
11992
|
+
return undefined;
|
|
11993
|
+
const { sheetId, zone } = range;
|
|
11994
|
+
const formats = positions(zone).map((position) => getters.getEvaluatedCell({ sheetId, ...position }).format);
|
|
11995
|
+
if (shouldRemoveFirstLabel) {
|
|
11996
|
+
formats.shift();
|
|
11997
|
+
}
|
|
11998
|
+
return formats.find((format) => format !== undefined);
|
|
11999
|
+
}
|
|
12000
|
+
function getChartLabelValues(getters, dataSets, labelRange) {
|
|
12001
|
+
let labels = { values: [], formattedValues: [] };
|
|
12002
|
+
if (labelRange) {
|
|
12003
|
+
if (!labelRange.invalidXc && !labelRange.invalidSheetName) {
|
|
12004
|
+
labels = {
|
|
12005
|
+
formattedValues: getters.getRangeFormattedValues(labelRange),
|
|
12006
|
+
values: getters.getRangeValues(labelRange).map((val) => String(val ?? "")),
|
|
12007
|
+
};
|
|
12008
|
+
}
|
|
12009
|
+
}
|
|
12010
|
+
else if (dataSets.length === 1) {
|
|
12011
|
+
for (let i = 0; i < getData(getters, dataSets[0]).length; i++) {
|
|
12012
|
+
labels.formattedValues.push("");
|
|
12013
|
+
labels.values.push("");
|
|
12014
|
+
}
|
|
12015
|
+
}
|
|
12016
|
+
else {
|
|
12017
|
+
if (dataSets[0]) {
|
|
12018
|
+
const ranges = getData(getters, dataSets[0]);
|
|
12019
|
+
labels = {
|
|
12020
|
+
formattedValues: range(0, ranges.length).map((r) => r.toString()),
|
|
12021
|
+
values: labels.formattedValues,
|
|
12022
|
+
};
|
|
12023
|
+
}
|
|
12024
|
+
}
|
|
12025
|
+
return labels;
|
|
12026
|
+
}
|
|
12027
|
+
/**
|
|
12028
|
+
* Get the format to apply to the the dataset values. This format is defined as the first format
|
|
12029
|
+
* found in the dataset ranges that isn't a date format.
|
|
12030
|
+
*/
|
|
12031
|
+
function getChartDatasetFormat(getters, dataSets) {
|
|
12032
|
+
for (const ds of dataSets) {
|
|
12033
|
+
const formatsInDataset = getters.getRangeFormats(ds.dataRange);
|
|
12034
|
+
const format = formatsInDataset.find((f) => f !== undefined && !isDateTimeFormat(f));
|
|
12035
|
+
if (format)
|
|
12036
|
+
return format;
|
|
12037
|
+
}
|
|
12038
|
+
return undefined;
|
|
12039
|
+
}
|
|
12040
|
+
function getChartDatasetValues(getters, dataSets) {
|
|
12041
|
+
const datasetValues = [];
|
|
12042
|
+
for (const [dsIndex, ds] of Object.entries(dataSets)) {
|
|
12043
|
+
let label;
|
|
12044
|
+
if (ds.labelCell) {
|
|
12045
|
+
const labelRange = ds.labelCell;
|
|
12046
|
+
const cell = labelRange
|
|
12047
|
+
? getters.getEvaluatedCell({
|
|
12048
|
+
sheetId: labelRange.sheetId,
|
|
12049
|
+
col: labelRange.zone.left,
|
|
12050
|
+
row: labelRange.zone.top,
|
|
12051
|
+
})
|
|
12052
|
+
: undefined;
|
|
12053
|
+
label =
|
|
12054
|
+
cell && labelRange
|
|
12055
|
+
? truncateLabel(cell.formattedValue)
|
|
12056
|
+
: (label = `${ChartTerms.Series} ${parseInt(dsIndex) + 1}`);
|
|
12057
|
+
}
|
|
12058
|
+
else {
|
|
12059
|
+
label = `${ChartTerms.Series} ${parseInt(dsIndex) + 1}`;
|
|
12060
|
+
}
|
|
12061
|
+
let data = ds.dataRange ? getData(getters, ds) : [];
|
|
12062
|
+
if (data.every((e) => typeof e === "string" && !isEvaluationError(e))) {
|
|
12063
|
+
// In this case, we want a chart based on the string occurrences count
|
|
12064
|
+
// This will be done by associating each string with a value of 1 and
|
|
12065
|
+
// the using the classical aggregation method to sum the values.
|
|
12066
|
+
data.fill(1);
|
|
12067
|
+
}
|
|
12068
|
+
datasetValues.push({ data, label });
|
|
12069
|
+
}
|
|
12070
|
+
return datasetValues;
|
|
12071
|
+
}
|
|
12072
|
+
/**
|
|
12073
|
+
* If the chart is a stacked area chart, we want to fill until the next dataset.
|
|
12074
|
+
* If the chart is a simple area chart, we want to fill until the origin (bottom axis).
|
|
12075
|
+
*
|
|
12076
|
+
* See https://www.chartjs.org/docs/latest/charts/area.html#filling-modes
|
|
12077
|
+
*/
|
|
12078
|
+
function getFillingMode(index, stackedChart) {
|
|
12079
|
+
if (!stackedChart) {
|
|
12080
|
+
return "origin";
|
|
12081
|
+
}
|
|
12082
|
+
return index === 0 ? "origin" : "-1";
|
|
12083
|
+
}
|
|
12084
|
+
function chartToImage(runtime, figure, type) {
|
|
12085
|
+
// wrap the canvas in a div with a fixed size because chart.js would
|
|
12086
|
+
// fill the whole page otherwise
|
|
12087
|
+
const div = document.createElement("div");
|
|
12088
|
+
div.style.width = `${figure.width}px`;
|
|
12089
|
+
div.style.height = `${figure.height}px`;
|
|
12090
|
+
const canvas = document.createElement("canvas");
|
|
12091
|
+
div.append(canvas);
|
|
12092
|
+
canvas.setAttribute("width", figure.width.toString());
|
|
12093
|
+
canvas.setAttribute("height", figure.height.toString());
|
|
12094
|
+
// we have to add the canvas to the DOM otherwise it won't be rendered
|
|
12095
|
+
document.body.append(div);
|
|
12096
|
+
if ("chartJsConfig" in runtime) {
|
|
12097
|
+
const config = deepCopy(runtime.chartJsConfig);
|
|
12098
|
+
config.plugins = [backgroundColorChartJSPlugin];
|
|
12099
|
+
const Chart = getChartJSConstructor();
|
|
12100
|
+
const chart = new Chart(canvas, config);
|
|
12101
|
+
const imgContent = chart.toBase64Image();
|
|
12102
|
+
chart.destroy();
|
|
12103
|
+
div.remove();
|
|
12104
|
+
return imgContent;
|
|
12105
|
+
}
|
|
12106
|
+
else if (type === "scorecard") {
|
|
12107
|
+
const design = getScorecardConfiguration(figure, runtime);
|
|
12108
|
+
drawScoreChart(design, canvas);
|
|
12109
|
+
const imgContent = canvas.toDataURL();
|
|
12110
|
+
div.remove();
|
|
12111
|
+
return imgContent;
|
|
12112
|
+
}
|
|
12113
|
+
else if (type === "gauge") {
|
|
12114
|
+
drawGaugeChart(canvas, runtime);
|
|
12115
|
+
const imgContent = canvas.toDataURL();
|
|
12116
|
+
div.remove();
|
|
12117
|
+
return imgContent;
|
|
12118
|
+
}
|
|
12119
|
+
return undefined;
|
|
12120
|
+
}
|
|
12121
|
+
/**
|
|
12122
|
+
* Custom chart.js plugin to set the background color of the canvas
|
|
12123
|
+
* https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md
|
|
12124
|
+
*/
|
|
12125
|
+
const backgroundColorChartJSPlugin = {
|
|
12126
|
+
id: "customCanvasBackgroundColor",
|
|
12127
|
+
beforeDraw: (chart) => {
|
|
12128
|
+
const { ctx } = chart;
|
|
12129
|
+
ctx.save();
|
|
12130
|
+
ctx.globalCompositeOperation = "destination-over";
|
|
12131
|
+
ctx.fillStyle = "#ffffff";
|
|
12132
|
+
ctx.fillRect(0, 0, chart.width, chart.height);
|
|
12133
|
+
ctx.restore();
|
|
12134
|
+
},
|
|
12135
|
+
};
|
|
12136
|
+
/** Return window.Chart, making sure all our extensions are loaded in ChartJS */
|
|
12137
|
+
function getChartJSConstructor() {
|
|
12138
|
+
if (window.Chart && !window.Chart?.registry.plugins.get("chartShowValuesPlugin")) {
|
|
12139
|
+
window.Chart.register(chartShowValuesPlugin);
|
|
12140
|
+
window.Chart.register(waterfallLinesPlugin);
|
|
12141
|
+
}
|
|
12142
|
+
return window.Chart;
|
|
12143
|
+
}
|
|
12144
|
+
|
|
12145
|
+
class ChartJsComponent extends owl.Component {
|
|
12146
|
+
static template = "o-spreadsheet-ChartJsComponent";
|
|
12147
|
+
static props = {
|
|
12148
|
+
figure: Object,
|
|
12149
|
+
};
|
|
12150
|
+
canvas = owl.useRef("graphContainer");
|
|
12151
|
+
chart;
|
|
12152
|
+
currentRuntime;
|
|
12153
|
+
get background() {
|
|
12154
|
+
return this.chartRuntime.background;
|
|
12155
|
+
}
|
|
12156
|
+
get canvasStyle() {
|
|
12157
|
+
return `background-color: ${this.background}`;
|
|
12158
|
+
}
|
|
12159
|
+
get chartRuntime() {
|
|
12160
|
+
const runtime = this.env.model.getters.getChartRuntime(this.props.figure.id);
|
|
12161
|
+
if (!("chartJsConfig" in runtime)) {
|
|
12162
|
+
throw new Error("Unsupported chart runtime");
|
|
12163
|
+
}
|
|
12164
|
+
return runtime;
|
|
12165
|
+
}
|
|
12166
|
+
setup() {
|
|
12167
|
+
owl.onMounted(() => {
|
|
12168
|
+
const runtime = this.chartRuntime;
|
|
12169
|
+
this.currentRuntime = runtime;
|
|
12170
|
+
// Note: chartJS modify the runtime in place, so it's important to give it a copy
|
|
12171
|
+
this.createChart(deepCopy(runtime.chartJsConfig));
|
|
12172
|
+
});
|
|
12173
|
+
owl.onWillUnmount(() => this.chart?.destroy());
|
|
12174
|
+
owl.useEffect(() => {
|
|
12175
|
+
const runtime = this.chartRuntime;
|
|
12176
|
+
if (runtime !== this.currentRuntime) {
|
|
12177
|
+
if (runtime.chartJsConfig.type !== this.currentRuntime.chartJsConfig.type) {
|
|
12178
|
+
this.chart?.destroy();
|
|
12179
|
+
this.createChart(deepCopy(runtime.chartJsConfig));
|
|
12180
|
+
}
|
|
12181
|
+
else {
|
|
12182
|
+
this.updateChartJs(deepCopy(runtime));
|
|
12183
|
+
}
|
|
12184
|
+
this.currentRuntime = runtime;
|
|
12185
|
+
}
|
|
12186
|
+
});
|
|
12187
|
+
}
|
|
12188
|
+
createChart(chartData) {
|
|
12189
|
+
const canvas = this.canvas.el;
|
|
12190
|
+
const ctx = canvas.getContext("2d");
|
|
12191
|
+
const Chart = getChartJSConstructor();
|
|
12192
|
+
this.chart = new Chart(ctx, chartData);
|
|
12193
|
+
}
|
|
12194
|
+
updateChartJs(chartRuntime) {
|
|
12195
|
+
const chartData = chartRuntime.chartJsConfig;
|
|
12196
|
+
if (chartData.data && chartData.data.datasets) {
|
|
12197
|
+
this.chart.data = chartData.data;
|
|
12198
|
+
if (chartData.options?.plugins?.title) {
|
|
12199
|
+
this.chart.config.options.plugins.title = chartData.options.plugins.title;
|
|
12200
|
+
}
|
|
12201
|
+
}
|
|
12202
|
+
else {
|
|
12203
|
+
this.chart.data.datasets = [];
|
|
12204
|
+
}
|
|
12205
|
+
this.chart.config.options = chartData.options;
|
|
12206
|
+
this.chart.update();
|
|
12207
|
+
}
|
|
12208
|
+
}
|
|
12209
|
+
|
|
11564
12210
|
class ScorecardChart extends owl.Component {
|
|
11565
12211
|
static template = "o-spreadsheet-ScorecardChart";
|
|
11566
12212
|
static props = {
|
|
@@ -11590,6 +12236,7 @@ class ScorecardChart extends owl.Component {
|
|
|
11590
12236
|
}
|
|
11591
12237
|
|
|
11592
12238
|
autoCompleteProviders.add("dataValidation", {
|
|
12239
|
+
displayAllOnInitialContent: true,
|
|
11593
12240
|
getProposals(tokenAtCursor, content) {
|
|
11594
12241
|
if (content.startsWith("=")) {
|
|
11595
12242
|
return [];
|
|
@@ -24401,7 +25048,7 @@ autofillRulesRegistry
|
|
|
24401
25048
|
condition: (cell) => !cell.isFormula &&
|
|
24402
25049
|
evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.text &&
|
|
24403
25050
|
alphaNumericValueRegExp.test(cell.content),
|
|
24404
|
-
generateRule: (cell, cells) => {
|
|
25051
|
+
generateRule: (cell, cells, direction) => {
|
|
24405
25052
|
const numberPostfix = parseInt(cell.content.match(numberPostfixRegExp)[0]);
|
|
24406
25053
|
const prefix = cell.content.match(stringPrefixRegExp)[0];
|
|
24407
25054
|
const numberPostfixLength = cell.content.length - prefix.length;
|
|
@@ -24409,7 +25056,10 @@ autofillRulesRegistry
|
|
|
24409
25056
|
alphaNumericValueRegExp.test(evaluatedCell.value)) // get consecutive alphanumeric cells, no matter what the prefix is
|
|
24410
25057
|
.filter((cell) => prefix === (cell.value ?? "").toString().match(stringPrefixRegExp)[0])
|
|
24411
25058
|
.map((cell) => parseInt((cell.value ?? "").toString().match(numberPostfixRegExp)[0]));
|
|
24412
|
-
|
|
25059
|
+
let increment = calculateIncrementBasedOnGroup(group);
|
|
25060
|
+
if (["up", "left"].includes(direction) && group.length === 1) {
|
|
25061
|
+
increment = -increment;
|
|
25062
|
+
}
|
|
24413
25063
|
return {
|
|
24414
25064
|
type: "ALPHANUMERIC_INCREMENT_MODIFIER",
|
|
24415
25065
|
prefix,
|
|
@@ -24438,9 +25088,12 @@ autofillRulesRegistry
|
|
|
24438
25088
|
.add("increment_number", {
|
|
24439
25089
|
condition: (cell) => !cell.isFormula &&
|
|
24440
25090
|
evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.number,
|
|
24441
|
-
generateRule: (cell, cells) => {
|
|
25091
|
+
generateRule: (cell, cells, direction) => {
|
|
24442
25092
|
const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number).map((cell) => Number(cell.value));
|
|
24443
|
-
|
|
25093
|
+
let increment = calculateIncrementBasedOnGroup(group);
|
|
25094
|
+
if (["up", "left"].includes(direction) && group.length === 1) {
|
|
25095
|
+
increment = -increment;
|
|
25096
|
+
}
|
|
24444
25097
|
const evaluation = evaluateLiteral(cell, { locale: DEFAULT_LOCALE });
|
|
24445
25098
|
return {
|
|
24446
25099
|
type: "INCREMENT_MODIFIER",
|
|
@@ -24451,349 +25104,6 @@ autofillRulesRegistry
|
|
|
24451
25104
|
sequence: 40,
|
|
24452
25105
|
});
|
|
24453
25106
|
|
|
24454
|
-
const GAUGE_PADDING_SIDE = 30;
|
|
24455
|
-
const GAUGE_PADDING_TOP = 10;
|
|
24456
|
-
const GAUGE_PADDING_BOTTOM = 20;
|
|
24457
|
-
const GAUGE_LABELS_FONT_SIZE = 12;
|
|
24458
|
-
const GAUGE_DEFAULT_VALUE_FONT_SIZE = 80;
|
|
24459
|
-
const GAUGE_BACKGROUND_COLOR = "#F3F2F1";
|
|
24460
|
-
const GAUGE_TEXT_COLOR = "#666666";
|
|
24461
|
-
const GAUGE_TEXT_COLOR_HIGH_CONTRAST = "#C8C8C8";
|
|
24462
|
-
const GAUGE_INFLECTION_MARKER_COLOR = "#666666aa";
|
|
24463
|
-
const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
|
|
24464
|
-
const GAUGE_TITLE_SECTION_HEIGHT = 25;
|
|
24465
|
-
const GAUGE_TITLE_FONT_SIZE = DEFAULT_CHART_FONT_SIZE;
|
|
24466
|
-
const GAUGE_TITLE_PADDING_LEFT = DEFAULT_CHART_PADDING;
|
|
24467
|
-
const GAUGE_TITLE_PADDING_TOP = DEFAULT_CHART_PADDING;
|
|
24468
|
-
function drawGaugeChart(canvas, runtime) {
|
|
24469
|
-
const canvasBoundingRect = canvas.getBoundingClientRect();
|
|
24470
|
-
canvas.width = canvasBoundingRect.width;
|
|
24471
|
-
canvas.height = canvasBoundingRect.height;
|
|
24472
|
-
const ctx = canvas.getContext("2d");
|
|
24473
|
-
const config = getGaugeRenderingConfig(canvasBoundingRect, runtime, ctx);
|
|
24474
|
-
drawBackground(ctx, config);
|
|
24475
|
-
drawGauge(ctx, config);
|
|
24476
|
-
drawInflectionValues(ctx, config);
|
|
24477
|
-
drawLabels(ctx, config);
|
|
24478
|
-
drawTitle(ctx, config);
|
|
24479
|
-
}
|
|
24480
|
-
function drawGauge(ctx, config) {
|
|
24481
|
-
ctx.save();
|
|
24482
|
-
const gauge = config.gauge;
|
|
24483
|
-
const arcCenterX = gauge.rect.x + gauge.rect.width / 2;
|
|
24484
|
-
const arcCenterY = gauge.rect.y + gauge.rect.height;
|
|
24485
|
-
const arcRadius = gauge.rect.height - gauge.arcWidth / 2;
|
|
24486
|
-
if (arcRadius < 0) {
|
|
24487
|
-
return;
|
|
24488
|
-
}
|
|
24489
|
-
const gaugeAngle = gauge.percentage === 1 ? 0 : Math.PI * (1 + gauge.percentage);
|
|
24490
|
-
// Gauge background
|
|
24491
|
-
ctx.strokeStyle = GAUGE_BACKGROUND_COLOR;
|
|
24492
|
-
ctx.beginPath();
|
|
24493
|
-
ctx.lineWidth = gauge.arcWidth;
|
|
24494
|
-
ctx.arc(arcCenterX, arcCenterY, arcRadius, gaugeAngle, 0);
|
|
24495
|
-
ctx.stroke();
|
|
24496
|
-
// Gauge value
|
|
24497
|
-
ctx.strokeStyle = gauge.color;
|
|
24498
|
-
ctx.beginPath();
|
|
24499
|
-
ctx.arc(arcCenterX, arcCenterY, arcRadius, Math.PI, gaugeAngle);
|
|
24500
|
-
ctx.stroke();
|
|
24501
|
-
ctx.restore();
|
|
24502
|
-
}
|
|
24503
|
-
function drawBackground(ctx, config) {
|
|
24504
|
-
ctx.save();
|
|
24505
|
-
ctx.fillStyle = config.backgroundColor;
|
|
24506
|
-
ctx.fillRect(0, 0, config.width, config.height);
|
|
24507
|
-
ctx.restore();
|
|
24508
|
-
}
|
|
24509
|
-
function drawLabels(ctx, config) {
|
|
24510
|
-
for (const label of [config.minLabel, config.maxLabel, config.gaugeValue]) {
|
|
24511
|
-
ctx.save();
|
|
24512
|
-
ctx.textAlign = "center";
|
|
24513
|
-
ctx.fillStyle = label.color;
|
|
24514
|
-
ctx.font = `${label.fontSize}px ${DEFAULT_FONT}`;
|
|
24515
|
-
ctx.fillText(label.label, label.textPosition.x, label.textPosition.y);
|
|
24516
|
-
ctx.restore();
|
|
24517
|
-
}
|
|
24518
|
-
}
|
|
24519
|
-
function drawInflectionValues(ctx, config) {
|
|
24520
|
-
const { x: rectX, y: rectY, width, height } = config.gauge.rect;
|
|
24521
|
-
for (const inflectionValue of config.inflectionValues) {
|
|
24522
|
-
ctx.save();
|
|
24523
|
-
ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment
|
|
24524
|
-
ctx.rotate(Math.PI / 2 - inflectionValue.rotation);
|
|
24525
|
-
ctx.lineWidth = 2;
|
|
24526
|
-
ctx.strokeStyle = GAUGE_INFLECTION_MARKER_COLOR;
|
|
24527
|
-
ctx.beginPath();
|
|
24528
|
-
ctx.moveTo(0, -(height - config.gauge.arcWidth));
|
|
24529
|
-
ctx.lineTo(0, -height - 3);
|
|
24530
|
-
ctx.stroke();
|
|
24531
|
-
ctx.textAlign = "center";
|
|
24532
|
-
ctx.font = `${inflectionValue.fontSize}px ${DEFAULT_FONT}`;
|
|
24533
|
-
ctx.fillStyle = inflectionValue.color;
|
|
24534
|
-
const textY = -height - GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN - inflectionValue.offset;
|
|
24535
|
-
ctx.fillText(inflectionValue.label, 0, textY);
|
|
24536
|
-
ctx.restore();
|
|
24537
|
-
}
|
|
24538
|
-
}
|
|
24539
|
-
function drawTitle(ctx, config) {
|
|
24540
|
-
ctx.save();
|
|
24541
|
-
const title = config.title;
|
|
24542
|
-
ctx.font = getDefaultContextFont(title.fontSize, title.bold, title.italic);
|
|
24543
|
-
ctx.textBaseline = "middle";
|
|
24544
|
-
ctx.fillStyle = title.color;
|
|
24545
|
-
ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
|
|
24546
|
-
ctx.restore();
|
|
24547
|
-
}
|
|
24548
|
-
function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
|
|
24549
|
-
const maxValue = runtime.maxValue;
|
|
24550
|
-
const minValue = runtime.minValue;
|
|
24551
|
-
const gaugeValue = runtime.gaugeValue;
|
|
24552
|
-
const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
|
|
24553
|
-
const gaugeArcWidth = gaugeRect.width / 6;
|
|
24554
|
-
const gaugePercentage = gaugeValue
|
|
24555
|
-
? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
|
|
24556
|
-
: 0;
|
|
24557
|
-
const gaugeValuePosition = {
|
|
24558
|
-
x: boundingRect.width / 2,
|
|
24559
|
-
y: gaugeRect.y + gaugeRect.height - gaugeRect.height / 12,
|
|
24560
|
-
};
|
|
24561
|
-
let gaugeValueFontSize = GAUGE_DEFAULT_VALUE_FONT_SIZE;
|
|
24562
|
-
// Scale down the font size if the gaugeRect is too small
|
|
24563
|
-
if (gaugeRect.height < 300) {
|
|
24564
|
-
gaugeValueFontSize = gaugeValueFontSize * (gaugeRect.height / 300);
|
|
24565
|
-
}
|
|
24566
|
-
// Scale down the font size if the text is too long
|
|
24567
|
-
const maxTextWidth = gaugeRect.width / 2;
|
|
24568
|
-
const gaugeLabel = gaugeValue?.label || "-";
|
|
24569
|
-
if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
|
|
24570
|
-
gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
|
|
24571
|
-
}
|
|
24572
|
-
const minLabelPosition = {
|
|
24573
|
-
x: gaugeRect.x + gaugeArcWidth / 2,
|
|
24574
|
-
y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
|
|
24575
|
-
};
|
|
24576
|
-
const maxLabelPosition = {
|
|
24577
|
-
x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
|
|
24578
|
-
y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
|
|
24579
|
-
};
|
|
24580
|
-
const textColor = getContrastedTextColor(runtime.background);
|
|
24581
|
-
const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);
|
|
24582
|
-
let x = 0, titleWidth = 0, titleHeight = 0;
|
|
24583
|
-
if (runtime.title.text) {
|
|
24584
|
-
({ width: titleWidth, height: titleHeight } = computeTextDimension(ctx, runtime.title.text, { ...runtime.title, fontSize: GAUGE_TITLE_FONT_SIZE }, "px"));
|
|
24585
|
-
}
|
|
24586
|
-
switch (runtime.title.align) {
|
|
24587
|
-
case "right":
|
|
24588
|
-
x = boundingRect.width - titleWidth - GAUGE_TITLE_PADDING_LEFT;
|
|
24589
|
-
break;
|
|
24590
|
-
case "center":
|
|
24591
|
-
x = (boundingRect.width - titleWidth) / 2;
|
|
24592
|
-
break;
|
|
24593
|
-
case "left":
|
|
24594
|
-
default:
|
|
24595
|
-
x = GAUGE_TITLE_PADDING_LEFT;
|
|
24596
|
-
break;
|
|
24597
|
-
}
|
|
24598
|
-
return {
|
|
24599
|
-
width: boundingRect.width,
|
|
24600
|
-
height: boundingRect.height,
|
|
24601
|
-
title: {
|
|
24602
|
-
label: runtime.title.text ?? "",
|
|
24603
|
-
fontSize: GAUGE_TITLE_FONT_SIZE,
|
|
24604
|
-
textPosition: {
|
|
24605
|
-
x,
|
|
24606
|
-
y: GAUGE_TITLE_PADDING_TOP + titleHeight / 2,
|
|
24607
|
-
},
|
|
24608
|
-
color: runtime.title.color ?? textColor,
|
|
24609
|
-
bold: runtime.title.bold,
|
|
24610
|
-
italic: runtime.title.italic,
|
|
24611
|
-
},
|
|
24612
|
-
backgroundColor: runtime.background,
|
|
24613
|
-
gauge: {
|
|
24614
|
-
rect: gaugeRect,
|
|
24615
|
-
arcWidth: gaugeArcWidth,
|
|
24616
|
-
percentage: clip(gaugePercentage, 0, 1),
|
|
24617
|
-
color: getGaugeColor(runtime),
|
|
24618
|
-
},
|
|
24619
|
-
inflectionValues,
|
|
24620
|
-
gaugeValue: {
|
|
24621
|
-
label: gaugeLabel,
|
|
24622
|
-
textPosition: gaugeValuePosition,
|
|
24623
|
-
fontSize: gaugeValueFontSize,
|
|
24624
|
-
color: textColor,
|
|
24625
|
-
},
|
|
24626
|
-
minLabel: {
|
|
24627
|
-
label: runtime.minValue.label,
|
|
24628
|
-
textPosition: minLabelPosition,
|
|
24629
|
-
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
24630
|
-
color: textColor,
|
|
24631
|
-
},
|
|
24632
|
-
maxLabel: {
|
|
24633
|
-
label: runtime.maxValue.label,
|
|
24634
|
-
textPosition: maxLabelPosition,
|
|
24635
|
-
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
24636
|
-
color: textColor,
|
|
24637
|
-
},
|
|
24638
|
-
};
|
|
24639
|
-
}
|
|
24640
|
-
/**
|
|
24641
|
-
* Get the rectangle in which the gauge will be drawn, based on the bounding rectangle of the canvas and leaving
|
|
24642
|
-
* space for the title and labels.
|
|
24643
|
-
*/
|
|
24644
|
-
function getGaugeRect(boundingRect, title) {
|
|
24645
|
-
const titleHeight = title ? GAUGE_TITLE_SECTION_HEIGHT : 0;
|
|
24646
|
-
const drawHeight = boundingRect.height - GAUGE_PADDING_BOTTOM - titleHeight - GAUGE_PADDING_TOP;
|
|
24647
|
-
const drawWidth = boundingRect.width - GAUGE_PADDING_SIDE * 2;
|
|
24648
|
-
let gaugeWidth;
|
|
24649
|
-
let gaugeHeight;
|
|
24650
|
-
if (drawWidth > 2 * drawHeight) {
|
|
24651
|
-
gaugeWidth = 2 * drawHeight;
|
|
24652
|
-
gaugeHeight = drawHeight;
|
|
24653
|
-
}
|
|
24654
|
-
else {
|
|
24655
|
-
gaugeWidth = drawWidth;
|
|
24656
|
-
gaugeHeight = drawWidth / 2;
|
|
24657
|
-
}
|
|
24658
|
-
const gaugeX = GAUGE_PADDING_SIDE + (drawWidth - gaugeWidth) / 2;
|
|
24659
|
-
const gaugeY = titleHeight + GAUGE_PADDING_TOP + (drawHeight - gaugeHeight) / 2;
|
|
24660
|
-
return {
|
|
24661
|
-
x: gaugeX,
|
|
24662
|
-
y: gaugeY,
|
|
24663
|
-
width: gaugeWidth,
|
|
24664
|
-
height: gaugeHeight,
|
|
24665
|
-
};
|
|
24666
|
-
}
|
|
24667
|
-
/**
|
|
24668
|
-
* 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).
|
|
24669
|
-
*
|
|
24670
|
-
* Also compute an offset for the text so that it doesn't overlap with other text.
|
|
24671
|
-
*/
|
|
24672
|
-
function getInflectionValues(runtime, gaugeRect, textColor, ctx) {
|
|
24673
|
-
const maxValue = runtime.maxValue;
|
|
24674
|
-
const minValue = runtime.minValue;
|
|
24675
|
-
const gaugeCircleCenter = {
|
|
24676
|
-
x: gaugeRect.x + gaugeRect.width / 2,
|
|
24677
|
-
y: gaugeRect.y + gaugeRect.height,
|
|
24678
|
-
};
|
|
24679
|
-
const textStyle = { fontSize: GAUGE_LABELS_FONT_SIZE };
|
|
24680
|
-
const inflectionValues = [];
|
|
24681
|
-
const inflectionValuesTextRects = [];
|
|
24682
|
-
for (const inflectionValue of runtime.inflectionValues) {
|
|
24683
|
-
const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
|
|
24684
|
-
const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
|
|
24685
|
-
const angle = Math.PI - Math.PI * percentage;
|
|
24686
|
-
const textRect = getRectangleTangentToCircle(angle, // angle between X axis and the point where the rectangle is tangent to the circle
|
|
24687
|
-
gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, // radius of the gauge circle + margin below text
|
|
24688
|
-
gaugeCircleCenter.x, // center of the gauge circle
|
|
24689
|
-
gaugeCircleCenter.y, // center of the gauge circle
|
|
24690
|
-
labelWidth + 2, // width of the text + some margin
|
|
24691
|
-
GAUGE_LABELS_FONT_SIZE // height of the text
|
|
24692
|
-
);
|
|
24693
|
-
let offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect))
|
|
24694
|
-
? GAUGE_LABELS_FONT_SIZE
|
|
24695
|
-
: 0;
|
|
24696
|
-
inflectionValuesTextRects.push(textRect);
|
|
24697
|
-
inflectionValues.push({
|
|
24698
|
-
rotation: angle,
|
|
24699
|
-
label: inflectionValue.label,
|
|
24700
|
-
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
24701
|
-
color: textColor,
|
|
24702
|
-
offset,
|
|
24703
|
-
});
|
|
24704
|
-
}
|
|
24705
|
-
return inflectionValues;
|
|
24706
|
-
}
|
|
24707
|
-
function getGaugeColor(runtime) {
|
|
24708
|
-
const gaugeValue = runtime.gaugeValue?.value;
|
|
24709
|
-
if (gaugeValue === undefined) {
|
|
24710
|
-
return GAUGE_BACKGROUND_COLOR;
|
|
24711
|
-
}
|
|
24712
|
-
let colorIndex = 0;
|
|
24713
|
-
while (runtime.inflectionValues[colorIndex]?.value <= gaugeValue) {
|
|
24714
|
-
colorIndex++;
|
|
24715
|
-
}
|
|
24716
|
-
return runtime.colors[colorIndex];
|
|
24717
|
-
}
|
|
24718
|
-
function getContrastedTextColor(backgroundColor) {
|
|
24719
|
-
return relativeLuminance(backgroundColor) > 0.3
|
|
24720
|
-
? GAUGE_TEXT_COLOR
|
|
24721
|
-
: GAUGE_TEXT_COLOR_HIGH_CONTRAST;
|
|
24722
|
-
}
|
|
24723
|
-
function getSegmentsOfRectangle(rectangle) {
|
|
24724
|
-
return [
|
|
24725
|
-
{ start: rectangle.topLeft, end: rectangle.topRight },
|
|
24726
|
-
{ start: rectangle.topRight, end: rectangle.bottomRight },
|
|
24727
|
-
{ start: rectangle.bottomRight, end: rectangle.bottomLeft },
|
|
24728
|
-
{ start: rectangle.bottomLeft, end: rectangle.topLeft },
|
|
24729
|
-
];
|
|
24730
|
-
}
|
|
24731
|
-
/**
|
|
24732
|
-
* Check if two segment intersect. The case where the segments are colinear (both segments on the same line)
|
|
24733
|
-
* is not handled.
|
|
24734
|
-
*/
|
|
24735
|
-
function doSegmentIntersect(segment1, segment2) {
|
|
24736
|
-
const A = segment1.start;
|
|
24737
|
-
const B = segment1.end;
|
|
24738
|
-
const C = segment2.start;
|
|
24739
|
-
const D = segment2.end;
|
|
24740
|
-
/**
|
|
24741
|
-
* Line segment intersection algorithm
|
|
24742
|
-
* https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
|
|
24743
|
-
*/
|
|
24744
|
-
function ccw(a, b, c) {
|
|
24745
|
-
return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
|
|
24746
|
-
}
|
|
24747
|
-
return ccw(A, C, D) !== ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D);
|
|
24748
|
-
}
|
|
24749
|
-
function doRectanglesIntersect(rect1, rect2) {
|
|
24750
|
-
const segments1 = getSegmentsOfRectangle(rect1);
|
|
24751
|
-
const segments2 = getSegmentsOfRectangle(rect2);
|
|
24752
|
-
for (const segment1 of segments1) {
|
|
24753
|
-
for (const segment2 of segments2) {
|
|
24754
|
-
if (doSegmentIntersect(segment1, segment2)) {
|
|
24755
|
-
return true;
|
|
24756
|
-
}
|
|
24757
|
-
}
|
|
24758
|
-
}
|
|
24759
|
-
return false;
|
|
24760
|
-
}
|
|
24761
|
-
/**
|
|
24762
|
-
* Get the rectangle that is tangent to a circle at a given angle.
|
|
24763
|
-
*
|
|
24764
|
-
* @param angle angle between X axis and the point where the rectangle is tangent to the circle
|
|
24765
|
-
*/
|
|
24766
|
-
function getRectangleTangentToCircle(angle, radius, circleCenterX, circleCenterY, rectWidth, rectHeight) {
|
|
24767
|
-
const cos = Math.cos(angle);
|
|
24768
|
-
const sin = Math.sin(angle);
|
|
24769
|
-
// x, y are the distance from the center of the circle to the point where the rectangle is tangent to the circle
|
|
24770
|
-
const x = cos * radius;
|
|
24771
|
-
const y = sin * radius;
|
|
24772
|
-
// x2, y2 are the distance from the point the rectangle is tangent to the circle to the bottom left corner of the rectangle
|
|
24773
|
-
const x2 = sin * (rectWidth / 2); // cos(angle + 90°) = sin(angle)
|
|
24774
|
-
const y2 = cos * (rectWidth / 2);
|
|
24775
|
-
const bottomRight = {
|
|
24776
|
-
x: x + x2 + circleCenterX,
|
|
24777
|
-
y: circleCenterY - (y - y2),
|
|
24778
|
-
};
|
|
24779
|
-
const bottomLeft = {
|
|
24780
|
-
x: x - x2 + circleCenterX,
|
|
24781
|
-
y: circleCenterY - (y + y2),
|
|
24782
|
-
};
|
|
24783
|
-
// Same as above but for the top corners of the rectangle (radius + rectangle height instead of radius)
|
|
24784
|
-
const xp = cos * (radius + rectHeight);
|
|
24785
|
-
const yp = sin * (radius + rectHeight);
|
|
24786
|
-
const topLeft = {
|
|
24787
|
-
x: xp - x2 + circleCenterX,
|
|
24788
|
-
y: circleCenterY - (yp + y2),
|
|
24789
|
-
};
|
|
24790
|
-
const topRight = {
|
|
24791
|
-
x: xp + x2 + circleCenterX,
|
|
24792
|
-
y: circleCenterY - (yp - y2),
|
|
24793
|
-
};
|
|
24794
|
-
return { bottomLeft, bottomRight, topRight, topLeft };
|
|
24795
|
-
}
|
|
24796
|
-
|
|
24797
25107
|
class GaugeChartComponent extends owl.Component {
|
|
24798
25108
|
static template = "o-spreadsheet-GaugeChartComponent";
|
|
24799
25109
|
canvas = owl.useRef("chartContainer");
|
|
@@ -24826,290 +25136,6 @@ function toXlsxHexColor(color) {
|
|
|
24826
25136
|
return color;
|
|
24827
25137
|
}
|
|
24828
25138
|
|
|
24829
|
-
/**
|
|
24830
|
-
* This file contains helpers that are common to different runtime charts (mainly
|
|
24831
|
-
* line, bar and pie charts)
|
|
24832
|
-
*/
|
|
24833
|
-
/**
|
|
24834
|
-
* Get the data from a dataSet
|
|
24835
|
-
*/
|
|
24836
|
-
function getData(getters, ds) {
|
|
24837
|
-
if (ds.dataRange) {
|
|
24838
|
-
const labelCellZone = ds.labelCell ? [ds.labelCell.zone] : [];
|
|
24839
|
-
const dataZone = recomputeZones([ds.dataRange.zone], labelCellZone)[0];
|
|
24840
|
-
if (dataZone === undefined) {
|
|
24841
|
-
return [];
|
|
24842
|
-
}
|
|
24843
|
-
const dataRange = getters.getRangeFromZone(ds.dataRange.sheetId, dataZone);
|
|
24844
|
-
return getters.getRangeValues(dataRange).map((value) => (value === "" ? undefined : value));
|
|
24845
|
-
}
|
|
24846
|
-
return [];
|
|
24847
|
-
}
|
|
24848
|
-
function filterEmptyDataPoints(labels, datasets) {
|
|
24849
|
-
const numberOfDataPoints = Math.max(labels.length, ...datasets.map((dataset) => dataset.data?.length || 0));
|
|
24850
|
-
const dataPointsIndexes = range(0, numberOfDataPoints).filter((dataPointIndex) => {
|
|
24851
|
-
const label = labels[dataPointIndex];
|
|
24852
|
-
const values = datasets.map((dataset) => dataset.data?.[dataPointIndex]);
|
|
24853
|
-
return label || values.some((value) => value === 0 || Boolean(value));
|
|
24854
|
-
});
|
|
24855
|
-
return {
|
|
24856
|
-
labels: dataPointsIndexes.map((i) => labels[i] || ""),
|
|
24857
|
-
dataSetsValues: datasets.map((dataset) => ({
|
|
24858
|
-
...dataset,
|
|
24859
|
-
data: dataPointsIndexes.map((i) => dataset.data[i]),
|
|
24860
|
-
})),
|
|
24861
|
-
};
|
|
24862
|
-
}
|
|
24863
|
-
/**
|
|
24864
|
-
* Aggregates data based on labels
|
|
24865
|
-
*/
|
|
24866
|
-
function aggregateDataForLabels(labels, datasets) {
|
|
24867
|
-
const parseNumber = (value) => (typeof value === "number" ? value : 0);
|
|
24868
|
-
const labelSet = new Set(labels);
|
|
24869
|
-
const labelMap = {};
|
|
24870
|
-
labelSet.forEach((label) => {
|
|
24871
|
-
labelMap[label] = new Array(datasets.length).fill(0);
|
|
24872
|
-
});
|
|
24873
|
-
for (const indexOfLabel of range(0, labels.length)) {
|
|
24874
|
-
const label = labels[indexOfLabel];
|
|
24875
|
-
for (const indexOfDataset of range(0, datasets.length)) {
|
|
24876
|
-
labelMap[label][indexOfDataset] += parseNumber(datasets[indexOfDataset].data[indexOfLabel]);
|
|
24877
|
-
}
|
|
24878
|
-
}
|
|
24879
|
-
return {
|
|
24880
|
-
labels: Array.from(labelSet),
|
|
24881
|
-
dataSetsValues: datasets.map((dataset, indexOfDataset) => ({
|
|
24882
|
-
...dataset,
|
|
24883
|
-
data: Array.from(labelSet).map((label) => labelMap[label][indexOfDataset]),
|
|
24884
|
-
})),
|
|
24885
|
-
};
|
|
24886
|
-
}
|
|
24887
|
-
function truncateLabel(label) {
|
|
24888
|
-
if (!label) {
|
|
24889
|
-
return "";
|
|
24890
|
-
}
|
|
24891
|
-
if (label.length > MAX_CHAR_LABEL) {
|
|
24892
|
-
return label.substring(0, MAX_CHAR_LABEL) + "…";
|
|
24893
|
-
}
|
|
24894
|
-
return label;
|
|
24895
|
-
}
|
|
24896
|
-
/**
|
|
24897
|
-
* Get a default chart js configuration
|
|
24898
|
-
*/
|
|
24899
|
-
function getDefaultChartJsRuntime(chart, labels, fontColor, { format, locale, truncateLabels = true, horizontalChart, }) {
|
|
24900
|
-
const chartTitle = chart.title.text ? chart.title : { ...chart.title, content: "" };
|
|
24901
|
-
const options = {
|
|
24902
|
-
// https://www.chartjs.org/docs/latest/general/responsive.html
|
|
24903
|
-
responsive: true, // will resize when its container is resized
|
|
24904
|
-
maintainAspectRatio: false, // doesn't maintain the aspect ration (width/height =2 by default) so the user has the choice of the exact layout
|
|
24905
|
-
layout: {
|
|
24906
|
-
padding: {
|
|
24907
|
-
left: DEFAULT_CHART_PADDING,
|
|
24908
|
-
right: DEFAULT_CHART_PADDING,
|
|
24909
|
-
top: chartTitle.text ? DEFAULT_CHART_PADDING / 2 : DEFAULT_CHART_PADDING + 5,
|
|
24910
|
-
bottom: DEFAULT_CHART_PADDING,
|
|
24911
|
-
},
|
|
24912
|
-
},
|
|
24913
|
-
elements: {
|
|
24914
|
-
line: {
|
|
24915
|
-
fill: false, // do not fill the area under line charts
|
|
24916
|
-
},
|
|
24917
|
-
point: {
|
|
24918
|
-
hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
|
|
24919
|
-
},
|
|
24920
|
-
},
|
|
24921
|
-
animation: false,
|
|
24922
|
-
plugins: {
|
|
24923
|
-
title: {
|
|
24924
|
-
display: !!chartTitle.text,
|
|
24925
|
-
text: _t(chartTitle.text),
|
|
24926
|
-
color: chartTitle?.color ?? fontColor,
|
|
24927
|
-
align: chartTitle.align === "center" ? "center" : chartTitle.align === "right" ? "end" : "start",
|
|
24928
|
-
font: {
|
|
24929
|
-
size: DEFAULT_CHART_FONT_SIZE,
|
|
24930
|
-
weight: chartTitle.bold ? "bold" : "normal",
|
|
24931
|
-
style: chartTitle.italic ? "italic" : "normal",
|
|
24932
|
-
},
|
|
24933
|
-
},
|
|
24934
|
-
legend: {
|
|
24935
|
-
// Disable default legend onClick (show/hide dataset), to allow us to set a global onClick on the chart container.
|
|
24936
|
-
// If we want to re-enable this in the future, we need to override the default onClick to stop the event propagation
|
|
24937
|
-
onClick: () => { },
|
|
24938
|
-
},
|
|
24939
|
-
tooltip: {
|
|
24940
|
-
callbacks: {
|
|
24941
|
-
label: function (tooltipItem) {
|
|
24942
|
-
const xLabel = tooltipItem.dataset?.label || tooltipItem.label;
|
|
24943
|
-
// tooltipItem.parsed can be an object or a number for pie charts
|
|
24944
|
-
let yLabel = horizontalChart ? tooltipItem.parsed.x : tooltipItem.parsed.y;
|
|
24945
|
-
if (yLabel === undefined || yLabel === null) {
|
|
24946
|
-
yLabel = tooltipItem.parsed;
|
|
24947
|
-
}
|
|
24948
|
-
const toolTipFormat = !format && Math.abs(yLabel) >= 1000 ? "#,##" : format;
|
|
24949
|
-
const yLabelStr = formatValue(yLabel, { format: toolTipFormat, locale });
|
|
24950
|
-
return xLabel ? `${xLabel}: ${yLabelStr}` : yLabelStr;
|
|
24951
|
-
},
|
|
24952
|
-
},
|
|
24953
|
-
},
|
|
24954
|
-
},
|
|
24955
|
-
};
|
|
24956
|
-
return {
|
|
24957
|
-
type: chart.type,
|
|
24958
|
-
options,
|
|
24959
|
-
data: {
|
|
24960
|
-
labels: truncateLabels ? labels.map(truncateLabel) : labels,
|
|
24961
|
-
datasets: [],
|
|
24962
|
-
},
|
|
24963
|
-
platform: undefined, // This key is optional and will be set by chart.js
|
|
24964
|
-
plugins: [],
|
|
24965
|
-
};
|
|
24966
|
-
}
|
|
24967
|
-
function getChartLabelFormat(getters, range, shouldRemoveFirstLabel) {
|
|
24968
|
-
if (!range)
|
|
24969
|
-
return undefined;
|
|
24970
|
-
const { sheetId, zone } = range;
|
|
24971
|
-
const formats = positions(zone).map((position) => getters.getEvaluatedCell({ sheetId, ...position }).format);
|
|
24972
|
-
if (shouldRemoveFirstLabel) {
|
|
24973
|
-
formats.shift();
|
|
24974
|
-
}
|
|
24975
|
-
return formats.find((format) => format !== undefined);
|
|
24976
|
-
}
|
|
24977
|
-
function getChartLabelValues(getters, dataSets, labelRange) {
|
|
24978
|
-
let labels = { values: [], formattedValues: [] };
|
|
24979
|
-
if (labelRange) {
|
|
24980
|
-
if (!labelRange.invalidXc && !labelRange.invalidSheetName) {
|
|
24981
|
-
labels = {
|
|
24982
|
-
formattedValues: getters.getRangeFormattedValues(labelRange),
|
|
24983
|
-
values: getters.getRangeValues(labelRange).map((val) => String(val ?? "")),
|
|
24984
|
-
};
|
|
24985
|
-
}
|
|
24986
|
-
}
|
|
24987
|
-
else if (dataSets.length === 1) {
|
|
24988
|
-
for (let i = 0; i < getData(getters, dataSets[0]).length; i++) {
|
|
24989
|
-
labels.formattedValues.push("");
|
|
24990
|
-
labels.values.push("");
|
|
24991
|
-
}
|
|
24992
|
-
}
|
|
24993
|
-
else {
|
|
24994
|
-
if (dataSets[0]) {
|
|
24995
|
-
const ranges = getData(getters, dataSets[0]);
|
|
24996
|
-
labels = {
|
|
24997
|
-
formattedValues: range(0, ranges.length).map((r) => r.toString()),
|
|
24998
|
-
values: labels.formattedValues,
|
|
24999
|
-
};
|
|
25000
|
-
}
|
|
25001
|
-
}
|
|
25002
|
-
return labels;
|
|
25003
|
-
}
|
|
25004
|
-
/**
|
|
25005
|
-
* Get the format to apply to the the dataset values. This format is defined as the first format
|
|
25006
|
-
* found in the dataset ranges that isn't a date format.
|
|
25007
|
-
*/
|
|
25008
|
-
function getChartDatasetFormat(getters, dataSets) {
|
|
25009
|
-
for (const ds of dataSets) {
|
|
25010
|
-
const formatsInDataset = getters.getRangeFormats(ds.dataRange);
|
|
25011
|
-
const format = formatsInDataset.find((f) => f !== undefined && !isDateTimeFormat(f));
|
|
25012
|
-
if (format)
|
|
25013
|
-
return format;
|
|
25014
|
-
}
|
|
25015
|
-
return undefined;
|
|
25016
|
-
}
|
|
25017
|
-
function getChartDatasetValues(getters, dataSets) {
|
|
25018
|
-
const datasetValues = [];
|
|
25019
|
-
for (const [dsIndex, ds] of Object.entries(dataSets)) {
|
|
25020
|
-
let label;
|
|
25021
|
-
if (ds.labelCell) {
|
|
25022
|
-
const labelRange = ds.labelCell;
|
|
25023
|
-
const cell = labelRange
|
|
25024
|
-
? getters.getEvaluatedCell({
|
|
25025
|
-
sheetId: labelRange.sheetId,
|
|
25026
|
-
col: labelRange.zone.left,
|
|
25027
|
-
row: labelRange.zone.top,
|
|
25028
|
-
})
|
|
25029
|
-
: undefined;
|
|
25030
|
-
label =
|
|
25031
|
-
cell && labelRange
|
|
25032
|
-
? truncateLabel(cell.formattedValue)
|
|
25033
|
-
: (label = `${ChartTerms.Series} ${parseInt(dsIndex) + 1}`);
|
|
25034
|
-
}
|
|
25035
|
-
else {
|
|
25036
|
-
label = `${ChartTerms.Series} ${parseInt(dsIndex) + 1}`;
|
|
25037
|
-
}
|
|
25038
|
-
let data = ds.dataRange ? getData(getters, ds) : [];
|
|
25039
|
-
if (data.every((e) => typeof e === "string" && !isEvaluationError(e))) {
|
|
25040
|
-
// In this case, we want a chart based on the string occurrences count
|
|
25041
|
-
// This will be done by associating each string with a value of 1 and
|
|
25042
|
-
// the using the classical aggregation method to sum the values.
|
|
25043
|
-
data.fill(1);
|
|
25044
|
-
}
|
|
25045
|
-
datasetValues.push({ data, label });
|
|
25046
|
-
}
|
|
25047
|
-
return datasetValues;
|
|
25048
|
-
}
|
|
25049
|
-
/**
|
|
25050
|
-
* If the chart is a stacked area chart, we want to fill until the next dataset.
|
|
25051
|
-
* If the chart is a simple area chart, we want to fill until the origin (bottom axis).
|
|
25052
|
-
*
|
|
25053
|
-
* See https://www.chartjs.org/docs/latest/charts/area.html#filling-modes
|
|
25054
|
-
*/
|
|
25055
|
-
function getFillingMode(index, stackedChart) {
|
|
25056
|
-
if (!stackedChart) {
|
|
25057
|
-
return "origin";
|
|
25058
|
-
}
|
|
25059
|
-
return index === 0 ? "origin" : "-1";
|
|
25060
|
-
}
|
|
25061
|
-
function chartToImage(runtime, figure, type) {
|
|
25062
|
-
// wrap the canvas in a div with a fixed size because chart.js would
|
|
25063
|
-
// fill the whole page otherwise
|
|
25064
|
-
const div = document.createElement("div");
|
|
25065
|
-
div.style.width = `${figure.width}px`;
|
|
25066
|
-
div.style.height = `${figure.height}px`;
|
|
25067
|
-
const canvas = document.createElement("canvas");
|
|
25068
|
-
div.append(canvas);
|
|
25069
|
-
canvas.setAttribute("width", figure.width.toString());
|
|
25070
|
-
canvas.setAttribute("height", figure.height.toString());
|
|
25071
|
-
// we have to add the canvas to the DOM otherwise it won't be rendered
|
|
25072
|
-
document.body.append(div);
|
|
25073
|
-
if ("chartJsConfig" in runtime) {
|
|
25074
|
-
const config = deepCopy(runtime.chartJsConfig);
|
|
25075
|
-
config.plugins = [backgroundColorChartJSPlugin];
|
|
25076
|
-
const chart = new window.Chart(canvas, config);
|
|
25077
|
-
const imgContent = chart.toBase64Image();
|
|
25078
|
-
chart.destroy();
|
|
25079
|
-
div.remove();
|
|
25080
|
-
return imgContent;
|
|
25081
|
-
}
|
|
25082
|
-
else if (type === "scorecard") {
|
|
25083
|
-
const design = getScorecardConfiguration(figure, runtime);
|
|
25084
|
-
drawScoreChart(design, canvas);
|
|
25085
|
-
const imgContent = canvas.toDataURL();
|
|
25086
|
-
div.remove();
|
|
25087
|
-
return imgContent;
|
|
25088
|
-
}
|
|
25089
|
-
else if (type === "gauge") {
|
|
25090
|
-
drawGaugeChart(canvas, runtime);
|
|
25091
|
-
const imgContent = canvas.toDataURL();
|
|
25092
|
-
div.remove();
|
|
25093
|
-
return imgContent;
|
|
25094
|
-
}
|
|
25095
|
-
return undefined;
|
|
25096
|
-
}
|
|
25097
|
-
/**
|
|
25098
|
-
* Custom chart.js plugin to set the background color of the canvas
|
|
25099
|
-
* https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md
|
|
25100
|
-
*/
|
|
25101
|
-
const backgroundColorChartJSPlugin = {
|
|
25102
|
-
id: "customCanvasBackgroundColor",
|
|
25103
|
-
beforeDraw: (chart) => {
|
|
25104
|
-
const { ctx } = chart;
|
|
25105
|
-
ctx.save();
|
|
25106
|
-
ctx.globalCompositeOperation = "destination-over";
|
|
25107
|
-
ctx.fillStyle = "#ffffff";
|
|
25108
|
-
ctx.fillRect(0, 0, chart.width, chart.height);
|
|
25109
|
-
ctx.restore();
|
|
25110
|
-
},
|
|
25111
|
-
};
|
|
25112
|
-
|
|
25113
25139
|
class BarChart extends AbstractChart {
|
|
25114
25140
|
dataSets;
|
|
25115
25141
|
labelRange;
|
|
@@ -25530,11 +25556,12 @@ function canBeLinearChart(chart, getters) {
|
|
|
25530
25556
|
}
|
|
25531
25557
|
let missingTimeAdapterAlreadyWarned = false;
|
|
25532
25558
|
function isLuxonTimeAdapterInstalled() {
|
|
25533
|
-
|
|
25559
|
+
const Chart = getChartJSConstructor();
|
|
25560
|
+
if (!Chart) {
|
|
25534
25561
|
return false;
|
|
25535
25562
|
}
|
|
25536
25563
|
// @ts-ignore
|
|
25537
|
-
const adapter = new
|
|
25564
|
+
const adapter = new Chart._adapters._date({});
|
|
25538
25565
|
const isInstalled = adapter._id === "luxon";
|
|
25539
25566
|
if (!isInstalled && !missingTimeAdapterAlreadyWarned) {
|
|
25540
25567
|
missingTimeAdapterAlreadyWarned = true;
|
|
@@ -25551,7 +25578,8 @@ function getLineOrScatterConfiguration(chart, labels, options) {
|
|
|
25551
25578
|
generateLabels(chart) {
|
|
25552
25579
|
// color the legend labels with the dataset color, without any transparency
|
|
25553
25580
|
const { data } = chart;
|
|
25554
|
-
const
|
|
25581
|
+
const Chart = getChartJSConstructor();
|
|
25582
|
+
const labels = Chart.defaults.plugins.legend.labels.generateLabels(chart);
|
|
25555
25583
|
for (const [index, label] of labels.entries()) {
|
|
25556
25584
|
label.fillStyle = data.datasets[index].borderColor;
|
|
25557
25585
|
}
|
|
@@ -40933,6 +40961,9 @@ class ColResizer extends AbstractResizer {
|
|
|
40933
40961
|
this.MAX_SIZE_MARGIN = 90;
|
|
40934
40962
|
this.MIN_ELEMENT_SIZE = MIN_COL_WIDTH;
|
|
40935
40963
|
}
|
|
40964
|
+
get sheetId() {
|
|
40965
|
+
return this.env.model.getters.getActiveSheetId();
|
|
40966
|
+
}
|
|
40936
40967
|
_getEvOffset(ev) {
|
|
40937
40968
|
return ev.offsetX;
|
|
40938
40969
|
}
|
|
@@ -40955,10 +40986,10 @@ class ColResizer extends AbstractResizer {
|
|
|
40955
40986
|
return this.env.model.getters.getEdgeScrollCol(position, position, position);
|
|
40956
40987
|
}
|
|
40957
40988
|
_getDimensionsInViewport(index) {
|
|
40958
|
-
return this.env.model.getters.getColDimensionsInViewport(this.
|
|
40989
|
+
return this.env.model.getters.getColDimensionsInViewport(this.sheetId, index);
|
|
40959
40990
|
}
|
|
40960
40991
|
_getElementSize(index) {
|
|
40961
|
-
return this.env.model.getters.getColSize(this.
|
|
40992
|
+
return this.env.model.getters.getColSize(this.sheetId, index);
|
|
40962
40993
|
}
|
|
40963
40994
|
_getMaxSize() {
|
|
40964
40995
|
return this.colResizerRef.el.clientWidth;
|
|
@@ -40969,7 +41000,7 @@ class ColResizer extends AbstractResizer {
|
|
|
40969
41000
|
const cols = this.env.model.getters.getActiveCols();
|
|
40970
41001
|
this.env.model.dispatch("RESIZE_COLUMNS_ROWS", {
|
|
40971
41002
|
dimension: "COL",
|
|
40972
|
-
sheetId: this.
|
|
41003
|
+
sheetId: this.sheetId,
|
|
40973
41004
|
elements: cols.has(index) ? [...cols] : [index],
|
|
40974
41005
|
size,
|
|
40975
41006
|
});
|
|
@@ -40982,7 +41013,7 @@ class ColResizer extends AbstractResizer {
|
|
|
40982
41013
|
elements.push(colIndex);
|
|
40983
41014
|
}
|
|
40984
41015
|
const result = this.env.model.dispatch("MOVE_COLUMNS_ROWS", {
|
|
40985
|
-
sheetId: this.
|
|
41016
|
+
sheetId: this.sheetId,
|
|
40986
41017
|
dimension: "COL",
|
|
40987
41018
|
base: this.state.base,
|
|
40988
41019
|
elements,
|
|
@@ -41001,7 +41032,7 @@ class ColResizer extends AbstractResizer {
|
|
|
41001
41032
|
_fitElementSize(index) {
|
|
41002
41033
|
const cols = this.env.model.getters.getActiveCols();
|
|
41003
41034
|
this.env.model.dispatch("AUTORESIZE_COLUMNS", {
|
|
41004
|
-
sheetId: this.
|
|
41035
|
+
sheetId: this.sheetId,
|
|
41005
41036
|
cols: cols.has(index) ? [...cols] : [index],
|
|
41006
41037
|
});
|
|
41007
41038
|
}
|
|
@@ -41012,7 +41043,7 @@ class ColResizer extends AbstractResizer {
|
|
|
41012
41043
|
return this.env.model.getters.getActiveCols();
|
|
41013
41044
|
}
|
|
41014
41045
|
_getPreviousVisibleElement(index) {
|
|
41015
|
-
const sheetId = this.
|
|
41046
|
+
const sheetId = this.sheetId;
|
|
41016
41047
|
let row;
|
|
41017
41048
|
for (row = index - 1; row >= 0; row--) {
|
|
41018
41049
|
if (!this.env.model.getters.isColHidden(sheetId, row)) {
|
|
@@ -41023,7 +41054,7 @@ class ColResizer extends AbstractResizer {
|
|
|
41023
41054
|
}
|
|
41024
41055
|
unhide(hiddenElements) {
|
|
41025
41056
|
this.env.model.dispatch("UNHIDE_COLUMNS_ROWS", {
|
|
41026
|
-
sheetId: this.
|
|
41057
|
+
sheetId: this.sheetId,
|
|
41027
41058
|
elements: hiddenElements,
|
|
41028
41059
|
dimension: "COL",
|
|
41029
41060
|
});
|
|
@@ -41039,7 +41070,7 @@ css /* scss */ `
|
|
|
41039
41070
|
left: 0;
|
|
41040
41071
|
right: 0;
|
|
41041
41072
|
width: ${HEADER_WIDTH}px;
|
|
41042
|
-
height: 100
|
|
41073
|
+
height: calc(100% - ${HEADER_HEIGHT + SCROLLBAR_WIDTH}px);
|
|
41043
41074
|
&.o-dragging {
|
|
41044
41075
|
cursor: grabbing;
|
|
41045
41076
|
}
|
|
@@ -41097,6 +41128,9 @@ class RowResizer extends AbstractResizer {
|
|
|
41097
41128
|
this.MIN_ELEMENT_SIZE = MIN_ROW_HEIGHT;
|
|
41098
41129
|
}
|
|
41099
41130
|
rowResizerRef;
|
|
41131
|
+
get sheetId() {
|
|
41132
|
+
return this.env.model.getters.getActiveSheetId();
|
|
41133
|
+
}
|
|
41100
41134
|
_getEvOffset(ev) {
|
|
41101
41135
|
return ev.offsetY;
|
|
41102
41136
|
}
|
|
@@ -41119,10 +41153,10 @@ class RowResizer extends AbstractResizer {
|
|
|
41119
41153
|
return this.env.model.getters.getEdgeScrollRow(position, position, position);
|
|
41120
41154
|
}
|
|
41121
41155
|
_getDimensionsInViewport(index) {
|
|
41122
|
-
return this.env.model.getters.getRowDimensionsInViewport(this.
|
|
41156
|
+
return this.env.model.getters.getRowDimensionsInViewport(this.sheetId, index);
|
|
41123
41157
|
}
|
|
41124
41158
|
_getElementSize(index) {
|
|
41125
|
-
return this.env.model.getters.getRowSize(this.
|
|
41159
|
+
return this.env.model.getters.getRowSize(this.sheetId, index);
|
|
41126
41160
|
}
|
|
41127
41161
|
_getMaxSize() {
|
|
41128
41162
|
return this.rowResizerRef.el.clientHeight;
|
|
@@ -41133,7 +41167,7 @@ class RowResizer extends AbstractResizer {
|
|
|
41133
41167
|
const rows = this.env.model.getters.getActiveRows();
|
|
41134
41168
|
this.env.model.dispatch("RESIZE_COLUMNS_ROWS", {
|
|
41135
41169
|
dimension: "ROW",
|
|
41136
|
-
sheetId: this.
|
|
41170
|
+
sheetId: this.sheetId,
|
|
41137
41171
|
elements: rows.has(index) ? [...rows] : [index],
|
|
41138
41172
|
size,
|
|
41139
41173
|
});
|
|
@@ -41146,7 +41180,7 @@ class RowResizer extends AbstractResizer {
|
|
|
41146
41180
|
elements.push(rowIndex);
|
|
41147
41181
|
}
|
|
41148
41182
|
const result = this.env.model.dispatch("MOVE_COLUMNS_ROWS", {
|
|
41149
|
-
sheetId: this.
|
|
41183
|
+
sheetId: this.sheetId,
|
|
41150
41184
|
dimension: "ROW",
|
|
41151
41185
|
base: this.state.base,
|
|
41152
41186
|
elements,
|
|
@@ -41165,7 +41199,7 @@ class RowResizer extends AbstractResizer {
|
|
|
41165
41199
|
_fitElementSize(index) {
|
|
41166
41200
|
const rows = this.env.model.getters.getActiveRows();
|
|
41167
41201
|
this.env.model.dispatch("AUTORESIZE_ROWS", {
|
|
41168
|
-
sheetId: this.
|
|
41202
|
+
sheetId: this.sheetId,
|
|
41169
41203
|
rows: rows.has(index) ? [...rows] : [index],
|
|
41170
41204
|
});
|
|
41171
41205
|
}
|
|
@@ -41176,7 +41210,7 @@ class RowResizer extends AbstractResizer {
|
|
|
41176
41210
|
return this.env.model.getters.getActiveRows();
|
|
41177
41211
|
}
|
|
41178
41212
|
_getPreviousVisibleElement(index) {
|
|
41179
|
-
const sheetId = this.
|
|
41213
|
+
const sheetId = this.sheetId;
|
|
41180
41214
|
let row;
|
|
41181
41215
|
for (row = index - 1; row >= 0; row--) {
|
|
41182
41216
|
if (!this.env.model.getters.isRowHidden(sheetId, row)) {
|
|
@@ -41187,7 +41221,7 @@ class RowResizer extends AbstractResizer {
|
|
|
41187
41221
|
}
|
|
41188
41222
|
unhide(hiddenElements) {
|
|
41189
41223
|
this.env.model.dispatch("UNHIDE_COLUMNS_ROWS", {
|
|
41190
|
-
sheetId: this.
|
|
41224
|
+
sheetId: this.sheetId,
|
|
41191
41225
|
dimension: "ROW",
|
|
41192
41226
|
elements: hiddenElements,
|
|
41193
41227
|
});
|
|
@@ -43150,6 +43184,7 @@ const DRAWING_NS_C = "http://schemas.openxmlformats.org/drawingml/2006/chart";
|
|
|
43150
43184
|
const CONTENT_TYPES = {
|
|
43151
43185
|
workbook: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml",
|
|
43152
43186
|
sheet: "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml",
|
|
43187
|
+
metadata: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheetMetadata+xml",
|
|
43153
43188
|
sharedStrings: "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml",
|
|
43154
43189
|
styles: "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml",
|
|
43155
43190
|
drawing: "application/vnd.openxmlformats-officedocument.drawing+xml",
|
|
@@ -43162,6 +43197,7 @@ const CONTENT_TYPES = {
|
|
|
43162
43197
|
const XLSX_RELATION_TYPE = {
|
|
43163
43198
|
document: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument",
|
|
43164
43199
|
sheet: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet",
|
|
43200
|
+
metadata: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sheetMetadata",
|
|
43165
43201
|
sharedStrings: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings",
|
|
43166
43202
|
styles: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles",
|
|
43167
43203
|
drawing: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing",
|
|
@@ -43171,6 +43207,7 @@ const XLSX_RELATION_TYPE = {
|
|
|
43171
43207
|
hyperlink: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
|
|
43172
43208
|
image: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
|
|
43173
43209
|
};
|
|
43210
|
+
const ARRAY_FORMULA_URI = "bdbb8cdc-fa1e-496e-a857-3c3f30c029c3";
|
|
43174
43211
|
const RELATIONSHIP_NSR = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
|
43175
43212
|
const HEIGHT_FACTOR = 0.75; // 100px => 75 u
|
|
43176
43213
|
/**
|
|
@@ -45018,29 +45055,33 @@ function convertPivotTableConfig(pivotTable) {
|
|
|
45018
45055
|
* In all the sheets, replace the table-only references in the formula cells with standard references.
|
|
45019
45056
|
*/
|
|
45020
45057
|
function convertTableFormulaReferences(convertedSheets, xlsxSheets) {
|
|
45021
|
-
for (let
|
|
45022
|
-
const tables = xlsxSheets.find((s) => s.sheetName ===
|
|
45058
|
+
for (let tableSheet of convertedSheets) {
|
|
45059
|
+
const tables = xlsxSheets.find((s) => s.sheetName === tableSheet.name).tables;
|
|
45023
45060
|
for (let table of tables) {
|
|
45024
45061
|
const tabRef = table.name + "[";
|
|
45025
|
-
for (let
|
|
45026
|
-
|
|
45027
|
-
|
|
45028
|
-
|
|
45029
|
-
|
|
45030
|
-
|
|
45031
|
-
|
|
45032
|
-
|
|
45033
|
-
|
|
45034
|
-
|
|
45035
|
-
|
|
45036
|
-
|
|
45062
|
+
for (let sheet of convertedSheets) {
|
|
45063
|
+
for (let xc in sheet.cells) {
|
|
45064
|
+
const cell = sheet.cells[xc];
|
|
45065
|
+
if (cell && cell.content && cell.content.startsWith("=")) {
|
|
45066
|
+
let refIndex;
|
|
45067
|
+
while ((refIndex = cell.content.indexOf(tabRef)) !== -1) {
|
|
45068
|
+
let endIndex = refIndex + tabRef.length;
|
|
45069
|
+
let openBrackets = 1;
|
|
45070
|
+
while (openBrackets > 0 && endIndex < cell.content.length) {
|
|
45071
|
+
if (cell.content[endIndex] === "[") {
|
|
45072
|
+
openBrackets++;
|
|
45073
|
+
}
|
|
45074
|
+
else if (cell.content[endIndex] === "]") {
|
|
45075
|
+
openBrackets--;
|
|
45076
|
+
}
|
|
45077
|
+
endIndex++;
|
|
45078
|
+
}
|
|
45079
|
+
let reference = cell.content.slice(refIndex + tabRef.length, endIndex - 1);
|
|
45080
|
+
const sheetPrefix = tableSheet.id === sheet.id ? "" : tableSheet.name + "!";
|
|
45081
|
+
const convertedRef = convertTableReference(sheetPrefix, reference, table, xc);
|
|
45082
|
+
cell.content =
|
|
45083
|
+
cell.content.slice(0, refIndex) + convertedRef + cell.content.slice(endIndex);
|
|
45037
45084
|
}
|
|
45038
|
-
reference = reference.slice(0, endIndex);
|
|
45039
|
-
const convertedRef = convertTableReference(reference, table, xc);
|
|
45040
|
-
cell.content =
|
|
45041
|
-
cell.content.slice(0, refIndex) +
|
|
45042
|
-
convertedRef +
|
|
45043
|
-
cell.content.slice(tabRef.length + refIndex + endIndex + 1);
|
|
45044
45085
|
}
|
|
45045
45086
|
}
|
|
45046
45087
|
}
|
|
@@ -45048,11 +45089,17 @@ function convertTableFormulaReferences(convertedSheets, xlsxSheets) {
|
|
|
45048
45089
|
}
|
|
45049
45090
|
}
|
|
45050
45091
|
/**
|
|
45051
|
-
* Convert table-specific references in formulas into standard references.
|
|
45092
|
+
* Convert table-specific references in formulas into standard references. A table reference is composed of columns names,
|
|
45093
|
+
* and of keywords determining the rows of the table to reference.
|
|
45052
45094
|
*
|
|
45053
45095
|
* A reference in a table can have the form (only the part between brackets should be given to this function):
|
|
45054
45096
|
* - tableName[colName] : reference to the whole column "colName"
|
|
45097
|
+
* - tableName[#keyword] : reference to the whatever row the keyword refers to
|
|
45055
45098
|
* - tableName[[#keyword], [colName]] : reference to some of the element(s) of the column colName
|
|
45099
|
+
* - tableName[[#keyword], [colName]:[col2Name]] : reference to some of the element(s) of the columns colName to col2Name
|
|
45100
|
+
* - tableName[[#keyword1], [#keyword2], [colName]] : reference to all the rows referenced by the keywords in the column colName
|
|
45101
|
+
* - tableName[[#keyword1], [colName], [#keyword2]]: the keywords and colName can be in any order
|
|
45102
|
+
*
|
|
45056
45103
|
*
|
|
45057
45104
|
* The available keywords are :
|
|
45058
45105
|
* - #All : all the column (including totals)
|
|
@@ -45060,58 +45107,109 @@ function convertTableFormulaReferences(convertedSheets, xlsxSheets) {
|
|
|
45060
45107
|
* - #Headers : only the header of the column
|
|
45061
45108
|
* - #Totals : only the totals of the column
|
|
45062
45109
|
* - #This Row : only the element in the same row as the cell
|
|
45110
|
+
*
|
|
45111
|
+
* Note that the only valid combination of multiple keywords are #Data + #Totals and #Headers + #Data.
|
|
45063
45112
|
*/
|
|
45064
|
-
function convertTableReference(expr, table, cellXc) {
|
|
45065
|
-
|
|
45113
|
+
function convertTableReference(sheetPrefix, expr, table, cellXc) {
|
|
45114
|
+
// TODO: Ideally we'd want to make a real tokenizer, this simple approach won't work if for example the column name
|
|
45115
|
+
// contain # or , characters. But that's probably an edge case that we can ignore for now.
|
|
45116
|
+
const parts = expr.split(",").map((part) => part.trim());
|
|
45066
45117
|
const tableZone = toZone(table.ref);
|
|
45067
|
-
const
|
|
45068
|
-
|
|
45069
|
-
|
|
45070
|
-
|
|
45071
|
-
|
|
45072
|
-
|
|
45073
|
-
|
|
45074
|
-
|
|
45075
|
-
|
|
45076
|
-
|
|
45077
|
-
|
|
45118
|
+
const colIndexes = [];
|
|
45119
|
+
const rowIndexes = [];
|
|
45120
|
+
const foundKeywords = [];
|
|
45121
|
+
for (const part of parts) {
|
|
45122
|
+
if (removeBrackets(part).startsWith("#")) {
|
|
45123
|
+
const keyWord = removeBrackets(part);
|
|
45124
|
+
foundKeywords.push(keyWord);
|
|
45125
|
+
switch (keyWord) {
|
|
45126
|
+
case "#All":
|
|
45127
|
+
rowIndexes.push(tableZone.top, tableZone.bottom);
|
|
45128
|
+
break;
|
|
45129
|
+
case "#Data":
|
|
45130
|
+
const top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
|
|
45131
|
+
const bottom = table.totalsRowCount
|
|
45132
|
+
? tableZone.bottom - table.totalsRowCount
|
|
45133
|
+
: tableZone.bottom;
|
|
45134
|
+
rowIndexes.push(top, bottom);
|
|
45135
|
+
break;
|
|
45136
|
+
case "#This Row":
|
|
45137
|
+
rowIndexes.push(toCartesian(cellXc).row);
|
|
45138
|
+
break;
|
|
45139
|
+
case "#Headers":
|
|
45140
|
+
if (!table.headerRowCount) {
|
|
45141
|
+
return CellErrorType.InvalidReference;
|
|
45142
|
+
}
|
|
45143
|
+
rowIndexes.push(tableZone.top);
|
|
45144
|
+
break;
|
|
45145
|
+
case "#Totals":
|
|
45146
|
+
if (!table.totalsRowCount) {
|
|
45147
|
+
return CellErrorType.InvalidReference;
|
|
45148
|
+
}
|
|
45149
|
+
rowIndexes.push(tableZone.bottom);
|
|
45150
|
+
break;
|
|
45151
|
+
}
|
|
45078
45152
|
}
|
|
45079
|
-
|
|
45080
|
-
|
|
45081
|
-
|
|
45082
|
-
|
|
45083
|
-
|
|
45084
|
-
|
|
45085
|
-
|
|
45086
|
-
|
|
45087
|
-
|
|
45088
|
-
|
|
45089
|
-
|
|
45090
|
-
|
|
45091
|
-
|
|
45092
|
-
|
|
45093
|
-
|
|
45094
|
-
|
|
45095
|
-
|
|
45096
|
-
if (!table.headerRowCount) {
|
|
45097
|
-
isReferencedZoneValid = false;
|
|
45098
|
-
}
|
|
45099
|
-
break;
|
|
45100
|
-
case "#Totals":
|
|
45101
|
-
refZone.top = refZone.bottom = tableZone.bottom;
|
|
45102
|
-
if (!table.totalsRowCount) {
|
|
45103
|
-
isReferencedZoneValid = false;
|
|
45153
|
+
else {
|
|
45154
|
+
const columns = part
|
|
45155
|
+
.split(":")
|
|
45156
|
+
.map((part) => part.trim())
|
|
45157
|
+
.map(removeBrackets);
|
|
45158
|
+
if (colIndexes.length) {
|
|
45159
|
+
return CellErrorType.InvalidReference;
|
|
45160
|
+
}
|
|
45161
|
+
const colRelativeIndex = table.cols.findIndex((col) => col.name === columns[0]);
|
|
45162
|
+
if (colRelativeIndex === -1) {
|
|
45163
|
+
return CellErrorType.InvalidReference;
|
|
45164
|
+
}
|
|
45165
|
+
colIndexes.push(colRelativeIndex + tableZone.left);
|
|
45166
|
+
if (columns[1]) {
|
|
45167
|
+
const colRelativeIndex2 = table.cols.findIndex((col) => col.name === columns[1]);
|
|
45168
|
+
if (colRelativeIndex2 === -1) {
|
|
45169
|
+
return CellErrorType.InvalidReference;
|
|
45104
45170
|
}
|
|
45105
|
-
|
|
45171
|
+
colIndexes.push(colRelativeIndex2 + tableZone.left);
|
|
45172
|
+
}
|
|
45106
45173
|
}
|
|
45107
|
-
const colRef = refElements[1].slice(1, refElements[1].length - 1);
|
|
45108
|
-
const colRelativeIndex = table.cols.findIndex((col) => col.name === colRef);
|
|
45109
|
-
refZone.left = refZone.right = colRelativeIndex + tableZone.left;
|
|
45110
45174
|
}
|
|
45111
|
-
if (!
|
|
45175
|
+
if (!areKeywordsCompatible(foundKeywords)) {
|
|
45112
45176
|
return CellErrorType.InvalidReference;
|
|
45113
45177
|
}
|
|
45114
|
-
|
|
45178
|
+
if (rowIndexes.length === 0) {
|
|
45179
|
+
const top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
|
|
45180
|
+
const bottom = table.totalsRowCount
|
|
45181
|
+
? tableZone.bottom - table.totalsRowCount
|
|
45182
|
+
: tableZone.bottom;
|
|
45183
|
+
rowIndexes.push(top, bottom);
|
|
45184
|
+
}
|
|
45185
|
+
if (colIndexes.length === 0) {
|
|
45186
|
+
colIndexes.push(tableZone.left, tableZone.right);
|
|
45187
|
+
}
|
|
45188
|
+
const refZone = {
|
|
45189
|
+
top: Math.min(...rowIndexes),
|
|
45190
|
+
left: Math.min(...colIndexes),
|
|
45191
|
+
bottom: Math.max(...rowIndexes),
|
|
45192
|
+
right: Math.max(...colIndexes),
|
|
45193
|
+
};
|
|
45194
|
+
return sheetPrefix + zoneToXc(refZone);
|
|
45195
|
+
}
|
|
45196
|
+
function removeBrackets(str) {
|
|
45197
|
+
return str.startsWith("[") && str.endsWith("]") ? str.slice(1, str.length - 1) : str;
|
|
45198
|
+
}
|
|
45199
|
+
function areKeywordsCompatible(keywords) {
|
|
45200
|
+
if (keywords.length < 2) {
|
|
45201
|
+
return true;
|
|
45202
|
+
}
|
|
45203
|
+
else if (keywords.length > 2) {
|
|
45204
|
+
return false;
|
|
45205
|
+
}
|
|
45206
|
+
else if (keywords.includes("#Data") && keywords.includes("#Totals")) {
|
|
45207
|
+
return true;
|
|
45208
|
+
}
|
|
45209
|
+
else if (keywords.includes("#Headers") && keywords.includes("#Data")) {
|
|
45210
|
+
return true;
|
|
45211
|
+
}
|
|
45212
|
+
return false;
|
|
45115
45213
|
}
|
|
45116
45214
|
|
|
45117
45215
|
// -------------------------------------
|
|
@@ -45726,7 +45824,7 @@ class XlsxChartExtractor extends XlsxBaseExtractor {
|
|
|
45726
45824
|
title: { text: chartTitle },
|
|
45727
45825
|
type: CHART_TYPE_CONVERSION_MAP[chartType],
|
|
45728
45826
|
dataSets: this.extractChartDatasets(this.querySelectorAll(rootChartElement, `c:${chartType}`), chartType),
|
|
45729
|
-
labelRange: this.
|
|
45827
|
+
labelRange: this.extractLabelRange(chartType, rootChartElement),
|
|
45730
45828
|
backgroundColor: this.extractChildAttr(rootChartElement, "c:chartSpace > c:spPr a:srgbClr", "val", {
|
|
45731
45829
|
default: "ffffff",
|
|
45732
45830
|
}).asString(),
|
|
@@ -45738,6 +45836,13 @@ class XlsxChartExtractor extends XlsxBaseExtractor {
|
|
|
45738
45836
|
};
|
|
45739
45837
|
})[0];
|
|
45740
45838
|
}
|
|
45839
|
+
extractLabelRange(chartType, rootChartElement) {
|
|
45840
|
+
if (chartType === "scatterChart") {
|
|
45841
|
+
return (this.extractChildTextContent(rootChartElement, `c:ser c:strRef c:f`) ||
|
|
45842
|
+
this.extractChildTextContent(rootChartElement, `c:ser c:numRef c:f`));
|
|
45843
|
+
}
|
|
45844
|
+
return this.extractChildTextContent(rootChartElement, `c:ser c:cat c:f`);
|
|
45845
|
+
}
|
|
45741
45846
|
extractComboChart(chartElement) {
|
|
45742
45847
|
// Title can be separated into multiple xml elements (for styling and such), we only import the text
|
|
45743
45848
|
const chartTitle = this.mapOnElements({ parent: chartElement, query: "c:title a:t" }, (textElement) => {
|
|
@@ -54941,6 +55046,9 @@ class EvaluationPlugin extends UIPlugin {
|
|
|
54941
55046
|
// Export
|
|
54942
55047
|
// ---------------------------------------------------------------------------
|
|
54943
55048
|
exportForExcel(data) {
|
|
55049
|
+
for (const sheet of data.sheets) {
|
|
55050
|
+
sheet.formulaSpillRanges = {};
|
|
55051
|
+
}
|
|
54944
55052
|
for (const position of this.evaluator.getEvaluatedPositions()) {
|
|
54945
55053
|
const evaluatedCell = this.evaluator.getEvaluatedCell(position);
|
|
54946
55054
|
const xc = toXC(position.col, position.row);
|
|
@@ -54952,8 +55060,9 @@ class EvaluationPlugin extends UIPlugin {
|
|
|
54952
55060
|
const exportedSheetData = data.sheets.find((sheet) => sheet.id === position.sheetId);
|
|
54953
55061
|
const formulaCell = this.getCorrespondingFormulaCell(position);
|
|
54954
55062
|
if (formulaCell) {
|
|
55063
|
+
const cell = this.getters.getCell(position);
|
|
54955
55064
|
isExported = isExportableToExcel(formulaCell.compiledFormula.tokens);
|
|
54956
|
-
isFormula = isExported;
|
|
55065
|
+
isFormula = isExported && cell?.content === formulaCell.content;
|
|
54957
55066
|
if (!isExported) {
|
|
54958
55067
|
// If the cell contains a non-exported formula and that is evaluates to
|
|
54959
55068
|
// nothing* ,we don't export it.
|
|
@@ -54977,6 +55086,10 @@ class EvaluationPlugin extends UIPlugin {
|
|
|
54977
55086
|
content = !isExported ? newContent : exportedCellData.content;
|
|
54978
55087
|
}
|
|
54979
55088
|
exportedSheetData.cells[xc] = { ...exportedCellData, value, isFormula, content, format };
|
|
55089
|
+
const spillZone = this.getSpreadZone(position);
|
|
55090
|
+
if (spillZone) {
|
|
55091
|
+
exportedSheetData.formulaSpillRanges[xc] = this.getters.getRangeString(this.getters.getRangeFromZone(position.sheetId, spillZone), position.sheetId);
|
|
55092
|
+
}
|
|
54980
55093
|
}
|
|
54981
55094
|
}
|
|
54982
55095
|
/**
|
|
@@ -56627,7 +56740,7 @@ class AutofillPlugin extends UIPlugin {
|
|
|
56627
56740
|
getRule(cell, cells) {
|
|
56628
56741
|
const rules = autofillRulesRegistry.getAll().sort((a, b) => a.sequence - b.sequence);
|
|
56629
56742
|
const rule = rules.find((rule) => rule.condition(cell, cells));
|
|
56630
|
-
return rule && rule.generateRule(cell, cells);
|
|
56743
|
+
return rule && this.direction && rule.generateRule(cell, cells, this.direction);
|
|
56631
56744
|
}
|
|
56632
56745
|
/**
|
|
56633
56746
|
* Create the generator to be able to autofill the next cells.
|
|
@@ -61709,7 +61822,8 @@ class SheetViewPlugin extends UIPlugin {
|
|
|
61709
61822
|
? this.getters.getSheetViewVisibleCols()
|
|
61710
61823
|
: this.getters.getSheetViewVisibleRows();
|
|
61711
61824
|
const startIndex = visibleHeaders.findIndex((header) => referenceHeaderIndex >= header);
|
|
61712
|
-
|
|
61825
|
+
let endIndex = visibleHeaders.findIndex((header) => targetHeaderIndex <= header);
|
|
61826
|
+
endIndex = endIndex === -1 ? visibleHeaders.length : endIndex;
|
|
61713
61827
|
const relevantIndexes = visibleHeaders.slice(startIndex, endIndex);
|
|
61714
61828
|
let offset = 0;
|
|
61715
61829
|
for (const i of relevantIndexes) {
|
|
@@ -66955,7 +67069,7 @@ function numberRef(reference) {
|
|
|
66955
67069
|
`;
|
|
66956
67070
|
}
|
|
66957
67071
|
|
|
66958
|
-
function addFormula(cell) {
|
|
67072
|
+
function addFormula(cell, formulaSpillRange) {
|
|
66959
67073
|
const formula = cell.content;
|
|
66960
67074
|
if (!formula) {
|
|
66961
67075
|
return { attrs: [], node: escapeXml `` };
|
|
@@ -66964,10 +67078,17 @@ function addFormula(cell) {
|
|
|
66964
67078
|
if (type === undefined) {
|
|
66965
67079
|
return { attrs: [], node: escapeXml `` };
|
|
66966
67080
|
}
|
|
66967
|
-
const attrs = [
|
|
67081
|
+
const attrs = [
|
|
67082
|
+
["cm", "1"],
|
|
67083
|
+
["t", type],
|
|
67084
|
+
];
|
|
66968
67085
|
const XlsxFormula = adaptFormulaToExcel(formula);
|
|
66969
67086
|
const exportedValue = adaptFormulaValueToExcel(cell.value);
|
|
66970
|
-
|
|
67087
|
+
// We treat all formulas as array formulas (a simple formula
|
|
67088
|
+
// is an array formula that spills on only one cell) to avoid
|
|
67089
|
+
// trying to detect spilling sub-formulas which is not a trivial task.
|
|
67090
|
+
let node;
|
|
67091
|
+
node = escapeXml /*xml*/ `<f t="array" ref="${formulaSpillRange}">${XlsxFormula}</f><v>${exportedValue}</v>`;
|
|
66971
67092
|
return { attrs, node };
|
|
66972
67093
|
}
|
|
66973
67094
|
function addContent(content, sharedStrings, forceString = false) {
|
|
@@ -67612,7 +67733,7 @@ function addStyles(styles) {
|
|
|
67612
67733
|
}
|
|
67613
67734
|
if (alignAttrs.length > 0) {
|
|
67614
67735
|
attributes.push(["applyAlignment", "1"]); // for Libre Office
|
|
67615
|
-
styleNodes.push(escapeXml /*xml*/ `<xf ${formatAttributes(attributes)}
|
|
67736
|
+
styleNodes.push(escapeXml /*xml*/ `<xf ${formatAttributes(attributes)}><alignment ${formatAttributes(alignAttrs)} /></xf> `);
|
|
67616
67737
|
}
|
|
67617
67738
|
else {
|
|
67618
67739
|
styleNodes.push(escapeXml /*xml*/ `<xf ${formatAttributes(attributes)} />`);
|
|
@@ -67811,7 +67932,7 @@ function addRows(construct, data, sheet) {
|
|
|
67811
67932
|
let cellNode = escapeXml ``;
|
|
67812
67933
|
// Either formula or static value inside the cell
|
|
67813
67934
|
if (cell.isFormula) {
|
|
67814
|
-
const res = addFormula(cell);
|
|
67935
|
+
const res = addFormula(cell, sheet.formulaSpillRanges[xc] ?? xc);
|
|
67815
67936
|
if (!res) {
|
|
67816
67937
|
continue;
|
|
67817
67938
|
}
|
|
@@ -68086,6 +68207,30 @@ function createWorksheets(data, construct) {
|
|
|
68086
68207
|
`;
|
|
68087
68208
|
files.push(createXMLFile(parseXML(sheetXml), `xl/worksheets/sheet${sheetIndex}.xml`, "sheet"));
|
|
68088
68209
|
}
|
|
68210
|
+
const sheetMetadataXml = escapeXml /*xml*/ `
|
|
68211
|
+
<metadata xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:xda="http://schemas.microsoft.com/office/spreadsheetml/2017/dynamicarray">
|
|
68212
|
+
<metadataTypes count="1">
|
|
68213
|
+
<metadataType name="XLDAPR" minSupportedVersion="120000" copy="1" pasteAll="1"
|
|
68214
|
+
pasteValues="1" merge="1" splitFirst="1" rowColShift="1" clearFormats="1"
|
|
68215
|
+
clearComments="1" assign="1" coerce="1" cellMeta="1" />
|
|
68216
|
+
</metadataTypes>
|
|
68217
|
+
<futureMetadata name="XLDAPR" count="1">
|
|
68218
|
+
<bk>
|
|
68219
|
+
<extLst>
|
|
68220
|
+
<ext uri="{${ARRAY_FORMULA_URI}}">
|
|
68221
|
+
<xda:dynamicArrayProperties fDynamic="1" fCollapsed="0" />
|
|
68222
|
+
</ext>
|
|
68223
|
+
</extLst>
|
|
68224
|
+
</bk>
|
|
68225
|
+
</futureMetadata>
|
|
68226
|
+
<cellMetadata count="1">
|
|
68227
|
+
<bk>
|
|
68228
|
+
<rc t="1" v="0" />
|
|
68229
|
+
</bk>
|
|
68230
|
+
</cellMetadata>
|
|
68231
|
+
</metadata>
|
|
68232
|
+
`;
|
|
68233
|
+
files.push(createXMLFile(parseXML(sheetMetadataXml), "xl/metadata.xml", "metadata"));
|
|
68089
68234
|
addRelsToFile(construct.relsFiles, "xl/_rels/workbook.xml.rels", {
|
|
68090
68235
|
type: XLSX_RELATION_TYPE.sharedStrings,
|
|
68091
68236
|
target: "sharedStrings.xml",
|
|
@@ -68094,6 +68239,10 @@ function createWorksheets(data, construct) {
|
|
|
68094
68239
|
type: XLSX_RELATION_TYPE.styles,
|
|
68095
68240
|
target: "styles.xml",
|
|
68096
68241
|
});
|
|
68242
|
+
addRelsToFile(construct.relsFiles, "xl/_rels/workbook.xml.rels", {
|
|
68243
|
+
type: XLSX_RELATION_TYPE.metadata,
|
|
68244
|
+
target: "metadata.xml",
|
|
68245
|
+
});
|
|
68097
68246
|
return files;
|
|
68098
68247
|
}
|
|
68099
68248
|
/**
|
|
@@ -69024,6 +69173,6 @@ exports.tokenColors = tokenColors;
|
|
|
69024
69173
|
exports.tokenize = tokenize;
|
|
69025
69174
|
|
|
69026
69175
|
|
|
69027
|
-
__info__.version = "17.4.
|
|
69028
|
-
__info__.date = "2025-
|
|
69029
|
-
__info__.hash = "
|
|
69176
|
+
__info__.version = "17.4.26";
|
|
69177
|
+
__info__.date = "2025-03-12T15:31:45.184Z";
|
|
69178
|
+
__info__.hash = "a18429e";
|