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