@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
|
(function (exports, owl) {
|
|
@@ -423,7 +423,6 @@
|
|
|
423
423
|
* Sparse arrays remain sparse.
|
|
424
424
|
*/
|
|
425
425
|
function deepCopy(obj) {
|
|
426
|
-
const result = Array.isArray(obj) ? [] : {};
|
|
427
426
|
switch (typeof obj) {
|
|
428
427
|
case "object": {
|
|
429
428
|
if (obj === null) {
|
|
@@ -435,8 +434,18 @@
|
|
|
435
434
|
else if (!(isPlainObject(obj) || obj instanceof Array)) {
|
|
436
435
|
throw new Error("Unsupported type: only objects and arrays are supported");
|
|
437
436
|
}
|
|
438
|
-
|
|
439
|
-
|
|
437
|
+
const result = Array.isArray(obj) ? new Array(obj.length) : {};
|
|
438
|
+
if (Array.isArray(obj)) {
|
|
439
|
+
for (let i = 0, len = obj.length; i < len; i++) {
|
|
440
|
+
if (i in obj) {
|
|
441
|
+
result[i] = deepCopy(obj[i]);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
else {
|
|
446
|
+
for (const key in obj) {
|
|
447
|
+
result[key] = deepCopy(obj[key]);
|
|
448
|
+
}
|
|
440
449
|
}
|
|
441
450
|
return result;
|
|
442
451
|
}
|
|
@@ -2689,21 +2698,30 @@
|
|
|
2689
2698
|
return mergedZones;
|
|
2690
2699
|
}
|
|
2691
2700
|
|
|
2701
|
+
const globalReverseLookup$1 = new WeakMap();
|
|
2702
|
+
const globalIdCounter = new WeakMap();
|
|
2692
2703
|
/**
|
|
2693
2704
|
* Get the id of the given item (its key in the given dictionary).
|
|
2694
2705
|
* If the given item does not exist in the dictionary, it creates one with a new id.
|
|
2695
2706
|
*/
|
|
2696
2707
|
function getItemId(item, itemsDic) {
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2708
|
+
if (!globalReverseLookup$1.has(itemsDic)) {
|
|
2709
|
+
globalReverseLookup$1.set(itemsDic, new Map());
|
|
2710
|
+
globalIdCounter.set(itemsDic, 0);
|
|
2711
|
+
}
|
|
2712
|
+
const reverseLookup = globalReverseLookup$1.get(itemsDic);
|
|
2713
|
+
const canonical = getCanonicalRepresentation(item);
|
|
2714
|
+
if (reverseLookup.has(canonical)) {
|
|
2715
|
+
const id = reverseLookup.get(canonical);
|
|
2716
|
+
itemsDic[id] = item;
|
|
2717
|
+
return id;
|
|
2701
2718
|
}
|
|
2702
2719
|
// Generate new Id if the item didn't exist in the dictionary
|
|
2703
|
-
const
|
|
2704
|
-
|
|
2705
|
-
itemsDic
|
|
2706
|
-
|
|
2720
|
+
const newId = globalIdCounter.get(itemsDic) + 1;
|
|
2721
|
+
reverseLookup.set(canonical, newId);
|
|
2722
|
+
globalIdCounter.set(itemsDic, newId);
|
|
2723
|
+
itemsDic[newId] = item;
|
|
2724
|
+
return newId;
|
|
2707
2725
|
}
|
|
2708
2726
|
function groupItemIdsByZones(positionsByItemId) {
|
|
2709
2727
|
const result = {};
|
|
@@ -2727,6 +2745,33 @@
|
|
|
2727
2745
|
}
|
|
2728
2746
|
}
|
|
2729
2747
|
}
|
|
2748
|
+
function getCanonicalRepresentation(item) {
|
|
2749
|
+
if (item === null)
|
|
2750
|
+
return "null";
|
|
2751
|
+
if (item === undefined)
|
|
2752
|
+
return "undefined";
|
|
2753
|
+
if (typeof item !== "object")
|
|
2754
|
+
return String(item);
|
|
2755
|
+
if (Array.isArray(item)) {
|
|
2756
|
+
const len = item.length;
|
|
2757
|
+
let result = "[";
|
|
2758
|
+
for (let i = 0; i < len; i++) {
|
|
2759
|
+
if (i > 0)
|
|
2760
|
+
result += ",";
|
|
2761
|
+
result += getCanonicalRepresentation(item[i]);
|
|
2762
|
+
}
|
|
2763
|
+
return result + "]";
|
|
2764
|
+
}
|
|
2765
|
+
const keys = Object.keys(item).sort();
|
|
2766
|
+
let repr = "{";
|
|
2767
|
+
for (const key of keys) {
|
|
2768
|
+
if (item[key] !== undefined) {
|
|
2769
|
+
repr += `"${key}":${getCanonicalRepresentation(item[key])},`;
|
|
2770
|
+
}
|
|
2771
|
+
}
|
|
2772
|
+
repr += "}";
|
|
2773
|
+
return repr;
|
|
2774
|
+
}
|
|
2730
2775
|
|
|
2731
2776
|
// -----------------------------------------------------------------------------
|
|
2732
2777
|
// Date Type
|
|
@@ -6092,8 +6137,9 @@
|
|
|
6092
6137
|
if (zone.bottom !== zone.top && zone.left != zone.right) {
|
|
6093
6138
|
if (zone.right) {
|
|
6094
6139
|
for (let j = zone.left; j <= zone.right; ++j) {
|
|
6140
|
+
const datasetOptions = j === zone.left ? dataSet : { yAxisId: dataSet.yAxisId };
|
|
6095
6141
|
postProcessedRanges.push({
|
|
6096
|
-
...
|
|
6142
|
+
...datasetOptions,
|
|
6097
6143
|
dataRange: `${sheetPrefix}${zoneToXc({
|
|
6098
6144
|
left: j,
|
|
6099
6145
|
right: j,
|
|
@@ -6105,8 +6151,9 @@
|
|
|
6105
6151
|
}
|
|
6106
6152
|
else {
|
|
6107
6153
|
for (let j = zone.top; j <= zone.bottom; ++j) {
|
|
6154
|
+
const datasetOptions = j === zone.top ? dataSet : { yAxisId: dataSet.yAxisId };
|
|
6108
6155
|
postProcessedRanges.push({
|
|
6109
|
-
...
|
|
6156
|
+
...datasetOptions,
|
|
6110
6157
|
dataRange: `${sheetPrefix}${zoneToXc({
|
|
6111
6158
|
left: zone.left,
|
|
6112
6159
|
right: zone.right,
|
|
@@ -8278,7 +8325,8 @@
|
|
|
8278
8325
|
const possibleValues = pivot
|
|
8279
8326
|
.getPossibleFieldValues(columns[i])
|
|
8280
8327
|
.map((v) => v.value);
|
|
8281
|
-
if (!possibleValues.includes(sortedColumn.domain[i].value)
|
|
8328
|
+
if (!possibleValues.includes(sortedColumn.domain[i].value) &&
|
|
8329
|
+
!(sortedColumn.domain[i].value === null && possibleValues.includes(""))) {
|
|
8282
8330
|
return false;
|
|
8283
8331
|
}
|
|
8284
8332
|
}
|
|
@@ -10057,70 +10105,341 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
10057
10105
|
return bars.find((bar, i) => i > startIndex && bar.height !== 0);
|
|
10058
10106
|
}
|
|
10059
10107
|
|
|
10060
|
-
|
|
10061
|
-
|
|
10062
|
-
|
|
10063
|
-
|
|
10064
|
-
|
|
10065
|
-
|
|
10108
|
+
const GAUGE_PADDING_SIDE = 30;
|
|
10109
|
+
const GAUGE_PADDING_TOP = 10;
|
|
10110
|
+
const GAUGE_PADDING_BOTTOM = 20;
|
|
10111
|
+
const GAUGE_LABELS_FONT_SIZE = 12;
|
|
10112
|
+
const GAUGE_DEFAULT_VALUE_FONT_SIZE = 80;
|
|
10113
|
+
const GAUGE_BACKGROUND_COLOR = "#F3F2F1";
|
|
10114
|
+
const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
|
|
10115
|
+
const GAUGE_TITLE_SECTION_HEIGHT = 25;
|
|
10116
|
+
function drawGaugeChart(canvas, runtime) {
|
|
10117
|
+
const canvasBoundingRect = canvas.getBoundingClientRect();
|
|
10118
|
+
canvas.width = canvasBoundingRect.width;
|
|
10119
|
+
canvas.height = canvasBoundingRect.height;
|
|
10120
|
+
const ctx = canvas.getContext("2d");
|
|
10121
|
+
const config = getGaugeRenderingConfig(canvasBoundingRect, runtime, ctx);
|
|
10122
|
+
drawBackground(ctx, config);
|
|
10123
|
+
drawGauge(ctx, config);
|
|
10124
|
+
drawInflectionValues(ctx, config);
|
|
10125
|
+
drawLabels(ctx, config);
|
|
10126
|
+
drawTitle(ctx, config);
|
|
10127
|
+
}
|
|
10128
|
+
function drawGauge(ctx, config) {
|
|
10129
|
+
ctx.save();
|
|
10130
|
+
const gauge = config.gauge;
|
|
10131
|
+
const arcCenterX = gauge.rect.x + gauge.rect.width / 2;
|
|
10132
|
+
const arcCenterY = gauge.rect.y + gauge.rect.height;
|
|
10133
|
+
const arcRadius = gauge.rect.height - gauge.arcWidth / 2;
|
|
10134
|
+
if (arcRadius < 0) {
|
|
10135
|
+
return;
|
|
10136
|
+
}
|
|
10137
|
+
const gaugeAngle = gauge.percentage === 1 ? 0 : Math.PI * (1 + gauge.percentage);
|
|
10138
|
+
// Gauge background
|
|
10139
|
+
ctx.strokeStyle = GAUGE_BACKGROUND_COLOR;
|
|
10140
|
+
ctx.beginPath();
|
|
10141
|
+
ctx.lineWidth = gauge.arcWidth;
|
|
10142
|
+
ctx.arc(arcCenterX, arcCenterY, arcRadius, gaugeAngle, 0);
|
|
10143
|
+
ctx.stroke();
|
|
10144
|
+
// Gauge value
|
|
10145
|
+
ctx.strokeStyle = gauge.color;
|
|
10146
|
+
ctx.beginPath();
|
|
10147
|
+
ctx.arc(arcCenterX, arcCenterY, arcRadius, Math.PI, gaugeAngle);
|
|
10148
|
+
ctx.stroke();
|
|
10149
|
+
ctx.restore();
|
|
10150
|
+
}
|
|
10151
|
+
function drawBackground(ctx, config) {
|
|
10152
|
+
ctx.save();
|
|
10153
|
+
ctx.fillStyle = config.backgroundColor;
|
|
10154
|
+
ctx.fillRect(0, 0, config.width, config.height);
|
|
10155
|
+
ctx.restore();
|
|
10156
|
+
}
|
|
10157
|
+
function drawLabels(ctx, config) {
|
|
10158
|
+
for (const label of [config.minLabel, config.maxLabel, config.gaugeValue]) {
|
|
10159
|
+
ctx.save();
|
|
10160
|
+
ctx.textAlign = "center";
|
|
10161
|
+
ctx.fillStyle = label.color;
|
|
10162
|
+
ctx.font = `${label.fontSize}px ${DEFAULT_FONT}`;
|
|
10163
|
+
ctx.fillText(label.label, label.textPosition.x, label.textPosition.y);
|
|
10164
|
+
ctx.restore();
|
|
10165
|
+
}
|
|
10166
|
+
}
|
|
10167
|
+
function drawInflectionValues(ctx, config) {
|
|
10168
|
+
const { x: rectX, y: rectY, width, height } = config.gauge.rect;
|
|
10169
|
+
for (const inflectionValue of config.inflectionValues) {
|
|
10170
|
+
ctx.save();
|
|
10171
|
+
ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment
|
|
10172
|
+
ctx.rotate(Math.PI / 2 - inflectionValue.rotation);
|
|
10173
|
+
ctx.lineWidth = 2;
|
|
10174
|
+
ctx.strokeStyle = chartMutedFontColor(config.backgroundColor) + "aa";
|
|
10175
|
+
ctx.beginPath();
|
|
10176
|
+
ctx.moveTo(0, -(height - config.gauge.arcWidth));
|
|
10177
|
+
ctx.lineTo(0, -height - 3);
|
|
10178
|
+
ctx.stroke();
|
|
10179
|
+
ctx.textAlign = "center";
|
|
10180
|
+
ctx.font = `${inflectionValue.fontSize}px ${DEFAULT_FONT}`;
|
|
10181
|
+
ctx.fillStyle = inflectionValue.color;
|
|
10182
|
+
const textY = -height - GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN - inflectionValue.offset;
|
|
10183
|
+
ctx.fillText(inflectionValue.label, 0, textY);
|
|
10184
|
+
ctx.restore();
|
|
10185
|
+
}
|
|
10186
|
+
}
|
|
10187
|
+
function drawTitle(ctx, config) {
|
|
10188
|
+
ctx.save();
|
|
10189
|
+
const title = config.title;
|
|
10190
|
+
ctx.font = getDefaultContextFont(title.fontSize, title.bold, title.italic);
|
|
10191
|
+
ctx.textBaseline = "middle";
|
|
10192
|
+
ctx.fillStyle = title.color;
|
|
10193
|
+
ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
|
|
10194
|
+
ctx.restore();
|
|
10195
|
+
}
|
|
10196
|
+
function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
|
|
10197
|
+
const maxValue = runtime.maxValue;
|
|
10198
|
+
const minValue = runtime.minValue;
|
|
10199
|
+
const gaugeValue = runtime.gaugeValue;
|
|
10200
|
+
const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
|
|
10201
|
+
const gaugeArcWidth = gaugeRect.width / 6;
|
|
10202
|
+
const gaugePercentage = gaugeValue
|
|
10203
|
+
? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
|
|
10204
|
+
: 0;
|
|
10205
|
+
const gaugeValuePosition = {
|
|
10206
|
+
x: boundingRect.width / 2,
|
|
10207
|
+
y: gaugeRect.y + gaugeRect.height - gaugeRect.height / 12,
|
|
10066
10208
|
};
|
|
10067
|
-
|
|
10068
|
-
|
|
10069
|
-
|
|
10070
|
-
|
|
10071
|
-
return this.chartRuntime.background;
|
|
10209
|
+
let gaugeValueFontSize = GAUGE_DEFAULT_VALUE_FONT_SIZE;
|
|
10210
|
+
// Scale down the font size if the gaugeRect is too small
|
|
10211
|
+
if (gaugeRect.height < 300) {
|
|
10212
|
+
gaugeValueFontSize = gaugeValueFontSize * (gaugeRect.height / 300);
|
|
10072
10213
|
}
|
|
10073
|
-
|
|
10074
|
-
|
|
10214
|
+
// Scale down the font size if the text is too long
|
|
10215
|
+
const maxTextWidth = gaugeRect.width / 2;
|
|
10216
|
+
const gaugeLabel = gaugeValue?.label || "-";
|
|
10217
|
+
if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
|
|
10218
|
+
gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
|
|
10075
10219
|
}
|
|
10076
|
-
|
|
10077
|
-
|
|
10078
|
-
|
|
10079
|
-
|
|
10080
|
-
|
|
10081
|
-
|
|
10220
|
+
const minLabelPosition = {
|
|
10221
|
+
x: gaugeRect.x + gaugeArcWidth / 2,
|
|
10222
|
+
y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
|
|
10223
|
+
};
|
|
10224
|
+
const maxLabelPosition = {
|
|
10225
|
+
x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
|
|
10226
|
+
y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
|
|
10227
|
+
};
|
|
10228
|
+
const textColor = chartMutedFontColor(runtime.background);
|
|
10229
|
+
const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);
|
|
10230
|
+
let x = 0, titleWidth = 0, titleHeight = 0;
|
|
10231
|
+
if (runtime.title.text) {
|
|
10232
|
+
({ width: titleWidth, height: titleHeight } = computeTextDimension(ctx, runtime.title.text, { fontSize: CHART_TITLE_FONT_SIZE, ...runtime.title }, "px"));
|
|
10082
10233
|
}
|
|
10083
|
-
|
|
10084
|
-
|
|
10085
|
-
|
|
10086
|
-
|
|
10087
|
-
|
|
10088
|
-
|
|
10089
|
-
|
|
10090
|
-
|
|
10091
|
-
|
|
10092
|
-
|
|
10093
|
-
|
|
10094
|
-
|
|
10095
|
-
|
|
10096
|
-
|
|
10097
|
-
|
|
10098
|
-
|
|
10099
|
-
|
|
10100
|
-
|
|
10101
|
-
|
|
10102
|
-
|
|
10234
|
+
switch (runtime.title.align) {
|
|
10235
|
+
case "right":
|
|
10236
|
+
x = boundingRect.width - titleWidth - CHART_PADDING$1;
|
|
10237
|
+
break;
|
|
10238
|
+
case "center":
|
|
10239
|
+
x = (boundingRect.width - titleWidth) / 2;
|
|
10240
|
+
break;
|
|
10241
|
+
case "left":
|
|
10242
|
+
default:
|
|
10243
|
+
x = CHART_PADDING$1;
|
|
10244
|
+
break;
|
|
10245
|
+
}
|
|
10246
|
+
return {
|
|
10247
|
+
width: boundingRect.width,
|
|
10248
|
+
height: boundingRect.height,
|
|
10249
|
+
title: {
|
|
10250
|
+
label: runtime.title.text ?? "",
|
|
10251
|
+
fontSize: runtime.title.fontSize ?? CHART_TITLE_FONT_SIZE,
|
|
10252
|
+
textPosition: {
|
|
10253
|
+
x,
|
|
10254
|
+
y: CHART_PADDING_TOP + titleHeight / 2,
|
|
10255
|
+
},
|
|
10256
|
+
color: runtime.title.color ?? textColor,
|
|
10257
|
+
bold: runtime.title.bold,
|
|
10258
|
+
italic: runtime.title.italic,
|
|
10259
|
+
},
|
|
10260
|
+
backgroundColor: runtime.background,
|
|
10261
|
+
gauge: {
|
|
10262
|
+
rect: gaugeRect,
|
|
10263
|
+
arcWidth: gaugeArcWidth,
|
|
10264
|
+
percentage: clip(gaugePercentage, 0, 1),
|
|
10265
|
+
color: getGaugeColor(runtime),
|
|
10266
|
+
},
|
|
10267
|
+
inflectionValues,
|
|
10268
|
+
gaugeValue: {
|
|
10269
|
+
label: gaugeLabel,
|
|
10270
|
+
textPosition: gaugeValuePosition,
|
|
10271
|
+
fontSize: gaugeValueFontSize,
|
|
10272
|
+
color: textColor,
|
|
10273
|
+
},
|
|
10274
|
+
minLabel: {
|
|
10275
|
+
label: runtime.minValue.label,
|
|
10276
|
+
textPosition: minLabelPosition,
|
|
10277
|
+
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
10278
|
+
color: textColor,
|
|
10279
|
+
},
|
|
10280
|
+
maxLabel: {
|
|
10281
|
+
label: runtime.maxValue.label,
|
|
10282
|
+
textPosition: maxLabelPosition,
|
|
10283
|
+
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
10284
|
+
color: textColor,
|
|
10285
|
+
},
|
|
10286
|
+
};
|
|
10287
|
+
}
|
|
10288
|
+
/**
|
|
10289
|
+
* Get the rectangle in which the gauge will be drawn, based on the bounding rectangle of the canvas and leaving
|
|
10290
|
+
* space for the title and labels.
|
|
10291
|
+
*/
|
|
10292
|
+
function getGaugeRect(boundingRect, title) {
|
|
10293
|
+
const titleHeight = title ? GAUGE_TITLE_SECTION_HEIGHT : 0;
|
|
10294
|
+
const drawHeight = boundingRect.height - GAUGE_PADDING_BOTTOM - titleHeight - GAUGE_PADDING_TOP;
|
|
10295
|
+
const drawWidth = boundingRect.width - GAUGE_PADDING_SIDE * 2;
|
|
10296
|
+
let gaugeWidth;
|
|
10297
|
+
let gaugeHeight;
|
|
10298
|
+
if (drawWidth > 2 * drawHeight) {
|
|
10299
|
+
gaugeWidth = 2 * drawHeight;
|
|
10300
|
+
gaugeHeight = drawHeight;
|
|
10301
|
+
}
|
|
10302
|
+
else {
|
|
10303
|
+
gaugeWidth = drawWidth;
|
|
10304
|
+
gaugeHeight = drawWidth / 2;
|
|
10305
|
+
}
|
|
10306
|
+
const gaugeX = GAUGE_PADDING_SIDE + (drawWidth - gaugeWidth) / 2;
|
|
10307
|
+
const gaugeY = titleHeight + GAUGE_PADDING_TOP + (drawHeight - gaugeHeight) / 2;
|
|
10308
|
+
return {
|
|
10309
|
+
x: gaugeX,
|
|
10310
|
+
y: gaugeY,
|
|
10311
|
+
width: gaugeWidth,
|
|
10312
|
+
height: gaugeHeight,
|
|
10313
|
+
};
|
|
10314
|
+
}
|
|
10315
|
+
/**
|
|
10316
|
+
* 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).
|
|
10317
|
+
*
|
|
10318
|
+
* Also compute an offset for the text so that it doesn't overlap with other text.
|
|
10319
|
+
*/
|
|
10320
|
+
function getInflectionValues(runtime, gaugeRect, textColor, ctx) {
|
|
10321
|
+
const maxValue = runtime.maxValue;
|
|
10322
|
+
const minValue = runtime.minValue;
|
|
10323
|
+
const gaugeCircleCenter = {
|
|
10324
|
+
x: gaugeRect.x + gaugeRect.width / 2,
|
|
10325
|
+
y: gaugeRect.y + gaugeRect.height,
|
|
10326
|
+
};
|
|
10327
|
+
const textStyle = { fontSize: GAUGE_LABELS_FONT_SIZE };
|
|
10328
|
+
const inflectionValues = [];
|
|
10329
|
+
const inflectionValuesTextRects = [];
|
|
10330
|
+
for (const inflectionValue of runtime.inflectionValues) {
|
|
10331
|
+
const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
|
|
10332
|
+
const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
|
|
10333
|
+
const angle = Math.PI - Math.PI * percentage;
|
|
10334
|
+
const textRect = getRectangleTangentToCircle(angle, // angle between X axis and the point where the rectangle is tangent to the circle
|
|
10335
|
+
gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, // radius of the gauge circle + margin below text
|
|
10336
|
+
gaugeCircleCenter.x, // center of the gauge circle
|
|
10337
|
+
gaugeCircleCenter.y, // center of the gauge circle
|
|
10338
|
+
labelWidth + 2, // width of the text + some margin
|
|
10339
|
+
GAUGE_LABELS_FONT_SIZE // height of the text
|
|
10340
|
+
);
|
|
10341
|
+
let offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect))
|
|
10342
|
+
? GAUGE_LABELS_FONT_SIZE
|
|
10343
|
+
: 0;
|
|
10344
|
+
inflectionValuesTextRects.push(textRect);
|
|
10345
|
+
inflectionValues.push({
|
|
10346
|
+
rotation: angle,
|
|
10347
|
+
label: inflectionValue.label,
|
|
10348
|
+
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
10349
|
+
color: textColor,
|
|
10350
|
+
offset,
|
|
10103
10351
|
});
|
|
10104
10352
|
}
|
|
10105
|
-
|
|
10106
|
-
|
|
10107
|
-
|
|
10108
|
-
|
|
10353
|
+
return inflectionValues;
|
|
10354
|
+
}
|
|
10355
|
+
function getGaugeColor(runtime) {
|
|
10356
|
+
const gaugeValue = runtime.gaugeValue?.value;
|
|
10357
|
+
if (gaugeValue === undefined) {
|
|
10358
|
+
return GAUGE_BACKGROUND_COLOR;
|
|
10109
10359
|
}
|
|
10110
|
-
|
|
10111
|
-
const
|
|
10112
|
-
if (
|
|
10113
|
-
|
|
10114
|
-
if (chartData.options?.plugins?.title) {
|
|
10115
|
-
this.chart.config.options.plugins.title = chartData.options.plugins.title;
|
|
10116
|
-
}
|
|
10360
|
+
for (let i = 0; i < runtime.inflectionValues.length; i++) {
|
|
10361
|
+
const inflectionValue = runtime.inflectionValues[i];
|
|
10362
|
+
if (inflectionValue.operator === "<" && gaugeValue < inflectionValue.value) {
|
|
10363
|
+
return runtime.colors[i];
|
|
10117
10364
|
}
|
|
10118
|
-
else {
|
|
10119
|
-
|
|
10365
|
+
else if (inflectionValue.operator === "<=" && gaugeValue <= inflectionValue.value) {
|
|
10366
|
+
return runtime.colors[i];
|
|
10120
10367
|
}
|
|
10121
|
-
this.chart.config.options = chartData.options;
|
|
10122
|
-
this.chart.update();
|
|
10123
10368
|
}
|
|
10369
|
+
return runtime.colors.at(-1);
|
|
10370
|
+
}
|
|
10371
|
+
function getSegmentsOfRectangle(rectangle) {
|
|
10372
|
+
return [
|
|
10373
|
+
{ start: rectangle.topLeft, end: rectangle.topRight },
|
|
10374
|
+
{ start: rectangle.topRight, end: rectangle.bottomRight },
|
|
10375
|
+
{ start: rectangle.bottomRight, end: rectangle.bottomLeft },
|
|
10376
|
+
{ start: rectangle.bottomLeft, end: rectangle.topLeft },
|
|
10377
|
+
];
|
|
10378
|
+
}
|
|
10379
|
+
/**
|
|
10380
|
+
* Check if two segment intersect. The case where the segments are colinear (both segments on the same line)
|
|
10381
|
+
* is not handled.
|
|
10382
|
+
*/
|
|
10383
|
+
function doSegmentIntersect(segment1, segment2) {
|
|
10384
|
+
const A = segment1.start;
|
|
10385
|
+
const B = segment1.end;
|
|
10386
|
+
const C = segment2.start;
|
|
10387
|
+
const D = segment2.end;
|
|
10388
|
+
/**
|
|
10389
|
+
* Line segment intersection algorithm
|
|
10390
|
+
* https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
|
|
10391
|
+
*/
|
|
10392
|
+
function ccw(a, b, c) {
|
|
10393
|
+
return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
|
|
10394
|
+
}
|
|
10395
|
+
return ccw(A, C, D) !== ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D);
|
|
10396
|
+
}
|
|
10397
|
+
function doRectanglesIntersect(rect1, rect2) {
|
|
10398
|
+
const segments1 = getSegmentsOfRectangle(rect1);
|
|
10399
|
+
const segments2 = getSegmentsOfRectangle(rect2);
|
|
10400
|
+
for (const segment1 of segments1) {
|
|
10401
|
+
for (const segment2 of segments2) {
|
|
10402
|
+
if (doSegmentIntersect(segment1, segment2)) {
|
|
10403
|
+
return true;
|
|
10404
|
+
}
|
|
10405
|
+
}
|
|
10406
|
+
}
|
|
10407
|
+
return false;
|
|
10408
|
+
}
|
|
10409
|
+
/**
|
|
10410
|
+
* Get the rectangle that is tangent to a circle at a given angle.
|
|
10411
|
+
*
|
|
10412
|
+
* @param angle angle between X axis and the point where the rectangle is tangent to the circle
|
|
10413
|
+
*/
|
|
10414
|
+
function getRectangleTangentToCircle(angle, radius, circleCenterX, circleCenterY, rectWidth, rectHeight) {
|
|
10415
|
+
const cos = Math.cos(angle);
|
|
10416
|
+
const sin = Math.sin(angle);
|
|
10417
|
+
// x, y are the distance from the center of the circle to the point where the rectangle is tangent to the circle
|
|
10418
|
+
const x = cos * radius;
|
|
10419
|
+
const y = sin * radius;
|
|
10420
|
+
// x2, y2 are the distance from the point the rectangle is tangent to the circle to the bottom left corner of the rectangle
|
|
10421
|
+
const x2 = sin * (rectWidth / 2); // cos(angle + 90°) = sin(angle)
|
|
10422
|
+
const y2 = cos * (rectWidth / 2);
|
|
10423
|
+
const bottomRight = {
|
|
10424
|
+
x: x + x2 + circleCenterX,
|
|
10425
|
+
y: circleCenterY - (y - y2),
|
|
10426
|
+
};
|
|
10427
|
+
const bottomLeft = {
|
|
10428
|
+
x: x - x2 + circleCenterX,
|
|
10429
|
+
y: circleCenterY - (y + y2),
|
|
10430
|
+
};
|
|
10431
|
+
// Same as above but for the top corners of the rectangle (radius + rectangle height instead of radius)
|
|
10432
|
+
const xp = cos * (radius + rectHeight);
|
|
10433
|
+
const yp = sin * (radius + rectHeight);
|
|
10434
|
+
const topLeft = {
|
|
10435
|
+
x: xp - x2 + circleCenterX,
|
|
10436
|
+
y: circleCenterY - (yp + y2),
|
|
10437
|
+
};
|
|
10438
|
+
const topRight = {
|
|
10439
|
+
x: xp + x2 + circleCenterX,
|
|
10440
|
+
y: circleCenterY - (yp - y2),
|
|
10441
|
+
};
|
|
10442
|
+
return { bottomLeft, bottomRight, topRight, topLeft };
|
|
10124
10443
|
}
|
|
10125
10444
|
|
|
10126
10445
|
/**
|
|
@@ -10702,6 +11021,155 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
10702
11021
|
}
|
|
10703
11022
|
}
|
|
10704
11023
|
|
|
11024
|
+
const CHART_COMMON_OPTIONS = {
|
|
11025
|
+
// https://www.chartjs.org/docs/latest/general/responsive.html
|
|
11026
|
+
responsive: true, // will resize when its container is resized
|
|
11027
|
+
maintainAspectRatio: false, // doesn't maintain the aspect ratio (width/height =2 by default) so the user has the choice of the exact layout
|
|
11028
|
+
elements: {
|
|
11029
|
+
line: {
|
|
11030
|
+
fill: false, // do not fill the area under line charts
|
|
11031
|
+
},
|
|
11032
|
+
point: {
|
|
11033
|
+
hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
|
|
11034
|
+
},
|
|
11035
|
+
},
|
|
11036
|
+
animation: false,
|
|
11037
|
+
};
|
|
11038
|
+
function truncateLabel(label) {
|
|
11039
|
+
if (!label) {
|
|
11040
|
+
return "";
|
|
11041
|
+
}
|
|
11042
|
+
if (label.length > MAX_CHAR_LABEL) {
|
|
11043
|
+
return label.substring(0, MAX_CHAR_LABEL) + "…";
|
|
11044
|
+
}
|
|
11045
|
+
return label;
|
|
11046
|
+
}
|
|
11047
|
+
function chartToImage(runtime, figure, type) {
|
|
11048
|
+
// wrap the canvas in a div with a fixed size because chart.js would
|
|
11049
|
+
// fill the whole page otherwise
|
|
11050
|
+
const div = document.createElement("div");
|
|
11051
|
+
div.style.width = `${figure.width}px`;
|
|
11052
|
+
div.style.height = `${figure.height}px`;
|
|
11053
|
+
const canvas = document.createElement("canvas");
|
|
11054
|
+
div.append(canvas);
|
|
11055
|
+
canvas.setAttribute("width", figure.width.toString());
|
|
11056
|
+
canvas.setAttribute("height", figure.height.toString());
|
|
11057
|
+
// we have to add the canvas to the DOM otherwise it won't be rendered
|
|
11058
|
+
document.body.append(div);
|
|
11059
|
+
if ("chartJsConfig" in runtime) {
|
|
11060
|
+
const config = deepCopy(runtime.chartJsConfig);
|
|
11061
|
+
config.plugins = [backgroundColorChartJSPlugin];
|
|
11062
|
+
const Chart = getChartJSConstructor();
|
|
11063
|
+
const chart = new Chart(canvas, config);
|
|
11064
|
+
const imgContent = chart.toBase64Image();
|
|
11065
|
+
chart.destroy();
|
|
11066
|
+
div.remove();
|
|
11067
|
+
return imgContent;
|
|
11068
|
+
}
|
|
11069
|
+
else if (type === "scorecard") {
|
|
11070
|
+
const design = getScorecardConfiguration(figure, runtime);
|
|
11071
|
+
drawScoreChart(design, canvas);
|
|
11072
|
+
const imgContent = canvas.toDataURL();
|
|
11073
|
+
div.remove();
|
|
11074
|
+
return imgContent;
|
|
11075
|
+
}
|
|
11076
|
+
else if (type === "gauge") {
|
|
11077
|
+
drawGaugeChart(canvas, runtime);
|
|
11078
|
+
const imgContent = canvas.toDataURL();
|
|
11079
|
+
div.remove();
|
|
11080
|
+
return imgContent;
|
|
11081
|
+
}
|
|
11082
|
+
return undefined;
|
|
11083
|
+
}
|
|
11084
|
+
/**
|
|
11085
|
+
* Custom chart.js plugin to set the background color of the canvas
|
|
11086
|
+
* https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md
|
|
11087
|
+
*/
|
|
11088
|
+
const backgroundColorChartJSPlugin = {
|
|
11089
|
+
id: "customCanvasBackgroundColor",
|
|
11090
|
+
beforeDraw: (chart) => {
|
|
11091
|
+
const { ctx } = chart;
|
|
11092
|
+
ctx.save();
|
|
11093
|
+
ctx.globalCompositeOperation = "destination-over";
|
|
11094
|
+
ctx.fillStyle = "#ffffff";
|
|
11095
|
+
ctx.fillRect(0, 0, chart.width, chart.height);
|
|
11096
|
+
ctx.restore();
|
|
11097
|
+
},
|
|
11098
|
+
};
|
|
11099
|
+
/** Return window.Chart, making sure all our extensions are loaded in ChartJS */
|
|
11100
|
+
function getChartJSConstructor() {
|
|
11101
|
+
if (window.Chart && !window.Chart?.registry.plugins.get("chartShowValuesPlugin")) {
|
|
11102
|
+
window.Chart.register(chartShowValuesPlugin);
|
|
11103
|
+
window.Chart.register(waterfallLinesPlugin);
|
|
11104
|
+
}
|
|
11105
|
+
return window.Chart;
|
|
11106
|
+
}
|
|
11107
|
+
|
|
11108
|
+
class ChartJsComponent extends owl.Component {
|
|
11109
|
+
static template = "o-spreadsheet-ChartJsComponent";
|
|
11110
|
+
static props = {
|
|
11111
|
+
figure: Object,
|
|
11112
|
+
};
|
|
11113
|
+
canvas = owl.useRef("graphContainer");
|
|
11114
|
+
chart;
|
|
11115
|
+
currentRuntime;
|
|
11116
|
+
get background() {
|
|
11117
|
+
return this.chartRuntime.background;
|
|
11118
|
+
}
|
|
11119
|
+
get canvasStyle() {
|
|
11120
|
+
return `background-color: ${this.background}`;
|
|
11121
|
+
}
|
|
11122
|
+
get chartRuntime() {
|
|
11123
|
+
const runtime = this.env.model.getters.getChartRuntime(this.props.figure.id);
|
|
11124
|
+
if (!("chartJsConfig" in runtime)) {
|
|
11125
|
+
throw new Error("Unsupported chart runtime");
|
|
11126
|
+
}
|
|
11127
|
+
return runtime;
|
|
11128
|
+
}
|
|
11129
|
+
setup() {
|
|
11130
|
+
owl.onMounted(() => {
|
|
11131
|
+
const runtime = this.chartRuntime;
|
|
11132
|
+
this.currentRuntime = runtime;
|
|
11133
|
+
// Note: chartJS modify the runtime in place, so it's important to give it a copy
|
|
11134
|
+
this.createChart(deepCopy(runtime.chartJsConfig));
|
|
11135
|
+
});
|
|
11136
|
+
owl.onWillUnmount(() => this.chart?.destroy());
|
|
11137
|
+
owl.useEffect(() => {
|
|
11138
|
+
const runtime = this.chartRuntime;
|
|
11139
|
+
if (runtime !== this.currentRuntime) {
|
|
11140
|
+
if (runtime.chartJsConfig.type !== this.currentRuntime.chartJsConfig.type) {
|
|
11141
|
+
this.chart?.destroy();
|
|
11142
|
+
this.createChart(deepCopy(runtime.chartJsConfig));
|
|
11143
|
+
}
|
|
11144
|
+
else {
|
|
11145
|
+
this.updateChartJs(deepCopy(runtime));
|
|
11146
|
+
}
|
|
11147
|
+
this.currentRuntime = runtime;
|
|
11148
|
+
}
|
|
11149
|
+
});
|
|
11150
|
+
}
|
|
11151
|
+
createChart(chartData) {
|
|
11152
|
+
const canvas = this.canvas.el;
|
|
11153
|
+
const ctx = canvas.getContext("2d");
|
|
11154
|
+
const Chart = getChartJSConstructor();
|
|
11155
|
+
this.chart = new Chart(ctx, chartData);
|
|
11156
|
+
}
|
|
11157
|
+
updateChartJs(chartRuntime) {
|
|
11158
|
+
const chartData = chartRuntime.chartJsConfig;
|
|
11159
|
+
if (chartData.data && chartData.data.datasets) {
|
|
11160
|
+
this.chart.data = chartData.data;
|
|
11161
|
+
if (chartData.options?.plugins?.title) {
|
|
11162
|
+
this.chart.config.options.plugins.title = chartData.options.plugins.title;
|
|
11163
|
+
}
|
|
11164
|
+
}
|
|
11165
|
+
else {
|
|
11166
|
+
this.chart.data.datasets = [];
|
|
11167
|
+
}
|
|
11168
|
+
this.chart.config.options = chartData.options;
|
|
11169
|
+
this.chart.update();
|
|
11170
|
+
}
|
|
11171
|
+
}
|
|
11172
|
+
|
|
10705
11173
|
class ScorecardChart extends owl.Component {
|
|
10706
11174
|
static template = "o-spreadsheet-ScorecardChart";
|
|
10707
11175
|
static props = {
|
|
@@ -10733,6 +11201,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
10733
11201
|
const autoCompleteProviders = new Registry();
|
|
10734
11202
|
|
|
10735
11203
|
autoCompleteProviders.add("dataValidation", {
|
|
11204
|
+
displayAllOnInitialContent: true,
|
|
10736
11205
|
getProposals(tokenAtCursor, content) {
|
|
10737
11206
|
if (content.startsWith("=")) {
|
|
10738
11207
|
return [];
|
|
@@ -21029,6 +21498,15 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
21029
21498
|
const exactMatch = proposals?.find((p) => p.text === tokenAtCursor.value);
|
|
21030
21499
|
// remove tokens that are likely to be other parts of the formula that slipped in the token if it's a string
|
|
21031
21500
|
const searchTerm = tokenAtCursor.value.replace(/[ ,\(\)]/g, "");
|
|
21501
|
+
if (this._currentContent === this.initialContent &&
|
|
21502
|
+
provider.displayAllOnInitialContent &&
|
|
21503
|
+
proposals?.length) {
|
|
21504
|
+
return {
|
|
21505
|
+
proposals,
|
|
21506
|
+
selectProposal: provider.selectProposal,
|
|
21507
|
+
autoSelectFirstProposal: provider.autoSelectFirstProposal ?? false,
|
|
21508
|
+
};
|
|
21509
|
+
}
|
|
21032
21510
|
if (exactMatch && this._currentContent !== this.initialContent) {
|
|
21033
21511
|
// this means the user has chosen a proposal
|
|
21034
21512
|
return;
|
|
@@ -22154,7 +22632,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
22154
22632
|
condition: (cell) => !cell.isFormula &&
|
|
22155
22633
|
evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.text &&
|
|
22156
22634
|
alphaNumericValueRegExp.test(cell.content),
|
|
22157
|
-
generateRule: (cell, cells) => {
|
|
22635
|
+
generateRule: (cell, cells, direction) => {
|
|
22158
22636
|
const numberPostfix = parseInt(cell.content.match(numberPostfixRegExp)[0]);
|
|
22159
22637
|
const prefix = cell.content.match(stringPrefixRegExp)[0];
|
|
22160
22638
|
const numberPostfixLength = cell.content.length - prefix.length;
|
|
@@ -22162,7 +22640,10 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
22162
22640
|
alphaNumericValueRegExp.test(evaluatedCell.value)) // get consecutive alphanumeric cells, no matter what the prefix is
|
|
22163
22641
|
.filter((cell) => prefix === (cell.value ?? "").toString().match(stringPrefixRegExp)[0])
|
|
22164
22642
|
.map((cell) => parseInt((cell.value ?? "").toString().match(numberPostfixRegExp)[0]));
|
|
22165
|
-
|
|
22643
|
+
let increment = calculateIncrementBasedOnGroup(group);
|
|
22644
|
+
if (["up", "left"].includes(direction) && group.length === 1) {
|
|
22645
|
+
increment = -increment;
|
|
22646
|
+
}
|
|
22166
22647
|
return {
|
|
22167
22648
|
type: "ALPHANUMERIC_INCREMENT_MODIFIER",
|
|
22168
22649
|
prefix,
|
|
@@ -22225,10 +22706,13 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
22225
22706
|
.add("increment_number", {
|
|
22226
22707
|
condition: (cell) => !cell.isFormula &&
|
|
22227
22708
|
evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.number,
|
|
22228
|
-
generateRule: (cell, cells) => {
|
|
22709
|
+
generateRule: (cell, cells, direction) => {
|
|
22229
22710
|
const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number &&
|
|
22230
22711
|
!isDateTimeFormat(evaluatedCell.format || "")).map((cell) => Number(cell.value));
|
|
22231
|
-
|
|
22712
|
+
let increment = calculateIncrementBasedOnGroup(group);
|
|
22713
|
+
if (["up", "left"].includes(direction) && group.length === 1) {
|
|
22714
|
+
increment = -increment;
|
|
22715
|
+
}
|
|
22232
22716
|
const evaluation = evaluateLiteral(cell, { locale: DEFAULT_LOCALE });
|
|
22233
22717
|
return {
|
|
22234
22718
|
type: "INCREMENT_MODIFIER",
|
|
@@ -22272,343 +22756,6 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
22272
22756
|
|
|
22273
22757
|
const cellPopoverRegistry = new Registry();
|
|
22274
22758
|
|
|
22275
|
-
const GAUGE_PADDING_SIDE = 30;
|
|
22276
|
-
const GAUGE_PADDING_TOP = 10;
|
|
22277
|
-
const GAUGE_PADDING_BOTTOM = 20;
|
|
22278
|
-
const GAUGE_LABELS_FONT_SIZE = 12;
|
|
22279
|
-
const GAUGE_DEFAULT_VALUE_FONT_SIZE = 80;
|
|
22280
|
-
const GAUGE_BACKGROUND_COLOR = "#F3F2F1";
|
|
22281
|
-
const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
|
|
22282
|
-
const GAUGE_TITLE_SECTION_HEIGHT = 25;
|
|
22283
|
-
function drawGaugeChart(canvas, runtime) {
|
|
22284
|
-
const canvasBoundingRect = canvas.getBoundingClientRect();
|
|
22285
|
-
canvas.width = canvasBoundingRect.width;
|
|
22286
|
-
canvas.height = canvasBoundingRect.height;
|
|
22287
|
-
const ctx = canvas.getContext("2d");
|
|
22288
|
-
const config = getGaugeRenderingConfig(canvasBoundingRect, runtime, ctx);
|
|
22289
|
-
drawBackground(ctx, config);
|
|
22290
|
-
drawGauge(ctx, config);
|
|
22291
|
-
drawInflectionValues(ctx, config);
|
|
22292
|
-
drawLabels(ctx, config);
|
|
22293
|
-
drawTitle(ctx, config);
|
|
22294
|
-
}
|
|
22295
|
-
function drawGauge(ctx, config) {
|
|
22296
|
-
ctx.save();
|
|
22297
|
-
const gauge = config.gauge;
|
|
22298
|
-
const arcCenterX = gauge.rect.x + gauge.rect.width / 2;
|
|
22299
|
-
const arcCenterY = gauge.rect.y + gauge.rect.height;
|
|
22300
|
-
const arcRadius = gauge.rect.height - gauge.arcWidth / 2;
|
|
22301
|
-
if (arcRadius < 0) {
|
|
22302
|
-
return;
|
|
22303
|
-
}
|
|
22304
|
-
const gaugeAngle = gauge.percentage === 1 ? 0 : Math.PI * (1 + gauge.percentage);
|
|
22305
|
-
// Gauge background
|
|
22306
|
-
ctx.strokeStyle = GAUGE_BACKGROUND_COLOR;
|
|
22307
|
-
ctx.beginPath();
|
|
22308
|
-
ctx.lineWidth = gauge.arcWidth;
|
|
22309
|
-
ctx.arc(arcCenterX, arcCenterY, arcRadius, gaugeAngle, 0);
|
|
22310
|
-
ctx.stroke();
|
|
22311
|
-
// Gauge value
|
|
22312
|
-
ctx.strokeStyle = gauge.color;
|
|
22313
|
-
ctx.beginPath();
|
|
22314
|
-
ctx.arc(arcCenterX, arcCenterY, arcRadius, Math.PI, gaugeAngle);
|
|
22315
|
-
ctx.stroke();
|
|
22316
|
-
ctx.restore();
|
|
22317
|
-
}
|
|
22318
|
-
function drawBackground(ctx, config) {
|
|
22319
|
-
ctx.save();
|
|
22320
|
-
ctx.fillStyle = config.backgroundColor;
|
|
22321
|
-
ctx.fillRect(0, 0, config.width, config.height);
|
|
22322
|
-
ctx.restore();
|
|
22323
|
-
}
|
|
22324
|
-
function drawLabels(ctx, config) {
|
|
22325
|
-
for (const label of [config.minLabel, config.maxLabel, config.gaugeValue]) {
|
|
22326
|
-
ctx.save();
|
|
22327
|
-
ctx.textAlign = "center";
|
|
22328
|
-
ctx.fillStyle = label.color;
|
|
22329
|
-
ctx.font = `${label.fontSize}px ${DEFAULT_FONT}`;
|
|
22330
|
-
ctx.fillText(label.label, label.textPosition.x, label.textPosition.y);
|
|
22331
|
-
ctx.restore();
|
|
22332
|
-
}
|
|
22333
|
-
}
|
|
22334
|
-
function drawInflectionValues(ctx, config) {
|
|
22335
|
-
const { x: rectX, y: rectY, width, height } = config.gauge.rect;
|
|
22336
|
-
for (const inflectionValue of config.inflectionValues) {
|
|
22337
|
-
ctx.save();
|
|
22338
|
-
ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment
|
|
22339
|
-
ctx.rotate(Math.PI / 2 - inflectionValue.rotation);
|
|
22340
|
-
ctx.lineWidth = 2;
|
|
22341
|
-
ctx.strokeStyle = chartMutedFontColor(config.backgroundColor) + "aa";
|
|
22342
|
-
ctx.beginPath();
|
|
22343
|
-
ctx.moveTo(0, -(height - config.gauge.arcWidth));
|
|
22344
|
-
ctx.lineTo(0, -height - 3);
|
|
22345
|
-
ctx.stroke();
|
|
22346
|
-
ctx.textAlign = "center";
|
|
22347
|
-
ctx.font = `${inflectionValue.fontSize}px ${DEFAULT_FONT}`;
|
|
22348
|
-
ctx.fillStyle = inflectionValue.color;
|
|
22349
|
-
const textY = -height - GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN - inflectionValue.offset;
|
|
22350
|
-
ctx.fillText(inflectionValue.label, 0, textY);
|
|
22351
|
-
ctx.restore();
|
|
22352
|
-
}
|
|
22353
|
-
}
|
|
22354
|
-
function drawTitle(ctx, config) {
|
|
22355
|
-
ctx.save();
|
|
22356
|
-
const title = config.title;
|
|
22357
|
-
ctx.font = getDefaultContextFont(title.fontSize, title.bold, title.italic);
|
|
22358
|
-
ctx.textBaseline = "middle";
|
|
22359
|
-
ctx.fillStyle = title.color;
|
|
22360
|
-
ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
|
|
22361
|
-
ctx.restore();
|
|
22362
|
-
}
|
|
22363
|
-
function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
|
|
22364
|
-
const maxValue = runtime.maxValue;
|
|
22365
|
-
const minValue = runtime.minValue;
|
|
22366
|
-
const gaugeValue = runtime.gaugeValue;
|
|
22367
|
-
const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
|
|
22368
|
-
const gaugeArcWidth = gaugeRect.width / 6;
|
|
22369
|
-
const gaugePercentage = gaugeValue
|
|
22370
|
-
? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
|
|
22371
|
-
: 0;
|
|
22372
|
-
const gaugeValuePosition = {
|
|
22373
|
-
x: boundingRect.width / 2,
|
|
22374
|
-
y: gaugeRect.y + gaugeRect.height - gaugeRect.height / 12,
|
|
22375
|
-
};
|
|
22376
|
-
let gaugeValueFontSize = GAUGE_DEFAULT_VALUE_FONT_SIZE;
|
|
22377
|
-
// Scale down the font size if the gaugeRect is too small
|
|
22378
|
-
if (gaugeRect.height < 300) {
|
|
22379
|
-
gaugeValueFontSize = gaugeValueFontSize * (gaugeRect.height / 300);
|
|
22380
|
-
}
|
|
22381
|
-
// Scale down the font size if the text is too long
|
|
22382
|
-
const maxTextWidth = gaugeRect.width / 2;
|
|
22383
|
-
const gaugeLabel = gaugeValue?.label || "-";
|
|
22384
|
-
if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
|
|
22385
|
-
gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
|
|
22386
|
-
}
|
|
22387
|
-
const minLabelPosition = {
|
|
22388
|
-
x: gaugeRect.x + gaugeArcWidth / 2,
|
|
22389
|
-
y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
|
|
22390
|
-
};
|
|
22391
|
-
const maxLabelPosition = {
|
|
22392
|
-
x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
|
|
22393
|
-
y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
|
|
22394
|
-
};
|
|
22395
|
-
const textColor = chartMutedFontColor(runtime.background);
|
|
22396
|
-
const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);
|
|
22397
|
-
let x = 0, titleWidth = 0, titleHeight = 0;
|
|
22398
|
-
if (runtime.title.text) {
|
|
22399
|
-
({ width: titleWidth, height: titleHeight } = computeTextDimension(ctx, runtime.title.text, { fontSize: CHART_TITLE_FONT_SIZE, ...runtime.title }, "px"));
|
|
22400
|
-
}
|
|
22401
|
-
switch (runtime.title.align) {
|
|
22402
|
-
case "right":
|
|
22403
|
-
x = boundingRect.width - titleWidth - CHART_PADDING$1;
|
|
22404
|
-
break;
|
|
22405
|
-
case "center":
|
|
22406
|
-
x = (boundingRect.width - titleWidth) / 2;
|
|
22407
|
-
break;
|
|
22408
|
-
case "left":
|
|
22409
|
-
default:
|
|
22410
|
-
x = CHART_PADDING$1;
|
|
22411
|
-
break;
|
|
22412
|
-
}
|
|
22413
|
-
return {
|
|
22414
|
-
width: boundingRect.width,
|
|
22415
|
-
height: boundingRect.height,
|
|
22416
|
-
title: {
|
|
22417
|
-
label: runtime.title.text ?? "",
|
|
22418
|
-
fontSize: runtime.title.fontSize ?? CHART_TITLE_FONT_SIZE,
|
|
22419
|
-
textPosition: {
|
|
22420
|
-
x,
|
|
22421
|
-
y: CHART_PADDING_TOP + titleHeight / 2,
|
|
22422
|
-
},
|
|
22423
|
-
color: runtime.title.color ?? textColor,
|
|
22424
|
-
bold: runtime.title.bold,
|
|
22425
|
-
italic: runtime.title.italic,
|
|
22426
|
-
},
|
|
22427
|
-
backgroundColor: runtime.background,
|
|
22428
|
-
gauge: {
|
|
22429
|
-
rect: gaugeRect,
|
|
22430
|
-
arcWidth: gaugeArcWidth,
|
|
22431
|
-
percentage: clip(gaugePercentage, 0, 1),
|
|
22432
|
-
color: getGaugeColor(runtime),
|
|
22433
|
-
},
|
|
22434
|
-
inflectionValues,
|
|
22435
|
-
gaugeValue: {
|
|
22436
|
-
label: gaugeLabel,
|
|
22437
|
-
textPosition: gaugeValuePosition,
|
|
22438
|
-
fontSize: gaugeValueFontSize,
|
|
22439
|
-
color: textColor,
|
|
22440
|
-
},
|
|
22441
|
-
minLabel: {
|
|
22442
|
-
label: runtime.minValue.label,
|
|
22443
|
-
textPosition: minLabelPosition,
|
|
22444
|
-
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
22445
|
-
color: textColor,
|
|
22446
|
-
},
|
|
22447
|
-
maxLabel: {
|
|
22448
|
-
label: runtime.maxValue.label,
|
|
22449
|
-
textPosition: maxLabelPosition,
|
|
22450
|
-
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
22451
|
-
color: textColor,
|
|
22452
|
-
},
|
|
22453
|
-
};
|
|
22454
|
-
}
|
|
22455
|
-
/**
|
|
22456
|
-
* Get the rectangle in which the gauge will be drawn, based on the bounding rectangle of the canvas and leaving
|
|
22457
|
-
* space for the title and labels.
|
|
22458
|
-
*/
|
|
22459
|
-
function getGaugeRect(boundingRect, title) {
|
|
22460
|
-
const titleHeight = title ? GAUGE_TITLE_SECTION_HEIGHT : 0;
|
|
22461
|
-
const drawHeight = boundingRect.height - GAUGE_PADDING_BOTTOM - titleHeight - GAUGE_PADDING_TOP;
|
|
22462
|
-
const drawWidth = boundingRect.width - GAUGE_PADDING_SIDE * 2;
|
|
22463
|
-
let gaugeWidth;
|
|
22464
|
-
let gaugeHeight;
|
|
22465
|
-
if (drawWidth > 2 * drawHeight) {
|
|
22466
|
-
gaugeWidth = 2 * drawHeight;
|
|
22467
|
-
gaugeHeight = drawHeight;
|
|
22468
|
-
}
|
|
22469
|
-
else {
|
|
22470
|
-
gaugeWidth = drawWidth;
|
|
22471
|
-
gaugeHeight = drawWidth / 2;
|
|
22472
|
-
}
|
|
22473
|
-
const gaugeX = GAUGE_PADDING_SIDE + (drawWidth - gaugeWidth) / 2;
|
|
22474
|
-
const gaugeY = titleHeight + GAUGE_PADDING_TOP + (drawHeight - gaugeHeight) / 2;
|
|
22475
|
-
return {
|
|
22476
|
-
x: gaugeX,
|
|
22477
|
-
y: gaugeY,
|
|
22478
|
-
width: gaugeWidth,
|
|
22479
|
-
height: gaugeHeight,
|
|
22480
|
-
};
|
|
22481
|
-
}
|
|
22482
|
-
/**
|
|
22483
|
-
* 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).
|
|
22484
|
-
*
|
|
22485
|
-
* Also compute an offset for the text so that it doesn't overlap with other text.
|
|
22486
|
-
*/
|
|
22487
|
-
function getInflectionValues(runtime, gaugeRect, textColor, ctx) {
|
|
22488
|
-
const maxValue = runtime.maxValue;
|
|
22489
|
-
const minValue = runtime.minValue;
|
|
22490
|
-
const gaugeCircleCenter = {
|
|
22491
|
-
x: gaugeRect.x + gaugeRect.width / 2,
|
|
22492
|
-
y: gaugeRect.y + gaugeRect.height,
|
|
22493
|
-
};
|
|
22494
|
-
const textStyle = { fontSize: GAUGE_LABELS_FONT_SIZE };
|
|
22495
|
-
const inflectionValues = [];
|
|
22496
|
-
const inflectionValuesTextRects = [];
|
|
22497
|
-
for (const inflectionValue of runtime.inflectionValues) {
|
|
22498
|
-
const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
|
|
22499
|
-
const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
|
|
22500
|
-
const angle = Math.PI - Math.PI * percentage;
|
|
22501
|
-
const textRect = getRectangleTangentToCircle(angle, // angle between X axis and the point where the rectangle is tangent to the circle
|
|
22502
|
-
gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, // radius of the gauge circle + margin below text
|
|
22503
|
-
gaugeCircleCenter.x, // center of the gauge circle
|
|
22504
|
-
gaugeCircleCenter.y, // center of the gauge circle
|
|
22505
|
-
labelWidth + 2, // width of the text + some margin
|
|
22506
|
-
GAUGE_LABELS_FONT_SIZE // height of the text
|
|
22507
|
-
);
|
|
22508
|
-
let offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect))
|
|
22509
|
-
? GAUGE_LABELS_FONT_SIZE
|
|
22510
|
-
: 0;
|
|
22511
|
-
inflectionValuesTextRects.push(textRect);
|
|
22512
|
-
inflectionValues.push({
|
|
22513
|
-
rotation: angle,
|
|
22514
|
-
label: inflectionValue.label,
|
|
22515
|
-
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
22516
|
-
color: textColor,
|
|
22517
|
-
offset,
|
|
22518
|
-
});
|
|
22519
|
-
}
|
|
22520
|
-
return inflectionValues;
|
|
22521
|
-
}
|
|
22522
|
-
function getGaugeColor(runtime) {
|
|
22523
|
-
const gaugeValue = runtime.gaugeValue?.value;
|
|
22524
|
-
if (gaugeValue === undefined) {
|
|
22525
|
-
return GAUGE_BACKGROUND_COLOR;
|
|
22526
|
-
}
|
|
22527
|
-
for (let i = 0; i < runtime.inflectionValues.length; i++) {
|
|
22528
|
-
const inflectionValue = runtime.inflectionValues[i];
|
|
22529
|
-
if (inflectionValue.operator === "<" && gaugeValue < inflectionValue.value) {
|
|
22530
|
-
return runtime.colors[i];
|
|
22531
|
-
}
|
|
22532
|
-
else if (inflectionValue.operator === "<=" && gaugeValue <= inflectionValue.value) {
|
|
22533
|
-
return runtime.colors[i];
|
|
22534
|
-
}
|
|
22535
|
-
}
|
|
22536
|
-
return runtime.colors.at(-1);
|
|
22537
|
-
}
|
|
22538
|
-
function getSegmentsOfRectangle(rectangle) {
|
|
22539
|
-
return [
|
|
22540
|
-
{ start: rectangle.topLeft, end: rectangle.topRight },
|
|
22541
|
-
{ start: rectangle.topRight, end: rectangle.bottomRight },
|
|
22542
|
-
{ start: rectangle.bottomRight, end: rectangle.bottomLeft },
|
|
22543
|
-
{ start: rectangle.bottomLeft, end: rectangle.topLeft },
|
|
22544
|
-
];
|
|
22545
|
-
}
|
|
22546
|
-
/**
|
|
22547
|
-
* Check if two segment intersect. The case where the segments are colinear (both segments on the same line)
|
|
22548
|
-
* is not handled.
|
|
22549
|
-
*/
|
|
22550
|
-
function doSegmentIntersect(segment1, segment2) {
|
|
22551
|
-
const A = segment1.start;
|
|
22552
|
-
const B = segment1.end;
|
|
22553
|
-
const C = segment2.start;
|
|
22554
|
-
const D = segment2.end;
|
|
22555
|
-
/**
|
|
22556
|
-
* Line segment intersection algorithm
|
|
22557
|
-
* https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
|
|
22558
|
-
*/
|
|
22559
|
-
function ccw(a, b, c) {
|
|
22560
|
-
return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
|
|
22561
|
-
}
|
|
22562
|
-
return ccw(A, C, D) !== ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D);
|
|
22563
|
-
}
|
|
22564
|
-
function doRectanglesIntersect(rect1, rect2) {
|
|
22565
|
-
const segments1 = getSegmentsOfRectangle(rect1);
|
|
22566
|
-
const segments2 = getSegmentsOfRectangle(rect2);
|
|
22567
|
-
for (const segment1 of segments1) {
|
|
22568
|
-
for (const segment2 of segments2) {
|
|
22569
|
-
if (doSegmentIntersect(segment1, segment2)) {
|
|
22570
|
-
return true;
|
|
22571
|
-
}
|
|
22572
|
-
}
|
|
22573
|
-
}
|
|
22574
|
-
return false;
|
|
22575
|
-
}
|
|
22576
|
-
/**
|
|
22577
|
-
* Get the rectangle that is tangent to a circle at a given angle.
|
|
22578
|
-
*
|
|
22579
|
-
* @param angle angle between X axis and the point where the rectangle is tangent to the circle
|
|
22580
|
-
*/
|
|
22581
|
-
function getRectangleTangentToCircle(angle, radius, circleCenterX, circleCenterY, rectWidth, rectHeight) {
|
|
22582
|
-
const cos = Math.cos(angle);
|
|
22583
|
-
const sin = Math.sin(angle);
|
|
22584
|
-
// x, y are the distance from the center of the circle to the point where the rectangle is tangent to the circle
|
|
22585
|
-
const x = cos * radius;
|
|
22586
|
-
const y = sin * radius;
|
|
22587
|
-
// x2, y2 are the distance from the point the rectangle is tangent to the circle to the bottom left corner of the rectangle
|
|
22588
|
-
const x2 = sin * (rectWidth / 2); // cos(angle + 90°) = sin(angle)
|
|
22589
|
-
const y2 = cos * (rectWidth / 2);
|
|
22590
|
-
const bottomRight = {
|
|
22591
|
-
x: x + x2 + circleCenterX,
|
|
22592
|
-
y: circleCenterY - (y - y2),
|
|
22593
|
-
};
|
|
22594
|
-
const bottomLeft = {
|
|
22595
|
-
x: x - x2 + circleCenterX,
|
|
22596
|
-
y: circleCenterY - (y + y2),
|
|
22597
|
-
};
|
|
22598
|
-
// Same as above but for the top corners of the rectangle (radius + rectangle height instead of radius)
|
|
22599
|
-
const xp = cos * (radius + rectHeight);
|
|
22600
|
-
const yp = sin * (radius + rectHeight);
|
|
22601
|
-
const topLeft = {
|
|
22602
|
-
x: xp - x2 + circleCenterX,
|
|
22603
|
-
y: circleCenterY - (yp + y2),
|
|
22604
|
-
};
|
|
22605
|
-
const topRight = {
|
|
22606
|
-
x: xp + x2 + circleCenterX,
|
|
22607
|
-
y: circleCenterY - (yp - y2),
|
|
22608
|
-
};
|
|
22609
|
-
return { bottomLeft, bottomRight, topRight, topLeft };
|
|
22610
|
-
}
|
|
22611
|
-
|
|
22612
22759
|
class GaugeChartComponent extends owl.Component {
|
|
22613
22760
|
static template = "o-spreadsheet-GaugeChartComponent";
|
|
22614
22761
|
canvas = owl.useRef("chartContainer");
|
|
@@ -22641,81 +22788,6 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
22641
22788
|
return color;
|
|
22642
22789
|
}
|
|
22643
22790
|
|
|
22644
|
-
const CHART_COMMON_OPTIONS = {
|
|
22645
|
-
// https://www.chartjs.org/docs/latest/general/responsive.html
|
|
22646
|
-
responsive: true, // will resize when its container is resized
|
|
22647
|
-
maintainAspectRatio: false, // doesn't maintain the aspect ratio (width/height =2 by default) so the user has the choice of the exact layout
|
|
22648
|
-
elements: {
|
|
22649
|
-
line: {
|
|
22650
|
-
fill: false, // do not fill the area under line charts
|
|
22651
|
-
},
|
|
22652
|
-
point: {
|
|
22653
|
-
hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
|
|
22654
|
-
},
|
|
22655
|
-
},
|
|
22656
|
-
animation: false,
|
|
22657
|
-
};
|
|
22658
|
-
function truncateLabel(label) {
|
|
22659
|
-
if (!label) {
|
|
22660
|
-
return "";
|
|
22661
|
-
}
|
|
22662
|
-
if (label.length > MAX_CHAR_LABEL) {
|
|
22663
|
-
return label.substring(0, MAX_CHAR_LABEL) + "…";
|
|
22664
|
-
}
|
|
22665
|
-
return label;
|
|
22666
|
-
}
|
|
22667
|
-
function chartToImage(runtime, figure, type) {
|
|
22668
|
-
// wrap the canvas in a div with a fixed size because chart.js would
|
|
22669
|
-
// fill the whole page otherwise
|
|
22670
|
-
const div = document.createElement("div");
|
|
22671
|
-
div.style.width = `${figure.width}px`;
|
|
22672
|
-
div.style.height = `${figure.height}px`;
|
|
22673
|
-
const canvas = document.createElement("canvas");
|
|
22674
|
-
div.append(canvas);
|
|
22675
|
-
canvas.setAttribute("width", figure.width.toString());
|
|
22676
|
-
canvas.setAttribute("height", figure.height.toString());
|
|
22677
|
-
// we have to add the canvas to the DOM otherwise it won't be rendered
|
|
22678
|
-
document.body.append(div);
|
|
22679
|
-
if ("chartJsConfig" in runtime) {
|
|
22680
|
-
const config = deepCopy(runtime.chartJsConfig);
|
|
22681
|
-
config.plugins = [backgroundColorChartJSPlugin];
|
|
22682
|
-
const chart = new window.Chart(canvas, config);
|
|
22683
|
-
const imgContent = chart.toBase64Image();
|
|
22684
|
-
chart.destroy();
|
|
22685
|
-
div.remove();
|
|
22686
|
-
return imgContent;
|
|
22687
|
-
}
|
|
22688
|
-
else if (type === "scorecard") {
|
|
22689
|
-
const design = getScorecardConfiguration(figure, runtime);
|
|
22690
|
-
drawScoreChart(design, canvas);
|
|
22691
|
-
const imgContent = canvas.toDataURL();
|
|
22692
|
-
div.remove();
|
|
22693
|
-
return imgContent;
|
|
22694
|
-
}
|
|
22695
|
-
else if (type === "gauge") {
|
|
22696
|
-
drawGaugeChart(canvas, runtime);
|
|
22697
|
-
const imgContent = canvas.toDataURL();
|
|
22698
|
-
div.remove();
|
|
22699
|
-
return imgContent;
|
|
22700
|
-
}
|
|
22701
|
-
return undefined;
|
|
22702
|
-
}
|
|
22703
|
-
/**
|
|
22704
|
-
* Custom chart.js plugin to set the background color of the canvas
|
|
22705
|
-
* https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md
|
|
22706
|
-
*/
|
|
22707
|
-
const backgroundColorChartJSPlugin = {
|
|
22708
|
-
id: "customCanvasBackgroundColor",
|
|
22709
|
-
beforeDraw: (chart) => {
|
|
22710
|
-
const { ctx } = chart;
|
|
22711
|
-
ctx.save();
|
|
22712
|
-
ctx.globalCompositeOperation = "destination-over";
|
|
22713
|
-
ctx.fillStyle = "#ffffff";
|
|
22714
|
-
ctx.fillRect(0, 0, chart.width, chart.height);
|
|
22715
|
-
ctx.restore();
|
|
22716
|
-
},
|
|
22717
|
-
};
|
|
22718
|
-
|
|
22719
22791
|
/**
|
|
22720
22792
|
* Represent a raw XML string
|
|
22721
22793
|
*/
|
|
@@ -22777,6 +22849,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
22777
22849
|
const CONTENT_TYPES = {
|
|
22778
22850
|
workbook: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml",
|
|
22779
22851
|
sheet: "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml",
|
|
22852
|
+
metadata: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheetMetadata+xml",
|
|
22780
22853
|
sharedStrings: "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml",
|
|
22781
22854
|
styles: "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml",
|
|
22782
22855
|
drawing: "application/vnd.openxmlformats-officedocument.drawing+xml",
|
|
@@ -22789,6 +22862,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
22789
22862
|
const XLSX_RELATION_TYPE = {
|
|
22790
22863
|
document: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument",
|
|
22791
22864
|
sheet: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet",
|
|
22865
|
+
metadata: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sheetMetadata",
|
|
22792
22866
|
sharedStrings: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings",
|
|
22793
22867
|
styles: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles",
|
|
22794
22868
|
drawing: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing",
|
|
@@ -22798,6 +22872,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
22798
22872
|
hyperlink: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
|
|
22799
22873
|
image: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
|
|
22800
22874
|
};
|
|
22875
|
+
const ARRAY_FORMULA_URI = "bdbb8cdc-fa1e-496e-a857-3c3f30c029c3";
|
|
22801
22876
|
const RELATIONSHIP_NSR = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
|
22802
22877
|
const HEIGHT_FACTOR = 0.75; // 100px => 75 u
|
|
22803
22878
|
/**
|
|
@@ -24317,16 +24392,25 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
24317
24392
|
}
|
|
24318
24393
|
return id;
|
|
24319
24394
|
}
|
|
24395
|
+
const globalReverseLookup = new WeakMap();
|
|
24320
24396
|
function pushElement(property, propertyList) {
|
|
24321
|
-
let
|
|
24322
|
-
|
|
24323
|
-
|
|
24324
|
-
|
|
24325
|
-
|
|
24397
|
+
let reverseLookup = globalReverseLookup.get(propertyList);
|
|
24398
|
+
if (!reverseLookup) {
|
|
24399
|
+
reverseLookup = new Map();
|
|
24400
|
+
for (let i = 0; i < propertyList.length; i++) {
|
|
24401
|
+
const canonical = getCanonicalRepresentation(propertyList[i]);
|
|
24402
|
+
reverseLookup.set(canonical, i);
|
|
24326
24403
|
}
|
|
24404
|
+
globalReverseLookup.set(propertyList, reverseLookup);
|
|
24327
24405
|
}
|
|
24328
|
-
|
|
24329
|
-
|
|
24406
|
+
const canonical = getCanonicalRepresentation(property);
|
|
24407
|
+
if (reverseLookup.has(canonical)) {
|
|
24408
|
+
return reverseLookup.get(canonical);
|
|
24409
|
+
}
|
|
24410
|
+
const maxId = propertyList.length;
|
|
24411
|
+
propertyList.push(property);
|
|
24412
|
+
reverseLookup.set(canonical, maxId);
|
|
24413
|
+
return maxId;
|
|
24330
24414
|
}
|
|
24331
24415
|
const chartIds = [];
|
|
24332
24416
|
/**
|
|
@@ -25363,29 +25447,34 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
25363
25447
|
* In all the sheets, replace the table-only references in the formula cells with standard references.
|
|
25364
25448
|
*/
|
|
25365
25449
|
function convertTableFormulaReferences(convertedSheets, xlsxSheets) {
|
|
25366
|
-
for (let
|
|
25367
|
-
const tables = xlsxSheets.find((s) => s.sheetName ===
|
|
25450
|
+
for (let tableSheet of convertedSheets) {
|
|
25451
|
+
const tables = xlsxSheets.find((s) => s.sheetName === tableSheet.name).tables;
|
|
25368
25452
|
for (let table of tables) {
|
|
25369
25453
|
const tabRef = table.name + "[";
|
|
25370
|
-
for (let
|
|
25371
|
-
|
|
25372
|
-
|
|
25373
|
-
|
|
25374
|
-
|
|
25375
|
-
|
|
25376
|
-
|
|
25377
|
-
|
|
25378
|
-
|
|
25379
|
-
|
|
25380
|
-
|
|
25381
|
-
|
|
25454
|
+
for (let sheet of convertedSheets) {
|
|
25455
|
+
for (let xc in sheet.cells) {
|
|
25456
|
+
const cell = sheet.cells[xc];
|
|
25457
|
+
let cellContent = sheet.cells[xc];
|
|
25458
|
+
if (cell && cellContent && cellContent.startsWith("=")) {
|
|
25459
|
+
let refIndex;
|
|
25460
|
+
while ((refIndex = cellContent.indexOf(tabRef)) !== -1) {
|
|
25461
|
+
let endIndex = refIndex + tabRef.length;
|
|
25462
|
+
let openBrackets = 1;
|
|
25463
|
+
while (openBrackets > 0 && endIndex < cellContent.length) {
|
|
25464
|
+
if (cellContent[endIndex] === "[") {
|
|
25465
|
+
openBrackets++;
|
|
25466
|
+
}
|
|
25467
|
+
else if (cellContent[endIndex] === "]") {
|
|
25468
|
+
openBrackets--;
|
|
25469
|
+
}
|
|
25470
|
+
endIndex++;
|
|
25471
|
+
}
|
|
25472
|
+
let reference = cellContent.slice(refIndex + tabRef.length, endIndex - 1);
|
|
25473
|
+
const sheetPrefix = tableSheet.id === sheet.id ? "" : tableSheet.name + "!";
|
|
25474
|
+
const convertedRef = convertTableReference(sheetPrefix, reference, table, xc);
|
|
25475
|
+
cellContent =
|
|
25476
|
+
cellContent.slice(0, refIndex) + convertedRef + cellContent.slice(endIndex);
|
|
25382
25477
|
}
|
|
25383
|
-
reference = reference.slice(0, endIndex);
|
|
25384
|
-
const convertedRef = convertTableReference(reference, table, xc);
|
|
25385
|
-
cellContent =
|
|
25386
|
-
cellContent.slice(0, refIndex) +
|
|
25387
|
-
convertedRef +
|
|
25388
|
-
cellContent.slice(tabRef.length + refIndex + endIndex + 1);
|
|
25389
25478
|
}
|
|
25390
25479
|
sheet.cells[xc] = cellContent;
|
|
25391
25480
|
}
|
|
@@ -25394,11 +25483,17 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
25394
25483
|
}
|
|
25395
25484
|
}
|
|
25396
25485
|
/**
|
|
25397
|
-
* Convert table-specific references in formulas into standard references.
|
|
25486
|
+
* Convert table-specific references in formulas into standard references. A table reference is composed of columns names,
|
|
25487
|
+
* and of keywords determining the rows of the table to reference.
|
|
25398
25488
|
*
|
|
25399
25489
|
* A reference in a table can have the form (only the part between brackets should be given to this function):
|
|
25400
25490
|
* - tableName[colName] : reference to the whole column "colName"
|
|
25491
|
+
* - tableName[#keyword] : reference to the whatever row the keyword refers to
|
|
25401
25492
|
* - tableName[[#keyword], [colName]] : reference to some of the element(s) of the column colName
|
|
25493
|
+
* - tableName[[#keyword], [colName]:[col2Name]] : reference to some of the element(s) of the columns colName to col2Name
|
|
25494
|
+
* - tableName[[#keyword1], [#keyword2], [colName]] : reference to all the rows referenced by the keywords in the column colName
|
|
25495
|
+
* - tableName[[#keyword1], [colName], [#keyword2]]: the keywords and colName can be in any order
|
|
25496
|
+
*
|
|
25402
25497
|
*
|
|
25403
25498
|
* The available keywords are :
|
|
25404
25499
|
* - #All : all the column (including totals)
|
|
@@ -25406,58 +25501,109 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
25406
25501
|
* - #Headers : only the header of the column
|
|
25407
25502
|
* - #Totals : only the totals of the column
|
|
25408
25503
|
* - #This Row : only the element in the same row as the cell
|
|
25504
|
+
*
|
|
25505
|
+
* Note that the only valid combination of multiple keywords are #Data + #Totals and #Headers + #Data.
|
|
25409
25506
|
*/
|
|
25410
|
-
function convertTableReference(expr, table, cellXc) {
|
|
25411
|
-
|
|
25507
|
+
function convertTableReference(sheetPrefix, expr, table, cellXc) {
|
|
25508
|
+
// TODO: Ideally we'd want to make a real tokenizer, this simple approach won't work if for example the column name
|
|
25509
|
+
// contain # or , characters. But that's probably an edge case that we can ignore for now.
|
|
25510
|
+
const parts = expr.split(",").map((part) => part.trim());
|
|
25412
25511
|
const tableZone = toZone(table.ref);
|
|
25413
|
-
const
|
|
25414
|
-
|
|
25415
|
-
|
|
25416
|
-
|
|
25417
|
-
|
|
25418
|
-
|
|
25419
|
-
|
|
25420
|
-
|
|
25421
|
-
|
|
25422
|
-
|
|
25423
|
-
|
|
25512
|
+
const colIndexes = [];
|
|
25513
|
+
const rowIndexes = [];
|
|
25514
|
+
const foundKeywords = [];
|
|
25515
|
+
for (const part of parts) {
|
|
25516
|
+
if (removeBrackets(part).startsWith("#")) {
|
|
25517
|
+
const keyWord = removeBrackets(part);
|
|
25518
|
+
foundKeywords.push(keyWord);
|
|
25519
|
+
switch (keyWord) {
|
|
25520
|
+
case "#All":
|
|
25521
|
+
rowIndexes.push(tableZone.top, tableZone.bottom);
|
|
25522
|
+
break;
|
|
25523
|
+
case "#Data":
|
|
25524
|
+
const top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
|
|
25525
|
+
const bottom = table.totalsRowCount
|
|
25526
|
+
? tableZone.bottom - table.totalsRowCount
|
|
25527
|
+
: tableZone.bottom;
|
|
25528
|
+
rowIndexes.push(top, bottom);
|
|
25529
|
+
break;
|
|
25530
|
+
case "#This Row":
|
|
25531
|
+
rowIndexes.push(toCartesian(cellXc).row);
|
|
25532
|
+
break;
|
|
25533
|
+
case "#Headers":
|
|
25534
|
+
if (!table.headerRowCount) {
|
|
25535
|
+
return CellErrorType.InvalidReference;
|
|
25536
|
+
}
|
|
25537
|
+
rowIndexes.push(tableZone.top);
|
|
25538
|
+
break;
|
|
25539
|
+
case "#Totals":
|
|
25540
|
+
if (!table.totalsRowCount) {
|
|
25541
|
+
return CellErrorType.InvalidReference;
|
|
25542
|
+
}
|
|
25543
|
+
rowIndexes.push(tableZone.bottom);
|
|
25544
|
+
break;
|
|
25545
|
+
}
|
|
25424
25546
|
}
|
|
25425
|
-
|
|
25426
|
-
|
|
25427
|
-
|
|
25428
|
-
|
|
25429
|
-
|
|
25430
|
-
|
|
25431
|
-
|
|
25432
|
-
|
|
25433
|
-
|
|
25434
|
-
|
|
25435
|
-
|
|
25436
|
-
|
|
25437
|
-
|
|
25438
|
-
|
|
25439
|
-
|
|
25440
|
-
|
|
25441
|
-
|
|
25442
|
-
if (!table.headerRowCount) {
|
|
25443
|
-
isReferencedZoneValid = false;
|
|
25444
|
-
}
|
|
25445
|
-
break;
|
|
25446
|
-
case "#Totals":
|
|
25447
|
-
refZone.top = refZone.bottom = tableZone.bottom;
|
|
25448
|
-
if (!table.totalsRowCount) {
|
|
25449
|
-
isReferencedZoneValid = false;
|
|
25547
|
+
else {
|
|
25548
|
+
const columns = part
|
|
25549
|
+
.split(":")
|
|
25550
|
+
.map((part) => part.trim())
|
|
25551
|
+
.map(removeBrackets);
|
|
25552
|
+
if (colIndexes.length) {
|
|
25553
|
+
return CellErrorType.InvalidReference;
|
|
25554
|
+
}
|
|
25555
|
+
const colRelativeIndex = table.cols.findIndex((col) => col.name === columns[0]);
|
|
25556
|
+
if (colRelativeIndex === -1) {
|
|
25557
|
+
return CellErrorType.InvalidReference;
|
|
25558
|
+
}
|
|
25559
|
+
colIndexes.push(colRelativeIndex + tableZone.left);
|
|
25560
|
+
if (columns[1]) {
|
|
25561
|
+
const colRelativeIndex2 = table.cols.findIndex((col) => col.name === columns[1]);
|
|
25562
|
+
if (colRelativeIndex2 === -1) {
|
|
25563
|
+
return CellErrorType.InvalidReference;
|
|
25450
25564
|
}
|
|
25451
|
-
|
|
25565
|
+
colIndexes.push(colRelativeIndex2 + tableZone.left);
|
|
25566
|
+
}
|
|
25452
25567
|
}
|
|
25453
|
-
const colRef = refElements[1].slice(1, refElements[1].length - 1);
|
|
25454
|
-
const colRelativeIndex = table.cols.findIndex((col) => col.name === colRef);
|
|
25455
|
-
refZone.left = refZone.right = colRelativeIndex + tableZone.left;
|
|
25456
25568
|
}
|
|
25457
|
-
if (!
|
|
25569
|
+
if (!areKeywordsCompatible(foundKeywords)) {
|
|
25458
25570
|
return CellErrorType.InvalidReference;
|
|
25459
25571
|
}
|
|
25460
|
-
|
|
25572
|
+
if (rowIndexes.length === 0) {
|
|
25573
|
+
const top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
|
|
25574
|
+
const bottom = table.totalsRowCount
|
|
25575
|
+
? tableZone.bottom - table.totalsRowCount
|
|
25576
|
+
: tableZone.bottom;
|
|
25577
|
+
rowIndexes.push(top, bottom);
|
|
25578
|
+
}
|
|
25579
|
+
if (colIndexes.length === 0) {
|
|
25580
|
+
colIndexes.push(tableZone.left, tableZone.right);
|
|
25581
|
+
}
|
|
25582
|
+
const refZone = {
|
|
25583
|
+
top: Math.min(...rowIndexes),
|
|
25584
|
+
left: Math.min(...colIndexes),
|
|
25585
|
+
bottom: Math.max(...rowIndexes),
|
|
25586
|
+
right: Math.max(...colIndexes),
|
|
25587
|
+
};
|
|
25588
|
+
return sheetPrefix + zoneToXc(refZone);
|
|
25589
|
+
}
|
|
25590
|
+
function removeBrackets(str) {
|
|
25591
|
+
return str.startsWith("[") && str.endsWith("]") ? str.slice(1, str.length - 1) : str;
|
|
25592
|
+
}
|
|
25593
|
+
function areKeywordsCompatible(keywords) {
|
|
25594
|
+
if (keywords.length < 2) {
|
|
25595
|
+
return true;
|
|
25596
|
+
}
|
|
25597
|
+
else if (keywords.length > 2) {
|
|
25598
|
+
return false;
|
|
25599
|
+
}
|
|
25600
|
+
else if (keywords.includes("#Data") && keywords.includes("#Totals")) {
|
|
25601
|
+
return true;
|
|
25602
|
+
}
|
|
25603
|
+
else if (keywords.includes("#Headers") && keywords.includes("#Data")) {
|
|
25604
|
+
return true;
|
|
25605
|
+
}
|
|
25606
|
+
return false;
|
|
25461
25607
|
}
|
|
25462
25608
|
|
|
25463
25609
|
// -------------------------------------
|
|
@@ -26111,7 +26257,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
26111
26257
|
title: { text: chartTitle },
|
|
26112
26258
|
type: CHART_TYPE_CONVERSION_MAP[chartType],
|
|
26113
26259
|
dataSets: this.extractChartDatasets(this.querySelectorAll(rootChartElement, `c:${chartType}`), chartType),
|
|
26114
|
-
labelRange: this.
|
|
26260
|
+
labelRange: this.extractLabelRange(chartType, rootChartElement),
|
|
26115
26261
|
backgroundColor: this.extractChildAttr(rootChartElement, "c:chartSpace > c:spPr a:srgbClr", "val", {
|
|
26116
26262
|
default: "ffffff",
|
|
26117
26263
|
}).asString(),
|
|
@@ -26123,6 +26269,13 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
26123
26269
|
};
|
|
26124
26270
|
})[0];
|
|
26125
26271
|
}
|
|
26272
|
+
extractLabelRange(chartType, rootChartElement) {
|
|
26273
|
+
if (chartType === "scatterChart") {
|
|
26274
|
+
return (this.extractChildTextContent(rootChartElement, `c:ser c:strRef c:f`) ||
|
|
26275
|
+
this.extractChildTextContent(rootChartElement, `c:ser c:numRef c:f`));
|
|
26276
|
+
}
|
|
26277
|
+
return this.extractChildTextContent(rootChartElement, `c:ser c:cat c:f`);
|
|
26278
|
+
}
|
|
26126
26279
|
extractComboChart(chartElement) {
|
|
26127
26280
|
// Title can be separated into multiple xml elements (for styling and such), we only import the text
|
|
26128
26281
|
const chartTitle = this.mapOnElements({ parent: chartElement, query: "c:title a:t" }, (textElement) => {
|
|
@@ -28625,11 +28778,12 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
28625
28778
|
}
|
|
28626
28779
|
let missingTimeAdapterAlreadyWarned = false;
|
|
28627
28780
|
function isLuxonTimeAdapterInstalled() {
|
|
28628
|
-
|
|
28781
|
+
const Chart = getChartJSConstructor();
|
|
28782
|
+
if (!Chart) {
|
|
28629
28783
|
return false;
|
|
28630
28784
|
}
|
|
28631
28785
|
// @ts-ignore
|
|
28632
|
-
const adapter = new
|
|
28786
|
+
const adapter = new Chart._adapters._date({});
|
|
28633
28787
|
const isInstalled = adapter._id === "luxon";
|
|
28634
28788
|
if (!isInstalled && !missingTimeAdapterAlreadyWarned) {
|
|
28635
28789
|
missingTimeAdapterAlreadyWarned = true;
|
|
@@ -32326,10 +32480,6 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
32326
32480
|
this.currentDisplayValue = newDisplay;
|
|
32327
32481
|
if (!anchor)
|
|
32328
32482
|
return;
|
|
32329
|
-
el.style.top = "";
|
|
32330
|
-
el.style.left = "";
|
|
32331
|
-
el.style["max-height"] = "";
|
|
32332
|
-
el.style["max-width"] = "";
|
|
32333
32483
|
const propsMaxSize = { width: this.props.maxWidth, height: this.props.maxHeight };
|
|
32334
32484
|
let elDims = {
|
|
32335
32485
|
width: el.getBoundingClientRect().width,
|
|
@@ -33867,6 +34017,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
33867
34017
|
drawScoreChart: drawScoreChart,
|
|
33868
34018
|
formatChartDatasetValue: formatChartDatasetValue,
|
|
33869
34019
|
formatTickValue: formatTickValue,
|
|
34020
|
+
getChartJSConstructor: getChartJSConstructor,
|
|
33870
34021
|
getChartPositionAtCenterOfViewport: getChartPositionAtCenterOfViewport,
|
|
33871
34022
|
getDefinedAxis: getDefinedAxis,
|
|
33872
34023
|
getPieColors: getPieColors,
|
|
@@ -36126,6 +36277,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
36126
36277
|
fingerprintStore.enable();
|
|
36127
36278
|
}
|
|
36128
36279
|
},
|
|
36280
|
+
isReadonlyAllowed: true,
|
|
36129
36281
|
icon: "o-spreadsheet-Icon.IRREGULARITY_MAP",
|
|
36130
36282
|
};
|
|
36131
36283
|
const viewFormulas = {
|
|
@@ -37750,6 +37902,9 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
37750
37902
|
this.state.datasetDispatchResult = this.props.updateChart(this.props.figureId, {
|
|
37751
37903
|
dataSets: this.dataSeriesRanges,
|
|
37752
37904
|
});
|
|
37905
|
+
if (this.state.datasetDispatchResult.isSuccessful) {
|
|
37906
|
+
this.dataSeriesRanges = this.env.model.getters.getChartDefinition(this.props.figureId).dataSets;
|
|
37907
|
+
}
|
|
37753
37908
|
}
|
|
37754
37909
|
getDataSeriesRanges() {
|
|
37755
37910
|
return this.dataSeriesRanges;
|
|
@@ -40389,8 +40544,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
40389
40544
|
}
|
|
40390
40545
|
|
|
40391
40546
|
.o-composer-assistant {
|
|
40392
|
-
|
|
40393
|
-
margin: 1px 4px;
|
|
40547
|
+
margin-top: 1px;
|
|
40394
40548
|
|
|
40395
40549
|
.o-semi-bold {
|
|
40396
40550
|
/* FIXME: to remove in favor of Bootstrap
|
|
@@ -40441,10 +40595,11 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
40441
40595
|
});
|
|
40442
40596
|
compositionActive = false;
|
|
40443
40597
|
spreadsheetRect = useSpreadsheetRect();
|
|
40444
|
-
get
|
|
40598
|
+
get assistantStyleProperties() {
|
|
40445
40599
|
const composerRect = this.composerRef.el.getBoundingClientRect();
|
|
40446
40600
|
const assistantStyle = {};
|
|
40447
|
-
|
|
40601
|
+
const minWidth = Math.min(this.props.rect?.width || Infinity, ASSISTANT_WIDTH);
|
|
40602
|
+
assistantStyle["min-width"] = `${minWidth}px`;
|
|
40448
40603
|
const proposals = this.autoCompleteState.provider?.proposals;
|
|
40449
40604
|
const proposalsHaveDescription = proposals?.some((proposal) => proposal.description);
|
|
40450
40605
|
if (this.functionDescriptionState.showDescription || proposalsHaveDescription) {
|
|
@@ -40468,13 +40623,29 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
40468
40623
|
}
|
|
40469
40624
|
}
|
|
40470
40625
|
else {
|
|
40471
|
-
assistantStyle["max-height"] = `${this.spreadsheetRect.height - composerRect.bottom}px`;
|
|
40626
|
+
assistantStyle["max-height"] = `${this.spreadsheetRect.height - composerRect.bottom - 1}px`; // -1: margin
|
|
40472
40627
|
if (composerRect.left + ASSISTANT_WIDTH + SCROLLBAR_WIDTH + CLOSE_ICON_RADIUS >
|
|
40473
40628
|
this.spreadsheetRect.width) {
|
|
40474
40629
|
assistantStyle.right = `${CLOSE_ICON_RADIUS}px`;
|
|
40475
40630
|
}
|
|
40476
40631
|
}
|
|
40477
|
-
return
|
|
40632
|
+
return assistantStyle;
|
|
40633
|
+
}
|
|
40634
|
+
get assistantStyle() {
|
|
40635
|
+
const allProperties = this.assistantStyleProperties;
|
|
40636
|
+
return cssPropertiesToCss({
|
|
40637
|
+
"max-height": allProperties["max-height"],
|
|
40638
|
+
width: allProperties["width"],
|
|
40639
|
+
"min-width": allProperties["min-width"],
|
|
40640
|
+
});
|
|
40641
|
+
}
|
|
40642
|
+
get assistantContainerStyle() {
|
|
40643
|
+
const allProperties = this.assistantStyleProperties;
|
|
40644
|
+
return cssPropertiesToCss({
|
|
40645
|
+
top: allProperties["top"],
|
|
40646
|
+
right: allProperties["right"],
|
|
40647
|
+
transform: allProperties["transform"],
|
|
40648
|
+
});
|
|
40478
40649
|
}
|
|
40479
40650
|
// we can't allow input events to be triggered while we remove and add back the content of the composer in processContent
|
|
40480
40651
|
shouldProcessInputEvents = false;
|
|
@@ -46411,9 +46582,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
46411
46582
|
pivot: this.draft,
|
|
46412
46583
|
});
|
|
46413
46584
|
this.draft = null;
|
|
46414
|
-
if (!this.alreadyNotified &&
|
|
46415
|
-
!this.isDynamicPivotInViewport() &&
|
|
46416
|
-
this.isStaticPivotInViewport()) {
|
|
46585
|
+
if (!this.alreadyNotified && this.isUpdatedPivotVisibleInViewportOnlyAsStaticPivot()) {
|
|
46417
46586
|
const formulaId = this.getters.getPivotFormulaId(this.pivotId);
|
|
46418
46587
|
const pivotExample = `=PIVOT(${formulaId})`;
|
|
46419
46588
|
this.alreadyNotified = true;
|
|
@@ -46469,29 +46638,33 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
46469
46638
|
this.applyUpdate();
|
|
46470
46639
|
}
|
|
46471
46640
|
}
|
|
46472
|
-
|
|
46473
|
-
|
|
46474
|
-
|
|
46475
|
-
|
|
46476
|
-
|
|
46477
|
-
|
|
46478
|
-
|
|
46479
|
-
}
|
|
46480
|
-
}
|
|
46481
|
-
}
|
|
46482
|
-
return false;
|
|
46483
|
-
}
|
|
46484
|
-
isStaticPivotInViewport() {
|
|
46641
|
+
/**
|
|
46642
|
+
* @returns true if the updated pivot is visible in the viewport only as a
|
|
46643
|
+
* static pivot and not as a dynamic pivot
|
|
46644
|
+
*/
|
|
46645
|
+
isUpdatedPivotVisibleInViewportOnlyAsStaticPivot() {
|
|
46646
|
+
let staticPivotCount = 0;
|
|
46647
|
+
const updatedPivotFormulaId = this.getters.getPivotFormulaId(this.pivotId);
|
|
46485
46648
|
for (const position of this.getters.getVisibleCellPositions()) {
|
|
46486
46649
|
const cell = this.getters.getCell(position);
|
|
46487
46650
|
if (cell?.isFormula) {
|
|
46488
46651
|
const pivotFunction = getFirstPivotFunction(cell.compiledFormula.tokens);
|
|
46489
|
-
|
|
46490
|
-
|
|
46652
|
+
const pivotFormulaId = pivotFunction?.args[0]?.value;
|
|
46653
|
+
if (pivotFunction && updatedPivotFormulaId === pivotFormulaId.toString()) {
|
|
46654
|
+
if (pivotFunction.functionName === "PIVOT") {
|
|
46655
|
+
// if we have at least one dynamic pivot visible inserted the viewport
|
|
46656
|
+
// we return false
|
|
46657
|
+
return false;
|
|
46658
|
+
}
|
|
46659
|
+
else {
|
|
46660
|
+
staticPivotCount++;
|
|
46661
|
+
}
|
|
46491
46662
|
}
|
|
46492
46663
|
}
|
|
46493
46664
|
}
|
|
46494
|
-
return
|
|
46665
|
+
// we return true if there are only static pivots visible inserted the viewport,
|
|
46666
|
+
// otherwise false
|
|
46667
|
+
return staticPivotCount > 0;
|
|
46495
46668
|
}
|
|
46496
46669
|
addDefaultDateTimeGranularity(fields, definition) {
|
|
46497
46670
|
const { columns, rows } = definition;
|
|
@@ -50310,6 +50483,9 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
50310
50483
|
this.MAX_SIZE_MARGIN = 90;
|
|
50311
50484
|
this.MIN_ELEMENT_SIZE = MIN_COL_WIDTH;
|
|
50312
50485
|
}
|
|
50486
|
+
get sheetId() {
|
|
50487
|
+
return this.env.model.getters.getActiveSheetId();
|
|
50488
|
+
}
|
|
50313
50489
|
_getEvOffset(ev) {
|
|
50314
50490
|
return ev.offsetX;
|
|
50315
50491
|
}
|
|
@@ -50332,10 +50508,10 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
50332
50508
|
return this.env.model.getters.getEdgeScrollCol(position, position, position);
|
|
50333
50509
|
}
|
|
50334
50510
|
_getDimensionsInViewport(index) {
|
|
50335
|
-
return this.env.model.getters.getColDimensionsInViewport(this.
|
|
50511
|
+
return this.env.model.getters.getColDimensionsInViewport(this.sheetId, index);
|
|
50336
50512
|
}
|
|
50337
50513
|
_getElementSize(index) {
|
|
50338
|
-
return this.env.model.getters.getColSize(this.
|
|
50514
|
+
return this.env.model.getters.getColSize(this.sheetId, index);
|
|
50339
50515
|
}
|
|
50340
50516
|
_getMaxSize() {
|
|
50341
50517
|
return this.colResizerRef.el.clientWidth;
|
|
@@ -50346,7 +50522,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
50346
50522
|
const cols = this.env.model.getters.getActiveCols();
|
|
50347
50523
|
this.env.model.dispatch("RESIZE_COLUMNS_ROWS", {
|
|
50348
50524
|
dimension: "COL",
|
|
50349
|
-
sheetId: this.
|
|
50525
|
+
sheetId: this.sheetId,
|
|
50350
50526
|
elements: cols.has(index) ? [...cols] : [index],
|
|
50351
50527
|
size,
|
|
50352
50528
|
});
|
|
@@ -50359,7 +50535,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
50359
50535
|
elements.push(colIndex);
|
|
50360
50536
|
}
|
|
50361
50537
|
const result = this.env.model.dispatch("MOVE_COLUMNS_ROWS", {
|
|
50362
|
-
sheetId: this.
|
|
50538
|
+
sheetId: this.sheetId,
|
|
50363
50539
|
dimension: "COL",
|
|
50364
50540
|
base: this.state.base,
|
|
50365
50541
|
elements,
|
|
@@ -50378,7 +50554,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
50378
50554
|
_fitElementSize(index) {
|
|
50379
50555
|
const cols = this.env.model.getters.getActiveCols();
|
|
50380
50556
|
this.env.model.dispatch("AUTORESIZE_COLUMNS", {
|
|
50381
|
-
sheetId: this.
|
|
50557
|
+
sheetId: this.sheetId,
|
|
50382
50558
|
cols: cols.has(index) ? [...cols] : [index],
|
|
50383
50559
|
});
|
|
50384
50560
|
}
|
|
@@ -50389,7 +50565,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
50389
50565
|
return this.env.model.getters.getActiveCols();
|
|
50390
50566
|
}
|
|
50391
50567
|
_getPreviousVisibleElement(index) {
|
|
50392
|
-
const sheetId = this.
|
|
50568
|
+
const sheetId = this.sheetId;
|
|
50393
50569
|
let row;
|
|
50394
50570
|
for (row = index - 1; row >= 0; row--) {
|
|
50395
50571
|
if (!this.env.model.getters.isColHidden(sheetId, row)) {
|
|
@@ -50400,7 +50576,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
50400
50576
|
}
|
|
50401
50577
|
unhide(hiddenElements) {
|
|
50402
50578
|
this.env.model.dispatch("UNHIDE_COLUMNS_ROWS", {
|
|
50403
|
-
sheetId: this.
|
|
50579
|
+
sheetId: this.sheetId,
|
|
50404
50580
|
elements: hiddenElements,
|
|
50405
50581
|
dimension: "COL",
|
|
50406
50582
|
});
|
|
@@ -50416,7 +50592,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
50416
50592
|
left: 0;
|
|
50417
50593
|
right: 0;
|
|
50418
50594
|
width: ${HEADER_WIDTH}px;
|
|
50419
|
-
height: 100
|
|
50595
|
+
height: calc(100% - ${HEADER_HEIGHT + SCROLLBAR_WIDTH}px);
|
|
50420
50596
|
&.o-dragging {
|
|
50421
50597
|
cursor: grabbing;
|
|
50422
50598
|
}
|
|
@@ -50474,6 +50650,9 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
50474
50650
|
this.MIN_ELEMENT_SIZE = MIN_ROW_HEIGHT;
|
|
50475
50651
|
}
|
|
50476
50652
|
rowResizerRef;
|
|
50653
|
+
get sheetId() {
|
|
50654
|
+
return this.env.model.getters.getActiveSheetId();
|
|
50655
|
+
}
|
|
50477
50656
|
_getEvOffset(ev) {
|
|
50478
50657
|
return ev.offsetY;
|
|
50479
50658
|
}
|
|
@@ -50496,10 +50675,10 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
50496
50675
|
return this.env.model.getters.getEdgeScrollRow(position, position, position);
|
|
50497
50676
|
}
|
|
50498
50677
|
_getDimensionsInViewport(index) {
|
|
50499
|
-
return this.env.model.getters.getRowDimensionsInViewport(this.
|
|
50678
|
+
return this.env.model.getters.getRowDimensionsInViewport(this.sheetId, index);
|
|
50500
50679
|
}
|
|
50501
50680
|
_getElementSize(index) {
|
|
50502
|
-
return this.env.model.getters.getRowSize(this.
|
|
50681
|
+
return this.env.model.getters.getRowSize(this.sheetId, index);
|
|
50503
50682
|
}
|
|
50504
50683
|
_getMaxSize() {
|
|
50505
50684
|
return this.rowResizerRef.el.clientHeight;
|
|
@@ -50510,7 +50689,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
50510
50689
|
const rows = this.env.model.getters.getActiveRows();
|
|
50511
50690
|
this.env.model.dispatch("RESIZE_COLUMNS_ROWS", {
|
|
50512
50691
|
dimension: "ROW",
|
|
50513
|
-
sheetId: this.
|
|
50692
|
+
sheetId: this.sheetId,
|
|
50514
50693
|
elements: rows.has(index) ? [...rows] : [index],
|
|
50515
50694
|
size,
|
|
50516
50695
|
});
|
|
@@ -50523,7 +50702,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
50523
50702
|
elements.push(rowIndex);
|
|
50524
50703
|
}
|
|
50525
50704
|
const result = this.env.model.dispatch("MOVE_COLUMNS_ROWS", {
|
|
50526
|
-
sheetId: this.
|
|
50705
|
+
sheetId: this.sheetId,
|
|
50527
50706
|
dimension: "ROW",
|
|
50528
50707
|
base: this.state.base,
|
|
50529
50708
|
elements,
|
|
@@ -50542,7 +50721,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
50542
50721
|
_fitElementSize(index) {
|
|
50543
50722
|
const rows = this.env.model.getters.getActiveRows();
|
|
50544
50723
|
this.env.model.dispatch("AUTORESIZE_ROWS", {
|
|
50545
|
-
sheetId: this.
|
|
50724
|
+
sheetId: this.sheetId,
|
|
50546
50725
|
rows: rows.has(index) ? [...rows] : [index],
|
|
50547
50726
|
});
|
|
50548
50727
|
}
|
|
@@ -50553,7 +50732,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
50553
50732
|
return this.env.model.getters.getActiveRows();
|
|
50554
50733
|
}
|
|
50555
50734
|
_getPreviousVisibleElement(index) {
|
|
50556
|
-
const sheetId = this.
|
|
50735
|
+
const sheetId = this.sheetId;
|
|
50557
50736
|
let row;
|
|
50558
50737
|
for (row = index - 1; row >= 0; row--) {
|
|
50559
50738
|
if (!this.env.model.getters.isRowHidden(sheetId, row)) {
|
|
@@ -50564,7 +50743,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
50564
50743
|
}
|
|
50565
50744
|
unhide(hiddenElements) {
|
|
50566
50745
|
this.env.model.dispatch("UNHIDE_COLUMNS_ROWS", {
|
|
50567
|
-
sheetId: this.
|
|
50746
|
+
sheetId: this.sheetId,
|
|
50568
50747
|
dimension: "ROW",
|
|
50569
50748
|
elements: hiddenElements,
|
|
50570
50749
|
});
|
|
@@ -60331,6 +60510,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
60331
60510
|
exportForExcel(data) {
|
|
60332
60511
|
for (const sheet of data.sheets) {
|
|
60333
60512
|
sheet.cellValues = {};
|
|
60513
|
+
sheet.formulaSpillRanges = {};
|
|
60334
60514
|
}
|
|
60335
60515
|
for (const position of this.evaluator.getEvaluatedPositions()) {
|
|
60336
60516
|
const evaluatedCell = this.evaluator.getEvaluatedCell(position);
|
|
@@ -60342,8 +60522,9 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
60342
60522
|
const exportedSheetData = data.sheets.find((sheet) => sheet.id === position.sheetId);
|
|
60343
60523
|
const formulaCell = this.getCorrespondingFormulaCell(position);
|
|
60344
60524
|
if (formulaCell) {
|
|
60525
|
+
const cell = this.getters.getCell(position);
|
|
60345
60526
|
isExported = isExportableToExcel(formulaCell.compiledFormula.tokens);
|
|
60346
|
-
isFormula = isExported;
|
|
60527
|
+
isFormula = isExported && cell?.content === formulaCell.content;
|
|
60347
60528
|
// If the cell contains a non-exported formula and that is evaluates to
|
|
60348
60529
|
// nothing* ,we don't export it.
|
|
60349
60530
|
// * non-falsy value are relevant and so are 0 and FALSE, which only leaves
|
|
@@ -60366,7 +60547,11 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
60366
60547
|
content = !isExported ? newContent : exportedCellData;
|
|
60367
60548
|
}
|
|
60368
60549
|
exportedSheetData.cells[xc] = content;
|
|
60369
|
-
exportedSheetData.cellValues[xc] = value;
|
|
60550
|
+
exportedSheetData.cellValues[xc] = evaluatedCell.type !== "error" ? value : undefined;
|
|
60551
|
+
const spillZone = this.getSpreadZone(position);
|
|
60552
|
+
if (spillZone) {
|
|
60553
|
+
exportedSheetData.formulaSpillRanges[xc] = this.getters.getRangeString(this.getters.getRangeFromZone(position.sheetId, spillZone), position.sheetId);
|
|
60554
|
+
}
|
|
60370
60555
|
}
|
|
60371
60556
|
}
|
|
60372
60557
|
/**
|
|
@@ -62573,7 +62758,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
62573
62758
|
getRule(cell, cells) {
|
|
62574
62759
|
const rules = autofillRulesRegistry.getAll().sort((a, b) => a.sequence - b.sequence);
|
|
62575
62760
|
const rule = rules.find((rule) => rule.condition(cell, cells));
|
|
62576
|
-
return rule && rule.generateRule(cell, cells);
|
|
62761
|
+
return rule && this.direction && rule.generateRule(cell, cells, this.direction);
|
|
62577
62762
|
}
|
|
62578
62763
|
/**
|
|
62579
62764
|
* Create the generator to be able to autofill the next cells.
|
|
@@ -67797,7 +67982,8 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
67797
67982
|
? this.getters.getSheetViewVisibleCols()
|
|
67798
67983
|
: this.getters.getSheetViewVisibleRows();
|
|
67799
67984
|
const startIndex = visibleHeaders.findIndex((header) => referenceHeaderIndex >= header);
|
|
67800
|
-
|
|
67985
|
+
let endIndex = visibleHeaders.findIndex((header) => targetHeaderIndex <= header);
|
|
67986
|
+
endIndex = endIndex === -1 ? visibleHeaders.length : endIndex;
|
|
67801
67987
|
const relevantIndexes = visibleHeaders.slice(startIndex, endIndex);
|
|
67802
67988
|
let offset = 0;
|
|
67803
67989
|
for (const i of relevantIndexes) {
|
|
@@ -73103,7 +73289,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
73103
73289
|
`;
|
|
73104
73290
|
}
|
|
73105
73291
|
|
|
73106
|
-
function addFormula(formula, value) {
|
|
73292
|
+
function addFormula(formula, value, formulaSpillRange) {
|
|
73107
73293
|
if (!formula) {
|
|
73108
73294
|
return { attrs: [], node: escapeXml `` };
|
|
73109
73295
|
}
|
|
@@ -73111,10 +73297,17 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
73111
73297
|
if (type === undefined) {
|
|
73112
73298
|
return { attrs: [], node: escapeXml `` };
|
|
73113
73299
|
}
|
|
73114
|
-
const attrs = [
|
|
73300
|
+
const attrs = [
|
|
73301
|
+
["cm", "1"],
|
|
73302
|
+
["t", type],
|
|
73303
|
+
];
|
|
73115
73304
|
const XlsxFormula = adaptFormulaToExcel(formula);
|
|
73116
73305
|
const exportedValue = adaptFormulaValueToExcel(value);
|
|
73117
|
-
|
|
73306
|
+
// We treat all formulas as array formulas (a simple formula
|
|
73307
|
+
// is an array formula that spills on only one cell) to avoid
|
|
73308
|
+
// trying to detect spilling sub-formulas which is not a trivial task.
|
|
73309
|
+
let node;
|
|
73310
|
+
node = escapeXml /*xml*/ `<f t="array" ref="${formulaSpillRange}">${XlsxFormula}</f><v>${exportedValue}</v>`;
|
|
73118
73311
|
return { attrs, node };
|
|
73119
73312
|
}
|
|
73120
73313
|
function addContent(content, sharedStrings, forceString = false) {
|
|
@@ -73897,7 +74090,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
73897
74090
|
}
|
|
73898
74091
|
if (alignAttrs.length > 0) {
|
|
73899
74092
|
attributes.push(["applyAlignment", "1"]); // for Libre Office
|
|
73900
|
-
styleNodes.push(escapeXml /*xml*/ `<xf ${formatAttributes(attributes)}
|
|
74093
|
+
styleNodes.push(escapeXml /*xml*/ `<xf ${formatAttributes(attributes)}><alignment ${formatAttributes(alignAttrs)} /></xf> `);
|
|
73901
74094
|
}
|
|
73902
74095
|
else {
|
|
73903
74096
|
styleNodes.push(escapeXml /*xml*/ `<xf ${formatAttributes(attributes)} />`);
|
|
@@ -74065,6 +74258,9 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
74065
74258
|
}
|
|
74066
74259
|
function addRows(construct, data, sheet) {
|
|
74067
74260
|
const rowNodes = [];
|
|
74261
|
+
const styles = new PositionMap(iterateItemIdsPositions(sheet.id, sheet.styles));
|
|
74262
|
+
const borders = new PositionMap(iterateItemIdsPositions(sheet.id, sheet.borders));
|
|
74263
|
+
const formats = new PositionMap(iterateItemIdsPositions(sheet.id, sheet.formats));
|
|
74068
74264
|
for (let r = 0; r < sheet.rowNumber; r++) {
|
|
74069
74265
|
const rowAttrs = [["r", r + 1]];
|
|
74070
74266
|
const row = sheet.rows[r] || {};
|
|
@@ -74080,9 +74276,6 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
74080
74276
|
if (row.collapsed) {
|
|
74081
74277
|
rowAttrs.push(["collapsed", 1]);
|
|
74082
74278
|
}
|
|
74083
|
-
const styles = new PositionMap(iterateItemIdsPositions(sheet.id, sheet.styles));
|
|
74084
|
-
const borders = new PositionMap(iterateItemIdsPositions(sheet.id, sheet.borders));
|
|
74085
|
-
const formats = new PositionMap(iterateItemIdsPositions(sheet.id, sheet.formats));
|
|
74086
74279
|
const cellNodes = [];
|
|
74087
74280
|
for (let c = 0; c < sheet.colNumber; c++) {
|
|
74088
74281
|
const xc = toXC(c, r);
|
|
@@ -74104,7 +74297,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
74104
74297
|
let cellNode = escapeXml ``;
|
|
74105
74298
|
// Either formula or static value inside the cell
|
|
74106
74299
|
if (content?.startsWith("=") && value !== undefined) {
|
|
74107
|
-
const res = addFormula(content, value);
|
|
74300
|
+
const res = addFormula(content, value, sheet.formulaSpillRanges[xc] ?? xc);
|
|
74108
74301
|
if (!res) {
|
|
74109
74302
|
continue;
|
|
74110
74303
|
}
|
|
@@ -74390,6 +74583,30 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
74390
74583
|
`;
|
|
74391
74584
|
files.push(createXMLFile(parseXML(sheetXml), `xl/worksheets/sheet${sheetIndex}.xml`, "sheet"));
|
|
74392
74585
|
}
|
|
74586
|
+
const sheetMetadataXml = escapeXml /*xml*/ `
|
|
74587
|
+
<metadata xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:xda="http://schemas.microsoft.com/office/spreadsheetml/2017/dynamicarray">
|
|
74588
|
+
<metadataTypes count="1">
|
|
74589
|
+
<metadataType name="XLDAPR" minSupportedVersion="120000" copy="1" pasteAll="1"
|
|
74590
|
+
pasteValues="1" merge="1" splitFirst="1" rowColShift="1" clearFormats="1"
|
|
74591
|
+
clearComments="1" assign="1" coerce="1" cellMeta="1" />
|
|
74592
|
+
</metadataTypes>
|
|
74593
|
+
<futureMetadata name="XLDAPR" count="1">
|
|
74594
|
+
<bk>
|
|
74595
|
+
<extLst>
|
|
74596
|
+
<ext uri="{${ARRAY_FORMULA_URI}}">
|
|
74597
|
+
<xda:dynamicArrayProperties fDynamic="1" fCollapsed="0" />
|
|
74598
|
+
</ext>
|
|
74599
|
+
</extLst>
|
|
74600
|
+
</bk>
|
|
74601
|
+
</futureMetadata>
|
|
74602
|
+
<cellMetadata count="1">
|
|
74603
|
+
<bk>
|
|
74604
|
+
<rc t="1" v="0" />
|
|
74605
|
+
</bk>
|
|
74606
|
+
</cellMetadata>
|
|
74607
|
+
</metadata>
|
|
74608
|
+
`;
|
|
74609
|
+
files.push(createXMLFile(parseXML(sheetMetadataXml), "xl/metadata.xml", "metadata"));
|
|
74393
74610
|
addRelsToFile(construct.relsFiles, "xl/_rels/workbook.xml.rels", {
|
|
74394
74611
|
type: XLSX_RELATION_TYPE.sharedStrings,
|
|
74395
74612
|
target: "sharedStrings.xml",
|
|
@@ -74398,6 +74615,10 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
74398
74615
|
type: XLSX_RELATION_TYPE.styles,
|
|
74399
74616
|
target: "styles.xml",
|
|
74400
74617
|
});
|
|
74618
|
+
addRelsToFile(construct.relsFiles, "xl/_rels/workbook.xml.rels", {
|
|
74619
|
+
type: XLSX_RELATION_TYPE.metadata,
|
|
74620
|
+
target: "metadata.xml",
|
|
74621
|
+
});
|
|
74401
74622
|
return files;
|
|
74402
74623
|
}
|
|
74403
74624
|
/**
|
|
@@ -75326,9 +75547,9 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
75326
75547
|
exports.tokenize = tokenize;
|
|
75327
75548
|
|
|
75328
75549
|
|
|
75329
|
-
__info__.version = "18.1.
|
|
75330
|
-
__info__.date = "2025-
|
|
75331
|
-
__info__.hash = "
|
|
75550
|
+
__info__.version = "18.1.11";
|
|
75551
|
+
__info__.date = "2025-03-12T15:31:44.276Z";
|
|
75552
|
+
__info__.hash = "7de2363";
|
|
75332
75553
|
|
|
75333
75554
|
|
|
75334
75555
|
})(this.o_spreadsheet = this.o_spreadsheet || {}, owl);
|