@odoo/o-spreadsheet 18.1.9 → 18.1.11
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 +842 -621
- package/dist/o-spreadsheet.d.ts +16 -4
- package/dist/o-spreadsheet.esm.js +842 -621
- package/dist/o-spreadsheet.iife.js +842 -621
- package/dist/o-spreadsheet.iife.min.js +401 -380
- package/dist/o_spreadsheet.xml +55 -37
- 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.11
|
|
6
|
+
* @date 2025-03-12T15:31:44.276Z
|
|
7
|
+
* @hash 7de2363
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
'use strict';
|
|
@@ -424,7 +424,6 @@ function escapeRegExp(str) {
|
|
|
424
424
|
* Sparse arrays remain sparse.
|
|
425
425
|
*/
|
|
426
426
|
function deepCopy(obj) {
|
|
427
|
-
const result = Array.isArray(obj) ? [] : {};
|
|
428
427
|
switch (typeof obj) {
|
|
429
428
|
case "object": {
|
|
430
429
|
if (obj === null) {
|
|
@@ -436,8 +435,18 @@ function deepCopy(obj) {
|
|
|
436
435
|
else if (!(isPlainObject(obj) || obj instanceof Array)) {
|
|
437
436
|
throw new Error("Unsupported type: only objects and arrays are supported");
|
|
438
437
|
}
|
|
439
|
-
|
|
440
|
-
|
|
438
|
+
const result = Array.isArray(obj) ? new Array(obj.length) : {};
|
|
439
|
+
if (Array.isArray(obj)) {
|
|
440
|
+
for (let i = 0, len = obj.length; i < len; i++) {
|
|
441
|
+
if (i in obj) {
|
|
442
|
+
result[i] = deepCopy(obj[i]);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
for (const key in obj) {
|
|
448
|
+
result[key] = deepCopy(obj[key]);
|
|
449
|
+
}
|
|
441
450
|
}
|
|
442
451
|
return result;
|
|
443
452
|
}
|
|
@@ -2690,21 +2699,30 @@ function mergeContiguousZones(zones) {
|
|
|
2690
2699
|
return mergedZones;
|
|
2691
2700
|
}
|
|
2692
2701
|
|
|
2702
|
+
const globalReverseLookup$1 = new WeakMap();
|
|
2703
|
+
const globalIdCounter = new WeakMap();
|
|
2693
2704
|
/**
|
|
2694
2705
|
* Get the id of the given item (its key in the given dictionary).
|
|
2695
2706
|
* If the given item does not exist in the dictionary, it creates one with a new id.
|
|
2696
2707
|
*/
|
|
2697
2708
|
function getItemId(item, itemsDic) {
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2709
|
+
if (!globalReverseLookup$1.has(itemsDic)) {
|
|
2710
|
+
globalReverseLookup$1.set(itemsDic, new Map());
|
|
2711
|
+
globalIdCounter.set(itemsDic, 0);
|
|
2712
|
+
}
|
|
2713
|
+
const reverseLookup = globalReverseLookup$1.get(itemsDic);
|
|
2714
|
+
const canonical = getCanonicalRepresentation(item);
|
|
2715
|
+
if (reverseLookup.has(canonical)) {
|
|
2716
|
+
const id = reverseLookup.get(canonical);
|
|
2717
|
+
itemsDic[id] = item;
|
|
2718
|
+
return id;
|
|
2702
2719
|
}
|
|
2703
2720
|
// Generate new Id if the item didn't exist in the dictionary
|
|
2704
|
-
const
|
|
2705
|
-
|
|
2706
|
-
itemsDic
|
|
2707
|
-
|
|
2721
|
+
const newId = globalIdCounter.get(itemsDic) + 1;
|
|
2722
|
+
reverseLookup.set(canonical, newId);
|
|
2723
|
+
globalIdCounter.set(itemsDic, newId);
|
|
2724
|
+
itemsDic[newId] = item;
|
|
2725
|
+
return newId;
|
|
2708
2726
|
}
|
|
2709
2727
|
function groupItemIdsByZones(positionsByItemId) {
|
|
2710
2728
|
const result = {};
|
|
@@ -2728,6 +2746,33 @@ function* iterateItemIdsPositions(sheetId, itemIdsByZones) {
|
|
|
2728
2746
|
}
|
|
2729
2747
|
}
|
|
2730
2748
|
}
|
|
2749
|
+
function getCanonicalRepresentation(item) {
|
|
2750
|
+
if (item === null)
|
|
2751
|
+
return "null";
|
|
2752
|
+
if (item === undefined)
|
|
2753
|
+
return "undefined";
|
|
2754
|
+
if (typeof item !== "object")
|
|
2755
|
+
return String(item);
|
|
2756
|
+
if (Array.isArray(item)) {
|
|
2757
|
+
const len = item.length;
|
|
2758
|
+
let result = "[";
|
|
2759
|
+
for (let i = 0; i < len; i++) {
|
|
2760
|
+
if (i > 0)
|
|
2761
|
+
result += ",";
|
|
2762
|
+
result += getCanonicalRepresentation(item[i]);
|
|
2763
|
+
}
|
|
2764
|
+
return result + "]";
|
|
2765
|
+
}
|
|
2766
|
+
const keys = Object.keys(item).sort();
|
|
2767
|
+
let repr = "{";
|
|
2768
|
+
for (const key of keys) {
|
|
2769
|
+
if (item[key] !== undefined) {
|
|
2770
|
+
repr += `"${key}":${getCanonicalRepresentation(item[key])},`;
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
repr += "}";
|
|
2774
|
+
return repr;
|
|
2775
|
+
}
|
|
2731
2776
|
|
|
2732
2777
|
// -----------------------------------------------------------------------------
|
|
2733
2778
|
// Date Type
|
|
@@ -6093,8 +6138,9 @@ function spreadRange(getters, dataSets) {
|
|
|
6093
6138
|
if (zone.bottom !== zone.top && zone.left != zone.right) {
|
|
6094
6139
|
if (zone.right) {
|
|
6095
6140
|
for (let j = zone.left; j <= zone.right; ++j) {
|
|
6141
|
+
const datasetOptions = j === zone.left ? dataSet : { yAxisId: dataSet.yAxisId };
|
|
6096
6142
|
postProcessedRanges.push({
|
|
6097
|
-
...
|
|
6143
|
+
...datasetOptions,
|
|
6098
6144
|
dataRange: `${sheetPrefix}${zoneToXc({
|
|
6099
6145
|
left: j,
|
|
6100
6146
|
right: j,
|
|
@@ -6106,8 +6152,9 @@ function spreadRange(getters, dataSets) {
|
|
|
6106
6152
|
}
|
|
6107
6153
|
else {
|
|
6108
6154
|
for (let j = zone.top; j <= zone.bottom; ++j) {
|
|
6155
|
+
const datasetOptions = j === zone.top ? dataSet : { yAxisId: dataSet.yAxisId };
|
|
6109
6156
|
postProcessedRanges.push({
|
|
6110
|
-
...
|
|
6157
|
+
...datasetOptions,
|
|
6111
6158
|
dataRange: `${sheetPrefix}${zoneToXc({
|
|
6112
6159
|
left: zone.left,
|
|
6113
6160
|
right: zone.right,
|
|
@@ -8279,7 +8326,8 @@ function isSortedColumnValid(sortedColumn, pivot) {
|
|
|
8279
8326
|
const possibleValues = pivot
|
|
8280
8327
|
.getPossibleFieldValues(columns[i])
|
|
8281
8328
|
.map((v) => v.value);
|
|
8282
|
-
if (!possibleValues.includes(sortedColumn.domain[i].value)
|
|
8329
|
+
if (!possibleValues.includes(sortedColumn.domain[i].value) &&
|
|
8330
|
+
!(sortedColumn.domain[i].value === null && possibleValues.includes(""))) {
|
|
8283
8331
|
return false;
|
|
8284
8332
|
}
|
|
8285
8333
|
}
|
|
@@ -10058,70 +10106,341 @@ function getNextNonEmptyBar(bars, startIndex) {
|
|
|
10058
10106
|
return bars.find((bar, i) => i > startIndex && bar.height !== 0);
|
|
10059
10107
|
}
|
|
10060
10108
|
|
|
10061
|
-
|
|
10062
|
-
|
|
10063
|
-
|
|
10064
|
-
|
|
10065
|
-
|
|
10066
|
-
|
|
10109
|
+
const GAUGE_PADDING_SIDE = 30;
|
|
10110
|
+
const GAUGE_PADDING_TOP = 10;
|
|
10111
|
+
const GAUGE_PADDING_BOTTOM = 20;
|
|
10112
|
+
const GAUGE_LABELS_FONT_SIZE = 12;
|
|
10113
|
+
const GAUGE_DEFAULT_VALUE_FONT_SIZE = 80;
|
|
10114
|
+
const GAUGE_BACKGROUND_COLOR = "#F3F2F1";
|
|
10115
|
+
const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
|
|
10116
|
+
const GAUGE_TITLE_SECTION_HEIGHT = 25;
|
|
10117
|
+
function drawGaugeChart(canvas, runtime) {
|
|
10118
|
+
const canvasBoundingRect = canvas.getBoundingClientRect();
|
|
10119
|
+
canvas.width = canvasBoundingRect.width;
|
|
10120
|
+
canvas.height = canvasBoundingRect.height;
|
|
10121
|
+
const ctx = canvas.getContext("2d");
|
|
10122
|
+
const config = getGaugeRenderingConfig(canvasBoundingRect, runtime, ctx);
|
|
10123
|
+
drawBackground(ctx, config);
|
|
10124
|
+
drawGauge(ctx, config);
|
|
10125
|
+
drawInflectionValues(ctx, config);
|
|
10126
|
+
drawLabels(ctx, config);
|
|
10127
|
+
drawTitle(ctx, config);
|
|
10128
|
+
}
|
|
10129
|
+
function drawGauge(ctx, config) {
|
|
10130
|
+
ctx.save();
|
|
10131
|
+
const gauge = config.gauge;
|
|
10132
|
+
const arcCenterX = gauge.rect.x + gauge.rect.width / 2;
|
|
10133
|
+
const arcCenterY = gauge.rect.y + gauge.rect.height;
|
|
10134
|
+
const arcRadius = gauge.rect.height - gauge.arcWidth / 2;
|
|
10135
|
+
if (arcRadius < 0) {
|
|
10136
|
+
return;
|
|
10137
|
+
}
|
|
10138
|
+
const gaugeAngle = gauge.percentage === 1 ? 0 : Math.PI * (1 + gauge.percentage);
|
|
10139
|
+
// Gauge background
|
|
10140
|
+
ctx.strokeStyle = GAUGE_BACKGROUND_COLOR;
|
|
10141
|
+
ctx.beginPath();
|
|
10142
|
+
ctx.lineWidth = gauge.arcWidth;
|
|
10143
|
+
ctx.arc(arcCenterX, arcCenterY, arcRadius, gaugeAngle, 0);
|
|
10144
|
+
ctx.stroke();
|
|
10145
|
+
// Gauge value
|
|
10146
|
+
ctx.strokeStyle = gauge.color;
|
|
10147
|
+
ctx.beginPath();
|
|
10148
|
+
ctx.arc(arcCenterX, arcCenterY, arcRadius, Math.PI, gaugeAngle);
|
|
10149
|
+
ctx.stroke();
|
|
10150
|
+
ctx.restore();
|
|
10151
|
+
}
|
|
10152
|
+
function drawBackground(ctx, config) {
|
|
10153
|
+
ctx.save();
|
|
10154
|
+
ctx.fillStyle = config.backgroundColor;
|
|
10155
|
+
ctx.fillRect(0, 0, config.width, config.height);
|
|
10156
|
+
ctx.restore();
|
|
10157
|
+
}
|
|
10158
|
+
function drawLabels(ctx, config) {
|
|
10159
|
+
for (const label of [config.minLabel, config.maxLabel, config.gaugeValue]) {
|
|
10160
|
+
ctx.save();
|
|
10161
|
+
ctx.textAlign = "center";
|
|
10162
|
+
ctx.fillStyle = label.color;
|
|
10163
|
+
ctx.font = `${label.fontSize}px ${DEFAULT_FONT}`;
|
|
10164
|
+
ctx.fillText(label.label, label.textPosition.x, label.textPosition.y);
|
|
10165
|
+
ctx.restore();
|
|
10166
|
+
}
|
|
10167
|
+
}
|
|
10168
|
+
function drawInflectionValues(ctx, config) {
|
|
10169
|
+
const { x: rectX, y: rectY, width, height } = config.gauge.rect;
|
|
10170
|
+
for (const inflectionValue of config.inflectionValues) {
|
|
10171
|
+
ctx.save();
|
|
10172
|
+
ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment
|
|
10173
|
+
ctx.rotate(Math.PI / 2 - inflectionValue.rotation);
|
|
10174
|
+
ctx.lineWidth = 2;
|
|
10175
|
+
ctx.strokeStyle = chartMutedFontColor(config.backgroundColor) + "aa";
|
|
10176
|
+
ctx.beginPath();
|
|
10177
|
+
ctx.moveTo(0, -(height - config.gauge.arcWidth));
|
|
10178
|
+
ctx.lineTo(0, -height - 3);
|
|
10179
|
+
ctx.stroke();
|
|
10180
|
+
ctx.textAlign = "center";
|
|
10181
|
+
ctx.font = `${inflectionValue.fontSize}px ${DEFAULT_FONT}`;
|
|
10182
|
+
ctx.fillStyle = inflectionValue.color;
|
|
10183
|
+
const textY = -height - GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN - inflectionValue.offset;
|
|
10184
|
+
ctx.fillText(inflectionValue.label, 0, textY);
|
|
10185
|
+
ctx.restore();
|
|
10186
|
+
}
|
|
10187
|
+
}
|
|
10188
|
+
function drawTitle(ctx, config) {
|
|
10189
|
+
ctx.save();
|
|
10190
|
+
const title = config.title;
|
|
10191
|
+
ctx.font = getDefaultContextFont(title.fontSize, title.bold, title.italic);
|
|
10192
|
+
ctx.textBaseline = "middle";
|
|
10193
|
+
ctx.fillStyle = title.color;
|
|
10194
|
+
ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
|
|
10195
|
+
ctx.restore();
|
|
10196
|
+
}
|
|
10197
|
+
function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
|
|
10198
|
+
const maxValue = runtime.maxValue;
|
|
10199
|
+
const minValue = runtime.minValue;
|
|
10200
|
+
const gaugeValue = runtime.gaugeValue;
|
|
10201
|
+
const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
|
|
10202
|
+
const gaugeArcWidth = gaugeRect.width / 6;
|
|
10203
|
+
const gaugePercentage = gaugeValue
|
|
10204
|
+
? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
|
|
10205
|
+
: 0;
|
|
10206
|
+
const gaugeValuePosition = {
|
|
10207
|
+
x: boundingRect.width / 2,
|
|
10208
|
+
y: gaugeRect.y + gaugeRect.height - gaugeRect.height / 12,
|
|
10067
10209
|
};
|
|
10068
|
-
|
|
10069
|
-
|
|
10070
|
-
|
|
10071
|
-
|
|
10072
|
-
return this.chartRuntime.background;
|
|
10210
|
+
let gaugeValueFontSize = GAUGE_DEFAULT_VALUE_FONT_SIZE;
|
|
10211
|
+
// Scale down the font size if the gaugeRect is too small
|
|
10212
|
+
if (gaugeRect.height < 300) {
|
|
10213
|
+
gaugeValueFontSize = gaugeValueFontSize * (gaugeRect.height / 300);
|
|
10073
10214
|
}
|
|
10074
|
-
|
|
10075
|
-
|
|
10215
|
+
// Scale down the font size if the text is too long
|
|
10216
|
+
const maxTextWidth = gaugeRect.width / 2;
|
|
10217
|
+
const gaugeLabel = gaugeValue?.label || "-";
|
|
10218
|
+
if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
|
|
10219
|
+
gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
|
|
10076
10220
|
}
|
|
10077
|
-
|
|
10078
|
-
|
|
10079
|
-
|
|
10080
|
-
|
|
10081
|
-
|
|
10082
|
-
|
|
10221
|
+
const minLabelPosition = {
|
|
10222
|
+
x: gaugeRect.x + gaugeArcWidth / 2,
|
|
10223
|
+
y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
|
|
10224
|
+
};
|
|
10225
|
+
const maxLabelPosition = {
|
|
10226
|
+
x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
|
|
10227
|
+
y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
|
|
10228
|
+
};
|
|
10229
|
+
const textColor = chartMutedFontColor(runtime.background);
|
|
10230
|
+
const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);
|
|
10231
|
+
let x = 0, titleWidth = 0, titleHeight = 0;
|
|
10232
|
+
if (runtime.title.text) {
|
|
10233
|
+
({ width: titleWidth, height: titleHeight } = computeTextDimension(ctx, runtime.title.text, { fontSize: CHART_TITLE_FONT_SIZE, ...runtime.title }, "px"));
|
|
10083
10234
|
}
|
|
10084
|
-
|
|
10085
|
-
|
|
10086
|
-
|
|
10087
|
-
|
|
10088
|
-
|
|
10089
|
-
|
|
10090
|
-
|
|
10091
|
-
|
|
10092
|
-
|
|
10093
|
-
|
|
10094
|
-
|
|
10095
|
-
|
|
10096
|
-
|
|
10097
|
-
|
|
10098
|
-
|
|
10099
|
-
|
|
10100
|
-
|
|
10101
|
-
|
|
10102
|
-
|
|
10103
|
-
|
|
10235
|
+
switch (runtime.title.align) {
|
|
10236
|
+
case "right":
|
|
10237
|
+
x = boundingRect.width - titleWidth - CHART_PADDING$1;
|
|
10238
|
+
break;
|
|
10239
|
+
case "center":
|
|
10240
|
+
x = (boundingRect.width - titleWidth) / 2;
|
|
10241
|
+
break;
|
|
10242
|
+
case "left":
|
|
10243
|
+
default:
|
|
10244
|
+
x = CHART_PADDING$1;
|
|
10245
|
+
break;
|
|
10246
|
+
}
|
|
10247
|
+
return {
|
|
10248
|
+
width: boundingRect.width,
|
|
10249
|
+
height: boundingRect.height,
|
|
10250
|
+
title: {
|
|
10251
|
+
label: runtime.title.text ?? "",
|
|
10252
|
+
fontSize: runtime.title.fontSize ?? CHART_TITLE_FONT_SIZE,
|
|
10253
|
+
textPosition: {
|
|
10254
|
+
x,
|
|
10255
|
+
y: CHART_PADDING_TOP + titleHeight / 2,
|
|
10256
|
+
},
|
|
10257
|
+
color: runtime.title.color ?? textColor,
|
|
10258
|
+
bold: runtime.title.bold,
|
|
10259
|
+
italic: runtime.title.italic,
|
|
10260
|
+
},
|
|
10261
|
+
backgroundColor: runtime.background,
|
|
10262
|
+
gauge: {
|
|
10263
|
+
rect: gaugeRect,
|
|
10264
|
+
arcWidth: gaugeArcWidth,
|
|
10265
|
+
percentage: clip(gaugePercentage, 0, 1),
|
|
10266
|
+
color: getGaugeColor(runtime),
|
|
10267
|
+
},
|
|
10268
|
+
inflectionValues,
|
|
10269
|
+
gaugeValue: {
|
|
10270
|
+
label: gaugeLabel,
|
|
10271
|
+
textPosition: gaugeValuePosition,
|
|
10272
|
+
fontSize: gaugeValueFontSize,
|
|
10273
|
+
color: textColor,
|
|
10274
|
+
},
|
|
10275
|
+
minLabel: {
|
|
10276
|
+
label: runtime.minValue.label,
|
|
10277
|
+
textPosition: minLabelPosition,
|
|
10278
|
+
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
10279
|
+
color: textColor,
|
|
10280
|
+
},
|
|
10281
|
+
maxLabel: {
|
|
10282
|
+
label: runtime.maxValue.label,
|
|
10283
|
+
textPosition: maxLabelPosition,
|
|
10284
|
+
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
10285
|
+
color: textColor,
|
|
10286
|
+
},
|
|
10287
|
+
};
|
|
10288
|
+
}
|
|
10289
|
+
/**
|
|
10290
|
+
* Get the rectangle in which the gauge will be drawn, based on the bounding rectangle of the canvas and leaving
|
|
10291
|
+
* space for the title and labels.
|
|
10292
|
+
*/
|
|
10293
|
+
function getGaugeRect(boundingRect, title) {
|
|
10294
|
+
const titleHeight = title ? GAUGE_TITLE_SECTION_HEIGHT : 0;
|
|
10295
|
+
const drawHeight = boundingRect.height - GAUGE_PADDING_BOTTOM - titleHeight - GAUGE_PADDING_TOP;
|
|
10296
|
+
const drawWidth = boundingRect.width - GAUGE_PADDING_SIDE * 2;
|
|
10297
|
+
let gaugeWidth;
|
|
10298
|
+
let gaugeHeight;
|
|
10299
|
+
if (drawWidth > 2 * drawHeight) {
|
|
10300
|
+
gaugeWidth = 2 * drawHeight;
|
|
10301
|
+
gaugeHeight = drawHeight;
|
|
10302
|
+
}
|
|
10303
|
+
else {
|
|
10304
|
+
gaugeWidth = drawWidth;
|
|
10305
|
+
gaugeHeight = drawWidth / 2;
|
|
10306
|
+
}
|
|
10307
|
+
const gaugeX = GAUGE_PADDING_SIDE + (drawWidth - gaugeWidth) / 2;
|
|
10308
|
+
const gaugeY = titleHeight + GAUGE_PADDING_TOP + (drawHeight - gaugeHeight) / 2;
|
|
10309
|
+
return {
|
|
10310
|
+
x: gaugeX,
|
|
10311
|
+
y: gaugeY,
|
|
10312
|
+
width: gaugeWidth,
|
|
10313
|
+
height: gaugeHeight,
|
|
10314
|
+
};
|
|
10315
|
+
}
|
|
10316
|
+
/**
|
|
10317
|
+
* 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).
|
|
10318
|
+
*
|
|
10319
|
+
* Also compute an offset for the text so that it doesn't overlap with other text.
|
|
10320
|
+
*/
|
|
10321
|
+
function getInflectionValues(runtime, gaugeRect, textColor, ctx) {
|
|
10322
|
+
const maxValue = runtime.maxValue;
|
|
10323
|
+
const minValue = runtime.minValue;
|
|
10324
|
+
const gaugeCircleCenter = {
|
|
10325
|
+
x: gaugeRect.x + gaugeRect.width / 2,
|
|
10326
|
+
y: gaugeRect.y + gaugeRect.height,
|
|
10327
|
+
};
|
|
10328
|
+
const textStyle = { fontSize: GAUGE_LABELS_FONT_SIZE };
|
|
10329
|
+
const inflectionValues = [];
|
|
10330
|
+
const inflectionValuesTextRects = [];
|
|
10331
|
+
for (const inflectionValue of runtime.inflectionValues) {
|
|
10332
|
+
const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
|
|
10333
|
+
const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
|
|
10334
|
+
const angle = Math.PI - Math.PI * percentage;
|
|
10335
|
+
const textRect = getRectangleTangentToCircle(angle, // angle between X axis and the point where the rectangle is tangent to the circle
|
|
10336
|
+
gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, // radius of the gauge circle + margin below text
|
|
10337
|
+
gaugeCircleCenter.x, // center of the gauge circle
|
|
10338
|
+
gaugeCircleCenter.y, // center of the gauge circle
|
|
10339
|
+
labelWidth + 2, // width of the text + some margin
|
|
10340
|
+
GAUGE_LABELS_FONT_SIZE // height of the text
|
|
10341
|
+
);
|
|
10342
|
+
let offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect))
|
|
10343
|
+
? GAUGE_LABELS_FONT_SIZE
|
|
10344
|
+
: 0;
|
|
10345
|
+
inflectionValuesTextRects.push(textRect);
|
|
10346
|
+
inflectionValues.push({
|
|
10347
|
+
rotation: angle,
|
|
10348
|
+
label: inflectionValue.label,
|
|
10349
|
+
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
10350
|
+
color: textColor,
|
|
10351
|
+
offset,
|
|
10104
10352
|
});
|
|
10105
10353
|
}
|
|
10106
|
-
|
|
10107
|
-
|
|
10108
|
-
|
|
10109
|
-
|
|
10354
|
+
return inflectionValues;
|
|
10355
|
+
}
|
|
10356
|
+
function getGaugeColor(runtime) {
|
|
10357
|
+
const gaugeValue = runtime.gaugeValue?.value;
|
|
10358
|
+
if (gaugeValue === undefined) {
|
|
10359
|
+
return GAUGE_BACKGROUND_COLOR;
|
|
10110
10360
|
}
|
|
10111
|
-
|
|
10112
|
-
const
|
|
10113
|
-
if (
|
|
10114
|
-
|
|
10115
|
-
if (chartData.options?.plugins?.title) {
|
|
10116
|
-
this.chart.config.options.plugins.title = chartData.options.plugins.title;
|
|
10117
|
-
}
|
|
10361
|
+
for (let i = 0; i < runtime.inflectionValues.length; i++) {
|
|
10362
|
+
const inflectionValue = runtime.inflectionValues[i];
|
|
10363
|
+
if (inflectionValue.operator === "<" && gaugeValue < inflectionValue.value) {
|
|
10364
|
+
return runtime.colors[i];
|
|
10118
10365
|
}
|
|
10119
|
-
else {
|
|
10120
|
-
|
|
10366
|
+
else if (inflectionValue.operator === "<=" && gaugeValue <= inflectionValue.value) {
|
|
10367
|
+
return runtime.colors[i];
|
|
10121
10368
|
}
|
|
10122
|
-
this.chart.config.options = chartData.options;
|
|
10123
|
-
this.chart.update();
|
|
10124
10369
|
}
|
|
10370
|
+
return runtime.colors.at(-1);
|
|
10371
|
+
}
|
|
10372
|
+
function getSegmentsOfRectangle(rectangle) {
|
|
10373
|
+
return [
|
|
10374
|
+
{ start: rectangle.topLeft, end: rectangle.topRight },
|
|
10375
|
+
{ start: rectangle.topRight, end: rectangle.bottomRight },
|
|
10376
|
+
{ start: rectangle.bottomRight, end: rectangle.bottomLeft },
|
|
10377
|
+
{ start: rectangle.bottomLeft, end: rectangle.topLeft },
|
|
10378
|
+
];
|
|
10379
|
+
}
|
|
10380
|
+
/**
|
|
10381
|
+
* Check if two segment intersect. The case where the segments are colinear (both segments on the same line)
|
|
10382
|
+
* is not handled.
|
|
10383
|
+
*/
|
|
10384
|
+
function doSegmentIntersect(segment1, segment2) {
|
|
10385
|
+
const A = segment1.start;
|
|
10386
|
+
const B = segment1.end;
|
|
10387
|
+
const C = segment2.start;
|
|
10388
|
+
const D = segment2.end;
|
|
10389
|
+
/**
|
|
10390
|
+
* Line segment intersection algorithm
|
|
10391
|
+
* https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
|
|
10392
|
+
*/
|
|
10393
|
+
function ccw(a, b, c) {
|
|
10394
|
+
return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
|
|
10395
|
+
}
|
|
10396
|
+
return ccw(A, C, D) !== ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D);
|
|
10397
|
+
}
|
|
10398
|
+
function doRectanglesIntersect(rect1, rect2) {
|
|
10399
|
+
const segments1 = getSegmentsOfRectangle(rect1);
|
|
10400
|
+
const segments2 = getSegmentsOfRectangle(rect2);
|
|
10401
|
+
for (const segment1 of segments1) {
|
|
10402
|
+
for (const segment2 of segments2) {
|
|
10403
|
+
if (doSegmentIntersect(segment1, segment2)) {
|
|
10404
|
+
return true;
|
|
10405
|
+
}
|
|
10406
|
+
}
|
|
10407
|
+
}
|
|
10408
|
+
return false;
|
|
10409
|
+
}
|
|
10410
|
+
/**
|
|
10411
|
+
* Get the rectangle that is tangent to a circle at a given angle.
|
|
10412
|
+
*
|
|
10413
|
+
* @param angle angle between X axis and the point where the rectangle is tangent to the circle
|
|
10414
|
+
*/
|
|
10415
|
+
function getRectangleTangentToCircle(angle, radius, circleCenterX, circleCenterY, rectWidth, rectHeight) {
|
|
10416
|
+
const cos = Math.cos(angle);
|
|
10417
|
+
const sin = Math.sin(angle);
|
|
10418
|
+
// x, y are the distance from the center of the circle to the point where the rectangle is tangent to the circle
|
|
10419
|
+
const x = cos * radius;
|
|
10420
|
+
const y = sin * radius;
|
|
10421
|
+
// x2, y2 are the distance from the point the rectangle is tangent to the circle to the bottom left corner of the rectangle
|
|
10422
|
+
const x2 = sin * (rectWidth / 2); // cos(angle + 90°) = sin(angle)
|
|
10423
|
+
const y2 = cos * (rectWidth / 2);
|
|
10424
|
+
const bottomRight = {
|
|
10425
|
+
x: x + x2 + circleCenterX,
|
|
10426
|
+
y: circleCenterY - (y - y2),
|
|
10427
|
+
};
|
|
10428
|
+
const bottomLeft = {
|
|
10429
|
+
x: x - x2 + circleCenterX,
|
|
10430
|
+
y: circleCenterY - (y + y2),
|
|
10431
|
+
};
|
|
10432
|
+
// Same as above but for the top corners of the rectangle (radius + rectangle height instead of radius)
|
|
10433
|
+
const xp = cos * (radius + rectHeight);
|
|
10434
|
+
const yp = sin * (radius + rectHeight);
|
|
10435
|
+
const topLeft = {
|
|
10436
|
+
x: xp - x2 + circleCenterX,
|
|
10437
|
+
y: circleCenterY - (yp + y2),
|
|
10438
|
+
};
|
|
10439
|
+
const topRight = {
|
|
10440
|
+
x: xp + x2 + circleCenterX,
|
|
10441
|
+
y: circleCenterY - (yp - y2),
|
|
10442
|
+
};
|
|
10443
|
+
return { bottomLeft, bottomRight, topRight, topLeft };
|
|
10125
10444
|
}
|
|
10126
10445
|
|
|
10127
10446
|
/**
|
|
@@ -10703,6 +11022,155 @@ class ScorecardChartConfigBuilder {
|
|
|
10703
11022
|
}
|
|
10704
11023
|
}
|
|
10705
11024
|
|
|
11025
|
+
const CHART_COMMON_OPTIONS = {
|
|
11026
|
+
// https://www.chartjs.org/docs/latest/general/responsive.html
|
|
11027
|
+
responsive: true, // will resize when its container is resized
|
|
11028
|
+
maintainAspectRatio: false, // doesn't maintain the aspect ratio (width/height =2 by default) so the user has the choice of the exact layout
|
|
11029
|
+
elements: {
|
|
11030
|
+
line: {
|
|
11031
|
+
fill: false, // do not fill the area under line charts
|
|
11032
|
+
},
|
|
11033
|
+
point: {
|
|
11034
|
+
hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
|
|
11035
|
+
},
|
|
11036
|
+
},
|
|
11037
|
+
animation: false,
|
|
11038
|
+
};
|
|
11039
|
+
function truncateLabel(label) {
|
|
11040
|
+
if (!label) {
|
|
11041
|
+
return "";
|
|
11042
|
+
}
|
|
11043
|
+
if (label.length > MAX_CHAR_LABEL) {
|
|
11044
|
+
return label.substring(0, MAX_CHAR_LABEL) + "…";
|
|
11045
|
+
}
|
|
11046
|
+
return label;
|
|
11047
|
+
}
|
|
11048
|
+
function chartToImage(runtime, figure, type) {
|
|
11049
|
+
// wrap the canvas in a div with a fixed size because chart.js would
|
|
11050
|
+
// fill the whole page otherwise
|
|
11051
|
+
const div = document.createElement("div");
|
|
11052
|
+
div.style.width = `${figure.width}px`;
|
|
11053
|
+
div.style.height = `${figure.height}px`;
|
|
11054
|
+
const canvas = document.createElement("canvas");
|
|
11055
|
+
div.append(canvas);
|
|
11056
|
+
canvas.setAttribute("width", figure.width.toString());
|
|
11057
|
+
canvas.setAttribute("height", figure.height.toString());
|
|
11058
|
+
// we have to add the canvas to the DOM otherwise it won't be rendered
|
|
11059
|
+
document.body.append(div);
|
|
11060
|
+
if ("chartJsConfig" in runtime) {
|
|
11061
|
+
const config = deepCopy(runtime.chartJsConfig);
|
|
11062
|
+
config.plugins = [backgroundColorChartJSPlugin];
|
|
11063
|
+
const Chart = getChartJSConstructor();
|
|
11064
|
+
const chart = new Chart(canvas, config);
|
|
11065
|
+
const imgContent = chart.toBase64Image();
|
|
11066
|
+
chart.destroy();
|
|
11067
|
+
div.remove();
|
|
11068
|
+
return imgContent;
|
|
11069
|
+
}
|
|
11070
|
+
else if (type === "scorecard") {
|
|
11071
|
+
const design = getScorecardConfiguration(figure, runtime);
|
|
11072
|
+
drawScoreChart(design, canvas);
|
|
11073
|
+
const imgContent = canvas.toDataURL();
|
|
11074
|
+
div.remove();
|
|
11075
|
+
return imgContent;
|
|
11076
|
+
}
|
|
11077
|
+
else if (type === "gauge") {
|
|
11078
|
+
drawGaugeChart(canvas, runtime);
|
|
11079
|
+
const imgContent = canvas.toDataURL();
|
|
11080
|
+
div.remove();
|
|
11081
|
+
return imgContent;
|
|
11082
|
+
}
|
|
11083
|
+
return undefined;
|
|
11084
|
+
}
|
|
11085
|
+
/**
|
|
11086
|
+
* Custom chart.js plugin to set the background color of the canvas
|
|
11087
|
+
* https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md
|
|
11088
|
+
*/
|
|
11089
|
+
const backgroundColorChartJSPlugin = {
|
|
11090
|
+
id: "customCanvasBackgroundColor",
|
|
11091
|
+
beforeDraw: (chart) => {
|
|
11092
|
+
const { ctx } = chart;
|
|
11093
|
+
ctx.save();
|
|
11094
|
+
ctx.globalCompositeOperation = "destination-over";
|
|
11095
|
+
ctx.fillStyle = "#ffffff";
|
|
11096
|
+
ctx.fillRect(0, 0, chart.width, chart.height);
|
|
11097
|
+
ctx.restore();
|
|
11098
|
+
},
|
|
11099
|
+
};
|
|
11100
|
+
/** Return window.Chart, making sure all our extensions are loaded in ChartJS */
|
|
11101
|
+
function getChartJSConstructor() {
|
|
11102
|
+
if (window.Chart && !window.Chart?.registry.plugins.get("chartShowValuesPlugin")) {
|
|
11103
|
+
window.Chart.register(chartShowValuesPlugin);
|
|
11104
|
+
window.Chart.register(waterfallLinesPlugin);
|
|
11105
|
+
}
|
|
11106
|
+
return window.Chart;
|
|
11107
|
+
}
|
|
11108
|
+
|
|
11109
|
+
class ChartJsComponent extends owl.Component {
|
|
11110
|
+
static template = "o-spreadsheet-ChartJsComponent";
|
|
11111
|
+
static props = {
|
|
11112
|
+
figure: Object,
|
|
11113
|
+
};
|
|
11114
|
+
canvas = owl.useRef("graphContainer");
|
|
11115
|
+
chart;
|
|
11116
|
+
currentRuntime;
|
|
11117
|
+
get background() {
|
|
11118
|
+
return this.chartRuntime.background;
|
|
11119
|
+
}
|
|
11120
|
+
get canvasStyle() {
|
|
11121
|
+
return `background-color: ${this.background}`;
|
|
11122
|
+
}
|
|
11123
|
+
get chartRuntime() {
|
|
11124
|
+
const runtime = this.env.model.getters.getChartRuntime(this.props.figure.id);
|
|
11125
|
+
if (!("chartJsConfig" in runtime)) {
|
|
11126
|
+
throw new Error("Unsupported chart runtime");
|
|
11127
|
+
}
|
|
11128
|
+
return runtime;
|
|
11129
|
+
}
|
|
11130
|
+
setup() {
|
|
11131
|
+
owl.onMounted(() => {
|
|
11132
|
+
const runtime = this.chartRuntime;
|
|
11133
|
+
this.currentRuntime = runtime;
|
|
11134
|
+
// Note: chartJS modify the runtime in place, so it's important to give it a copy
|
|
11135
|
+
this.createChart(deepCopy(runtime.chartJsConfig));
|
|
11136
|
+
});
|
|
11137
|
+
owl.onWillUnmount(() => this.chart?.destroy());
|
|
11138
|
+
owl.useEffect(() => {
|
|
11139
|
+
const runtime = this.chartRuntime;
|
|
11140
|
+
if (runtime !== this.currentRuntime) {
|
|
11141
|
+
if (runtime.chartJsConfig.type !== this.currentRuntime.chartJsConfig.type) {
|
|
11142
|
+
this.chart?.destroy();
|
|
11143
|
+
this.createChart(deepCopy(runtime.chartJsConfig));
|
|
11144
|
+
}
|
|
11145
|
+
else {
|
|
11146
|
+
this.updateChartJs(deepCopy(runtime));
|
|
11147
|
+
}
|
|
11148
|
+
this.currentRuntime = runtime;
|
|
11149
|
+
}
|
|
11150
|
+
});
|
|
11151
|
+
}
|
|
11152
|
+
createChart(chartData) {
|
|
11153
|
+
const canvas = this.canvas.el;
|
|
11154
|
+
const ctx = canvas.getContext("2d");
|
|
11155
|
+
const Chart = getChartJSConstructor();
|
|
11156
|
+
this.chart = new Chart(ctx, chartData);
|
|
11157
|
+
}
|
|
11158
|
+
updateChartJs(chartRuntime) {
|
|
11159
|
+
const chartData = chartRuntime.chartJsConfig;
|
|
11160
|
+
if (chartData.data && chartData.data.datasets) {
|
|
11161
|
+
this.chart.data = chartData.data;
|
|
11162
|
+
if (chartData.options?.plugins?.title) {
|
|
11163
|
+
this.chart.config.options.plugins.title = chartData.options.plugins.title;
|
|
11164
|
+
}
|
|
11165
|
+
}
|
|
11166
|
+
else {
|
|
11167
|
+
this.chart.data.datasets = [];
|
|
11168
|
+
}
|
|
11169
|
+
this.chart.config.options = chartData.options;
|
|
11170
|
+
this.chart.update();
|
|
11171
|
+
}
|
|
11172
|
+
}
|
|
11173
|
+
|
|
10706
11174
|
class ScorecardChart extends owl.Component {
|
|
10707
11175
|
static template = "o-spreadsheet-ScorecardChart";
|
|
10708
11176
|
static props = {
|
|
@@ -10734,6 +11202,7 @@ class ScorecardChart extends owl.Component {
|
|
|
10734
11202
|
const autoCompleteProviders = new Registry();
|
|
10735
11203
|
|
|
10736
11204
|
autoCompleteProviders.add("dataValidation", {
|
|
11205
|
+
displayAllOnInitialContent: true,
|
|
10737
11206
|
getProposals(tokenAtCursor, content) {
|
|
10738
11207
|
if (content.startsWith("=")) {
|
|
10739
11208
|
return [];
|
|
@@ -21030,6 +21499,15 @@ class AbstractComposerStore extends SpreadsheetStore {
|
|
|
21030
21499
|
const exactMatch = proposals?.find((p) => p.text === tokenAtCursor.value);
|
|
21031
21500
|
// remove tokens that are likely to be other parts of the formula that slipped in the token if it's a string
|
|
21032
21501
|
const searchTerm = tokenAtCursor.value.replace(/[ ,\(\)]/g, "");
|
|
21502
|
+
if (this._currentContent === this.initialContent &&
|
|
21503
|
+
provider.displayAllOnInitialContent &&
|
|
21504
|
+
proposals?.length) {
|
|
21505
|
+
return {
|
|
21506
|
+
proposals,
|
|
21507
|
+
selectProposal: provider.selectProposal,
|
|
21508
|
+
autoSelectFirstProposal: provider.autoSelectFirstProposal ?? false,
|
|
21509
|
+
};
|
|
21510
|
+
}
|
|
21033
21511
|
if (exactMatch && this._currentContent !== this.initialContent) {
|
|
21034
21512
|
// this means the user has chosen a proposal
|
|
21035
21513
|
return;
|
|
@@ -22155,7 +22633,7 @@ autofillRulesRegistry
|
|
|
22155
22633
|
condition: (cell) => !cell.isFormula &&
|
|
22156
22634
|
evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.text &&
|
|
22157
22635
|
alphaNumericValueRegExp.test(cell.content),
|
|
22158
|
-
generateRule: (cell, cells) => {
|
|
22636
|
+
generateRule: (cell, cells, direction) => {
|
|
22159
22637
|
const numberPostfix = parseInt(cell.content.match(numberPostfixRegExp)[0]);
|
|
22160
22638
|
const prefix = cell.content.match(stringPrefixRegExp)[0];
|
|
22161
22639
|
const numberPostfixLength = cell.content.length - prefix.length;
|
|
@@ -22163,7 +22641,10 @@ autofillRulesRegistry
|
|
|
22163
22641
|
alphaNumericValueRegExp.test(evaluatedCell.value)) // get consecutive alphanumeric cells, no matter what the prefix is
|
|
22164
22642
|
.filter((cell) => prefix === (cell.value ?? "").toString().match(stringPrefixRegExp)[0])
|
|
22165
22643
|
.map((cell) => parseInt((cell.value ?? "").toString().match(numberPostfixRegExp)[0]));
|
|
22166
|
-
|
|
22644
|
+
let increment = calculateIncrementBasedOnGroup(group);
|
|
22645
|
+
if (["up", "left"].includes(direction) && group.length === 1) {
|
|
22646
|
+
increment = -increment;
|
|
22647
|
+
}
|
|
22167
22648
|
return {
|
|
22168
22649
|
type: "ALPHANUMERIC_INCREMENT_MODIFIER",
|
|
22169
22650
|
prefix,
|
|
@@ -22226,10 +22707,13 @@ autofillRulesRegistry
|
|
|
22226
22707
|
.add("increment_number", {
|
|
22227
22708
|
condition: (cell) => !cell.isFormula &&
|
|
22228
22709
|
evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.number,
|
|
22229
|
-
generateRule: (cell, cells) => {
|
|
22710
|
+
generateRule: (cell, cells, direction) => {
|
|
22230
22711
|
const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number &&
|
|
22231
22712
|
!isDateTimeFormat(evaluatedCell.format || "")).map((cell) => Number(cell.value));
|
|
22232
|
-
|
|
22713
|
+
let increment = calculateIncrementBasedOnGroup(group);
|
|
22714
|
+
if (["up", "left"].includes(direction) && group.length === 1) {
|
|
22715
|
+
increment = -increment;
|
|
22716
|
+
}
|
|
22233
22717
|
const evaluation = evaluateLiteral(cell, { locale: DEFAULT_LOCALE });
|
|
22234
22718
|
return {
|
|
22235
22719
|
type: "INCREMENT_MODIFIER",
|
|
@@ -22273,343 +22757,6 @@ function getDateIntervals(dates) {
|
|
|
22273
22757
|
|
|
22274
22758
|
const cellPopoverRegistry = new Registry();
|
|
22275
22759
|
|
|
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
22760
|
class GaugeChartComponent extends owl.Component {
|
|
22614
22761
|
static template = "o-spreadsheet-GaugeChartComponent";
|
|
22615
22762
|
canvas = owl.useRef("chartContainer");
|
|
@@ -22642,81 +22789,6 @@ function toXlsxHexColor(color) {
|
|
|
22642
22789
|
return color;
|
|
22643
22790
|
}
|
|
22644
22791
|
|
|
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
22792
|
/**
|
|
22721
22793
|
* Represent a raw XML string
|
|
22722
22794
|
*/
|
|
@@ -22778,6 +22850,7 @@ const DRAWING_NS_C = "http://schemas.openxmlformats.org/drawingml/2006/chart";
|
|
|
22778
22850
|
const CONTENT_TYPES = {
|
|
22779
22851
|
workbook: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml",
|
|
22780
22852
|
sheet: "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml",
|
|
22853
|
+
metadata: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheetMetadata+xml",
|
|
22781
22854
|
sharedStrings: "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml",
|
|
22782
22855
|
styles: "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml",
|
|
22783
22856
|
drawing: "application/vnd.openxmlformats-officedocument.drawing+xml",
|
|
@@ -22790,6 +22863,7 @@ const CONTENT_TYPES = {
|
|
|
22790
22863
|
const XLSX_RELATION_TYPE = {
|
|
22791
22864
|
document: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument",
|
|
22792
22865
|
sheet: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet",
|
|
22866
|
+
metadata: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sheetMetadata",
|
|
22793
22867
|
sharedStrings: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings",
|
|
22794
22868
|
styles: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles",
|
|
22795
22869
|
drawing: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing",
|
|
@@ -22799,6 +22873,7 @@ const XLSX_RELATION_TYPE = {
|
|
|
22799
22873
|
hyperlink: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
|
|
22800
22874
|
image: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
|
|
22801
22875
|
};
|
|
22876
|
+
const ARRAY_FORMULA_URI = "bdbb8cdc-fa1e-496e-a857-3c3f30c029c3";
|
|
22802
22877
|
const RELATIONSHIP_NSR = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
|
22803
22878
|
const HEIGHT_FACTOR = 0.75; // 100px => 75 u
|
|
22804
22879
|
/**
|
|
@@ -24318,16 +24393,25 @@ function addRelsToFile(relsFiles, path, rel) {
|
|
|
24318
24393
|
}
|
|
24319
24394
|
return id;
|
|
24320
24395
|
}
|
|
24396
|
+
const globalReverseLookup = new WeakMap();
|
|
24321
24397
|
function pushElement(property, propertyList) {
|
|
24322
|
-
let
|
|
24323
|
-
|
|
24324
|
-
|
|
24325
|
-
|
|
24326
|
-
|
|
24398
|
+
let reverseLookup = globalReverseLookup.get(propertyList);
|
|
24399
|
+
if (!reverseLookup) {
|
|
24400
|
+
reverseLookup = new Map();
|
|
24401
|
+
for (let i = 0; i < propertyList.length; i++) {
|
|
24402
|
+
const canonical = getCanonicalRepresentation(propertyList[i]);
|
|
24403
|
+
reverseLookup.set(canonical, i);
|
|
24327
24404
|
}
|
|
24405
|
+
globalReverseLookup.set(propertyList, reverseLookup);
|
|
24328
24406
|
}
|
|
24329
|
-
|
|
24330
|
-
|
|
24407
|
+
const canonical = getCanonicalRepresentation(property);
|
|
24408
|
+
if (reverseLookup.has(canonical)) {
|
|
24409
|
+
return reverseLookup.get(canonical);
|
|
24410
|
+
}
|
|
24411
|
+
const maxId = propertyList.length;
|
|
24412
|
+
propertyList.push(property);
|
|
24413
|
+
reverseLookup.set(canonical, maxId);
|
|
24414
|
+
return maxId;
|
|
24331
24415
|
}
|
|
24332
24416
|
const chartIds = [];
|
|
24333
24417
|
/**
|
|
@@ -25364,29 +25448,34 @@ function convertPivotTableConfig(pivotTable) {
|
|
|
25364
25448
|
* In all the sheets, replace the table-only references in the formula cells with standard references.
|
|
25365
25449
|
*/
|
|
25366
25450
|
function convertTableFormulaReferences(convertedSheets, xlsxSheets) {
|
|
25367
|
-
for (let
|
|
25368
|
-
const tables = xlsxSheets.find((s) => s.sheetName ===
|
|
25451
|
+
for (let tableSheet of convertedSheets) {
|
|
25452
|
+
const tables = xlsxSheets.find((s) => s.sheetName === tableSheet.name).tables;
|
|
25369
25453
|
for (let table of tables) {
|
|
25370
25454
|
const tabRef = table.name + "[";
|
|
25371
|
-
for (let
|
|
25372
|
-
|
|
25373
|
-
|
|
25374
|
-
|
|
25375
|
-
|
|
25376
|
-
|
|
25377
|
-
|
|
25378
|
-
|
|
25379
|
-
|
|
25380
|
-
|
|
25381
|
-
|
|
25382
|
-
|
|
25455
|
+
for (let sheet of convertedSheets) {
|
|
25456
|
+
for (let xc in sheet.cells) {
|
|
25457
|
+
const cell = sheet.cells[xc];
|
|
25458
|
+
let cellContent = sheet.cells[xc];
|
|
25459
|
+
if (cell && cellContent && cellContent.startsWith("=")) {
|
|
25460
|
+
let refIndex;
|
|
25461
|
+
while ((refIndex = cellContent.indexOf(tabRef)) !== -1) {
|
|
25462
|
+
let endIndex = refIndex + tabRef.length;
|
|
25463
|
+
let openBrackets = 1;
|
|
25464
|
+
while (openBrackets > 0 && endIndex < cellContent.length) {
|
|
25465
|
+
if (cellContent[endIndex] === "[") {
|
|
25466
|
+
openBrackets++;
|
|
25467
|
+
}
|
|
25468
|
+
else if (cellContent[endIndex] === "]") {
|
|
25469
|
+
openBrackets--;
|
|
25470
|
+
}
|
|
25471
|
+
endIndex++;
|
|
25472
|
+
}
|
|
25473
|
+
let reference = cellContent.slice(refIndex + tabRef.length, endIndex - 1);
|
|
25474
|
+
const sheetPrefix = tableSheet.id === sheet.id ? "" : tableSheet.name + "!";
|
|
25475
|
+
const convertedRef = convertTableReference(sheetPrefix, reference, table, xc);
|
|
25476
|
+
cellContent =
|
|
25477
|
+
cellContent.slice(0, refIndex) + convertedRef + cellContent.slice(endIndex);
|
|
25383
25478
|
}
|
|
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
25479
|
}
|
|
25391
25480
|
sheet.cells[xc] = cellContent;
|
|
25392
25481
|
}
|
|
@@ -25395,11 +25484,17 @@ function convertTableFormulaReferences(convertedSheets, xlsxSheets) {
|
|
|
25395
25484
|
}
|
|
25396
25485
|
}
|
|
25397
25486
|
/**
|
|
25398
|
-
* Convert table-specific references in formulas into standard references.
|
|
25487
|
+
* Convert table-specific references in formulas into standard references. A table reference is composed of columns names,
|
|
25488
|
+
* and of keywords determining the rows of the table to reference.
|
|
25399
25489
|
*
|
|
25400
25490
|
* A reference in a table can have the form (only the part between brackets should be given to this function):
|
|
25401
25491
|
* - tableName[colName] : reference to the whole column "colName"
|
|
25492
|
+
* - tableName[#keyword] : reference to the whatever row the keyword refers to
|
|
25402
25493
|
* - tableName[[#keyword], [colName]] : reference to some of the element(s) of the column colName
|
|
25494
|
+
* - tableName[[#keyword], [colName]:[col2Name]] : reference to some of the element(s) of the columns colName to col2Name
|
|
25495
|
+
* - tableName[[#keyword1], [#keyword2], [colName]] : reference to all the rows referenced by the keywords in the column colName
|
|
25496
|
+
* - tableName[[#keyword1], [colName], [#keyword2]]: the keywords and colName can be in any order
|
|
25497
|
+
*
|
|
25403
25498
|
*
|
|
25404
25499
|
* The available keywords are :
|
|
25405
25500
|
* - #All : all the column (including totals)
|
|
@@ -25407,58 +25502,109 @@ function convertTableFormulaReferences(convertedSheets, xlsxSheets) {
|
|
|
25407
25502
|
* - #Headers : only the header of the column
|
|
25408
25503
|
* - #Totals : only the totals of the column
|
|
25409
25504
|
* - #This Row : only the element in the same row as the cell
|
|
25505
|
+
*
|
|
25506
|
+
* Note that the only valid combination of multiple keywords are #Data + #Totals and #Headers + #Data.
|
|
25410
25507
|
*/
|
|
25411
|
-
function convertTableReference(expr, table, cellXc) {
|
|
25412
|
-
|
|
25508
|
+
function convertTableReference(sheetPrefix, expr, table, cellXc) {
|
|
25509
|
+
// TODO: Ideally we'd want to make a real tokenizer, this simple approach won't work if for example the column name
|
|
25510
|
+
// contain # or , characters. But that's probably an edge case that we can ignore for now.
|
|
25511
|
+
const parts = expr.split(",").map((part) => part.trim());
|
|
25413
25512
|
const tableZone = toZone(table.ref);
|
|
25414
|
-
const
|
|
25415
|
-
|
|
25416
|
-
|
|
25417
|
-
|
|
25418
|
-
|
|
25419
|
-
|
|
25420
|
-
|
|
25421
|
-
|
|
25422
|
-
|
|
25423
|
-
|
|
25424
|
-
|
|
25513
|
+
const colIndexes = [];
|
|
25514
|
+
const rowIndexes = [];
|
|
25515
|
+
const foundKeywords = [];
|
|
25516
|
+
for (const part of parts) {
|
|
25517
|
+
if (removeBrackets(part).startsWith("#")) {
|
|
25518
|
+
const keyWord = removeBrackets(part);
|
|
25519
|
+
foundKeywords.push(keyWord);
|
|
25520
|
+
switch (keyWord) {
|
|
25521
|
+
case "#All":
|
|
25522
|
+
rowIndexes.push(tableZone.top, tableZone.bottom);
|
|
25523
|
+
break;
|
|
25524
|
+
case "#Data":
|
|
25525
|
+
const top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
|
|
25526
|
+
const bottom = table.totalsRowCount
|
|
25527
|
+
? tableZone.bottom - table.totalsRowCount
|
|
25528
|
+
: tableZone.bottom;
|
|
25529
|
+
rowIndexes.push(top, bottom);
|
|
25530
|
+
break;
|
|
25531
|
+
case "#This Row":
|
|
25532
|
+
rowIndexes.push(toCartesian(cellXc).row);
|
|
25533
|
+
break;
|
|
25534
|
+
case "#Headers":
|
|
25535
|
+
if (!table.headerRowCount) {
|
|
25536
|
+
return CellErrorType.InvalidReference;
|
|
25537
|
+
}
|
|
25538
|
+
rowIndexes.push(tableZone.top);
|
|
25539
|
+
break;
|
|
25540
|
+
case "#Totals":
|
|
25541
|
+
if (!table.totalsRowCount) {
|
|
25542
|
+
return CellErrorType.InvalidReference;
|
|
25543
|
+
}
|
|
25544
|
+
rowIndexes.push(tableZone.bottom);
|
|
25545
|
+
break;
|
|
25546
|
+
}
|
|
25425
25547
|
}
|
|
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;
|
|
25548
|
+
else {
|
|
25549
|
+
const columns = part
|
|
25550
|
+
.split(":")
|
|
25551
|
+
.map((part) => part.trim())
|
|
25552
|
+
.map(removeBrackets);
|
|
25553
|
+
if (colIndexes.length) {
|
|
25554
|
+
return CellErrorType.InvalidReference;
|
|
25555
|
+
}
|
|
25556
|
+
const colRelativeIndex = table.cols.findIndex((col) => col.name === columns[0]);
|
|
25557
|
+
if (colRelativeIndex === -1) {
|
|
25558
|
+
return CellErrorType.InvalidReference;
|
|
25559
|
+
}
|
|
25560
|
+
colIndexes.push(colRelativeIndex + tableZone.left);
|
|
25561
|
+
if (columns[1]) {
|
|
25562
|
+
const colRelativeIndex2 = table.cols.findIndex((col) => col.name === columns[1]);
|
|
25563
|
+
if (colRelativeIndex2 === -1) {
|
|
25564
|
+
return CellErrorType.InvalidReference;
|
|
25451
25565
|
}
|
|
25452
|
-
|
|
25566
|
+
colIndexes.push(colRelativeIndex2 + tableZone.left);
|
|
25567
|
+
}
|
|
25453
25568
|
}
|
|
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
25569
|
}
|
|
25458
|
-
if (!
|
|
25570
|
+
if (!areKeywordsCompatible(foundKeywords)) {
|
|
25459
25571
|
return CellErrorType.InvalidReference;
|
|
25460
25572
|
}
|
|
25461
|
-
|
|
25573
|
+
if (rowIndexes.length === 0) {
|
|
25574
|
+
const top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
|
|
25575
|
+
const bottom = table.totalsRowCount
|
|
25576
|
+
? tableZone.bottom - table.totalsRowCount
|
|
25577
|
+
: tableZone.bottom;
|
|
25578
|
+
rowIndexes.push(top, bottom);
|
|
25579
|
+
}
|
|
25580
|
+
if (colIndexes.length === 0) {
|
|
25581
|
+
colIndexes.push(tableZone.left, tableZone.right);
|
|
25582
|
+
}
|
|
25583
|
+
const refZone = {
|
|
25584
|
+
top: Math.min(...rowIndexes),
|
|
25585
|
+
left: Math.min(...colIndexes),
|
|
25586
|
+
bottom: Math.max(...rowIndexes),
|
|
25587
|
+
right: Math.max(...colIndexes),
|
|
25588
|
+
};
|
|
25589
|
+
return sheetPrefix + zoneToXc(refZone);
|
|
25590
|
+
}
|
|
25591
|
+
function removeBrackets(str) {
|
|
25592
|
+
return str.startsWith("[") && str.endsWith("]") ? str.slice(1, str.length - 1) : str;
|
|
25593
|
+
}
|
|
25594
|
+
function areKeywordsCompatible(keywords) {
|
|
25595
|
+
if (keywords.length < 2) {
|
|
25596
|
+
return true;
|
|
25597
|
+
}
|
|
25598
|
+
else if (keywords.length > 2) {
|
|
25599
|
+
return false;
|
|
25600
|
+
}
|
|
25601
|
+
else if (keywords.includes("#Data") && keywords.includes("#Totals")) {
|
|
25602
|
+
return true;
|
|
25603
|
+
}
|
|
25604
|
+
else if (keywords.includes("#Headers") && keywords.includes("#Data")) {
|
|
25605
|
+
return true;
|
|
25606
|
+
}
|
|
25607
|
+
return false;
|
|
25462
25608
|
}
|
|
25463
25609
|
|
|
25464
25610
|
// -------------------------------------
|
|
@@ -26112,7 +26258,7 @@ class XlsxChartExtractor extends XlsxBaseExtractor {
|
|
|
26112
26258
|
title: { text: chartTitle },
|
|
26113
26259
|
type: CHART_TYPE_CONVERSION_MAP[chartType],
|
|
26114
26260
|
dataSets: this.extractChartDatasets(this.querySelectorAll(rootChartElement, `c:${chartType}`), chartType),
|
|
26115
|
-
labelRange: this.
|
|
26261
|
+
labelRange: this.extractLabelRange(chartType, rootChartElement),
|
|
26116
26262
|
backgroundColor: this.extractChildAttr(rootChartElement, "c:chartSpace > c:spPr a:srgbClr", "val", {
|
|
26117
26263
|
default: "ffffff",
|
|
26118
26264
|
}).asString(),
|
|
@@ -26124,6 +26270,13 @@ class XlsxChartExtractor extends XlsxBaseExtractor {
|
|
|
26124
26270
|
};
|
|
26125
26271
|
})[0];
|
|
26126
26272
|
}
|
|
26273
|
+
extractLabelRange(chartType, rootChartElement) {
|
|
26274
|
+
if (chartType === "scatterChart") {
|
|
26275
|
+
return (this.extractChildTextContent(rootChartElement, `c:ser c:strRef c:f`) ||
|
|
26276
|
+
this.extractChildTextContent(rootChartElement, `c:ser c:numRef c:f`));
|
|
26277
|
+
}
|
|
26278
|
+
return this.extractChildTextContent(rootChartElement, `c:ser c:cat c:f`);
|
|
26279
|
+
}
|
|
26127
26280
|
extractComboChart(chartElement) {
|
|
26128
26281
|
// Title can be separated into multiple xml elements (for styling and such), we only import the text
|
|
26129
26282
|
const chartTitle = this.mapOnElements({ parent: chartElement, query: "c:title a:t" }, (textElement) => {
|
|
@@ -28626,11 +28779,12 @@ function canBeLinearChart(definition, dataSets, labelRange, getters) {
|
|
|
28626
28779
|
}
|
|
28627
28780
|
let missingTimeAdapterAlreadyWarned = false;
|
|
28628
28781
|
function isLuxonTimeAdapterInstalled() {
|
|
28629
|
-
|
|
28782
|
+
const Chart = getChartJSConstructor();
|
|
28783
|
+
if (!Chart) {
|
|
28630
28784
|
return false;
|
|
28631
28785
|
}
|
|
28632
28786
|
// @ts-ignore
|
|
28633
|
-
const adapter = new
|
|
28787
|
+
const adapter = new Chart._adapters._date({});
|
|
28634
28788
|
const isInstalled = adapter._id === "luxon";
|
|
28635
28789
|
if (!isInstalled && !missingTimeAdapterAlreadyWarned) {
|
|
28636
28790
|
missingTimeAdapterAlreadyWarned = true;
|
|
@@ -32327,10 +32481,6 @@ class Popover extends owl.Component {
|
|
|
32327
32481
|
this.currentDisplayValue = newDisplay;
|
|
32328
32482
|
if (!anchor)
|
|
32329
32483
|
return;
|
|
32330
|
-
el.style.top = "";
|
|
32331
|
-
el.style.left = "";
|
|
32332
|
-
el.style["max-height"] = "";
|
|
32333
|
-
el.style["max-width"] = "";
|
|
32334
32484
|
const propsMaxSize = { width: this.props.maxWidth, height: this.props.maxHeight };
|
|
32335
32485
|
let elDims = {
|
|
32336
32486
|
width: el.getBoundingClientRect().width,
|
|
@@ -33868,6 +34018,7 @@ var CHART_HELPERS = /*#__PURE__*/Object.freeze({
|
|
|
33868
34018
|
drawScoreChart: drawScoreChart,
|
|
33869
34019
|
formatChartDatasetValue: formatChartDatasetValue,
|
|
33870
34020
|
formatTickValue: formatTickValue,
|
|
34021
|
+
getChartJSConstructor: getChartJSConstructor,
|
|
33871
34022
|
getChartPositionAtCenterOfViewport: getChartPositionAtCenterOfViewport,
|
|
33872
34023
|
getDefinedAxis: getDefinedAxis,
|
|
33873
34024
|
getPieColors: getPieColors,
|
|
@@ -36127,6 +36278,7 @@ const irregularityMap = {
|
|
|
36127
36278
|
fingerprintStore.enable();
|
|
36128
36279
|
}
|
|
36129
36280
|
},
|
|
36281
|
+
isReadonlyAllowed: true,
|
|
36130
36282
|
icon: "o-spreadsheet-Icon.IRREGULARITY_MAP",
|
|
36131
36283
|
};
|
|
36132
36284
|
const viewFormulas = {
|
|
@@ -37751,6 +37903,9 @@ class GenericChartConfigPanel extends owl.Component {
|
|
|
37751
37903
|
this.state.datasetDispatchResult = this.props.updateChart(this.props.figureId, {
|
|
37752
37904
|
dataSets: this.dataSeriesRanges,
|
|
37753
37905
|
});
|
|
37906
|
+
if (this.state.datasetDispatchResult.isSuccessful) {
|
|
37907
|
+
this.dataSeriesRanges = this.env.model.getters.getChartDefinition(this.props.figureId).dataSets;
|
|
37908
|
+
}
|
|
37754
37909
|
}
|
|
37755
37910
|
getDataSeriesRanges() {
|
|
37756
37911
|
return this.dataSeriesRanges;
|
|
@@ -40390,8 +40545,7 @@ css /* scss */ `
|
|
|
40390
40545
|
}
|
|
40391
40546
|
|
|
40392
40547
|
.o-composer-assistant {
|
|
40393
|
-
|
|
40394
|
-
margin: 1px 4px;
|
|
40548
|
+
margin-top: 1px;
|
|
40395
40549
|
|
|
40396
40550
|
.o-semi-bold {
|
|
40397
40551
|
/* FIXME: to remove in favor of Bootstrap
|
|
@@ -40442,10 +40596,11 @@ class Composer extends owl.Component {
|
|
|
40442
40596
|
});
|
|
40443
40597
|
compositionActive = false;
|
|
40444
40598
|
spreadsheetRect = useSpreadsheetRect();
|
|
40445
|
-
get
|
|
40599
|
+
get assistantStyleProperties() {
|
|
40446
40600
|
const composerRect = this.composerRef.el.getBoundingClientRect();
|
|
40447
40601
|
const assistantStyle = {};
|
|
40448
|
-
|
|
40602
|
+
const minWidth = Math.min(this.props.rect?.width || Infinity, ASSISTANT_WIDTH);
|
|
40603
|
+
assistantStyle["min-width"] = `${minWidth}px`;
|
|
40449
40604
|
const proposals = this.autoCompleteState.provider?.proposals;
|
|
40450
40605
|
const proposalsHaveDescription = proposals?.some((proposal) => proposal.description);
|
|
40451
40606
|
if (this.functionDescriptionState.showDescription || proposalsHaveDescription) {
|
|
@@ -40469,13 +40624,29 @@ class Composer extends owl.Component {
|
|
|
40469
40624
|
}
|
|
40470
40625
|
}
|
|
40471
40626
|
else {
|
|
40472
|
-
assistantStyle["max-height"] = `${this.spreadsheetRect.height - composerRect.bottom}px`;
|
|
40627
|
+
assistantStyle["max-height"] = `${this.spreadsheetRect.height - composerRect.bottom - 1}px`; // -1: margin
|
|
40473
40628
|
if (composerRect.left + ASSISTANT_WIDTH + SCROLLBAR_WIDTH + CLOSE_ICON_RADIUS >
|
|
40474
40629
|
this.spreadsheetRect.width) {
|
|
40475
40630
|
assistantStyle.right = `${CLOSE_ICON_RADIUS}px`;
|
|
40476
40631
|
}
|
|
40477
40632
|
}
|
|
40478
|
-
return
|
|
40633
|
+
return assistantStyle;
|
|
40634
|
+
}
|
|
40635
|
+
get assistantStyle() {
|
|
40636
|
+
const allProperties = this.assistantStyleProperties;
|
|
40637
|
+
return cssPropertiesToCss({
|
|
40638
|
+
"max-height": allProperties["max-height"],
|
|
40639
|
+
width: allProperties["width"],
|
|
40640
|
+
"min-width": allProperties["min-width"],
|
|
40641
|
+
});
|
|
40642
|
+
}
|
|
40643
|
+
get assistantContainerStyle() {
|
|
40644
|
+
const allProperties = this.assistantStyleProperties;
|
|
40645
|
+
return cssPropertiesToCss({
|
|
40646
|
+
top: allProperties["top"],
|
|
40647
|
+
right: allProperties["right"],
|
|
40648
|
+
transform: allProperties["transform"],
|
|
40649
|
+
});
|
|
40479
40650
|
}
|
|
40480
40651
|
// we can't allow input events to be triggered while we remove and add back the content of the composer in processContent
|
|
40481
40652
|
shouldProcessInputEvents = false;
|
|
@@ -46412,9 +46583,7 @@ class PivotSidePanelStore extends SpreadsheetStore {
|
|
|
46412
46583
|
pivot: this.draft,
|
|
46413
46584
|
});
|
|
46414
46585
|
this.draft = null;
|
|
46415
|
-
if (!this.alreadyNotified &&
|
|
46416
|
-
!this.isDynamicPivotInViewport() &&
|
|
46417
|
-
this.isStaticPivotInViewport()) {
|
|
46586
|
+
if (!this.alreadyNotified && this.isUpdatedPivotVisibleInViewportOnlyAsStaticPivot()) {
|
|
46418
46587
|
const formulaId = this.getters.getPivotFormulaId(this.pivotId);
|
|
46419
46588
|
const pivotExample = `=PIVOT(${formulaId})`;
|
|
46420
46589
|
this.alreadyNotified = true;
|
|
@@ -46470,29 +46639,33 @@ class PivotSidePanelStore extends SpreadsheetStore {
|
|
|
46470
46639
|
this.applyUpdate();
|
|
46471
46640
|
}
|
|
46472
46641
|
}
|
|
46473
|
-
|
|
46474
|
-
|
|
46475
|
-
|
|
46476
|
-
|
|
46477
|
-
|
|
46478
|
-
|
|
46479
|
-
|
|
46480
|
-
}
|
|
46481
|
-
}
|
|
46482
|
-
}
|
|
46483
|
-
return false;
|
|
46484
|
-
}
|
|
46485
|
-
isStaticPivotInViewport() {
|
|
46642
|
+
/**
|
|
46643
|
+
* @returns true if the updated pivot is visible in the viewport only as a
|
|
46644
|
+
* static pivot and not as a dynamic pivot
|
|
46645
|
+
*/
|
|
46646
|
+
isUpdatedPivotVisibleInViewportOnlyAsStaticPivot() {
|
|
46647
|
+
let staticPivotCount = 0;
|
|
46648
|
+
const updatedPivotFormulaId = this.getters.getPivotFormulaId(this.pivotId);
|
|
46486
46649
|
for (const position of this.getters.getVisibleCellPositions()) {
|
|
46487
46650
|
const cell = this.getters.getCell(position);
|
|
46488
46651
|
if (cell?.isFormula) {
|
|
46489
46652
|
const pivotFunction = getFirstPivotFunction(cell.compiledFormula.tokens);
|
|
46490
|
-
|
|
46491
|
-
|
|
46653
|
+
const pivotFormulaId = pivotFunction?.args[0]?.value;
|
|
46654
|
+
if (pivotFunction && updatedPivotFormulaId === pivotFormulaId.toString()) {
|
|
46655
|
+
if (pivotFunction.functionName === "PIVOT") {
|
|
46656
|
+
// if we have at least one dynamic pivot visible inserted the viewport
|
|
46657
|
+
// we return false
|
|
46658
|
+
return false;
|
|
46659
|
+
}
|
|
46660
|
+
else {
|
|
46661
|
+
staticPivotCount++;
|
|
46662
|
+
}
|
|
46492
46663
|
}
|
|
46493
46664
|
}
|
|
46494
46665
|
}
|
|
46495
|
-
return
|
|
46666
|
+
// we return true if there are only static pivots visible inserted the viewport,
|
|
46667
|
+
// otherwise false
|
|
46668
|
+
return staticPivotCount > 0;
|
|
46496
46669
|
}
|
|
46497
46670
|
addDefaultDateTimeGranularity(fields, definition) {
|
|
46498
46671
|
const { columns, rows } = definition;
|
|
@@ -50311,6 +50484,9 @@ class ColResizer extends AbstractResizer {
|
|
|
50311
50484
|
this.MAX_SIZE_MARGIN = 90;
|
|
50312
50485
|
this.MIN_ELEMENT_SIZE = MIN_COL_WIDTH;
|
|
50313
50486
|
}
|
|
50487
|
+
get sheetId() {
|
|
50488
|
+
return this.env.model.getters.getActiveSheetId();
|
|
50489
|
+
}
|
|
50314
50490
|
_getEvOffset(ev) {
|
|
50315
50491
|
return ev.offsetX;
|
|
50316
50492
|
}
|
|
@@ -50333,10 +50509,10 @@ class ColResizer extends AbstractResizer {
|
|
|
50333
50509
|
return this.env.model.getters.getEdgeScrollCol(position, position, position);
|
|
50334
50510
|
}
|
|
50335
50511
|
_getDimensionsInViewport(index) {
|
|
50336
|
-
return this.env.model.getters.getColDimensionsInViewport(this.
|
|
50512
|
+
return this.env.model.getters.getColDimensionsInViewport(this.sheetId, index);
|
|
50337
50513
|
}
|
|
50338
50514
|
_getElementSize(index) {
|
|
50339
|
-
return this.env.model.getters.getColSize(this.
|
|
50515
|
+
return this.env.model.getters.getColSize(this.sheetId, index);
|
|
50340
50516
|
}
|
|
50341
50517
|
_getMaxSize() {
|
|
50342
50518
|
return this.colResizerRef.el.clientWidth;
|
|
@@ -50347,7 +50523,7 @@ class ColResizer extends AbstractResizer {
|
|
|
50347
50523
|
const cols = this.env.model.getters.getActiveCols();
|
|
50348
50524
|
this.env.model.dispatch("RESIZE_COLUMNS_ROWS", {
|
|
50349
50525
|
dimension: "COL",
|
|
50350
|
-
sheetId: this.
|
|
50526
|
+
sheetId: this.sheetId,
|
|
50351
50527
|
elements: cols.has(index) ? [...cols] : [index],
|
|
50352
50528
|
size,
|
|
50353
50529
|
});
|
|
@@ -50360,7 +50536,7 @@ class ColResizer extends AbstractResizer {
|
|
|
50360
50536
|
elements.push(colIndex);
|
|
50361
50537
|
}
|
|
50362
50538
|
const result = this.env.model.dispatch("MOVE_COLUMNS_ROWS", {
|
|
50363
|
-
sheetId: this.
|
|
50539
|
+
sheetId: this.sheetId,
|
|
50364
50540
|
dimension: "COL",
|
|
50365
50541
|
base: this.state.base,
|
|
50366
50542
|
elements,
|
|
@@ -50379,7 +50555,7 @@ class ColResizer extends AbstractResizer {
|
|
|
50379
50555
|
_fitElementSize(index) {
|
|
50380
50556
|
const cols = this.env.model.getters.getActiveCols();
|
|
50381
50557
|
this.env.model.dispatch("AUTORESIZE_COLUMNS", {
|
|
50382
|
-
sheetId: this.
|
|
50558
|
+
sheetId: this.sheetId,
|
|
50383
50559
|
cols: cols.has(index) ? [...cols] : [index],
|
|
50384
50560
|
});
|
|
50385
50561
|
}
|
|
@@ -50390,7 +50566,7 @@ class ColResizer extends AbstractResizer {
|
|
|
50390
50566
|
return this.env.model.getters.getActiveCols();
|
|
50391
50567
|
}
|
|
50392
50568
|
_getPreviousVisibleElement(index) {
|
|
50393
|
-
const sheetId = this.
|
|
50569
|
+
const sheetId = this.sheetId;
|
|
50394
50570
|
let row;
|
|
50395
50571
|
for (row = index - 1; row >= 0; row--) {
|
|
50396
50572
|
if (!this.env.model.getters.isColHidden(sheetId, row)) {
|
|
@@ -50401,7 +50577,7 @@ class ColResizer extends AbstractResizer {
|
|
|
50401
50577
|
}
|
|
50402
50578
|
unhide(hiddenElements) {
|
|
50403
50579
|
this.env.model.dispatch("UNHIDE_COLUMNS_ROWS", {
|
|
50404
|
-
sheetId: this.
|
|
50580
|
+
sheetId: this.sheetId,
|
|
50405
50581
|
elements: hiddenElements,
|
|
50406
50582
|
dimension: "COL",
|
|
50407
50583
|
});
|
|
@@ -50417,7 +50593,7 @@ css /* scss */ `
|
|
|
50417
50593
|
left: 0;
|
|
50418
50594
|
right: 0;
|
|
50419
50595
|
width: ${HEADER_WIDTH}px;
|
|
50420
|
-
height: 100
|
|
50596
|
+
height: calc(100% - ${HEADER_HEIGHT + SCROLLBAR_WIDTH}px);
|
|
50421
50597
|
&.o-dragging {
|
|
50422
50598
|
cursor: grabbing;
|
|
50423
50599
|
}
|
|
@@ -50475,6 +50651,9 @@ class RowResizer extends AbstractResizer {
|
|
|
50475
50651
|
this.MIN_ELEMENT_SIZE = MIN_ROW_HEIGHT;
|
|
50476
50652
|
}
|
|
50477
50653
|
rowResizerRef;
|
|
50654
|
+
get sheetId() {
|
|
50655
|
+
return this.env.model.getters.getActiveSheetId();
|
|
50656
|
+
}
|
|
50478
50657
|
_getEvOffset(ev) {
|
|
50479
50658
|
return ev.offsetY;
|
|
50480
50659
|
}
|
|
@@ -50497,10 +50676,10 @@ class RowResizer extends AbstractResizer {
|
|
|
50497
50676
|
return this.env.model.getters.getEdgeScrollRow(position, position, position);
|
|
50498
50677
|
}
|
|
50499
50678
|
_getDimensionsInViewport(index) {
|
|
50500
|
-
return this.env.model.getters.getRowDimensionsInViewport(this.
|
|
50679
|
+
return this.env.model.getters.getRowDimensionsInViewport(this.sheetId, index);
|
|
50501
50680
|
}
|
|
50502
50681
|
_getElementSize(index) {
|
|
50503
|
-
return this.env.model.getters.getRowSize(this.
|
|
50682
|
+
return this.env.model.getters.getRowSize(this.sheetId, index);
|
|
50504
50683
|
}
|
|
50505
50684
|
_getMaxSize() {
|
|
50506
50685
|
return this.rowResizerRef.el.clientHeight;
|
|
@@ -50511,7 +50690,7 @@ class RowResizer extends AbstractResizer {
|
|
|
50511
50690
|
const rows = this.env.model.getters.getActiveRows();
|
|
50512
50691
|
this.env.model.dispatch("RESIZE_COLUMNS_ROWS", {
|
|
50513
50692
|
dimension: "ROW",
|
|
50514
|
-
sheetId: this.
|
|
50693
|
+
sheetId: this.sheetId,
|
|
50515
50694
|
elements: rows.has(index) ? [...rows] : [index],
|
|
50516
50695
|
size,
|
|
50517
50696
|
});
|
|
@@ -50524,7 +50703,7 @@ class RowResizer extends AbstractResizer {
|
|
|
50524
50703
|
elements.push(rowIndex);
|
|
50525
50704
|
}
|
|
50526
50705
|
const result = this.env.model.dispatch("MOVE_COLUMNS_ROWS", {
|
|
50527
|
-
sheetId: this.
|
|
50706
|
+
sheetId: this.sheetId,
|
|
50528
50707
|
dimension: "ROW",
|
|
50529
50708
|
base: this.state.base,
|
|
50530
50709
|
elements,
|
|
@@ -50543,7 +50722,7 @@ class RowResizer extends AbstractResizer {
|
|
|
50543
50722
|
_fitElementSize(index) {
|
|
50544
50723
|
const rows = this.env.model.getters.getActiveRows();
|
|
50545
50724
|
this.env.model.dispatch("AUTORESIZE_ROWS", {
|
|
50546
|
-
sheetId: this.
|
|
50725
|
+
sheetId: this.sheetId,
|
|
50547
50726
|
rows: rows.has(index) ? [...rows] : [index],
|
|
50548
50727
|
});
|
|
50549
50728
|
}
|
|
@@ -50554,7 +50733,7 @@ class RowResizer extends AbstractResizer {
|
|
|
50554
50733
|
return this.env.model.getters.getActiveRows();
|
|
50555
50734
|
}
|
|
50556
50735
|
_getPreviousVisibleElement(index) {
|
|
50557
|
-
const sheetId = this.
|
|
50736
|
+
const sheetId = this.sheetId;
|
|
50558
50737
|
let row;
|
|
50559
50738
|
for (row = index - 1; row >= 0; row--) {
|
|
50560
50739
|
if (!this.env.model.getters.isRowHidden(sheetId, row)) {
|
|
@@ -50565,7 +50744,7 @@ class RowResizer extends AbstractResizer {
|
|
|
50565
50744
|
}
|
|
50566
50745
|
unhide(hiddenElements) {
|
|
50567
50746
|
this.env.model.dispatch("UNHIDE_COLUMNS_ROWS", {
|
|
50568
|
-
sheetId: this.
|
|
50747
|
+
sheetId: this.sheetId,
|
|
50569
50748
|
dimension: "ROW",
|
|
50570
50749
|
elements: hiddenElements,
|
|
50571
50750
|
});
|
|
@@ -60332,6 +60511,7 @@ class EvaluationPlugin extends UIPlugin {
|
|
|
60332
60511
|
exportForExcel(data) {
|
|
60333
60512
|
for (const sheet of data.sheets) {
|
|
60334
60513
|
sheet.cellValues = {};
|
|
60514
|
+
sheet.formulaSpillRanges = {};
|
|
60335
60515
|
}
|
|
60336
60516
|
for (const position of this.evaluator.getEvaluatedPositions()) {
|
|
60337
60517
|
const evaluatedCell = this.evaluator.getEvaluatedCell(position);
|
|
@@ -60343,8 +60523,9 @@ class EvaluationPlugin extends UIPlugin {
|
|
|
60343
60523
|
const exportedSheetData = data.sheets.find((sheet) => sheet.id === position.sheetId);
|
|
60344
60524
|
const formulaCell = this.getCorrespondingFormulaCell(position);
|
|
60345
60525
|
if (formulaCell) {
|
|
60526
|
+
const cell = this.getters.getCell(position);
|
|
60346
60527
|
isExported = isExportableToExcel(formulaCell.compiledFormula.tokens);
|
|
60347
|
-
isFormula = isExported;
|
|
60528
|
+
isFormula = isExported && cell?.content === formulaCell.content;
|
|
60348
60529
|
// If the cell contains a non-exported formula and that is evaluates to
|
|
60349
60530
|
// nothing* ,we don't export it.
|
|
60350
60531
|
// * non-falsy value are relevant and so are 0 and FALSE, which only leaves
|
|
@@ -60367,7 +60548,11 @@ class EvaluationPlugin extends UIPlugin {
|
|
|
60367
60548
|
content = !isExported ? newContent : exportedCellData;
|
|
60368
60549
|
}
|
|
60369
60550
|
exportedSheetData.cells[xc] = content;
|
|
60370
|
-
exportedSheetData.cellValues[xc] = value;
|
|
60551
|
+
exportedSheetData.cellValues[xc] = evaluatedCell.type !== "error" ? value : undefined;
|
|
60552
|
+
const spillZone = this.getSpreadZone(position);
|
|
60553
|
+
if (spillZone) {
|
|
60554
|
+
exportedSheetData.formulaSpillRanges[xc] = this.getters.getRangeString(this.getters.getRangeFromZone(position.sheetId, spillZone), position.sheetId);
|
|
60555
|
+
}
|
|
60371
60556
|
}
|
|
60372
60557
|
}
|
|
60373
60558
|
/**
|
|
@@ -62574,7 +62759,7 @@ class AutofillPlugin extends UIPlugin {
|
|
|
62574
62759
|
getRule(cell, cells) {
|
|
62575
62760
|
const rules = autofillRulesRegistry.getAll().sort((a, b) => a.sequence - b.sequence);
|
|
62576
62761
|
const rule = rules.find((rule) => rule.condition(cell, cells));
|
|
62577
|
-
return rule && rule.generateRule(cell, cells);
|
|
62762
|
+
return rule && this.direction && rule.generateRule(cell, cells, this.direction);
|
|
62578
62763
|
}
|
|
62579
62764
|
/**
|
|
62580
62765
|
* Create the generator to be able to autofill the next cells.
|
|
@@ -67798,7 +67983,8 @@ class SheetViewPlugin extends UIPlugin {
|
|
|
67798
67983
|
? this.getters.getSheetViewVisibleCols()
|
|
67799
67984
|
: this.getters.getSheetViewVisibleRows();
|
|
67800
67985
|
const startIndex = visibleHeaders.findIndex((header) => referenceHeaderIndex >= header);
|
|
67801
|
-
|
|
67986
|
+
let endIndex = visibleHeaders.findIndex((header) => targetHeaderIndex <= header);
|
|
67987
|
+
endIndex = endIndex === -1 ? visibleHeaders.length : endIndex;
|
|
67802
67988
|
const relevantIndexes = visibleHeaders.slice(startIndex, endIndex);
|
|
67803
67989
|
let offset = 0;
|
|
67804
67990
|
for (const i of relevantIndexes) {
|
|
@@ -73104,7 +73290,7 @@ function numberRef(reference) {
|
|
|
73104
73290
|
`;
|
|
73105
73291
|
}
|
|
73106
73292
|
|
|
73107
|
-
function addFormula(formula, value) {
|
|
73293
|
+
function addFormula(formula, value, formulaSpillRange) {
|
|
73108
73294
|
if (!formula) {
|
|
73109
73295
|
return { attrs: [], node: escapeXml `` };
|
|
73110
73296
|
}
|
|
@@ -73112,10 +73298,17 @@ function addFormula(formula, value) {
|
|
|
73112
73298
|
if (type === undefined) {
|
|
73113
73299
|
return { attrs: [], node: escapeXml `` };
|
|
73114
73300
|
}
|
|
73115
|
-
const attrs = [
|
|
73301
|
+
const attrs = [
|
|
73302
|
+
["cm", "1"],
|
|
73303
|
+
["t", type],
|
|
73304
|
+
];
|
|
73116
73305
|
const XlsxFormula = adaptFormulaToExcel(formula);
|
|
73117
73306
|
const exportedValue = adaptFormulaValueToExcel(value);
|
|
73118
|
-
|
|
73307
|
+
// We treat all formulas as array formulas (a simple formula
|
|
73308
|
+
// is an array formula that spills on only one cell) to avoid
|
|
73309
|
+
// trying to detect spilling sub-formulas which is not a trivial task.
|
|
73310
|
+
let node;
|
|
73311
|
+
node = escapeXml /*xml*/ `<f t="array" ref="${formulaSpillRange}">${XlsxFormula}</f><v>${exportedValue}</v>`;
|
|
73119
73312
|
return { attrs, node };
|
|
73120
73313
|
}
|
|
73121
73314
|
function addContent(content, sharedStrings, forceString = false) {
|
|
@@ -73898,7 +74091,7 @@ function addStyles(styles) {
|
|
|
73898
74091
|
}
|
|
73899
74092
|
if (alignAttrs.length > 0) {
|
|
73900
74093
|
attributes.push(["applyAlignment", "1"]); // for Libre Office
|
|
73901
|
-
styleNodes.push(escapeXml /*xml*/ `<xf ${formatAttributes(attributes)}
|
|
74094
|
+
styleNodes.push(escapeXml /*xml*/ `<xf ${formatAttributes(attributes)}><alignment ${formatAttributes(alignAttrs)} /></xf> `);
|
|
73902
74095
|
}
|
|
73903
74096
|
else {
|
|
73904
74097
|
styleNodes.push(escapeXml /*xml*/ `<xf ${formatAttributes(attributes)} />`);
|
|
@@ -74066,6 +74259,9 @@ function addColumns(cols) {
|
|
|
74066
74259
|
}
|
|
74067
74260
|
function addRows(construct, data, sheet) {
|
|
74068
74261
|
const rowNodes = [];
|
|
74262
|
+
const styles = new PositionMap(iterateItemIdsPositions(sheet.id, sheet.styles));
|
|
74263
|
+
const borders = new PositionMap(iterateItemIdsPositions(sheet.id, sheet.borders));
|
|
74264
|
+
const formats = new PositionMap(iterateItemIdsPositions(sheet.id, sheet.formats));
|
|
74069
74265
|
for (let r = 0; r < sheet.rowNumber; r++) {
|
|
74070
74266
|
const rowAttrs = [["r", r + 1]];
|
|
74071
74267
|
const row = sheet.rows[r] || {};
|
|
@@ -74081,9 +74277,6 @@ function addRows(construct, data, sheet) {
|
|
|
74081
74277
|
if (row.collapsed) {
|
|
74082
74278
|
rowAttrs.push(["collapsed", 1]);
|
|
74083
74279
|
}
|
|
74084
|
-
const styles = new PositionMap(iterateItemIdsPositions(sheet.id, sheet.styles));
|
|
74085
|
-
const borders = new PositionMap(iterateItemIdsPositions(sheet.id, sheet.borders));
|
|
74086
|
-
const formats = new PositionMap(iterateItemIdsPositions(sheet.id, sheet.formats));
|
|
74087
74280
|
const cellNodes = [];
|
|
74088
74281
|
for (let c = 0; c < sheet.colNumber; c++) {
|
|
74089
74282
|
const xc = toXC(c, r);
|
|
@@ -74105,7 +74298,7 @@ function addRows(construct, data, sheet) {
|
|
|
74105
74298
|
let cellNode = escapeXml ``;
|
|
74106
74299
|
// Either formula or static value inside the cell
|
|
74107
74300
|
if (content?.startsWith("=") && value !== undefined) {
|
|
74108
|
-
const res = addFormula(content, value);
|
|
74301
|
+
const res = addFormula(content, value, sheet.formulaSpillRanges[xc] ?? xc);
|
|
74109
74302
|
if (!res) {
|
|
74110
74303
|
continue;
|
|
74111
74304
|
}
|
|
@@ -74391,6 +74584,30 @@ function createWorksheets(data, construct) {
|
|
|
74391
74584
|
`;
|
|
74392
74585
|
files.push(createXMLFile(parseXML(sheetXml), `xl/worksheets/sheet${sheetIndex}.xml`, "sheet"));
|
|
74393
74586
|
}
|
|
74587
|
+
const sheetMetadataXml = escapeXml /*xml*/ `
|
|
74588
|
+
<metadata xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:xda="http://schemas.microsoft.com/office/spreadsheetml/2017/dynamicarray">
|
|
74589
|
+
<metadataTypes count="1">
|
|
74590
|
+
<metadataType name="XLDAPR" minSupportedVersion="120000" copy="1" pasteAll="1"
|
|
74591
|
+
pasteValues="1" merge="1" splitFirst="1" rowColShift="1" clearFormats="1"
|
|
74592
|
+
clearComments="1" assign="1" coerce="1" cellMeta="1" />
|
|
74593
|
+
</metadataTypes>
|
|
74594
|
+
<futureMetadata name="XLDAPR" count="1">
|
|
74595
|
+
<bk>
|
|
74596
|
+
<extLst>
|
|
74597
|
+
<ext uri="{${ARRAY_FORMULA_URI}}">
|
|
74598
|
+
<xda:dynamicArrayProperties fDynamic="1" fCollapsed="0" />
|
|
74599
|
+
</ext>
|
|
74600
|
+
</extLst>
|
|
74601
|
+
</bk>
|
|
74602
|
+
</futureMetadata>
|
|
74603
|
+
<cellMetadata count="1">
|
|
74604
|
+
<bk>
|
|
74605
|
+
<rc t="1" v="0" />
|
|
74606
|
+
</bk>
|
|
74607
|
+
</cellMetadata>
|
|
74608
|
+
</metadata>
|
|
74609
|
+
`;
|
|
74610
|
+
files.push(createXMLFile(parseXML(sheetMetadataXml), "xl/metadata.xml", "metadata"));
|
|
74394
74611
|
addRelsToFile(construct.relsFiles, "xl/_rels/workbook.xml.rels", {
|
|
74395
74612
|
type: XLSX_RELATION_TYPE.sharedStrings,
|
|
74396
74613
|
target: "sharedStrings.xml",
|
|
@@ -74399,6 +74616,10 @@ function createWorksheets(data, construct) {
|
|
|
74399
74616
|
type: XLSX_RELATION_TYPE.styles,
|
|
74400
74617
|
target: "styles.xml",
|
|
74401
74618
|
});
|
|
74619
|
+
addRelsToFile(construct.relsFiles, "xl/_rels/workbook.xml.rels", {
|
|
74620
|
+
type: XLSX_RELATION_TYPE.metadata,
|
|
74621
|
+
target: "metadata.xml",
|
|
74622
|
+
});
|
|
74402
74623
|
return files;
|
|
74403
74624
|
}
|
|
74404
74625
|
/**
|
|
@@ -75327,6 +75548,6 @@ exports.tokenColors = tokenColors;
|
|
|
75327
75548
|
exports.tokenize = tokenize;
|
|
75328
75549
|
|
|
75329
75550
|
|
|
75330
|
-
__info__.version = "18.1.
|
|
75331
|
-
__info__.date = "2025-
|
|
75332
|
-
__info__.hash = "
|
|
75551
|
+
__info__.version = "18.1.11";
|
|
75552
|
+
__info__.date = "2025-03-12T15:31:44.276Z";
|
|
75553
|
+
__info__.hash = "7de2363";
|