@odoo/o-spreadsheet 18.1.9 → 18.1.10
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 +722 -581
- package/dist/o-spreadsheet.d.ts +12 -3
- package/dist/o-spreadsheet.esm.js +722 -581
- package/dist/o-spreadsheet.iife.js +722 -581
- package/dist/o-spreadsheet.iife.min.js +397 -376
- package/dist/o_spreadsheet.xml +31 -26
- package/package.json +1 -1
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* This file is generated by o-spreadsheet build tools. Do not edit it.
|
|
4
4
|
* @see https://github.com/odoo/o-spreadsheet
|
|
5
|
-
* @version 18.1.
|
|
6
|
-
* @date 2025-
|
|
7
|
-
* @hash
|
|
5
|
+
* @version 18.1.10
|
|
6
|
+
* @date 2025-03-07T10:34:41.861Z
|
|
7
|
+
* @hash 31e4526
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
'use strict';
|
|
@@ -6093,8 +6093,9 @@ function spreadRange(getters, dataSets) {
|
|
|
6093
6093
|
if (zone.bottom !== zone.top && zone.left != zone.right) {
|
|
6094
6094
|
if (zone.right) {
|
|
6095
6095
|
for (let j = zone.left; j <= zone.right; ++j) {
|
|
6096
|
+
const datasetOptions = j === zone.left ? dataSet : { yAxisId: dataSet.yAxisId };
|
|
6096
6097
|
postProcessedRanges.push({
|
|
6097
|
-
...
|
|
6098
|
+
...datasetOptions,
|
|
6098
6099
|
dataRange: `${sheetPrefix}${zoneToXc({
|
|
6099
6100
|
left: j,
|
|
6100
6101
|
right: j,
|
|
@@ -6106,8 +6107,9 @@ function spreadRange(getters, dataSets) {
|
|
|
6106
6107
|
}
|
|
6107
6108
|
else {
|
|
6108
6109
|
for (let j = zone.top; j <= zone.bottom; ++j) {
|
|
6110
|
+
const datasetOptions = j === zone.top ? dataSet : { yAxisId: dataSet.yAxisId };
|
|
6109
6111
|
postProcessedRanges.push({
|
|
6110
|
-
...
|
|
6112
|
+
...datasetOptions,
|
|
6111
6113
|
dataRange: `${sheetPrefix}${zoneToXc({
|
|
6112
6114
|
left: zone.left,
|
|
6113
6115
|
right: zone.right,
|
|
@@ -10058,70 +10060,341 @@ function getNextNonEmptyBar(bars, startIndex) {
|
|
|
10058
10060
|
return bars.find((bar, i) => i > startIndex && bar.height !== 0);
|
|
10059
10061
|
}
|
|
10060
10062
|
|
|
10061
|
-
|
|
10062
|
-
|
|
10063
|
-
|
|
10064
|
-
|
|
10065
|
-
|
|
10066
|
-
|
|
10063
|
+
const GAUGE_PADDING_SIDE = 30;
|
|
10064
|
+
const GAUGE_PADDING_TOP = 10;
|
|
10065
|
+
const GAUGE_PADDING_BOTTOM = 20;
|
|
10066
|
+
const GAUGE_LABELS_FONT_SIZE = 12;
|
|
10067
|
+
const GAUGE_DEFAULT_VALUE_FONT_SIZE = 80;
|
|
10068
|
+
const GAUGE_BACKGROUND_COLOR = "#F3F2F1";
|
|
10069
|
+
const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
|
|
10070
|
+
const GAUGE_TITLE_SECTION_HEIGHT = 25;
|
|
10071
|
+
function drawGaugeChart(canvas, runtime) {
|
|
10072
|
+
const canvasBoundingRect = canvas.getBoundingClientRect();
|
|
10073
|
+
canvas.width = canvasBoundingRect.width;
|
|
10074
|
+
canvas.height = canvasBoundingRect.height;
|
|
10075
|
+
const ctx = canvas.getContext("2d");
|
|
10076
|
+
const config = getGaugeRenderingConfig(canvasBoundingRect, runtime, ctx);
|
|
10077
|
+
drawBackground(ctx, config);
|
|
10078
|
+
drawGauge(ctx, config);
|
|
10079
|
+
drawInflectionValues(ctx, config);
|
|
10080
|
+
drawLabels(ctx, config);
|
|
10081
|
+
drawTitle(ctx, config);
|
|
10082
|
+
}
|
|
10083
|
+
function drawGauge(ctx, config) {
|
|
10084
|
+
ctx.save();
|
|
10085
|
+
const gauge = config.gauge;
|
|
10086
|
+
const arcCenterX = gauge.rect.x + gauge.rect.width / 2;
|
|
10087
|
+
const arcCenterY = gauge.rect.y + gauge.rect.height;
|
|
10088
|
+
const arcRadius = gauge.rect.height - gauge.arcWidth / 2;
|
|
10089
|
+
if (arcRadius < 0) {
|
|
10090
|
+
return;
|
|
10091
|
+
}
|
|
10092
|
+
const gaugeAngle = gauge.percentage === 1 ? 0 : Math.PI * (1 + gauge.percentage);
|
|
10093
|
+
// Gauge background
|
|
10094
|
+
ctx.strokeStyle = GAUGE_BACKGROUND_COLOR;
|
|
10095
|
+
ctx.beginPath();
|
|
10096
|
+
ctx.lineWidth = gauge.arcWidth;
|
|
10097
|
+
ctx.arc(arcCenterX, arcCenterY, arcRadius, gaugeAngle, 0);
|
|
10098
|
+
ctx.stroke();
|
|
10099
|
+
// Gauge value
|
|
10100
|
+
ctx.strokeStyle = gauge.color;
|
|
10101
|
+
ctx.beginPath();
|
|
10102
|
+
ctx.arc(arcCenterX, arcCenterY, arcRadius, Math.PI, gaugeAngle);
|
|
10103
|
+
ctx.stroke();
|
|
10104
|
+
ctx.restore();
|
|
10105
|
+
}
|
|
10106
|
+
function drawBackground(ctx, config) {
|
|
10107
|
+
ctx.save();
|
|
10108
|
+
ctx.fillStyle = config.backgroundColor;
|
|
10109
|
+
ctx.fillRect(0, 0, config.width, config.height);
|
|
10110
|
+
ctx.restore();
|
|
10111
|
+
}
|
|
10112
|
+
function drawLabels(ctx, config) {
|
|
10113
|
+
for (const label of [config.minLabel, config.maxLabel, config.gaugeValue]) {
|
|
10114
|
+
ctx.save();
|
|
10115
|
+
ctx.textAlign = "center";
|
|
10116
|
+
ctx.fillStyle = label.color;
|
|
10117
|
+
ctx.font = `${label.fontSize}px ${DEFAULT_FONT}`;
|
|
10118
|
+
ctx.fillText(label.label, label.textPosition.x, label.textPosition.y);
|
|
10119
|
+
ctx.restore();
|
|
10120
|
+
}
|
|
10121
|
+
}
|
|
10122
|
+
function drawInflectionValues(ctx, config) {
|
|
10123
|
+
const { x: rectX, y: rectY, width, height } = config.gauge.rect;
|
|
10124
|
+
for (const inflectionValue of config.inflectionValues) {
|
|
10125
|
+
ctx.save();
|
|
10126
|
+
ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment
|
|
10127
|
+
ctx.rotate(Math.PI / 2 - inflectionValue.rotation);
|
|
10128
|
+
ctx.lineWidth = 2;
|
|
10129
|
+
ctx.strokeStyle = chartMutedFontColor(config.backgroundColor) + "aa";
|
|
10130
|
+
ctx.beginPath();
|
|
10131
|
+
ctx.moveTo(0, -(height - config.gauge.arcWidth));
|
|
10132
|
+
ctx.lineTo(0, -height - 3);
|
|
10133
|
+
ctx.stroke();
|
|
10134
|
+
ctx.textAlign = "center";
|
|
10135
|
+
ctx.font = `${inflectionValue.fontSize}px ${DEFAULT_FONT}`;
|
|
10136
|
+
ctx.fillStyle = inflectionValue.color;
|
|
10137
|
+
const textY = -height - GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN - inflectionValue.offset;
|
|
10138
|
+
ctx.fillText(inflectionValue.label, 0, textY);
|
|
10139
|
+
ctx.restore();
|
|
10140
|
+
}
|
|
10141
|
+
}
|
|
10142
|
+
function drawTitle(ctx, config) {
|
|
10143
|
+
ctx.save();
|
|
10144
|
+
const title = config.title;
|
|
10145
|
+
ctx.font = getDefaultContextFont(title.fontSize, title.bold, title.italic);
|
|
10146
|
+
ctx.textBaseline = "middle";
|
|
10147
|
+
ctx.fillStyle = title.color;
|
|
10148
|
+
ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
|
|
10149
|
+
ctx.restore();
|
|
10150
|
+
}
|
|
10151
|
+
function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
|
|
10152
|
+
const maxValue = runtime.maxValue;
|
|
10153
|
+
const minValue = runtime.minValue;
|
|
10154
|
+
const gaugeValue = runtime.gaugeValue;
|
|
10155
|
+
const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
|
|
10156
|
+
const gaugeArcWidth = gaugeRect.width / 6;
|
|
10157
|
+
const gaugePercentage = gaugeValue
|
|
10158
|
+
? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
|
|
10159
|
+
: 0;
|
|
10160
|
+
const gaugeValuePosition = {
|
|
10161
|
+
x: boundingRect.width / 2,
|
|
10162
|
+
y: gaugeRect.y + gaugeRect.height - gaugeRect.height / 12,
|
|
10067
10163
|
};
|
|
10068
|
-
|
|
10069
|
-
|
|
10070
|
-
|
|
10071
|
-
|
|
10072
|
-
return this.chartRuntime.background;
|
|
10164
|
+
let gaugeValueFontSize = GAUGE_DEFAULT_VALUE_FONT_SIZE;
|
|
10165
|
+
// Scale down the font size if the gaugeRect is too small
|
|
10166
|
+
if (gaugeRect.height < 300) {
|
|
10167
|
+
gaugeValueFontSize = gaugeValueFontSize * (gaugeRect.height / 300);
|
|
10073
10168
|
}
|
|
10074
|
-
|
|
10075
|
-
|
|
10169
|
+
// Scale down the font size if the text is too long
|
|
10170
|
+
const maxTextWidth = gaugeRect.width / 2;
|
|
10171
|
+
const gaugeLabel = gaugeValue?.label || "-";
|
|
10172
|
+
if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
|
|
10173
|
+
gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
|
|
10076
10174
|
}
|
|
10077
|
-
|
|
10078
|
-
|
|
10079
|
-
|
|
10080
|
-
|
|
10081
|
-
|
|
10082
|
-
|
|
10175
|
+
const minLabelPosition = {
|
|
10176
|
+
x: gaugeRect.x + gaugeArcWidth / 2,
|
|
10177
|
+
y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
|
|
10178
|
+
};
|
|
10179
|
+
const maxLabelPosition = {
|
|
10180
|
+
x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
|
|
10181
|
+
y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
|
|
10182
|
+
};
|
|
10183
|
+
const textColor = chartMutedFontColor(runtime.background);
|
|
10184
|
+
const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);
|
|
10185
|
+
let x = 0, titleWidth = 0, titleHeight = 0;
|
|
10186
|
+
if (runtime.title.text) {
|
|
10187
|
+
({ width: titleWidth, height: titleHeight } = computeTextDimension(ctx, runtime.title.text, { fontSize: CHART_TITLE_FONT_SIZE, ...runtime.title }, "px"));
|
|
10083
10188
|
}
|
|
10084
|
-
|
|
10085
|
-
|
|
10086
|
-
|
|
10087
|
-
|
|
10088
|
-
|
|
10089
|
-
|
|
10090
|
-
|
|
10091
|
-
|
|
10092
|
-
|
|
10093
|
-
|
|
10094
|
-
|
|
10095
|
-
|
|
10096
|
-
|
|
10097
|
-
|
|
10098
|
-
|
|
10099
|
-
|
|
10100
|
-
|
|
10101
|
-
|
|
10102
|
-
|
|
10103
|
-
|
|
10189
|
+
switch (runtime.title.align) {
|
|
10190
|
+
case "right":
|
|
10191
|
+
x = boundingRect.width - titleWidth - CHART_PADDING$1;
|
|
10192
|
+
break;
|
|
10193
|
+
case "center":
|
|
10194
|
+
x = (boundingRect.width - titleWidth) / 2;
|
|
10195
|
+
break;
|
|
10196
|
+
case "left":
|
|
10197
|
+
default:
|
|
10198
|
+
x = CHART_PADDING$1;
|
|
10199
|
+
break;
|
|
10200
|
+
}
|
|
10201
|
+
return {
|
|
10202
|
+
width: boundingRect.width,
|
|
10203
|
+
height: boundingRect.height,
|
|
10204
|
+
title: {
|
|
10205
|
+
label: runtime.title.text ?? "",
|
|
10206
|
+
fontSize: runtime.title.fontSize ?? CHART_TITLE_FONT_SIZE,
|
|
10207
|
+
textPosition: {
|
|
10208
|
+
x,
|
|
10209
|
+
y: CHART_PADDING_TOP + titleHeight / 2,
|
|
10210
|
+
},
|
|
10211
|
+
color: runtime.title.color ?? textColor,
|
|
10212
|
+
bold: runtime.title.bold,
|
|
10213
|
+
italic: runtime.title.italic,
|
|
10214
|
+
},
|
|
10215
|
+
backgroundColor: runtime.background,
|
|
10216
|
+
gauge: {
|
|
10217
|
+
rect: gaugeRect,
|
|
10218
|
+
arcWidth: gaugeArcWidth,
|
|
10219
|
+
percentage: clip(gaugePercentage, 0, 1),
|
|
10220
|
+
color: getGaugeColor(runtime),
|
|
10221
|
+
},
|
|
10222
|
+
inflectionValues,
|
|
10223
|
+
gaugeValue: {
|
|
10224
|
+
label: gaugeLabel,
|
|
10225
|
+
textPosition: gaugeValuePosition,
|
|
10226
|
+
fontSize: gaugeValueFontSize,
|
|
10227
|
+
color: textColor,
|
|
10228
|
+
},
|
|
10229
|
+
minLabel: {
|
|
10230
|
+
label: runtime.minValue.label,
|
|
10231
|
+
textPosition: minLabelPosition,
|
|
10232
|
+
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
10233
|
+
color: textColor,
|
|
10234
|
+
},
|
|
10235
|
+
maxLabel: {
|
|
10236
|
+
label: runtime.maxValue.label,
|
|
10237
|
+
textPosition: maxLabelPosition,
|
|
10238
|
+
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
10239
|
+
color: textColor,
|
|
10240
|
+
},
|
|
10241
|
+
};
|
|
10242
|
+
}
|
|
10243
|
+
/**
|
|
10244
|
+
* Get the rectangle in which the gauge will be drawn, based on the bounding rectangle of the canvas and leaving
|
|
10245
|
+
* space for the title and labels.
|
|
10246
|
+
*/
|
|
10247
|
+
function getGaugeRect(boundingRect, title) {
|
|
10248
|
+
const titleHeight = title ? GAUGE_TITLE_SECTION_HEIGHT : 0;
|
|
10249
|
+
const drawHeight = boundingRect.height - GAUGE_PADDING_BOTTOM - titleHeight - GAUGE_PADDING_TOP;
|
|
10250
|
+
const drawWidth = boundingRect.width - GAUGE_PADDING_SIDE * 2;
|
|
10251
|
+
let gaugeWidth;
|
|
10252
|
+
let gaugeHeight;
|
|
10253
|
+
if (drawWidth > 2 * drawHeight) {
|
|
10254
|
+
gaugeWidth = 2 * drawHeight;
|
|
10255
|
+
gaugeHeight = drawHeight;
|
|
10256
|
+
}
|
|
10257
|
+
else {
|
|
10258
|
+
gaugeWidth = drawWidth;
|
|
10259
|
+
gaugeHeight = drawWidth / 2;
|
|
10260
|
+
}
|
|
10261
|
+
const gaugeX = GAUGE_PADDING_SIDE + (drawWidth - gaugeWidth) / 2;
|
|
10262
|
+
const gaugeY = titleHeight + GAUGE_PADDING_TOP + (drawHeight - gaugeHeight) / 2;
|
|
10263
|
+
return {
|
|
10264
|
+
x: gaugeX,
|
|
10265
|
+
y: gaugeY,
|
|
10266
|
+
width: gaugeWidth,
|
|
10267
|
+
height: gaugeHeight,
|
|
10268
|
+
};
|
|
10269
|
+
}
|
|
10270
|
+
/**
|
|
10271
|
+
* 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).
|
|
10272
|
+
*
|
|
10273
|
+
* Also compute an offset for the text so that it doesn't overlap with other text.
|
|
10274
|
+
*/
|
|
10275
|
+
function getInflectionValues(runtime, gaugeRect, textColor, ctx) {
|
|
10276
|
+
const maxValue = runtime.maxValue;
|
|
10277
|
+
const minValue = runtime.minValue;
|
|
10278
|
+
const gaugeCircleCenter = {
|
|
10279
|
+
x: gaugeRect.x + gaugeRect.width / 2,
|
|
10280
|
+
y: gaugeRect.y + gaugeRect.height,
|
|
10281
|
+
};
|
|
10282
|
+
const textStyle = { fontSize: GAUGE_LABELS_FONT_SIZE };
|
|
10283
|
+
const inflectionValues = [];
|
|
10284
|
+
const inflectionValuesTextRects = [];
|
|
10285
|
+
for (const inflectionValue of runtime.inflectionValues) {
|
|
10286
|
+
const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
|
|
10287
|
+
const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
|
|
10288
|
+
const angle = Math.PI - Math.PI * percentage;
|
|
10289
|
+
const textRect = getRectangleTangentToCircle(angle, // angle between X axis and the point where the rectangle is tangent to the circle
|
|
10290
|
+
gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, // radius of the gauge circle + margin below text
|
|
10291
|
+
gaugeCircleCenter.x, // center of the gauge circle
|
|
10292
|
+
gaugeCircleCenter.y, // center of the gauge circle
|
|
10293
|
+
labelWidth + 2, // width of the text + some margin
|
|
10294
|
+
GAUGE_LABELS_FONT_SIZE // height of the text
|
|
10295
|
+
);
|
|
10296
|
+
let offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect))
|
|
10297
|
+
? GAUGE_LABELS_FONT_SIZE
|
|
10298
|
+
: 0;
|
|
10299
|
+
inflectionValuesTextRects.push(textRect);
|
|
10300
|
+
inflectionValues.push({
|
|
10301
|
+
rotation: angle,
|
|
10302
|
+
label: inflectionValue.label,
|
|
10303
|
+
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
10304
|
+
color: textColor,
|
|
10305
|
+
offset,
|
|
10104
10306
|
});
|
|
10105
10307
|
}
|
|
10106
|
-
|
|
10107
|
-
|
|
10108
|
-
|
|
10109
|
-
|
|
10308
|
+
return inflectionValues;
|
|
10309
|
+
}
|
|
10310
|
+
function getGaugeColor(runtime) {
|
|
10311
|
+
const gaugeValue = runtime.gaugeValue?.value;
|
|
10312
|
+
if (gaugeValue === undefined) {
|
|
10313
|
+
return GAUGE_BACKGROUND_COLOR;
|
|
10110
10314
|
}
|
|
10111
|
-
|
|
10112
|
-
const
|
|
10113
|
-
if (
|
|
10114
|
-
|
|
10115
|
-
if (chartData.options?.plugins?.title) {
|
|
10116
|
-
this.chart.config.options.plugins.title = chartData.options.plugins.title;
|
|
10117
|
-
}
|
|
10315
|
+
for (let i = 0; i < runtime.inflectionValues.length; i++) {
|
|
10316
|
+
const inflectionValue = runtime.inflectionValues[i];
|
|
10317
|
+
if (inflectionValue.operator === "<" && gaugeValue < inflectionValue.value) {
|
|
10318
|
+
return runtime.colors[i];
|
|
10118
10319
|
}
|
|
10119
|
-
else {
|
|
10120
|
-
|
|
10320
|
+
else if (inflectionValue.operator === "<=" && gaugeValue <= inflectionValue.value) {
|
|
10321
|
+
return runtime.colors[i];
|
|
10121
10322
|
}
|
|
10122
|
-
this.chart.config.options = chartData.options;
|
|
10123
|
-
this.chart.update();
|
|
10124
10323
|
}
|
|
10324
|
+
return runtime.colors.at(-1);
|
|
10325
|
+
}
|
|
10326
|
+
function getSegmentsOfRectangle(rectangle) {
|
|
10327
|
+
return [
|
|
10328
|
+
{ start: rectangle.topLeft, end: rectangle.topRight },
|
|
10329
|
+
{ start: rectangle.topRight, end: rectangle.bottomRight },
|
|
10330
|
+
{ start: rectangle.bottomRight, end: rectangle.bottomLeft },
|
|
10331
|
+
{ start: rectangle.bottomLeft, end: rectangle.topLeft },
|
|
10332
|
+
];
|
|
10333
|
+
}
|
|
10334
|
+
/**
|
|
10335
|
+
* Check if two segment intersect. The case where the segments are colinear (both segments on the same line)
|
|
10336
|
+
* is not handled.
|
|
10337
|
+
*/
|
|
10338
|
+
function doSegmentIntersect(segment1, segment2) {
|
|
10339
|
+
const A = segment1.start;
|
|
10340
|
+
const B = segment1.end;
|
|
10341
|
+
const C = segment2.start;
|
|
10342
|
+
const D = segment2.end;
|
|
10343
|
+
/**
|
|
10344
|
+
* Line segment intersection algorithm
|
|
10345
|
+
* https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
|
|
10346
|
+
*/
|
|
10347
|
+
function ccw(a, b, c) {
|
|
10348
|
+
return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
|
|
10349
|
+
}
|
|
10350
|
+
return ccw(A, C, D) !== ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D);
|
|
10351
|
+
}
|
|
10352
|
+
function doRectanglesIntersect(rect1, rect2) {
|
|
10353
|
+
const segments1 = getSegmentsOfRectangle(rect1);
|
|
10354
|
+
const segments2 = getSegmentsOfRectangle(rect2);
|
|
10355
|
+
for (const segment1 of segments1) {
|
|
10356
|
+
for (const segment2 of segments2) {
|
|
10357
|
+
if (doSegmentIntersect(segment1, segment2)) {
|
|
10358
|
+
return true;
|
|
10359
|
+
}
|
|
10360
|
+
}
|
|
10361
|
+
}
|
|
10362
|
+
return false;
|
|
10363
|
+
}
|
|
10364
|
+
/**
|
|
10365
|
+
* Get the rectangle that is tangent to a circle at a given angle.
|
|
10366
|
+
*
|
|
10367
|
+
* @param angle angle between X axis and the point where the rectangle is tangent to the circle
|
|
10368
|
+
*/
|
|
10369
|
+
function getRectangleTangentToCircle(angle, radius, circleCenterX, circleCenterY, rectWidth, rectHeight) {
|
|
10370
|
+
const cos = Math.cos(angle);
|
|
10371
|
+
const sin = Math.sin(angle);
|
|
10372
|
+
// x, y are the distance from the center of the circle to the point where the rectangle is tangent to the circle
|
|
10373
|
+
const x = cos * radius;
|
|
10374
|
+
const y = sin * radius;
|
|
10375
|
+
// x2, y2 are the distance from the point the rectangle is tangent to the circle to the bottom left corner of the rectangle
|
|
10376
|
+
const x2 = sin * (rectWidth / 2); // cos(angle + 90°) = sin(angle)
|
|
10377
|
+
const y2 = cos * (rectWidth / 2);
|
|
10378
|
+
const bottomRight = {
|
|
10379
|
+
x: x + x2 + circleCenterX,
|
|
10380
|
+
y: circleCenterY - (y - y2),
|
|
10381
|
+
};
|
|
10382
|
+
const bottomLeft = {
|
|
10383
|
+
x: x - x2 + circleCenterX,
|
|
10384
|
+
y: circleCenterY - (y + y2),
|
|
10385
|
+
};
|
|
10386
|
+
// Same as above but for the top corners of the rectangle (radius + rectangle height instead of radius)
|
|
10387
|
+
const xp = cos * (radius + rectHeight);
|
|
10388
|
+
const yp = sin * (radius + rectHeight);
|
|
10389
|
+
const topLeft = {
|
|
10390
|
+
x: xp - x2 + circleCenterX,
|
|
10391
|
+
y: circleCenterY - (yp + y2),
|
|
10392
|
+
};
|
|
10393
|
+
const topRight = {
|
|
10394
|
+
x: xp + x2 + circleCenterX,
|
|
10395
|
+
y: circleCenterY - (yp - y2),
|
|
10396
|
+
};
|
|
10397
|
+
return { bottomLeft, bottomRight, topRight, topLeft };
|
|
10125
10398
|
}
|
|
10126
10399
|
|
|
10127
10400
|
/**
|
|
@@ -10703,6 +10976,155 @@ class ScorecardChartConfigBuilder {
|
|
|
10703
10976
|
}
|
|
10704
10977
|
}
|
|
10705
10978
|
|
|
10979
|
+
const CHART_COMMON_OPTIONS = {
|
|
10980
|
+
// https://www.chartjs.org/docs/latest/general/responsive.html
|
|
10981
|
+
responsive: true, // will resize when its container is resized
|
|
10982
|
+
maintainAspectRatio: false, // doesn't maintain the aspect ratio (width/height =2 by default) so the user has the choice of the exact layout
|
|
10983
|
+
elements: {
|
|
10984
|
+
line: {
|
|
10985
|
+
fill: false, // do not fill the area under line charts
|
|
10986
|
+
},
|
|
10987
|
+
point: {
|
|
10988
|
+
hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
|
|
10989
|
+
},
|
|
10990
|
+
},
|
|
10991
|
+
animation: false,
|
|
10992
|
+
};
|
|
10993
|
+
function truncateLabel(label) {
|
|
10994
|
+
if (!label) {
|
|
10995
|
+
return "";
|
|
10996
|
+
}
|
|
10997
|
+
if (label.length > MAX_CHAR_LABEL) {
|
|
10998
|
+
return label.substring(0, MAX_CHAR_LABEL) + "…";
|
|
10999
|
+
}
|
|
11000
|
+
return label;
|
|
11001
|
+
}
|
|
11002
|
+
function chartToImage(runtime, figure, type) {
|
|
11003
|
+
// wrap the canvas in a div with a fixed size because chart.js would
|
|
11004
|
+
// fill the whole page otherwise
|
|
11005
|
+
const div = document.createElement("div");
|
|
11006
|
+
div.style.width = `${figure.width}px`;
|
|
11007
|
+
div.style.height = `${figure.height}px`;
|
|
11008
|
+
const canvas = document.createElement("canvas");
|
|
11009
|
+
div.append(canvas);
|
|
11010
|
+
canvas.setAttribute("width", figure.width.toString());
|
|
11011
|
+
canvas.setAttribute("height", figure.height.toString());
|
|
11012
|
+
// we have to add the canvas to the DOM otherwise it won't be rendered
|
|
11013
|
+
document.body.append(div);
|
|
11014
|
+
if ("chartJsConfig" in runtime) {
|
|
11015
|
+
const config = deepCopy(runtime.chartJsConfig);
|
|
11016
|
+
config.plugins = [backgroundColorChartJSPlugin];
|
|
11017
|
+
const Chart = getChartJSConstructor();
|
|
11018
|
+
const chart = new Chart(canvas, config);
|
|
11019
|
+
const imgContent = chart.toBase64Image();
|
|
11020
|
+
chart.destroy();
|
|
11021
|
+
div.remove();
|
|
11022
|
+
return imgContent;
|
|
11023
|
+
}
|
|
11024
|
+
else if (type === "scorecard") {
|
|
11025
|
+
const design = getScorecardConfiguration(figure, runtime);
|
|
11026
|
+
drawScoreChart(design, canvas);
|
|
11027
|
+
const imgContent = canvas.toDataURL();
|
|
11028
|
+
div.remove();
|
|
11029
|
+
return imgContent;
|
|
11030
|
+
}
|
|
11031
|
+
else if (type === "gauge") {
|
|
11032
|
+
drawGaugeChart(canvas, runtime);
|
|
11033
|
+
const imgContent = canvas.toDataURL();
|
|
11034
|
+
div.remove();
|
|
11035
|
+
return imgContent;
|
|
11036
|
+
}
|
|
11037
|
+
return undefined;
|
|
11038
|
+
}
|
|
11039
|
+
/**
|
|
11040
|
+
* Custom chart.js plugin to set the background color of the canvas
|
|
11041
|
+
* https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md
|
|
11042
|
+
*/
|
|
11043
|
+
const backgroundColorChartJSPlugin = {
|
|
11044
|
+
id: "customCanvasBackgroundColor",
|
|
11045
|
+
beforeDraw: (chart) => {
|
|
11046
|
+
const { ctx } = chart;
|
|
11047
|
+
ctx.save();
|
|
11048
|
+
ctx.globalCompositeOperation = "destination-over";
|
|
11049
|
+
ctx.fillStyle = "#ffffff";
|
|
11050
|
+
ctx.fillRect(0, 0, chart.width, chart.height);
|
|
11051
|
+
ctx.restore();
|
|
11052
|
+
},
|
|
11053
|
+
};
|
|
11054
|
+
/** Return window.Chart, making sure all our extensions are loaded in ChartJS */
|
|
11055
|
+
function getChartJSConstructor() {
|
|
11056
|
+
if (window.Chart && !window.Chart?.registry.plugins.get("chartShowValuesPlugin")) {
|
|
11057
|
+
window.Chart.register(chartShowValuesPlugin);
|
|
11058
|
+
window.Chart.register(waterfallLinesPlugin);
|
|
11059
|
+
}
|
|
11060
|
+
return window.Chart;
|
|
11061
|
+
}
|
|
11062
|
+
|
|
11063
|
+
class ChartJsComponent extends owl.Component {
|
|
11064
|
+
static template = "o-spreadsheet-ChartJsComponent";
|
|
11065
|
+
static props = {
|
|
11066
|
+
figure: Object,
|
|
11067
|
+
};
|
|
11068
|
+
canvas = owl.useRef("graphContainer");
|
|
11069
|
+
chart;
|
|
11070
|
+
currentRuntime;
|
|
11071
|
+
get background() {
|
|
11072
|
+
return this.chartRuntime.background;
|
|
11073
|
+
}
|
|
11074
|
+
get canvasStyle() {
|
|
11075
|
+
return `background-color: ${this.background}`;
|
|
11076
|
+
}
|
|
11077
|
+
get chartRuntime() {
|
|
11078
|
+
const runtime = this.env.model.getters.getChartRuntime(this.props.figure.id);
|
|
11079
|
+
if (!("chartJsConfig" in runtime)) {
|
|
11080
|
+
throw new Error("Unsupported chart runtime");
|
|
11081
|
+
}
|
|
11082
|
+
return runtime;
|
|
11083
|
+
}
|
|
11084
|
+
setup() {
|
|
11085
|
+
owl.onMounted(() => {
|
|
11086
|
+
const runtime = this.chartRuntime;
|
|
11087
|
+
this.currentRuntime = runtime;
|
|
11088
|
+
// Note: chartJS modify the runtime in place, so it's important to give it a copy
|
|
11089
|
+
this.createChart(deepCopy(runtime.chartJsConfig));
|
|
11090
|
+
});
|
|
11091
|
+
owl.onWillUnmount(() => this.chart?.destroy());
|
|
11092
|
+
owl.useEffect(() => {
|
|
11093
|
+
const runtime = this.chartRuntime;
|
|
11094
|
+
if (runtime !== this.currentRuntime) {
|
|
11095
|
+
if (runtime.chartJsConfig.type !== this.currentRuntime.chartJsConfig.type) {
|
|
11096
|
+
this.chart?.destroy();
|
|
11097
|
+
this.createChart(deepCopy(runtime.chartJsConfig));
|
|
11098
|
+
}
|
|
11099
|
+
else {
|
|
11100
|
+
this.updateChartJs(deepCopy(runtime));
|
|
11101
|
+
}
|
|
11102
|
+
this.currentRuntime = runtime;
|
|
11103
|
+
}
|
|
11104
|
+
});
|
|
11105
|
+
}
|
|
11106
|
+
createChart(chartData) {
|
|
11107
|
+
const canvas = this.canvas.el;
|
|
11108
|
+
const ctx = canvas.getContext("2d");
|
|
11109
|
+
const Chart = getChartJSConstructor();
|
|
11110
|
+
this.chart = new Chart(ctx, chartData);
|
|
11111
|
+
}
|
|
11112
|
+
updateChartJs(chartRuntime) {
|
|
11113
|
+
const chartData = chartRuntime.chartJsConfig;
|
|
11114
|
+
if (chartData.data && chartData.data.datasets) {
|
|
11115
|
+
this.chart.data = chartData.data;
|
|
11116
|
+
if (chartData.options?.plugins?.title) {
|
|
11117
|
+
this.chart.config.options.plugins.title = chartData.options.plugins.title;
|
|
11118
|
+
}
|
|
11119
|
+
}
|
|
11120
|
+
else {
|
|
11121
|
+
this.chart.data.datasets = [];
|
|
11122
|
+
}
|
|
11123
|
+
this.chart.config.options = chartData.options;
|
|
11124
|
+
this.chart.update();
|
|
11125
|
+
}
|
|
11126
|
+
}
|
|
11127
|
+
|
|
10706
11128
|
class ScorecardChart extends owl.Component {
|
|
10707
11129
|
static template = "o-spreadsheet-ScorecardChart";
|
|
10708
11130
|
static props = {
|
|
@@ -22155,7 +22577,7 @@ autofillRulesRegistry
|
|
|
22155
22577
|
condition: (cell) => !cell.isFormula &&
|
|
22156
22578
|
evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.text &&
|
|
22157
22579
|
alphaNumericValueRegExp.test(cell.content),
|
|
22158
|
-
generateRule: (cell, cells) => {
|
|
22580
|
+
generateRule: (cell, cells, direction) => {
|
|
22159
22581
|
const numberPostfix = parseInt(cell.content.match(numberPostfixRegExp)[0]);
|
|
22160
22582
|
const prefix = cell.content.match(stringPrefixRegExp)[0];
|
|
22161
22583
|
const numberPostfixLength = cell.content.length - prefix.length;
|
|
@@ -22163,7 +22585,10 @@ autofillRulesRegistry
|
|
|
22163
22585
|
alphaNumericValueRegExp.test(evaluatedCell.value)) // get consecutive alphanumeric cells, no matter what the prefix is
|
|
22164
22586
|
.filter((cell) => prefix === (cell.value ?? "").toString().match(stringPrefixRegExp)[0])
|
|
22165
22587
|
.map((cell) => parseInt((cell.value ?? "").toString().match(numberPostfixRegExp)[0]));
|
|
22166
|
-
|
|
22588
|
+
let increment = calculateIncrementBasedOnGroup(group);
|
|
22589
|
+
if (["up", "left"].includes(direction) && group.length === 1) {
|
|
22590
|
+
increment = -increment;
|
|
22591
|
+
}
|
|
22167
22592
|
return {
|
|
22168
22593
|
type: "ALPHANUMERIC_INCREMENT_MODIFIER",
|
|
22169
22594
|
prefix,
|
|
@@ -22226,10 +22651,13 @@ autofillRulesRegistry
|
|
|
22226
22651
|
.add("increment_number", {
|
|
22227
22652
|
condition: (cell) => !cell.isFormula &&
|
|
22228
22653
|
evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.number,
|
|
22229
|
-
generateRule: (cell, cells) => {
|
|
22654
|
+
generateRule: (cell, cells, direction) => {
|
|
22230
22655
|
const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number &&
|
|
22231
22656
|
!isDateTimeFormat(evaluatedCell.format || "")).map((cell) => Number(cell.value));
|
|
22232
|
-
|
|
22657
|
+
let increment = calculateIncrementBasedOnGroup(group);
|
|
22658
|
+
if (["up", "left"].includes(direction) && group.length === 1) {
|
|
22659
|
+
increment = -increment;
|
|
22660
|
+
}
|
|
22233
22661
|
const evaluation = evaluateLiteral(cell, { locale: DEFAULT_LOCALE });
|
|
22234
22662
|
return {
|
|
22235
22663
|
type: "INCREMENT_MODIFIER",
|
|
@@ -22273,343 +22701,6 @@ function getDateIntervals(dates) {
|
|
|
22273
22701
|
|
|
22274
22702
|
const cellPopoverRegistry = new Registry();
|
|
22275
22703
|
|
|
22276
|
-
const GAUGE_PADDING_SIDE = 30;
|
|
22277
|
-
const GAUGE_PADDING_TOP = 10;
|
|
22278
|
-
const GAUGE_PADDING_BOTTOM = 20;
|
|
22279
|
-
const GAUGE_LABELS_FONT_SIZE = 12;
|
|
22280
|
-
const GAUGE_DEFAULT_VALUE_FONT_SIZE = 80;
|
|
22281
|
-
const GAUGE_BACKGROUND_COLOR = "#F3F2F1";
|
|
22282
|
-
const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
|
|
22283
|
-
const GAUGE_TITLE_SECTION_HEIGHT = 25;
|
|
22284
|
-
function drawGaugeChart(canvas, runtime) {
|
|
22285
|
-
const canvasBoundingRect = canvas.getBoundingClientRect();
|
|
22286
|
-
canvas.width = canvasBoundingRect.width;
|
|
22287
|
-
canvas.height = canvasBoundingRect.height;
|
|
22288
|
-
const ctx = canvas.getContext("2d");
|
|
22289
|
-
const config = getGaugeRenderingConfig(canvasBoundingRect, runtime, ctx);
|
|
22290
|
-
drawBackground(ctx, config);
|
|
22291
|
-
drawGauge(ctx, config);
|
|
22292
|
-
drawInflectionValues(ctx, config);
|
|
22293
|
-
drawLabels(ctx, config);
|
|
22294
|
-
drawTitle(ctx, config);
|
|
22295
|
-
}
|
|
22296
|
-
function drawGauge(ctx, config) {
|
|
22297
|
-
ctx.save();
|
|
22298
|
-
const gauge = config.gauge;
|
|
22299
|
-
const arcCenterX = gauge.rect.x + gauge.rect.width / 2;
|
|
22300
|
-
const arcCenterY = gauge.rect.y + gauge.rect.height;
|
|
22301
|
-
const arcRadius = gauge.rect.height - gauge.arcWidth / 2;
|
|
22302
|
-
if (arcRadius < 0) {
|
|
22303
|
-
return;
|
|
22304
|
-
}
|
|
22305
|
-
const gaugeAngle = gauge.percentage === 1 ? 0 : Math.PI * (1 + gauge.percentage);
|
|
22306
|
-
// Gauge background
|
|
22307
|
-
ctx.strokeStyle = GAUGE_BACKGROUND_COLOR;
|
|
22308
|
-
ctx.beginPath();
|
|
22309
|
-
ctx.lineWidth = gauge.arcWidth;
|
|
22310
|
-
ctx.arc(arcCenterX, arcCenterY, arcRadius, gaugeAngle, 0);
|
|
22311
|
-
ctx.stroke();
|
|
22312
|
-
// Gauge value
|
|
22313
|
-
ctx.strokeStyle = gauge.color;
|
|
22314
|
-
ctx.beginPath();
|
|
22315
|
-
ctx.arc(arcCenterX, arcCenterY, arcRadius, Math.PI, gaugeAngle);
|
|
22316
|
-
ctx.stroke();
|
|
22317
|
-
ctx.restore();
|
|
22318
|
-
}
|
|
22319
|
-
function drawBackground(ctx, config) {
|
|
22320
|
-
ctx.save();
|
|
22321
|
-
ctx.fillStyle = config.backgroundColor;
|
|
22322
|
-
ctx.fillRect(0, 0, config.width, config.height);
|
|
22323
|
-
ctx.restore();
|
|
22324
|
-
}
|
|
22325
|
-
function drawLabels(ctx, config) {
|
|
22326
|
-
for (const label of [config.minLabel, config.maxLabel, config.gaugeValue]) {
|
|
22327
|
-
ctx.save();
|
|
22328
|
-
ctx.textAlign = "center";
|
|
22329
|
-
ctx.fillStyle = label.color;
|
|
22330
|
-
ctx.font = `${label.fontSize}px ${DEFAULT_FONT}`;
|
|
22331
|
-
ctx.fillText(label.label, label.textPosition.x, label.textPosition.y);
|
|
22332
|
-
ctx.restore();
|
|
22333
|
-
}
|
|
22334
|
-
}
|
|
22335
|
-
function drawInflectionValues(ctx, config) {
|
|
22336
|
-
const { x: rectX, y: rectY, width, height } = config.gauge.rect;
|
|
22337
|
-
for (const inflectionValue of config.inflectionValues) {
|
|
22338
|
-
ctx.save();
|
|
22339
|
-
ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment
|
|
22340
|
-
ctx.rotate(Math.PI / 2 - inflectionValue.rotation);
|
|
22341
|
-
ctx.lineWidth = 2;
|
|
22342
|
-
ctx.strokeStyle = chartMutedFontColor(config.backgroundColor) + "aa";
|
|
22343
|
-
ctx.beginPath();
|
|
22344
|
-
ctx.moveTo(0, -(height - config.gauge.arcWidth));
|
|
22345
|
-
ctx.lineTo(0, -height - 3);
|
|
22346
|
-
ctx.stroke();
|
|
22347
|
-
ctx.textAlign = "center";
|
|
22348
|
-
ctx.font = `${inflectionValue.fontSize}px ${DEFAULT_FONT}`;
|
|
22349
|
-
ctx.fillStyle = inflectionValue.color;
|
|
22350
|
-
const textY = -height - GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN - inflectionValue.offset;
|
|
22351
|
-
ctx.fillText(inflectionValue.label, 0, textY);
|
|
22352
|
-
ctx.restore();
|
|
22353
|
-
}
|
|
22354
|
-
}
|
|
22355
|
-
function drawTitle(ctx, config) {
|
|
22356
|
-
ctx.save();
|
|
22357
|
-
const title = config.title;
|
|
22358
|
-
ctx.font = getDefaultContextFont(title.fontSize, title.bold, title.italic);
|
|
22359
|
-
ctx.textBaseline = "middle";
|
|
22360
|
-
ctx.fillStyle = title.color;
|
|
22361
|
-
ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
|
|
22362
|
-
ctx.restore();
|
|
22363
|
-
}
|
|
22364
|
-
function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
|
|
22365
|
-
const maxValue = runtime.maxValue;
|
|
22366
|
-
const minValue = runtime.minValue;
|
|
22367
|
-
const gaugeValue = runtime.gaugeValue;
|
|
22368
|
-
const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
|
|
22369
|
-
const gaugeArcWidth = gaugeRect.width / 6;
|
|
22370
|
-
const gaugePercentage = gaugeValue
|
|
22371
|
-
? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
|
|
22372
|
-
: 0;
|
|
22373
|
-
const gaugeValuePosition = {
|
|
22374
|
-
x: boundingRect.width / 2,
|
|
22375
|
-
y: gaugeRect.y + gaugeRect.height - gaugeRect.height / 12,
|
|
22376
|
-
};
|
|
22377
|
-
let gaugeValueFontSize = GAUGE_DEFAULT_VALUE_FONT_SIZE;
|
|
22378
|
-
// Scale down the font size if the gaugeRect is too small
|
|
22379
|
-
if (gaugeRect.height < 300) {
|
|
22380
|
-
gaugeValueFontSize = gaugeValueFontSize * (gaugeRect.height / 300);
|
|
22381
|
-
}
|
|
22382
|
-
// Scale down the font size if the text is too long
|
|
22383
|
-
const maxTextWidth = gaugeRect.width / 2;
|
|
22384
|
-
const gaugeLabel = gaugeValue?.label || "-";
|
|
22385
|
-
if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
|
|
22386
|
-
gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
|
|
22387
|
-
}
|
|
22388
|
-
const minLabelPosition = {
|
|
22389
|
-
x: gaugeRect.x + gaugeArcWidth / 2,
|
|
22390
|
-
y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
|
|
22391
|
-
};
|
|
22392
|
-
const maxLabelPosition = {
|
|
22393
|
-
x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
|
|
22394
|
-
y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
|
|
22395
|
-
};
|
|
22396
|
-
const textColor = chartMutedFontColor(runtime.background);
|
|
22397
|
-
const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);
|
|
22398
|
-
let x = 0, titleWidth = 0, titleHeight = 0;
|
|
22399
|
-
if (runtime.title.text) {
|
|
22400
|
-
({ width: titleWidth, height: titleHeight } = computeTextDimension(ctx, runtime.title.text, { fontSize: CHART_TITLE_FONT_SIZE, ...runtime.title }, "px"));
|
|
22401
|
-
}
|
|
22402
|
-
switch (runtime.title.align) {
|
|
22403
|
-
case "right":
|
|
22404
|
-
x = boundingRect.width - titleWidth - CHART_PADDING$1;
|
|
22405
|
-
break;
|
|
22406
|
-
case "center":
|
|
22407
|
-
x = (boundingRect.width - titleWidth) / 2;
|
|
22408
|
-
break;
|
|
22409
|
-
case "left":
|
|
22410
|
-
default:
|
|
22411
|
-
x = CHART_PADDING$1;
|
|
22412
|
-
break;
|
|
22413
|
-
}
|
|
22414
|
-
return {
|
|
22415
|
-
width: boundingRect.width,
|
|
22416
|
-
height: boundingRect.height,
|
|
22417
|
-
title: {
|
|
22418
|
-
label: runtime.title.text ?? "",
|
|
22419
|
-
fontSize: runtime.title.fontSize ?? CHART_TITLE_FONT_SIZE,
|
|
22420
|
-
textPosition: {
|
|
22421
|
-
x,
|
|
22422
|
-
y: CHART_PADDING_TOP + titleHeight / 2,
|
|
22423
|
-
},
|
|
22424
|
-
color: runtime.title.color ?? textColor,
|
|
22425
|
-
bold: runtime.title.bold,
|
|
22426
|
-
italic: runtime.title.italic,
|
|
22427
|
-
},
|
|
22428
|
-
backgroundColor: runtime.background,
|
|
22429
|
-
gauge: {
|
|
22430
|
-
rect: gaugeRect,
|
|
22431
|
-
arcWidth: gaugeArcWidth,
|
|
22432
|
-
percentage: clip(gaugePercentage, 0, 1),
|
|
22433
|
-
color: getGaugeColor(runtime),
|
|
22434
|
-
},
|
|
22435
|
-
inflectionValues,
|
|
22436
|
-
gaugeValue: {
|
|
22437
|
-
label: gaugeLabel,
|
|
22438
|
-
textPosition: gaugeValuePosition,
|
|
22439
|
-
fontSize: gaugeValueFontSize,
|
|
22440
|
-
color: textColor,
|
|
22441
|
-
},
|
|
22442
|
-
minLabel: {
|
|
22443
|
-
label: runtime.minValue.label,
|
|
22444
|
-
textPosition: minLabelPosition,
|
|
22445
|
-
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
22446
|
-
color: textColor,
|
|
22447
|
-
},
|
|
22448
|
-
maxLabel: {
|
|
22449
|
-
label: runtime.maxValue.label,
|
|
22450
|
-
textPosition: maxLabelPosition,
|
|
22451
|
-
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
22452
|
-
color: textColor,
|
|
22453
|
-
},
|
|
22454
|
-
};
|
|
22455
|
-
}
|
|
22456
|
-
/**
|
|
22457
|
-
* Get the rectangle in which the gauge will be drawn, based on the bounding rectangle of the canvas and leaving
|
|
22458
|
-
* space for the title and labels.
|
|
22459
|
-
*/
|
|
22460
|
-
function getGaugeRect(boundingRect, title) {
|
|
22461
|
-
const titleHeight = title ? GAUGE_TITLE_SECTION_HEIGHT : 0;
|
|
22462
|
-
const drawHeight = boundingRect.height - GAUGE_PADDING_BOTTOM - titleHeight - GAUGE_PADDING_TOP;
|
|
22463
|
-
const drawWidth = boundingRect.width - GAUGE_PADDING_SIDE * 2;
|
|
22464
|
-
let gaugeWidth;
|
|
22465
|
-
let gaugeHeight;
|
|
22466
|
-
if (drawWidth > 2 * drawHeight) {
|
|
22467
|
-
gaugeWidth = 2 * drawHeight;
|
|
22468
|
-
gaugeHeight = drawHeight;
|
|
22469
|
-
}
|
|
22470
|
-
else {
|
|
22471
|
-
gaugeWidth = drawWidth;
|
|
22472
|
-
gaugeHeight = drawWidth / 2;
|
|
22473
|
-
}
|
|
22474
|
-
const gaugeX = GAUGE_PADDING_SIDE + (drawWidth - gaugeWidth) / 2;
|
|
22475
|
-
const gaugeY = titleHeight + GAUGE_PADDING_TOP + (drawHeight - gaugeHeight) / 2;
|
|
22476
|
-
return {
|
|
22477
|
-
x: gaugeX,
|
|
22478
|
-
y: gaugeY,
|
|
22479
|
-
width: gaugeWidth,
|
|
22480
|
-
height: gaugeHeight,
|
|
22481
|
-
};
|
|
22482
|
-
}
|
|
22483
|
-
/**
|
|
22484
|
-
* 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).
|
|
22485
|
-
*
|
|
22486
|
-
* Also compute an offset for the text so that it doesn't overlap with other text.
|
|
22487
|
-
*/
|
|
22488
|
-
function getInflectionValues(runtime, gaugeRect, textColor, ctx) {
|
|
22489
|
-
const maxValue = runtime.maxValue;
|
|
22490
|
-
const minValue = runtime.minValue;
|
|
22491
|
-
const gaugeCircleCenter = {
|
|
22492
|
-
x: gaugeRect.x + gaugeRect.width / 2,
|
|
22493
|
-
y: gaugeRect.y + gaugeRect.height,
|
|
22494
|
-
};
|
|
22495
|
-
const textStyle = { fontSize: GAUGE_LABELS_FONT_SIZE };
|
|
22496
|
-
const inflectionValues = [];
|
|
22497
|
-
const inflectionValuesTextRects = [];
|
|
22498
|
-
for (const inflectionValue of runtime.inflectionValues) {
|
|
22499
|
-
const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
|
|
22500
|
-
const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
|
|
22501
|
-
const angle = Math.PI - Math.PI * percentage;
|
|
22502
|
-
const textRect = getRectangleTangentToCircle(angle, // angle between X axis and the point where the rectangle is tangent to the circle
|
|
22503
|
-
gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, // radius of the gauge circle + margin below text
|
|
22504
|
-
gaugeCircleCenter.x, // center of the gauge circle
|
|
22505
|
-
gaugeCircleCenter.y, // center of the gauge circle
|
|
22506
|
-
labelWidth + 2, // width of the text + some margin
|
|
22507
|
-
GAUGE_LABELS_FONT_SIZE // height of the text
|
|
22508
|
-
);
|
|
22509
|
-
let offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect))
|
|
22510
|
-
? GAUGE_LABELS_FONT_SIZE
|
|
22511
|
-
: 0;
|
|
22512
|
-
inflectionValuesTextRects.push(textRect);
|
|
22513
|
-
inflectionValues.push({
|
|
22514
|
-
rotation: angle,
|
|
22515
|
-
label: inflectionValue.label,
|
|
22516
|
-
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
22517
|
-
color: textColor,
|
|
22518
|
-
offset,
|
|
22519
|
-
});
|
|
22520
|
-
}
|
|
22521
|
-
return inflectionValues;
|
|
22522
|
-
}
|
|
22523
|
-
function getGaugeColor(runtime) {
|
|
22524
|
-
const gaugeValue = runtime.gaugeValue?.value;
|
|
22525
|
-
if (gaugeValue === undefined) {
|
|
22526
|
-
return GAUGE_BACKGROUND_COLOR;
|
|
22527
|
-
}
|
|
22528
|
-
for (let i = 0; i < runtime.inflectionValues.length; i++) {
|
|
22529
|
-
const inflectionValue = runtime.inflectionValues[i];
|
|
22530
|
-
if (inflectionValue.operator === "<" && gaugeValue < inflectionValue.value) {
|
|
22531
|
-
return runtime.colors[i];
|
|
22532
|
-
}
|
|
22533
|
-
else if (inflectionValue.operator === "<=" && gaugeValue <= inflectionValue.value) {
|
|
22534
|
-
return runtime.colors[i];
|
|
22535
|
-
}
|
|
22536
|
-
}
|
|
22537
|
-
return runtime.colors.at(-1);
|
|
22538
|
-
}
|
|
22539
|
-
function getSegmentsOfRectangle(rectangle) {
|
|
22540
|
-
return [
|
|
22541
|
-
{ start: rectangle.topLeft, end: rectangle.topRight },
|
|
22542
|
-
{ start: rectangle.topRight, end: rectangle.bottomRight },
|
|
22543
|
-
{ start: rectangle.bottomRight, end: rectangle.bottomLeft },
|
|
22544
|
-
{ start: rectangle.bottomLeft, end: rectangle.topLeft },
|
|
22545
|
-
];
|
|
22546
|
-
}
|
|
22547
|
-
/**
|
|
22548
|
-
* Check if two segment intersect. The case where the segments are colinear (both segments on the same line)
|
|
22549
|
-
* is not handled.
|
|
22550
|
-
*/
|
|
22551
|
-
function doSegmentIntersect(segment1, segment2) {
|
|
22552
|
-
const A = segment1.start;
|
|
22553
|
-
const B = segment1.end;
|
|
22554
|
-
const C = segment2.start;
|
|
22555
|
-
const D = segment2.end;
|
|
22556
|
-
/**
|
|
22557
|
-
* Line segment intersection algorithm
|
|
22558
|
-
* https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
|
|
22559
|
-
*/
|
|
22560
|
-
function ccw(a, b, c) {
|
|
22561
|
-
return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
|
|
22562
|
-
}
|
|
22563
|
-
return ccw(A, C, D) !== ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D);
|
|
22564
|
-
}
|
|
22565
|
-
function doRectanglesIntersect(rect1, rect2) {
|
|
22566
|
-
const segments1 = getSegmentsOfRectangle(rect1);
|
|
22567
|
-
const segments2 = getSegmentsOfRectangle(rect2);
|
|
22568
|
-
for (const segment1 of segments1) {
|
|
22569
|
-
for (const segment2 of segments2) {
|
|
22570
|
-
if (doSegmentIntersect(segment1, segment2)) {
|
|
22571
|
-
return true;
|
|
22572
|
-
}
|
|
22573
|
-
}
|
|
22574
|
-
}
|
|
22575
|
-
return false;
|
|
22576
|
-
}
|
|
22577
|
-
/**
|
|
22578
|
-
* Get the rectangle that is tangent to a circle at a given angle.
|
|
22579
|
-
*
|
|
22580
|
-
* @param angle angle between X axis and the point where the rectangle is tangent to the circle
|
|
22581
|
-
*/
|
|
22582
|
-
function getRectangleTangentToCircle(angle, radius, circleCenterX, circleCenterY, rectWidth, rectHeight) {
|
|
22583
|
-
const cos = Math.cos(angle);
|
|
22584
|
-
const sin = Math.sin(angle);
|
|
22585
|
-
// x, y are the distance from the center of the circle to the point where the rectangle is tangent to the circle
|
|
22586
|
-
const x = cos * radius;
|
|
22587
|
-
const y = sin * radius;
|
|
22588
|
-
// x2, y2 are the distance from the point the rectangle is tangent to the circle to the bottom left corner of the rectangle
|
|
22589
|
-
const x2 = sin * (rectWidth / 2); // cos(angle + 90°) = sin(angle)
|
|
22590
|
-
const y2 = cos * (rectWidth / 2);
|
|
22591
|
-
const bottomRight = {
|
|
22592
|
-
x: x + x2 + circleCenterX,
|
|
22593
|
-
y: circleCenterY - (y - y2),
|
|
22594
|
-
};
|
|
22595
|
-
const bottomLeft = {
|
|
22596
|
-
x: x - x2 + circleCenterX,
|
|
22597
|
-
y: circleCenterY - (y + y2),
|
|
22598
|
-
};
|
|
22599
|
-
// Same as above but for the top corners of the rectangle (radius + rectangle height instead of radius)
|
|
22600
|
-
const xp = cos * (radius + rectHeight);
|
|
22601
|
-
const yp = sin * (radius + rectHeight);
|
|
22602
|
-
const topLeft = {
|
|
22603
|
-
x: xp - x2 + circleCenterX,
|
|
22604
|
-
y: circleCenterY - (yp + y2),
|
|
22605
|
-
};
|
|
22606
|
-
const topRight = {
|
|
22607
|
-
x: xp + x2 + circleCenterX,
|
|
22608
|
-
y: circleCenterY - (yp - y2),
|
|
22609
|
-
};
|
|
22610
|
-
return { bottomLeft, bottomRight, topRight, topLeft };
|
|
22611
|
-
}
|
|
22612
|
-
|
|
22613
22704
|
class GaugeChartComponent extends owl.Component {
|
|
22614
22705
|
static template = "o-spreadsheet-GaugeChartComponent";
|
|
22615
22706
|
canvas = owl.useRef("chartContainer");
|
|
@@ -22642,81 +22733,6 @@ function toXlsxHexColor(color) {
|
|
|
22642
22733
|
return color;
|
|
22643
22734
|
}
|
|
22644
22735
|
|
|
22645
|
-
const CHART_COMMON_OPTIONS = {
|
|
22646
|
-
// https://www.chartjs.org/docs/latest/general/responsive.html
|
|
22647
|
-
responsive: true, // will resize when its container is resized
|
|
22648
|
-
maintainAspectRatio: false, // doesn't maintain the aspect ratio (width/height =2 by default) so the user has the choice of the exact layout
|
|
22649
|
-
elements: {
|
|
22650
|
-
line: {
|
|
22651
|
-
fill: false, // do not fill the area under line charts
|
|
22652
|
-
},
|
|
22653
|
-
point: {
|
|
22654
|
-
hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
|
|
22655
|
-
},
|
|
22656
|
-
},
|
|
22657
|
-
animation: false,
|
|
22658
|
-
};
|
|
22659
|
-
function truncateLabel(label) {
|
|
22660
|
-
if (!label) {
|
|
22661
|
-
return "";
|
|
22662
|
-
}
|
|
22663
|
-
if (label.length > MAX_CHAR_LABEL) {
|
|
22664
|
-
return label.substring(0, MAX_CHAR_LABEL) + "…";
|
|
22665
|
-
}
|
|
22666
|
-
return label;
|
|
22667
|
-
}
|
|
22668
|
-
function chartToImage(runtime, figure, type) {
|
|
22669
|
-
// wrap the canvas in a div with a fixed size because chart.js would
|
|
22670
|
-
// fill the whole page otherwise
|
|
22671
|
-
const div = document.createElement("div");
|
|
22672
|
-
div.style.width = `${figure.width}px`;
|
|
22673
|
-
div.style.height = `${figure.height}px`;
|
|
22674
|
-
const canvas = document.createElement("canvas");
|
|
22675
|
-
div.append(canvas);
|
|
22676
|
-
canvas.setAttribute("width", figure.width.toString());
|
|
22677
|
-
canvas.setAttribute("height", figure.height.toString());
|
|
22678
|
-
// we have to add the canvas to the DOM otherwise it won't be rendered
|
|
22679
|
-
document.body.append(div);
|
|
22680
|
-
if ("chartJsConfig" in runtime) {
|
|
22681
|
-
const config = deepCopy(runtime.chartJsConfig);
|
|
22682
|
-
config.plugins = [backgroundColorChartJSPlugin];
|
|
22683
|
-
const chart = new window.Chart(canvas, config);
|
|
22684
|
-
const imgContent = chart.toBase64Image();
|
|
22685
|
-
chart.destroy();
|
|
22686
|
-
div.remove();
|
|
22687
|
-
return imgContent;
|
|
22688
|
-
}
|
|
22689
|
-
else if (type === "scorecard") {
|
|
22690
|
-
const design = getScorecardConfiguration(figure, runtime);
|
|
22691
|
-
drawScoreChart(design, canvas);
|
|
22692
|
-
const imgContent = canvas.toDataURL();
|
|
22693
|
-
div.remove();
|
|
22694
|
-
return imgContent;
|
|
22695
|
-
}
|
|
22696
|
-
else if (type === "gauge") {
|
|
22697
|
-
drawGaugeChart(canvas, runtime);
|
|
22698
|
-
const imgContent = canvas.toDataURL();
|
|
22699
|
-
div.remove();
|
|
22700
|
-
return imgContent;
|
|
22701
|
-
}
|
|
22702
|
-
return undefined;
|
|
22703
|
-
}
|
|
22704
|
-
/**
|
|
22705
|
-
* Custom chart.js plugin to set the background color of the canvas
|
|
22706
|
-
* https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md
|
|
22707
|
-
*/
|
|
22708
|
-
const backgroundColorChartJSPlugin = {
|
|
22709
|
-
id: "customCanvasBackgroundColor",
|
|
22710
|
-
beforeDraw: (chart) => {
|
|
22711
|
-
const { ctx } = chart;
|
|
22712
|
-
ctx.save();
|
|
22713
|
-
ctx.globalCompositeOperation = "destination-over";
|
|
22714
|
-
ctx.fillStyle = "#ffffff";
|
|
22715
|
-
ctx.fillRect(0, 0, chart.width, chart.height);
|
|
22716
|
-
ctx.restore();
|
|
22717
|
-
},
|
|
22718
|
-
};
|
|
22719
|
-
|
|
22720
22736
|
/**
|
|
22721
22737
|
* Represent a raw XML string
|
|
22722
22738
|
*/
|
|
@@ -22778,6 +22794,7 @@ const DRAWING_NS_C = "http://schemas.openxmlformats.org/drawingml/2006/chart";
|
|
|
22778
22794
|
const CONTENT_TYPES = {
|
|
22779
22795
|
workbook: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml",
|
|
22780
22796
|
sheet: "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml",
|
|
22797
|
+
metadata: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheetMetadata+xml",
|
|
22781
22798
|
sharedStrings: "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml",
|
|
22782
22799
|
styles: "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml",
|
|
22783
22800
|
drawing: "application/vnd.openxmlformats-officedocument.drawing+xml",
|
|
@@ -22790,6 +22807,7 @@ const CONTENT_TYPES = {
|
|
|
22790
22807
|
const XLSX_RELATION_TYPE = {
|
|
22791
22808
|
document: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument",
|
|
22792
22809
|
sheet: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet",
|
|
22810
|
+
metadata: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sheetMetadata",
|
|
22793
22811
|
sharedStrings: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings",
|
|
22794
22812
|
styles: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles",
|
|
22795
22813
|
drawing: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing",
|
|
@@ -22799,6 +22817,7 @@ const XLSX_RELATION_TYPE = {
|
|
|
22799
22817
|
hyperlink: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
|
|
22800
22818
|
image: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
|
|
22801
22819
|
};
|
|
22820
|
+
const ARRAY_FORMULA_URI = "bdbb8cdc-fa1e-496e-a857-3c3f30c029c3";
|
|
22802
22821
|
const RELATIONSHIP_NSR = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
|
22803
22822
|
const HEIGHT_FACTOR = 0.75; // 100px => 75 u
|
|
22804
22823
|
/**
|
|
@@ -25364,29 +25383,34 @@ function convertPivotTableConfig(pivotTable) {
|
|
|
25364
25383
|
* In all the sheets, replace the table-only references in the formula cells with standard references.
|
|
25365
25384
|
*/
|
|
25366
25385
|
function convertTableFormulaReferences(convertedSheets, xlsxSheets) {
|
|
25367
|
-
for (let
|
|
25368
|
-
const tables = xlsxSheets.find((s) => s.sheetName ===
|
|
25386
|
+
for (let tableSheet of convertedSheets) {
|
|
25387
|
+
const tables = xlsxSheets.find((s) => s.sheetName === tableSheet.name).tables;
|
|
25369
25388
|
for (let table of tables) {
|
|
25370
25389
|
const tabRef = table.name + "[";
|
|
25371
|
-
for (let
|
|
25372
|
-
|
|
25373
|
-
|
|
25374
|
-
|
|
25375
|
-
|
|
25376
|
-
|
|
25377
|
-
|
|
25378
|
-
|
|
25379
|
-
|
|
25380
|
-
|
|
25381
|
-
|
|
25382
|
-
|
|
25390
|
+
for (let sheet of convertedSheets) {
|
|
25391
|
+
for (let xc in sheet.cells) {
|
|
25392
|
+
const cell = sheet.cells[xc];
|
|
25393
|
+
let cellContent = sheet.cells[xc];
|
|
25394
|
+
if (cell && cellContent && cellContent.startsWith("=")) {
|
|
25395
|
+
let refIndex;
|
|
25396
|
+
while ((refIndex = cellContent.indexOf(tabRef)) !== -1) {
|
|
25397
|
+
let endIndex = refIndex + tabRef.length;
|
|
25398
|
+
let openBrackets = 1;
|
|
25399
|
+
while (openBrackets > 0 && endIndex < cellContent.length) {
|
|
25400
|
+
if (cellContent[endIndex] === "[") {
|
|
25401
|
+
openBrackets++;
|
|
25402
|
+
}
|
|
25403
|
+
else if (cellContent[endIndex] === "]") {
|
|
25404
|
+
openBrackets--;
|
|
25405
|
+
}
|
|
25406
|
+
endIndex++;
|
|
25407
|
+
}
|
|
25408
|
+
let reference = cellContent.slice(refIndex + tabRef.length, endIndex - 1);
|
|
25409
|
+
const sheetPrefix = tableSheet.id === sheet.id ? "" : tableSheet.name + "!";
|
|
25410
|
+
const convertedRef = convertTableReference(sheetPrefix, reference, table, xc);
|
|
25411
|
+
cellContent =
|
|
25412
|
+
cellContent.slice(0, refIndex) + convertedRef + cellContent.slice(endIndex);
|
|
25383
25413
|
}
|
|
25384
|
-
reference = reference.slice(0, endIndex);
|
|
25385
|
-
const convertedRef = convertTableReference(reference, table, xc);
|
|
25386
|
-
cellContent =
|
|
25387
|
-
cellContent.slice(0, refIndex) +
|
|
25388
|
-
convertedRef +
|
|
25389
|
-
cellContent.slice(tabRef.length + refIndex + endIndex + 1);
|
|
25390
25414
|
}
|
|
25391
25415
|
sheet.cells[xc] = cellContent;
|
|
25392
25416
|
}
|
|
@@ -25395,11 +25419,17 @@ function convertTableFormulaReferences(convertedSheets, xlsxSheets) {
|
|
|
25395
25419
|
}
|
|
25396
25420
|
}
|
|
25397
25421
|
/**
|
|
25398
|
-
* Convert table-specific references in formulas into standard references.
|
|
25422
|
+
* Convert table-specific references in formulas into standard references. A table reference is composed of columns names,
|
|
25423
|
+
* and of keywords determining the rows of the table to reference.
|
|
25399
25424
|
*
|
|
25400
25425
|
* A reference in a table can have the form (only the part between brackets should be given to this function):
|
|
25401
25426
|
* - tableName[colName] : reference to the whole column "colName"
|
|
25427
|
+
* - tableName[#keyword] : reference to the whatever row the keyword refers to
|
|
25402
25428
|
* - tableName[[#keyword], [colName]] : reference to some of the element(s) of the column colName
|
|
25429
|
+
* - tableName[[#keyword], [colName]:[col2Name]] : reference to some of the element(s) of the columns colName to col2Name
|
|
25430
|
+
* - tableName[[#keyword1], [#keyword2], [colName]] : reference to all the rows referenced by the keywords in the column colName
|
|
25431
|
+
* - tableName[[#keyword1], [colName], [#keyword2]]: the keywords and colName can be in any order
|
|
25432
|
+
*
|
|
25403
25433
|
*
|
|
25404
25434
|
* The available keywords are :
|
|
25405
25435
|
* - #All : all the column (including totals)
|
|
@@ -25407,58 +25437,109 @@ function convertTableFormulaReferences(convertedSheets, xlsxSheets) {
|
|
|
25407
25437
|
* - #Headers : only the header of the column
|
|
25408
25438
|
* - #Totals : only the totals of the column
|
|
25409
25439
|
* - #This Row : only the element in the same row as the cell
|
|
25440
|
+
*
|
|
25441
|
+
* Note that the only valid combination of multiple keywords are #Data + #Totals and #Headers + #Data.
|
|
25410
25442
|
*/
|
|
25411
|
-
function convertTableReference(expr, table, cellXc) {
|
|
25412
|
-
|
|
25443
|
+
function convertTableReference(sheetPrefix, expr, table, cellXc) {
|
|
25444
|
+
// TODO: Ideally we'd want to make a real tokenizer, this simple approach won't work if for example the column name
|
|
25445
|
+
// contain # or , characters. But that's probably an edge case that we can ignore for now.
|
|
25446
|
+
const parts = expr.split(",").map((part) => part.trim());
|
|
25413
25447
|
const tableZone = toZone(table.ref);
|
|
25414
|
-
const
|
|
25415
|
-
|
|
25416
|
-
|
|
25417
|
-
|
|
25418
|
-
|
|
25419
|
-
|
|
25420
|
-
|
|
25421
|
-
|
|
25422
|
-
|
|
25423
|
-
|
|
25424
|
-
|
|
25448
|
+
const colIndexes = [];
|
|
25449
|
+
const rowIndexes = [];
|
|
25450
|
+
const foundKeywords = [];
|
|
25451
|
+
for (const part of parts) {
|
|
25452
|
+
if (removeBrackets(part).startsWith("#")) {
|
|
25453
|
+
const keyWord = removeBrackets(part);
|
|
25454
|
+
foundKeywords.push(keyWord);
|
|
25455
|
+
switch (keyWord) {
|
|
25456
|
+
case "#All":
|
|
25457
|
+
rowIndexes.push(tableZone.top, tableZone.bottom);
|
|
25458
|
+
break;
|
|
25459
|
+
case "#Data":
|
|
25460
|
+
const top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
|
|
25461
|
+
const bottom = table.totalsRowCount
|
|
25462
|
+
? tableZone.bottom - table.totalsRowCount
|
|
25463
|
+
: tableZone.bottom;
|
|
25464
|
+
rowIndexes.push(top, bottom);
|
|
25465
|
+
break;
|
|
25466
|
+
case "#This Row":
|
|
25467
|
+
rowIndexes.push(toCartesian(cellXc).row);
|
|
25468
|
+
break;
|
|
25469
|
+
case "#Headers":
|
|
25470
|
+
if (!table.headerRowCount) {
|
|
25471
|
+
return CellErrorType.InvalidReference;
|
|
25472
|
+
}
|
|
25473
|
+
rowIndexes.push(tableZone.top);
|
|
25474
|
+
break;
|
|
25475
|
+
case "#Totals":
|
|
25476
|
+
if (!table.totalsRowCount) {
|
|
25477
|
+
return CellErrorType.InvalidReference;
|
|
25478
|
+
}
|
|
25479
|
+
rowIndexes.push(tableZone.bottom);
|
|
25480
|
+
break;
|
|
25481
|
+
}
|
|
25425
25482
|
}
|
|
25426
|
-
|
|
25427
|
-
|
|
25428
|
-
|
|
25429
|
-
|
|
25430
|
-
|
|
25431
|
-
|
|
25432
|
-
|
|
25433
|
-
|
|
25434
|
-
|
|
25435
|
-
|
|
25436
|
-
|
|
25437
|
-
|
|
25438
|
-
|
|
25439
|
-
|
|
25440
|
-
|
|
25441
|
-
|
|
25442
|
-
|
|
25443
|
-
if (!table.headerRowCount) {
|
|
25444
|
-
isReferencedZoneValid = false;
|
|
25445
|
-
}
|
|
25446
|
-
break;
|
|
25447
|
-
case "#Totals":
|
|
25448
|
-
refZone.top = refZone.bottom = tableZone.bottom;
|
|
25449
|
-
if (!table.totalsRowCount) {
|
|
25450
|
-
isReferencedZoneValid = false;
|
|
25483
|
+
else {
|
|
25484
|
+
const columns = part
|
|
25485
|
+
.split(":")
|
|
25486
|
+
.map((part) => part.trim())
|
|
25487
|
+
.map(removeBrackets);
|
|
25488
|
+
if (colIndexes.length) {
|
|
25489
|
+
return CellErrorType.InvalidReference;
|
|
25490
|
+
}
|
|
25491
|
+
const colRelativeIndex = table.cols.findIndex((col) => col.name === columns[0]);
|
|
25492
|
+
if (colRelativeIndex === -1) {
|
|
25493
|
+
return CellErrorType.InvalidReference;
|
|
25494
|
+
}
|
|
25495
|
+
colIndexes.push(colRelativeIndex + tableZone.left);
|
|
25496
|
+
if (columns[1]) {
|
|
25497
|
+
const colRelativeIndex2 = table.cols.findIndex((col) => col.name === columns[1]);
|
|
25498
|
+
if (colRelativeIndex2 === -1) {
|
|
25499
|
+
return CellErrorType.InvalidReference;
|
|
25451
25500
|
}
|
|
25452
|
-
|
|
25501
|
+
colIndexes.push(colRelativeIndex2 + tableZone.left);
|
|
25502
|
+
}
|
|
25453
25503
|
}
|
|
25454
|
-
const colRef = refElements[1].slice(1, refElements[1].length - 1);
|
|
25455
|
-
const colRelativeIndex = table.cols.findIndex((col) => col.name === colRef);
|
|
25456
|
-
refZone.left = refZone.right = colRelativeIndex + tableZone.left;
|
|
25457
25504
|
}
|
|
25458
|
-
if (!
|
|
25505
|
+
if (!areKeywordsCompatible(foundKeywords)) {
|
|
25459
25506
|
return CellErrorType.InvalidReference;
|
|
25460
25507
|
}
|
|
25461
|
-
|
|
25508
|
+
if (rowIndexes.length === 0) {
|
|
25509
|
+
const top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
|
|
25510
|
+
const bottom = table.totalsRowCount
|
|
25511
|
+
? tableZone.bottom - table.totalsRowCount
|
|
25512
|
+
: tableZone.bottom;
|
|
25513
|
+
rowIndexes.push(top, bottom);
|
|
25514
|
+
}
|
|
25515
|
+
if (colIndexes.length === 0) {
|
|
25516
|
+
colIndexes.push(tableZone.left, tableZone.right);
|
|
25517
|
+
}
|
|
25518
|
+
const refZone = {
|
|
25519
|
+
top: Math.min(...rowIndexes),
|
|
25520
|
+
left: Math.min(...colIndexes),
|
|
25521
|
+
bottom: Math.max(...rowIndexes),
|
|
25522
|
+
right: Math.max(...colIndexes),
|
|
25523
|
+
};
|
|
25524
|
+
return sheetPrefix + zoneToXc(refZone);
|
|
25525
|
+
}
|
|
25526
|
+
function removeBrackets(str) {
|
|
25527
|
+
return str.startsWith("[") && str.endsWith("]") ? str.slice(1, str.length - 1) : str;
|
|
25528
|
+
}
|
|
25529
|
+
function areKeywordsCompatible(keywords) {
|
|
25530
|
+
if (keywords.length < 2) {
|
|
25531
|
+
return true;
|
|
25532
|
+
}
|
|
25533
|
+
else if (keywords.length > 2) {
|
|
25534
|
+
return false;
|
|
25535
|
+
}
|
|
25536
|
+
else if (keywords.includes("#Data") && keywords.includes("#Totals")) {
|
|
25537
|
+
return true;
|
|
25538
|
+
}
|
|
25539
|
+
else if (keywords.includes("#Headers") && keywords.includes("#Data")) {
|
|
25540
|
+
return true;
|
|
25541
|
+
}
|
|
25542
|
+
return false;
|
|
25462
25543
|
}
|
|
25463
25544
|
|
|
25464
25545
|
// -------------------------------------
|
|
@@ -28626,11 +28707,12 @@ function canBeLinearChart(definition, dataSets, labelRange, getters) {
|
|
|
28626
28707
|
}
|
|
28627
28708
|
let missingTimeAdapterAlreadyWarned = false;
|
|
28628
28709
|
function isLuxonTimeAdapterInstalled() {
|
|
28629
|
-
|
|
28710
|
+
const Chart = getChartJSConstructor();
|
|
28711
|
+
if (!Chart) {
|
|
28630
28712
|
return false;
|
|
28631
28713
|
}
|
|
28632
28714
|
// @ts-ignore
|
|
28633
|
-
const adapter = new
|
|
28715
|
+
const adapter = new Chart._adapters._date({});
|
|
28634
28716
|
const isInstalled = adapter._id === "luxon";
|
|
28635
28717
|
if (!isInstalled && !missingTimeAdapterAlreadyWarned) {
|
|
28636
28718
|
missingTimeAdapterAlreadyWarned = true;
|
|
@@ -32327,10 +32409,6 @@ class Popover extends owl.Component {
|
|
|
32327
32409
|
this.currentDisplayValue = newDisplay;
|
|
32328
32410
|
if (!anchor)
|
|
32329
32411
|
return;
|
|
32330
|
-
el.style.top = "";
|
|
32331
|
-
el.style.left = "";
|
|
32332
|
-
el.style["max-height"] = "";
|
|
32333
|
-
el.style["max-width"] = "";
|
|
32334
32412
|
const propsMaxSize = { width: this.props.maxWidth, height: this.props.maxHeight };
|
|
32335
32413
|
let elDims = {
|
|
32336
32414
|
width: el.getBoundingClientRect().width,
|
|
@@ -33868,6 +33946,7 @@ var CHART_HELPERS = /*#__PURE__*/Object.freeze({
|
|
|
33868
33946
|
drawScoreChart: drawScoreChart,
|
|
33869
33947
|
formatChartDatasetValue: formatChartDatasetValue,
|
|
33870
33948
|
formatTickValue: formatTickValue,
|
|
33949
|
+
getChartJSConstructor: getChartJSConstructor,
|
|
33871
33950
|
getChartPositionAtCenterOfViewport: getChartPositionAtCenterOfViewport,
|
|
33872
33951
|
getDefinedAxis: getDefinedAxis,
|
|
33873
33952
|
getPieColors: getPieColors,
|
|
@@ -37751,6 +37830,9 @@ class GenericChartConfigPanel extends owl.Component {
|
|
|
37751
37830
|
this.state.datasetDispatchResult = this.props.updateChart(this.props.figureId, {
|
|
37752
37831
|
dataSets: this.dataSeriesRanges,
|
|
37753
37832
|
});
|
|
37833
|
+
if (this.state.datasetDispatchResult.isSuccessful) {
|
|
37834
|
+
this.dataSeriesRanges = this.env.model.getters.getChartDefinition(this.props.figureId).dataSets;
|
|
37835
|
+
}
|
|
37754
37836
|
}
|
|
37755
37837
|
getDataSeriesRanges() {
|
|
37756
37838
|
return this.dataSeriesRanges;
|
|
@@ -40390,8 +40472,7 @@ css /* scss */ `
|
|
|
40390
40472
|
}
|
|
40391
40473
|
|
|
40392
40474
|
.o-composer-assistant {
|
|
40393
|
-
|
|
40394
|
-
margin: 1px 4px;
|
|
40475
|
+
margin-top: 1px;
|
|
40395
40476
|
|
|
40396
40477
|
.o-semi-bold {
|
|
40397
40478
|
/* FIXME: to remove in favor of Bootstrap
|
|
@@ -40442,10 +40523,11 @@ class Composer extends owl.Component {
|
|
|
40442
40523
|
});
|
|
40443
40524
|
compositionActive = false;
|
|
40444
40525
|
spreadsheetRect = useSpreadsheetRect();
|
|
40445
|
-
get
|
|
40526
|
+
get assistantStyleProperties() {
|
|
40446
40527
|
const composerRect = this.composerRef.el.getBoundingClientRect();
|
|
40447
40528
|
const assistantStyle = {};
|
|
40448
|
-
|
|
40529
|
+
const minWidth = Math.min(this.props.rect?.width || Infinity, ASSISTANT_WIDTH);
|
|
40530
|
+
assistantStyle["min-width"] = `${minWidth}px`;
|
|
40449
40531
|
const proposals = this.autoCompleteState.provider?.proposals;
|
|
40450
40532
|
const proposalsHaveDescription = proposals?.some((proposal) => proposal.description);
|
|
40451
40533
|
if (this.functionDescriptionState.showDescription || proposalsHaveDescription) {
|
|
@@ -40469,13 +40551,29 @@ class Composer extends owl.Component {
|
|
|
40469
40551
|
}
|
|
40470
40552
|
}
|
|
40471
40553
|
else {
|
|
40472
|
-
assistantStyle["max-height"] = `${this.spreadsheetRect.height - composerRect.bottom}px`;
|
|
40554
|
+
assistantStyle["max-height"] = `${this.spreadsheetRect.height - composerRect.bottom - 1}px`; // -1: margin
|
|
40473
40555
|
if (composerRect.left + ASSISTANT_WIDTH + SCROLLBAR_WIDTH + CLOSE_ICON_RADIUS >
|
|
40474
40556
|
this.spreadsheetRect.width) {
|
|
40475
40557
|
assistantStyle.right = `${CLOSE_ICON_RADIUS}px`;
|
|
40476
40558
|
}
|
|
40477
40559
|
}
|
|
40478
|
-
return
|
|
40560
|
+
return assistantStyle;
|
|
40561
|
+
}
|
|
40562
|
+
get assistantStyle() {
|
|
40563
|
+
const allProperties = this.assistantStyleProperties;
|
|
40564
|
+
return cssPropertiesToCss({
|
|
40565
|
+
"max-height": allProperties["max-height"],
|
|
40566
|
+
width: allProperties["width"],
|
|
40567
|
+
"min-width": allProperties["min-width"],
|
|
40568
|
+
});
|
|
40569
|
+
}
|
|
40570
|
+
get assistantContainerStyle() {
|
|
40571
|
+
const allProperties = this.assistantStyleProperties;
|
|
40572
|
+
return cssPropertiesToCss({
|
|
40573
|
+
top: allProperties["top"],
|
|
40574
|
+
right: allProperties["right"],
|
|
40575
|
+
transform: allProperties["transform"],
|
|
40576
|
+
});
|
|
40479
40577
|
}
|
|
40480
40578
|
// we can't allow input events to be triggered while we remove and add back the content of the composer in processContent
|
|
40481
40579
|
shouldProcessInputEvents = false;
|
|
@@ -46412,9 +46510,7 @@ class PivotSidePanelStore extends SpreadsheetStore {
|
|
|
46412
46510
|
pivot: this.draft,
|
|
46413
46511
|
});
|
|
46414
46512
|
this.draft = null;
|
|
46415
|
-
if (!this.alreadyNotified &&
|
|
46416
|
-
!this.isDynamicPivotInViewport() &&
|
|
46417
|
-
this.isStaticPivotInViewport()) {
|
|
46513
|
+
if (!this.alreadyNotified && this.isUpdatedPivotVisibleInViewportOnlyAsStaticPivot()) {
|
|
46418
46514
|
const formulaId = this.getters.getPivotFormulaId(this.pivotId);
|
|
46419
46515
|
const pivotExample = `=PIVOT(${formulaId})`;
|
|
46420
46516
|
this.alreadyNotified = true;
|
|
@@ -46470,29 +46566,33 @@ class PivotSidePanelStore extends SpreadsheetStore {
|
|
|
46470
46566
|
this.applyUpdate();
|
|
46471
46567
|
}
|
|
46472
46568
|
}
|
|
46473
|
-
|
|
46474
|
-
|
|
46475
|
-
|
|
46476
|
-
|
|
46477
|
-
|
|
46478
|
-
|
|
46479
|
-
|
|
46480
|
-
}
|
|
46481
|
-
}
|
|
46482
|
-
}
|
|
46483
|
-
return false;
|
|
46484
|
-
}
|
|
46485
|
-
isStaticPivotInViewport() {
|
|
46569
|
+
/**
|
|
46570
|
+
* @returns true if the updated pivot is visible in the viewport only as a
|
|
46571
|
+
* static pivot and not as a dynamic pivot
|
|
46572
|
+
*/
|
|
46573
|
+
isUpdatedPivotVisibleInViewportOnlyAsStaticPivot() {
|
|
46574
|
+
let staticPivotCount = 0;
|
|
46575
|
+
const updatedPivotFormulaId = this.getters.getPivotFormulaId(this.pivotId);
|
|
46486
46576
|
for (const position of this.getters.getVisibleCellPositions()) {
|
|
46487
46577
|
const cell = this.getters.getCell(position);
|
|
46488
46578
|
if (cell?.isFormula) {
|
|
46489
46579
|
const pivotFunction = getFirstPivotFunction(cell.compiledFormula.tokens);
|
|
46490
|
-
|
|
46491
|
-
|
|
46580
|
+
const pivotFormulaId = pivotFunction?.args[0]?.value;
|
|
46581
|
+
if (pivotFunction && updatedPivotFormulaId === pivotFormulaId.toString()) {
|
|
46582
|
+
if (pivotFunction.functionName === "PIVOT") {
|
|
46583
|
+
// if we have at least one dynamic pivot visible inserted the viewport
|
|
46584
|
+
// we return false
|
|
46585
|
+
return false;
|
|
46586
|
+
}
|
|
46587
|
+
else {
|
|
46588
|
+
staticPivotCount++;
|
|
46589
|
+
}
|
|
46492
46590
|
}
|
|
46493
46591
|
}
|
|
46494
46592
|
}
|
|
46495
|
-
return
|
|
46593
|
+
// we return true if there are only static pivots visible inserted the viewport,
|
|
46594
|
+
// otherwise false
|
|
46595
|
+
return staticPivotCount > 0;
|
|
46496
46596
|
}
|
|
46497
46597
|
addDefaultDateTimeGranularity(fields, definition) {
|
|
46498
46598
|
const { columns, rows } = definition;
|
|
@@ -60332,6 +60432,7 @@ class EvaluationPlugin extends UIPlugin {
|
|
|
60332
60432
|
exportForExcel(data) {
|
|
60333
60433
|
for (const sheet of data.sheets) {
|
|
60334
60434
|
sheet.cellValues = {};
|
|
60435
|
+
sheet.formulaSpillRanges = {};
|
|
60335
60436
|
}
|
|
60336
60437
|
for (const position of this.evaluator.getEvaluatedPositions()) {
|
|
60337
60438
|
const evaluatedCell = this.evaluator.getEvaluatedCell(position);
|
|
@@ -60343,8 +60444,9 @@ class EvaluationPlugin extends UIPlugin {
|
|
|
60343
60444
|
const exportedSheetData = data.sheets.find((sheet) => sheet.id === position.sheetId);
|
|
60344
60445
|
const formulaCell = this.getCorrespondingFormulaCell(position);
|
|
60345
60446
|
if (formulaCell) {
|
|
60447
|
+
const cell = this.getters.getCell(position);
|
|
60346
60448
|
isExported = isExportableToExcel(formulaCell.compiledFormula.tokens);
|
|
60347
|
-
isFormula = isExported;
|
|
60449
|
+
isFormula = isExported && cell?.content === formulaCell.content;
|
|
60348
60450
|
// If the cell contains a non-exported formula and that is evaluates to
|
|
60349
60451
|
// nothing* ,we don't export it.
|
|
60350
60452
|
// * non-falsy value are relevant and so are 0 and FALSE, which only leaves
|
|
@@ -60367,7 +60469,11 @@ class EvaluationPlugin extends UIPlugin {
|
|
|
60367
60469
|
content = !isExported ? newContent : exportedCellData;
|
|
60368
60470
|
}
|
|
60369
60471
|
exportedSheetData.cells[xc] = content;
|
|
60370
|
-
exportedSheetData.cellValues[xc] = value;
|
|
60472
|
+
exportedSheetData.cellValues[xc] = evaluatedCell.type !== "error" ? value : undefined;
|
|
60473
|
+
const spillZone = this.getSpreadZone(position);
|
|
60474
|
+
if (spillZone) {
|
|
60475
|
+
exportedSheetData.formulaSpillRanges[xc] = this.getters.getRangeString(this.getters.getRangeFromZone(position.sheetId, spillZone), position.sheetId);
|
|
60476
|
+
}
|
|
60371
60477
|
}
|
|
60372
60478
|
}
|
|
60373
60479
|
/**
|
|
@@ -62574,7 +62680,7 @@ class AutofillPlugin extends UIPlugin {
|
|
|
62574
62680
|
getRule(cell, cells) {
|
|
62575
62681
|
const rules = autofillRulesRegistry.getAll().sort((a, b) => a.sequence - b.sequence);
|
|
62576
62682
|
const rule = rules.find((rule) => rule.condition(cell, cells));
|
|
62577
|
-
return rule && rule.generateRule(cell, cells);
|
|
62683
|
+
return rule && this.direction && rule.generateRule(cell, cells, this.direction);
|
|
62578
62684
|
}
|
|
62579
62685
|
/**
|
|
62580
62686
|
* Create the generator to be able to autofill the next cells.
|
|
@@ -73104,7 +73210,7 @@ function numberRef(reference) {
|
|
|
73104
73210
|
`;
|
|
73105
73211
|
}
|
|
73106
73212
|
|
|
73107
|
-
function addFormula(formula, value) {
|
|
73213
|
+
function addFormula(formula, value, formulaSpillRange) {
|
|
73108
73214
|
if (!formula) {
|
|
73109
73215
|
return { attrs: [], node: escapeXml `` };
|
|
73110
73216
|
}
|
|
@@ -73112,10 +73218,17 @@ function addFormula(formula, value) {
|
|
|
73112
73218
|
if (type === undefined) {
|
|
73113
73219
|
return { attrs: [], node: escapeXml `` };
|
|
73114
73220
|
}
|
|
73115
|
-
const attrs = [
|
|
73221
|
+
const attrs = [
|
|
73222
|
+
["cm", "1"],
|
|
73223
|
+
["t", type],
|
|
73224
|
+
];
|
|
73116
73225
|
const XlsxFormula = adaptFormulaToExcel(formula);
|
|
73117
73226
|
const exportedValue = adaptFormulaValueToExcel(value);
|
|
73118
|
-
|
|
73227
|
+
// We treat all formulas as array formulas (a simple formula
|
|
73228
|
+
// is an array formula that spills on only one cell) to avoid
|
|
73229
|
+
// trying to detect spilling sub-formulas which is not a trivial task.
|
|
73230
|
+
let node;
|
|
73231
|
+
node = escapeXml /*xml*/ `<f t="array" ref="${formulaSpillRange}">${XlsxFormula}</f><v>${exportedValue}</v>`;
|
|
73119
73232
|
return { attrs, node };
|
|
73120
73233
|
}
|
|
73121
73234
|
function addContent(content, sharedStrings, forceString = false) {
|
|
@@ -74105,7 +74218,7 @@ function addRows(construct, data, sheet) {
|
|
|
74105
74218
|
let cellNode = escapeXml ``;
|
|
74106
74219
|
// Either formula or static value inside the cell
|
|
74107
74220
|
if (content?.startsWith("=") && value !== undefined) {
|
|
74108
|
-
const res = addFormula(content, value);
|
|
74221
|
+
const res = addFormula(content, value, sheet.formulaSpillRanges[xc] ?? xc);
|
|
74109
74222
|
if (!res) {
|
|
74110
74223
|
continue;
|
|
74111
74224
|
}
|
|
@@ -74391,6 +74504,30 @@ function createWorksheets(data, construct) {
|
|
|
74391
74504
|
`;
|
|
74392
74505
|
files.push(createXMLFile(parseXML(sheetXml), `xl/worksheets/sheet${sheetIndex}.xml`, "sheet"));
|
|
74393
74506
|
}
|
|
74507
|
+
const sheetMetadataXml = escapeXml /*xml*/ `
|
|
74508
|
+
<metadata xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:xda="http://schemas.microsoft.com/office/spreadsheetml/2017/dynamicarray">
|
|
74509
|
+
<metadataTypes count="1">
|
|
74510
|
+
<metadataType name="XLDAPR" minSupportedVersion="120000" copy="1" pasteAll="1"
|
|
74511
|
+
pasteValues="1" merge="1" splitFirst="1" rowColShift="1" clearFormats="1"
|
|
74512
|
+
clearComments="1" assign="1" coerce="1" cellMeta="1" />
|
|
74513
|
+
</metadataTypes>
|
|
74514
|
+
<futureMetadata name="XLDAPR" count="1">
|
|
74515
|
+
<bk>
|
|
74516
|
+
<extLst>
|
|
74517
|
+
<ext uri="{${ARRAY_FORMULA_URI}}">
|
|
74518
|
+
<xda:dynamicArrayProperties fDynamic="1" fCollapsed="0" />
|
|
74519
|
+
</ext>
|
|
74520
|
+
</extLst>
|
|
74521
|
+
</bk>
|
|
74522
|
+
</futureMetadata>
|
|
74523
|
+
<cellMetadata count="1">
|
|
74524
|
+
<bk>
|
|
74525
|
+
<rc t="1" v="0" />
|
|
74526
|
+
</bk>
|
|
74527
|
+
</cellMetadata>
|
|
74528
|
+
</metadata>
|
|
74529
|
+
`;
|
|
74530
|
+
files.push(createXMLFile(parseXML(sheetMetadataXml), "xl/metadata.xml", "metadata"));
|
|
74394
74531
|
addRelsToFile(construct.relsFiles, "xl/_rels/workbook.xml.rels", {
|
|
74395
74532
|
type: XLSX_RELATION_TYPE.sharedStrings,
|
|
74396
74533
|
target: "sharedStrings.xml",
|
|
@@ -74399,6 +74536,10 @@ function createWorksheets(data, construct) {
|
|
|
74399
74536
|
type: XLSX_RELATION_TYPE.styles,
|
|
74400
74537
|
target: "styles.xml",
|
|
74401
74538
|
});
|
|
74539
|
+
addRelsToFile(construct.relsFiles, "xl/_rels/workbook.xml.rels", {
|
|
74540
|
+
type: XLSX_RELATION_TYPE.metadata,
|
|
74541
|
+
target: "metadata.xml",
|
|
74542
|
+
});
|
|
74402
74543
|
return files;
|
|
74403
74544
|
}
|
|
74404
74545
|
/**
|
|
@@ -75327,6 +75468,6 @@ exports.tokenColors = tokenColors;
|
|
|
75327
75468
|
exports.tokenize = tokenize;
|
|
75328
75469
|
|
|
75329
75470
|
|
|
75330
|
-
__info__.version = "18.1.
|
|
75331
|
-
__info__.date = "2025-
|
|
75332
|
-
__info__.hash = "
|
|
75471
|
+
__info__.version = "18.1.10";
|
|
75472
|
+
__info__.date = "2025-03-07T10:34:41.861Z";
|
|
75473
|
+
__info__.hash = "31e4526";
|