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