@odoo/o-spreadsheet 18.1.9 → 18.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/o-spreadsheet.cjs.js +722 -581
- package/dist/o-spreadsheet.d.ts +12 -3
- package/dist/o-spreadsheet.esm.js +722 -581
- package/dist/o-spreadsheet.iife.js +722 -581
- package/dist/o-spreadsheet.iife.min.js +397 -376
- package/dist/o_spreadsheet.xml +31 -26
- package/package.json +1 -1
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* This file is generated by o-spreadsheet build tools. Do not edit it.
|
|
4
4
|
* @see https://github.com/odoo/o-spreadsheet
|
|
5
|
-
* @version 18.1.
|
|
6
|
-
* @date 2025-
|
|
7
|
-
* @hash
|
|
5
|
+
* @version 18.1.10
|
|
6
|
+
* @date 2025-03-07T10:34:41.861Z
|
|
7
|
+
* @hash 31e4526
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
(function (exports, owl) {
|
|
@@ -6092,8 +6092,9 @@
|
|
|
6092
6092
|
if (zone.bottom !== zone.top && zone.left != zone.right) {
|
|
6093
6093
|
if (zone.right) {
|
|
6094
6094
|
for (let j = zone.left; j <= zone.right; ++j) {
|
|
6095
|
+
const datasetOptions = j === zone.left ? dataSet : { yAxisId: dataSet.yAxisId };
|
|
6095
6096
|
postProcessedRanges.push({
|
|
6096
|
-
...
|
|
6097
|
+
...datasetOptions,
|
|
6097
6098
|
dataRange: `${sheetPrefix}${zoneToXc({
|
|
6098
6099
|
left: j,
|
|
6099
6100
|
right: j,
|
|
@@ -6105,8 +6106,9 @@
|
|
|
6105
6106
|
}
|
|
6106
6107
|
else {
|
|
6107
6108
|
for (let j = zone.top; j <= zone.bottom; ++j) {
|
|
6109
|
+
const datasetOptions = j === zone.top ? dataSet : { yAxisId: dataSet.yAxisId };
|
|
6108
6110
|
postProcessedRanges.push({
|
|
6109
|
-
...
|
|
6111
|
+
...datasetOptions,
|
|
6110
6112
|
dataRange: `${sheetPrefix}${zoneToXc({
|
|
6111
6113
|
left: zone.left,
|
|
6112
6114
|
right: zone.right,
|
|
@@ -10057,70 +10059,341 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
10057
10059
|
return bars.find((bar, i) => i > startIndex && bar.height !== 0);
|
|
10058
10060
|
}
|
|
10059
10061
|
|
|
10060
|
-
|
|
10061
|
-
|
|
10062
|
-
|
|
10063
|
-
|
|
10064
|
-
|
|
10065
|
-
|
|
10062
|
+
const GAUGE_PADDING_SIDE = 30;
|
|
10063
|
+
const GAUGE_PADDING_TOP = 10;
|
|
10064
|
+
const GAUGE_PADDING_BOTTOM = 20;
|
|
10065
|
+
const GAUGE_LABELS_FONT_SIZE = 12;
|
|
10066
|
+
const GAUGE_DEFAULT_VALUE_FONT_SIZE = 80;
|
|
10067
|
+
const GAUGE_BACKGROUND_COLOR = "#F3F2F1";
|
|
10068
|
+
const GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN = 6;
|
|
10069
|
+
const GAUGE_TITLE_SECTION_HEIGHT = 25;
|
|
10070
|
+
function drawGaugeChart(canvas, runtime) {
|
|
10071
|
+
const canvasBoundingRect = canvas.getBoundingClientRect();
|
|
10072
|
+
canvas.width = canvasBoundingRect.width;
|
|
10073
|
+
canvas.height = canvasBoundingRect.height;
|
|
10074
|
+
const ctx = canvas.getContext("2d");
|
|
10075
|
+
const config = getGaugeRenderingConfig(canvasBoundingRect, runtime, ctx);
|
|
10076
|
+
drawBackground(ctx, config);
|
|
10077
|
+
drawGauge(ctx, config);
|
|
10078
|
+
drawInflectionValues(ctx, config);
|
|
10079
|
+
drawLabels(ctx, config);
|
|
10080
|
+
drawTitle(ctx, config);
|
|
10081
|
+
}
|
|
10082
|
+
function drawGauge(ctx, config) {
|
|
10083
|
+
ctx.save();
|
|
10084
|
+
const gauge = config.gauge;
|
|
10085
|
+
const arcCenterX = gauge.rect.x + gauge.rect.width / 2;
|
|
10086
|
+
const arcCenterY = gauge.rect.y + gauge.rect.height;
|
|
10087
|
+
const arcRadius = gauge.rect.height - gauge.arcWidth / 2;
|
|
10088
|
+
if (arcRadius < 0) {
|
|
10089
|
+
return;
|
|
10090
|
+
}
|
|
10091
|
+
const gaugeAngle = gauge.percentage === 1 ? 0 : Math.PI * (1 + gauge.percentage);
|
|
10092
|
+
// Gauge background
|
|
10093
|
+
ctx.strokeStyle = GAUGE_BACKGROUND_COLOR;
|
|
10094
|
+
ctx.beginPath();
|
|
10095
|
+
ctx.lineWidth = gauge.arcWidth;
|
|
10096
|
+
ctx.arc(arcCenterX, arcCenterY, arcRadius, gaugeAngle, 0);
|
|
10097
|
+
ctx.stroke();
|
|
10098
|
+
// Gauge value
|
|
10099
|
+
ctx.strokeStyle = gauge.color;
|
|
10100
|
+
ctx.beginPath();
|
|
10101
|
+
ctx.arc(arcCenterX, arcCenterY, arcRadius, Math.PI, gaugeAngle);
|
|
10102
|
+
ctx.stroke();
|
|
10103
|
+
ctx.restore();
|
|
10104
|
+
}
|
|
10105
|
+
function drawBackground(ctx, config) {
|
|
10106
|
+
ctx.save();
|
|
10107
|
+
ctx.fillStyle = config.backgroundColor;
|
|
10108
|
+
ctx.fillRect(0, 0, config.width, config.height);
|
|
10109
|
+
ctx.restore();
|
|
10110
|
+
}
|
|
10111
|
+
function drawLabels(ctx, config) {
|
|
10112
|
+
for (const label of [config.minLabel, config.maxLabel, config.gaugeValue]) {
|
|
10113
|
+
ctx.save();
|
|
10114
|
+
ctx.textAlign = "center";
|
|
10115
|
+
ctx.fillStyle = label.color;
|
|
10116
|
+
ctx.font = `${label.fontSize}px ${DEFAULT_FONT}`;
|
|
10117
|
+
ctx.fillText(label.label, label.textPosition.x, label.textPosition.y);
|
|
10118
|
+
ctx.restore();
|
|
10119
|
+
}
|
|
10120
|
+
}
|
|
10121
|
+
function drawInflectionValues(ctx, config) {
|
|
10122
|
+
const { x: rectX, y: rectY, width, height } = config.gauge.rect;
|
|
10123
|
+
for (const inflectionValue of config.inflectionValues) {
|
|
10124
|
+
ctx.save();
|
|
10125
|
+
ctx.translate(rectX + width / 2 - 0.5, rectY + height - 0.5); // -0.5 for sharper lines. see RendererPlugin.drawBorders comment
|
|
10126
|
+
ctx.rotate(Math.PI / 2 - inflectionValue.rotation);
|
|
10127
|
+
ctx.lineWidth = 2;
|
|
10128
|
+
ctx.strokeStyle = chartMutedFontColor(config.backgroundColor) + "aa";
|
|
10129
|
+
ctx.beginPath();
|
|
10130
|
+
ctx.moveTo(0, -(height - config.gauge.arcWidth));
|
|
10131
|
+
ctx.lineTo(0, -height - 3);
|
|
10132
|
+
ctx.stroke();
|
|
10133
|
+
ctx.textAlign = "center";
|
|
10134
|
+
ctx.font = `${inflectionValue.fontSize}px ${DEFAULT_FONT}`;
|
|
10135
|
+
ctx.fillStyle = inflectionValue.color;
|
|
10136
|
+
const textY = -height - GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN - inflectionValue.offset;
|
|
10137
|
+
ctx.fillText(inflectionValue.label, 0, textY);
|
|
10138
|
+
ctx.restore();
|
|
10139
|
+
}
|
|
10140
|
+
}
|
|
10141
|
+
function drawTitle(ctx, config) {
|
|
10142
|
+
ctx.save();
|
|
10143
|
+
const title = config.title;
|
|
10144
|
+
ctx.font = getDefaultContextFont(title.fontSize, title.bold, title.italic);
|
|
10145
|
+
ctx.textBaseline = "middle";
|
|
10146
|
+
ctx.fillStyle = title.color;
|
|
10147
|
+
ctx.fillText(title.label, title.textPosition.x, title.textPosition.y);
|
|
10148
|
+
ctx.restore();
|
|
10149
|
+
}
|
|
10150
|
+
function getGaugeRenderingConfig(boundingRect, runtime, ctx) {
|
|
10151
|
+
const maxValue = runtime.maxValue;
|
|
10152
|
+
const minValue = runtime.minValue;
|
|
10153
|
+
const gaugeValue = runtime.gaugeValue;
|
|
10154
|
+
const gaugeRect = getGaugeRect(boundingRect, runtime.title.text);
|
|
10155
|
+
const gaugeArcWidth = gaugeRect.width / 6;
|
|
10156
|
+
const gaugePercentage = gaugeValue
|
|
10157
|
+
? (gaugeValue.value - minValue.value) / (maxValue.value - minValue.value)
|
|
10158
|
+
: 0;
|
|
10159
|
+
const gaugeValuePosition = {
|
|
10160
|
+
x: boundingRect.width / 2,
|
|
10161
|
+
y: gaugeRect.y + gaugeRect.height - gaugeRect.height / 12,
|
|
10066
10162
|
};
|
|
10067
|
-
|
|
10068
|
-
|
|
10069
|
-
|
|
10070
|
-
|
|
10071
|
-
return this.chartRuntime.background;
|
|
10163
|
+
let gaugeValueFontSize = GAUGE_DEFAULT_VALUE_FONT_SIZE;
|
|
10164
|
+
// Scale down the font size if the gaugeRect is too small
|
|
10165
|
+
if (gaugeRect.height < 300) {
|
|
10166
|
+
gaugeValueFontSize = gaugeValueFontSize * (gaugeRect.height / 300);
|
|
10072
10167
|
}
|
|
10073
|
-
|
|
10074
|
-
|
|
10168
|
+
// Scale down the font size if the text is too long
|
|
10169
|
+
const maxTextWidth = gaugeRect.width / 2;
|
|
10170
|
+
const gaugeLabel = gaugeValue?.label || "-";
|
|
10171
|
+
if (computeTextWidth(ctx, gaugeLabel, { fontSize: gaugeValueFontSize }, "px") > maxTextWidth) {
|
|
10172
|
+
gaugeValueFontSize = getFontSizeMatchingWidth(maxTextWidth, gaugeValueFontSize, (fontSize) => computeTextWidth(ctx, gaugeLabel, { fontSize }, "px"));
|
|
10075
10173
|
}
|
|
10076
|
-
|
|
10077
|
-
|
|
10078
|
-
|
|
10079
|
-
|
|
10080
|
-
|
|
10081
|
-
|
|
10174
|
+
const minLabelPosition = {
|
|
10175
|
+
x: gaugeRect.x + gaugeArcWidth / 2,
|
|
10176
|
+
y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
|
|
10177
|
+
};
|
|
10178
|
+
const maxLabelPosition = {
|
|
10179
|
+
x: gaugeRect.x + gaugeRect.width - gaugeArcWidth / 2,
|
|
10180
|
+
y: gaugeRect.y + gaugeRect.height + GAUGE_LABELS_FONT_SIZE,
|
|
10181
|
+
};
|
|
10182
|
+
const textColor = chartMutedFontColor(runtime.background);
|
|
10183
|
+
const inflectionValues = getInflectionValues(runtime, gaugeRect, textColor, ctx);
|
|
10184
|
+
let x = 0, titleWidth = 0, titleHeight = 0;
|
|
10185
|
+
if (runtime.title.text) {
|
|
10186
|
+
({ width: titleWidth, height: titleHeight } = computeTextDimension(ctx, runtime.title.text, { fontSize: CHART_TITLE_FONT_SIZE, ...runtime.title }, "px"));
|
|
10082
10187
|
}
|
|
10083
|
-
|
|
10084
|
-
|
|
10085
|
-
|
|
10086
|
-
|
|
10087
|
-
|
|
10088
|
-
|
|
10089
|
-
|
|
10090
|
-
|
|
10091
|
-
|
|
10092
|
-
|
|
10093
|
-
|
|
10094
|
-
|
|
10095
|
-
|
|
10096
|
-
|
|
10097
|
-
|
|
10098
|
-
|
|
10099
|
-
|
|
10100
|
-
|
|
10101
|
-
|
|
10102
|
-
|
|
10188
|
+
switch (runtime.title.align) {
|
|
10189
|
+
case "right":
|
|
10190
|
+
x = boundingRect.width - titleWidth - CHART_PADDING$1;
|
|
10191
|
+
break;
|
|
10192
|
+
case "center":
|
|
10193
|
+
x = (boundingRect.width - titleWidth) / 2;
|
|
10194
|
+
break;
|
|
10195
|
+
case "left":
|
|
10196
|
+
default:
|
|
10197
|
+
x = CHART_PADDING$1;
|
|
10198
|
+
break;
|
|
10199
|
+
}
|
|
10200
|
+
return {
|
|
10201
|
+
width: boundingRect.width,
|
|
10202
|
+
height: boundingRect.height,
|
|
10203
|
+
title: {
|
|
10204
|
+
label: runtime.title.text ?? "",
|
|
10205
|
+
fontSize: runtime.title.fontSize ?? CHART_TITLE_FONT_SIZE,
|
|
10206
|
+
textPosition: {
|
|
10207
|
+
x,
|
|
10208
|
+
y: CHART_PADDING_TOP + titleHeight / 2,
|
|
10209
|
+
},
|
|
10210
|
+
color: runtime.title.color ?? textColor,
|
|
10211
|
+
bold: runtime.title.bold,
|
|
10212
|
+
italic: runtime.title.italic,
|
|
10213
|
+
},
|
|
10214
|
+
backgroundColor: runtime.background,
|
|
10215
|
+
gauge: {
|
|
10216
|
+
rect: gaugeRect,
|
|
10217
|
+
arcWidth: gaugeArcWidth,
|
|
10218
|
+
percentage: clip(gaugePercentage, 0, 1),
|
|
10219
|
+
color: getGaugeColor(runtime),
|
|
10220
|
+
},
|
|
10221
|
+
inflectionValues,
|
|
10222
|
+
gaugeValue: {
|
|
10223
|
+
label: gaugeLabel,
|
|
10224
|
+
textPosition: gaugeValuePosition,
|
|
10225
|
+
fontSize: gaugeValueFontSize,
|
|
10226
|
+
color: textColor,
|
|
10227
|
+
},
|
|
10228
|
+
minLabel: {
|
|
10229
|
+
label: runtime.minValue.label,
|
|
10230
|
+
textPosition: minLabelPosition,
|
|
10231
|
+
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
10232
|
+
color: textColor,
|
|
10233
|
+
},
|
|
10234
|
+
maxLabel: {
|
|
10235
|
+
label: runtime.maxValue.label,
|
|
10236
|
+
textPosition: maxLabelPosition,
|
|
10237
|
+
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
10238
|
+
color: textColor,
|
|
10239
|
+
},
|
|
10240
|
+
};
|
|
10241
|
+
}
|
|
10242
|
+
/**
|
|
10243
|
+
* Get the rectangle in which the gauge will be drawn, based on the bounding rectangle of the canvas and leaving
|
|
10244
|
+
* space for the title and labels.
|
|
10245
|
+
*/
|
|
10246
|
+
function getGaugeRect(boundingRect, title) {
|
|
10247
|
+
const titleHeight = title ? GAUGE_TITLE_SECTION_HEIGHT : 0;
|
|
10248
|
+
const drawHeight = boundingRect.height - GAUGE_PADDING_BOTTOM - titleHeight - GAUGE_PADDING_TOP;
|
|
10249
|
+
const drawWidth = boundingRect.width - GAUGE_PADDING_SIDE * 2;
|
|
10250
|
+
let gaugeWidth;
|
|
10251
|
+
let gaugeHeight;
|
|
10252
|
+
if (drawWidth > 2 * drawHeight) {
|
|
10253
|
+
gaugeWidth = 2 * drawHeight;
|
|
10254
|
+
gaugeHeight = drawHeight;
|
|
10255
|
+
}
|
|
10256
|
+
else {
|
|
10257
|
+
gaugeWidth = drawWidth;
|
|
10258
|
+
gaugeHeight = drawWidth / 2;
|
|
10259
|
+
}
|
|
10260
|
+
const gaugeX = GAUGE_PADDING_SIDE + (drawWidth - gaugeWidth) / 2;
|
|
10261
|
+
const gaugeY = titleHeight + GAUGE_PADDING_TOP + (drawHeight - gaugeHeight) / 2;
|
|
10262
|
+
return {
|
|
10263
|
+
x: gaugeX,
|
|
10264
|
+
y: gaugeY,
|
|
10265
|
+
width: gaugeWidth,
|
|
10266
|
+
height: gaugeHeight,
|
|
10267
|
+
};
|
|
10268
|
+
}
|
|
10269
|
+
/**
|
|
10270
|
+
* 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).
|
|
10271
|
+
*
|
|
10272
|
+
* Also compute an offset for the text so that it doesn't overlap with other text.
|
|
10273
|
+
*/
|
|
10274
|
+
function getInflectionValues(runtime, gaugeRect, textColor, ctx) {
|
|
10275
|
+
const maxValue = runtime.maxValue;
|
|
10276
|
+
const minValue = runtime.minValue;
|
|
10277
|
+
const gaugeCircleCenter = {
|
|
10278
|
+
x: gaugeRect.x + gaugeRect.width / 2,
|
|
10279
|
+
y: gaugeRect.y + gaugeRect.height,
|
|
10280
|
+
};
|
|
10281
|
+
const textStyle = { fontSize: GAUGE_LABELS_FONT_SIZE };
|
|
10282
|
+
const inflectionValues = [];
|
|
10283
|
+
const inflectionValuesTextRects = [];
|
|
10284
|
+
for (const inflectionValue of runtime.inflectionValues) {
|
|
10285
|
+
const percentage = (inflectionValue.value - minValue.value) / (maxValue.value - minValue.value);
|
|
10286
|
+
const labelWidth = computeTextWidth(ctx, inflectionValue.label, textStyle, "px");
|
|
10287
|
+
const angle = Math.PI - Math.PI * percentage;
|
|
10288
|
+
const textRect = getRectangleTangentToCircle(angle, // angle between X axis and the point where the rectangle is tangent to the circle
|
|
10289
|
+
gaugeRect.height + GAUGE_INFLECTION_LABEL_BOTTOM_MARGIN, // radius of the gauge circle + margin below text
|
|
10290
|
+
gaugeCircleCenter.x, // center of the gauge circle
|
|
10291
|
+
gaugeCircleCenter.y, // center of the gauge circle
|
|
10292
|
+
labelWidth + 2, // width of the text + some margin
|
|
10293
|
+
GAUGE_LABELS_FONT_SIZE // height of the text
|
|
10294
|
+
);
|
|
10295
|
+
let offset = inflectionValuesTextRects.some((rect) => doRectanglesIntersect(rect, textRect))
|
|
10296
|
+
? GAUGE_LABELS_FONT_SIZE
|
|
10297
|
+
: 0;
|
|
10298
|
+
inflectionValuesTextRects.push(textRect);
|
|
10299
|
+
inflectionValues.push({
|
|
10300
|
+
rotation: angle,
|
|
10301
|
+
label: inflectionValue.label,
|
|
10302
|
+
fontSize: GAUGE_LABELS_FONT_SIZE,
|
|
10303
|
+
color: textColor,
|
|
10304
|
+
offset,
|
|
10103
10305
|
});
|
|
10104
10306
|
}
|
|
10105
|
-
|
|
10106
|
-
|
|
10107
|
-
|
|
10108
|
-
|
|
10307
|
+
return inflectionValues;
|
|
10308
|
+
}
|
|
10309
|
+
function getGaugeColor(runtime) {
|
|
10310
|
+
const gaugeValue = runtime.gaugeValue?.value;
|
|
10311
|
+
if (gaugeValue === undefined) {
|
|
10312
|
+
return GAUGE_BACKGROUND_COLOR;
|
|
10109
10313
|
}
|
|
10110
|
-
|
|
10111
|
-
const
|
|
10112
|
-
if (
|
|
10113
|
-
|
|
10114
|
-
if (chartData.options?.plugins?.title) {
|
|
10115
|
-
this.chart.config.options.plugins.title = chartData.options.plugins.title;
|
|
10116
|
-
}
|
|
10314
|
+
for (let i = 0; i < runtime.inflectionValues.length; i++) {
|
|
10315
|
+
const inflectionValue = runtime.inflectionValues[i];
|
|
10316
|
+
if (inflectionValue.operator === "<" && gaugeValue < inflectionValue.value) {
|
|
10317
|
+
return runtime.colors[i];
|
|
10117
10318
|
}
|
|
10118
|
-
else {
|
|
10119
|
-
|
|
10319
|
+
else if (inflectionValue.operator === "<=" && gaugeValue <= inflectionValue.value) {
|
|
10320
|
+
return runtime.colors[i];
|
|
10120
10321
|
}
|
|
10121
|
-
this.chart.config.options = chartData.options;
|
|
10122
|
-
this.chart.update();
|
|
10123
10322
|
}
|
|
10323
|
+
return runtime.colors.at(-1);
|
|
10324
|
+
}
|
|
10325
|
+
function getSegmentsOfRectangle(rectangle) {
|
|
10326
|
+
return [
|
|
10327
|
+
{ start: rectangle.topLeft, end: rectangle.topRight },
|
|
10328
|
+
{ start: rectangle.topRight, end: rectangle.bottomRight },
|
|
10329
|
+
{ start: rectangle.bottomRight, end: rectangle.bottomLeft },
|
|
10330
|
+
{ start: rectangle.bottomLeft, end: rectangle.topLeft },
|
|
10331
|
+
];
|
|
10332
|
+
}
|
|
10333
|
+
/**
|
|
10334
|
+
* Check if two segment intersect. The case where the segments are colinear (both segments on the same line)
|
|
10335
|
+
* is not handled.
|
|
10336
|
+
*/
|
|
10337
|
+
function doSegmentIntersect(segment1, segment2) {
|
|
10338
|
+
const A = segment1.start;
|
|
10339
|
+
const B = segment1.end;
|
|
10340
|
+
const C = segment2.start;
|
|
10341
|
+
const D = segment2.end;
|
|
10342
|
+
/**
|
|
10343
|
+
* Line segment intersection algorithm
|
|
10344
|
+
* https://bryceboe.com/2006/10/23/line-segment-intersection-algorithm/
|
|
10345
|
+
*/
|
|
10346
|
+
function ccw(a, b, c) {
|
|
10347
|
+
return (c.y - a.y) * (b.x - a.x) > (b.y - a.y) * (c.x - a.x);
|
|
10348
|
+
}
|
|
10349
|
+
return ccw(A, C, D) !== ccw(B, C, D) && ccw(A, B, C) !== ccw(A, B, D);
|
|
10350
|
+
}
|
|
10351
|
+
function doRectanglesIntersect(rect1, rect2) {
|
|
10352
|
+
const segments1 = getSegmentsOfRectangle(rect1);
|
|
10353
|
+
const segments2 = getSegmentsOfRectangle(rect2);
|
|
10354
|
+
for (const segment1 of segments1) {
|
|
10355
|
+
for (const segment2 of segments2) {
|
|
10356
|
+
if (doSegmentIntersect(segment1, segment2)) {
|
|
10357
|
+
return true;
|
|
10358
|
+
}
|
|
10359
|
+
}
|
|
10360
|
+
}
|
|
10361
|
+
return false;
|
|
10362
|
+
}
|
|
10363
|
+
/**
|
|
10364
|
+
* Get the rectangle that is tangent to a circle at a given angle.
|
|
10365
|
+
*
|
|
10366
|
+
* @param angle angle between X axis and the point where the rectangle is tangent to the circle
|
|
10367
|
+
*/
|
|
10368
|
+
function getRectangleTangentToCircle(angle, radius, circleCenterX, circleCenterY, rectWidth, rectHeight) {
|
|
10369
|
+
const cos = Math.cos(angle);
|
|
10370
|
+
const sin = Math.sin(angle);
|
|
10371
|
+
// x, y are the distance from the center of the circle to the point where the rectangle is tangent to the circle
|
|
10372
|
+
const x = cos * radius;
|
|
10373
|
+
const y = sin * radius;
|
|
10374
|
+
// x2, y2 are the distance from the point the rectangle is tangent to the circle to the bottom left corner of the rectangle
|
|
10375
|
+
const x2 = sin * (rectWidth / 2); // cos(angle + 90°) = sin(angle)
|
|
10376
|
+
const y2 = cos * (rectWidth / 2);
|
|
10377
|
+
const bottomRight = {
|
|
10378
|
+
x: x + x2 + circleCenterX,
|
|
10379
|
+
y: circleCenterY - (y - y2),
|
|
10380
|
+
};
|
|
10381
|
+
const bottomLeft = {
|
|
10382
|
+
x: x - x2 + circleCenterX,
|
|
10383
|
+
y: circleCenterY - (y + y2),
|
|
10384
|
+
};
|
|
10385
|
+
// Same as above but for the top corners of the rectangle (radius + rectangle height instead of radius)
|
|
10386
|
+
const xp = cos * (radius + rectHeight);
|
|
10387
|
+
const yp = sin * (radius + rectHeight);
|
|
10388
|
+
const topLeft = {
|
|
10389
|
+
x: xp - x2 + circleCenterX,
|
|
10390
|
+
y: circleCenterY - (yp + y2),
|
|
10391
|
+
};
|
|
10392
|
+
const topRight = {
|
|
10393
|
+
x: xp + x2 + circleCenterX,
|
|
10394
|
+
y: circleCenterY - (yp - y2),
|
|
10395
|
+
};
|
|
10396
|
+
return { bottomLeft, bottomRight, topRight, topLeft };
|
|
10124
10397
|
}
|
|
10125
10398
|
|
|
10126
10399
|
/**
|
|
@@ -10702,6 +10975,155 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
10702
10975
|
}
|
|
10703
10976
|
}
|
|
10704
10977
|
|
|
10978
|
+
const CHART_COMMON_OPTIONS = {
|
|
10979
|
+
// https://www.chartjs.org/docs/latest/general/responsive.html
|
|
10980
|
+
responsive: true, // will resize when its container is resized
|
|
10981
|
+
maintainAspectRatio: false, // doesn't maintain the aspect ratio (width/height =2 by default) so the user has the choice of the exact layout
|
|
10982
|
+
elements: {
|
|
10983
|
+
line: {
|
|
10984
|
+
fill: false, // do not fill the area under line charts
|
|
10985
|
+
},
|
|
10986
|
+
point: {
|
|
10987
|
+
hitRadius: 15, // increased hit radius to display point tooltip when hovering nearby
|
|
10988
|
+
},
|
|
10989
|
+
},
|
|
10990
|
+
animation: false,
|
|
10991
|
+
};
|
|
10992
|
+
function truncateLabel(label) {
|
|
10993
|
+
if (!label) {
|
|
10994
|
+
return "";
|
|
10995
|
+
}
|
|
10996
|
+
if (label.length > MAX_CHAR_LABEL) {
|
|
10997
|
+
return label.substring(0, MAX_CHAR_LABEL) + "…";
|
|
10998
|
+
}
|
|
10999
|
+
return label;
|
|
11000
|
+
}
|
|
11001
|
+
function chartToImage(runtime, figure, type) {
|
|
11002
|
+
// wrap the canvas in a div with a fixed size because chart.js would
|
|
11003
|
+
// fill the whole page otherwise
|
|
11004
|
+
const div = document.createElement("div");
|
|
11005
|
+
div.style.width = `${figure.width}px`;
|
|
11006
|
+
div.style.height = `${figure.height}px`;
|
|
11007
|
+
const canvas = document.createElement("canvas");
|
|
11008
|
+
div.append(canvas);
|
|
11009
|
+
canvas.setAttribute("width", figure.width.toString());
|
|
11010
|
+
canvas.setAttribute("height", figure.height.toString());
|
|
11011
|
+
// we have to add the canvas to the DOM otherwise it won't be rendered
|
|
11012
|
+
document.body.append(div);
|
|
11013
|
+
if ("chartJsConfig" in runtime) {
|
|
11014
|
+
const config = deepCopy(runtime.chartJsConfig);
|
|
11015
|
+
config.plugins = [backgroundColorChartJSPlugin];
|
|
11016
|
+
const Chart = getChartJSConstructor();
|
|
11017
|
+
const chart = new Chart(canvas, config);
|
|
11018
|
+
const imgContent = chart.toBase64Image();
|
|
11019
|
+
chart.destroy();
|
|
11020
|
+
div.remove();
|
|
11021
|
+
return imgContent;
|
|
11022
|
+
}
|
|
11023
|
+
else if (type === "scorecard") {
|
|
11024
|
+
const design = getScorecardConfiguration(figure, runtime);
|
|
11025
|
+
drawScoreChart(design, canvas);
|
|
11026
|
+
const imgContent = canvas.toDataURL();
|
|
11027
|
+
div.remove();
|
|
11028
|
+
return imgContent;
|
|
11029
|
+
}
|
|
11030
|
+
else if (type === "gauge") {
|
|
11031
|
+
drawGaugeChart(canvas, runtime);
|
|
11032
|
+
const imgContent = canvas.toDataURL();
|
|
11033
|
+
div.remove();
|
|
11034
|
+
return imgContent;
|
|
11035
|
+
}
|
|
11036
|
+
return undefined;
|
|
11037
|
+
}
|
|
11038
|
+
/**
|
|
11039
|
+
* Custom chart.js plugin to set the background color of the canvas
|
|
11040
|
+
* https://github.com/chartjs/Chart.js/blob/8fdf76f8f02d31684d34704341a5d9217e977491/docs/configuration/canvas-background.md
|
|
11041
|
+
*/
|
|
11042
|
+
const backgroundColorChartJSPlugin = {
|
|
11043
|
+
id: "customCanvasBackgroundColor",
|
|
11044
|
+
beforeDraw: (chart) => {
|
|
11045
|
+
const { ctx } = chart;
|
|
11046
|
+
ctx.save();
|
|
11047
|
+
ctx.globalCompositeOperation = "destination-over";
|
|
11048
|
+
ctx.fillStyle = "#ffffff";
|
|
11049
|
+
ctx.fillRect(0, 0, chart.width, chart.height);
|
|
11050
|
+
ctx.restore();
|
|
11051
|
+
},
|
|
11052
|
+
};
|
|
11053
|
+
/** Return window.Chart, making sure all our extensions are loaded in ChartJS */
|
|
11054
|
+
function getChartJSConstructor() {
|
|
11055
|
+
if (window.Chart && !window.Chart?.registry.plugins.get("chartShowValuesPlugin")) {
|
|
11056
|
+
window.Chart.register(chartShowValuesPlugin);
|
|
11057
|
+
window.Chart.register(waterfallLinesPlugin);
|
|
11058
|
+
}
|
|
11059
|
+
return window.Chart;
|
|
11060
|
+
}
|
|
11061
|
+
|
|
11062
|
+
class ChartJsComponent extends owl.Component {
|
|
11063
|
+
static template = "o-spreadsheet-ChartJsComponent";
|
|
11064
|
+
static props = {
|
|
11065
|
+
figure: Object,
|
|
11066
|
+
};
|
|
11067
|
+
canvas = owl.useRef("graphContainer");
|
|
11068
|
+
chart;
|
|
11069
|
+
currentRuntime;
|
|
11070
|
+
get background() {
|
|
11071
|
+
return this.chartRuntime.background;
|
|
11072
|
+
}
|
|
11073
|
+
get canvasStyle() {
|
|
11074
|
+
return `background-color: ${this.background}`;
|
|
11075
|
+
}
|
|
11076
|
+
get chartRuntime() {
|
|
11077
|
+
const runtime = this.env.model.getters.getChartRuntime(this.props.figure.id);
|
|
11078
|
+
if (!("chartJsConfig" in runtime)) {
|
|
11079
|
+
throw new Error("Unsupported chart runtime");
|
|
11080
|
+
}
|
|
11081
|
+
return runtime;
|
|
11082
|
+
}
|
|
11083
|
+
setup() {
|
|
11084
|
+
owl.onMounted(() => {
|
|
11085
|
+
const runtime = this.chartRuntime;
|
|
11086
|
+
this.currentRuntime = runtime;
|
|
11087
|
+
// Note: chartJS modify the runtime in place, so it's important to give it a copy
|
|
11088
|
+
this.createChart(deepCopy(runtime.chartJsConfig));
|
|
11089
|
+
});
|
|
11090
|
+
owl.onWillUnmount(() => this.chart?.destroy());
|
|
11091
|
+
owl.useEffect(() => {
|
|
11092
|
+
const runtime = this.chartRuntime;
|
|
11093
|
+
if (runtime !== this.currentRuntime) {
|
|
11094
|
+
if (runtime.chartJsConfig.type !== this.currentRuntime.chartJsConfig.type) {
|
|
11095
|
+
this.chart?.destroy();
|
|
11096
|
+
this.createChart(deepCopy(runtime.chartJsConfig));
|
|
11097
|
+
}
|
|
11098
|
+
else {
|
|
11099
|
+
this.updateChartJs(deepCopy(runtime));
|
|
11100
|
+
}
|
|
11101
|
+
this.currentRuntime = runtime;
|
|
11102
|
+
}
|
|
11103
|
+
});
|
|
11104
|
+
}
|
|
11105
|
+
createChart(chartData) {
|
|
11106
|
+
const canvas = this.canvas.el;
|
|
11107
|
+
const ctx = canvas.getContext("2d");
|
|
11108
|
+
const Chart = getChartJSConstructor();
|
|
11109
|
+
this.chart = new Chart(ctx, chartData);
|
|
11110
|
+
}
|
|
11111
|
+
updateChartJs(chartRuntime) {
|
|
11112
|
+
const chartData = chartRuntime.chartJsConfig;
|
|
11113
|
+
if (chartData.data && chartData.data.datasets) {
|
|
11114
|
+
this.chart.data = chartData.data;
|
|
11115
|
+
if (chartData.options?.plugins?.title) {
|
|
11116
|
+
this.chart.config.options.plugins.title = chartData.options.plugins.title;
|
|
11117
|
+
}
|
|
11118
|
+
}
|
|
11119
|
+
else {
|
|
11120
|
+
this.chart.data.datasets = [];
|
|
11121
|
+
}
|
|
11122
|
+
this.chart.config.options = chartData.options;
|
|
11123
|
+
this.chart.update();
|
|
11124
|
+
}
|
|
11125
|
+
}
|
|
11126
|
+
|
|
10705
11127
|
class ScorecardChart extends owl.Component {
|
|
10706
11128
|
static template = "o-spreadsheet-ScorecardChart";
|
|
10707
11129
|
static props = {
|
|
@@ -22154,7 +22576,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
22154
22576
|
condition: (cell) => !cell.isFormula &&
|
|
22155
22577
|
evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.text &&
|
|
22156
22578
|
alphaNumericValueRegExp.test(cell.content),
|
|
22157
|
-
generateRule: (cell, cells) => {
|
|
22579
|
+
generateRule: (cell, cells, direction) => {
|
|
22158
22580
|
const numberPostfix = parseInt(cell.content.match(numberPostfixRegExp)[0]);
|
|
22159
22581
|
const prefix = cell.content.match(stringPrefixRegExp)[0];
|
|
22160
22582
|
const numberPostfixLength = cell.content.length - prefix.length;
|
|
@@ -22162,7 +22584,10 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
22162
22584
|
alphaNumericValueRegExp.test(evaluatedCell.value)) // get consecutive alphanumeric cells, no matter what the prefix is
|
|
22163
22585
|
.filter((cell) => prefix === (cell.value ?? "").toString().match(stringPrefixRegExp)[0])
|
|
22164
22586
|
.map((cell) => parseInt((cell.value ?? "").toString().match(numberPostfixRegExp)[0]));
|
|
22165
|
-
|
|
22587
|
+
let increment = calculateIncrementBasedOnGroup(group);
|
|
22588
|
+
if (["up", "left"].includes(direction) && group.length === 1) {
|
|
22589
|
+
increment = -increment;
|
|
22590
|
+
}
|
|
22166
22591
|
return {
|
|
22167
22592
|
type: "ALPHANUMERIC_INCREMENT_MODIFIER",
|
|
22168
22593
|
prefix,
|
|
@@ -22225,10 +22650,13 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
22225
22650
|
.add("increment_number", {
|
|
22226
22651
|
condition: (cell) => !cell.isFormula &&
|
|
22227
22652
|
evaluateLiteral(cell, { locale: DEFAULT_LOCALE }).type === CellValueType.number,
|
|
22228
|
-
generateRule: (cell, cells) => {
|
|
22653
|
+
generateRule: (cell, cells, direction) => {
|
|
22229
22654
|
const group = getGroup(cell, cells, (evaluatedCell) => evaluatedCell.type === CellValueType.number &&
|
|
22230
22655
|
!isDateTimeFormat(evaluatedCell.format || "")).map((cell) => Number(cell.value));
|
|
22231
|
-
|
|
22656
|
+
let increment = calculateIncrementBasedOnGroup(group);
|
|
22657
|
+
if (["up", "left"].includes(direction) && group.length === 1) {
|
|
22658
|
+
increment = -increment;
|
|
22659
|
+
}
|
|
22232
22660
|
const evaluation = evaluateLiteral(cell, { locale: DEFAULT_LOCALE });
|
|
22233
22661
|
return {
|
|
22234
22662
|
type: "INCREMENT_MODIFIER",
|
|
@@ -22272,343 +22700,6 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
22272
22700
|
|
|
22273
22701
|
const cellPopoverRegistry = new Registry();
|
|
22274
22702
|
|
|
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
22703
|
class GaugeChartComponent extends owl.Component {
|
|
22613
22704
|
static template = "o-spreadsheet-GaugeChartComponent";
|
|
22614
22705
|
canvas = owl.useRef("chartContainer");
|
|
@@ -22641,81 +22732,6 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
22641
22732
|
return color;
|
|
22642
22733
|
}
|
|
22643
22734
|
|
|
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
22735
|
/**
|
|
22720
22736
|
* Represent a raw XML string
|
|
22721
22737
|
*/
|
|
@@ -22777,6 +22793,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
22777
22793
|
const CONTENT_TYPES = {
|
|
22778
22794
|
workbook: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml",
|
|
22779
22795
|
sheet: "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml",
|
|
22796
|
+
metadata: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheetMetadata+xml",
|
|
22780
22797
|
sharedStrings: "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml",
|
|
22781
22798
|
styles: "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml",
|
|
22782
22799
|
drawing: "application/vnd.openxmlformats-officedocument.drawing+xml",
|
|
@@ -22789,6 +22806,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
22789
22806
|
const XLSX_RELATION_TYPE = {
|
|
22790
22807
|
document: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument",
|
|
22791
22808
|
sheet: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet",
|
|
22809
|
+
metadata: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sheetMetadata",
|
|
22792
22810
|
sharedStrings: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings",
|
|
22793
22811
|
styles: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles",
|
|
22794
22812
|
drawing: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing",
|
|
@@ -22798,6 +22816,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
22798
22816
|
hyperlink: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
|
|
22799
22817
|
image: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image",
|
|
22800
22818
|
};
|
|
22819
|
+
const ARRAY_FORMULA_URI = "bdbb8cdc-fa1e-496e-a857-3c3f30c029c3";
|
|
22801
22820
|
const RELATIONSHIP_NSR = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
|
22802
22821
|
const HEIGHT_FACTOR = 0.75; // 100px => 75 u
|
|
22803
22822
|
/**
|
|
@@ -25363,29 +25382,34 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
25363
25382
|
* In all the sheets, replace the table-only references in the formula cells with standard references.
|
|
25364
25383
|
*/
|
|
25365
25384
|
function convertTableFormulaReferences(convertedSheets, xlsxSheets) {
|
|
25366
|
-
for (let
|
|
25367
|
-
const tables = xlsxSheets.find((s) => s.sheetName ===
|
|
25385
|
+
for (let tableSheet of convertedSheets) {
|
|
25386
|
+
const tables = xlsxSheets.find((s) => s.sheetName === tableSheet.name).tables;
|
|
25368
25387
|
for (let table of tables) {
|
|
25369
25388
|
const tabRef = table.name + "[";
|
|
25370
|
-
for (let
|
|
25371
|
-
|
|
25372
|
-
|
|
25373
|
-
|
|
25374
|
-
|
|
25375
|
-
|
|
25376
|
-
|
|
25377
|
-
|
|
25378
|
-
|
|
25379
|
-
|
|
25380
|
-
|
|
25381
|
-
|
|
25389
|
+
for (let sheet of convertedSheets) {
|
|
25390
|
+
for (let xc in sheet.cells) {
|
|
25391
|
+
const cell = sheet.cells[xc];
|
|
25392
|
+
let cellContent = sheet.cells[xc];
|
|
25393
|
+
if (cell && cellContent && cellContent.startsWith("=")) {
|
|
25394
|
+
let refIndex;
|
|
25395
|
+
while ((refIndex = cellContent.indexOf(tabRef)) !== -1) {
|
|
25396
|
+
let endIndex = refIndex + tabRef.length;
|
|
25397
|
+
let openBrackets = 1;
|
|
25398
|
+
while (openBrackets > 0 && endIndex < cellContent.length) {
|
|
25399
|
+
if (cellContent[endIndex] === "[") {
|
|
25400
|
+
openBrackets++;
|
|
25401
|
+
}
|
|
25402
|
+
else if (cellContent[endIndex] === "]") {
|
|
25403
|
+
openBrackets--;
|
|
25404
|
+
}
|
|
25405
|
+
endIndex++;
|
|
25406
|
+
}
|
|
25407
|
+
let reference = cellContent.slice(refIndex + tabRef.length, endIndex - 1);
|
|
25408
|
+
const sheetPrefix = tableSheet.id === sheet.id ? "" : tableSheet.name + "!";
|
|
25409
|
+
const convertedRef = convertTableReference(sheetPrefix, reference, table, xc);
|
|
25410
|
+
cellContent =
|
|
25411
|
+
cellContent.slice(0, refIndex) + convertedRef + cellContent.slice(endIndex);
|
|
25382
25412
|
}
|
|
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
25413
|
}
|
|
25390
25414
|
sheet.cells[xc] = cellContent;
|
|
25391
25415
|
}
|
|
@@ -25394,11 +25418,17 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
25394
25418
|
}
|
|
25395
25419
|
}
|
|
25396
25420
|
/**
|
|
25397
|
-
* Convert table-specific references in formulas into standard references.
|
|
25421
|
+
* Convert table-specific references in formulas into standard references. A table reference is composed of columns names,
|
|
25422
|
+
* and of keywords determining the rows of the table to reference.
|
|
25398
25423
|
*
|
|
25399
25424
|
* A reference in a table can have the form (only the part between brackets should be given to this function):
|
|
25400
25425
|
* - tableName[colName] : reference to the whole column "colName"
|
|
25426
|
+
* - tableName[#keyword] : reference to the whatever row the keyword refers to
|
|
25401
25427
|
* - tableName[[#keyword], [colName]] : reference to some of the element(s) of the column colName
|
|
25428
|
+
* - tableName[[#keyword], [colName]:[col2Name]] : reference to some of the element(s) of the columns colName to col2Name
|
|
25429
|
+
* - tableName[[#keyword1], [#keyword2], [colName]] : reference to all the rows referenced by the keywords in the column colName
|
|
25430
|
+
* - tableName[[#keyword1], [colName], [#keyword2]]: the keywords and colName can be in any order
|
|
25431
|
+
*
|
|
25402
25432
|
*
|
|
25403
25433
|
* The available keywords are :
|
|
25404
25434
|
* - #All : all the column (including totals)
|
|
@@ -25406,58 +25436,109 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
25406
25436
|
* - #Headers : only the header of the column
|
|
25407
25437
|
* - #Totals : only the totals of the column
|
|
25408
25438
|
* - #This Row : only the element in the same row as the cell
|
|
25439
|
+
*
|
|
25440
|
+
* Note that the only valid combination of multiple keywords are #Data + #Totals and #Headers + #Data.
|
|
25409
25441
|
*/
|
|
25410
|
-
function convertTableReference(expr, table, cellXc) {
|
|
25411
|
-
|
|
25442
|
+
function convertTableReference(sheetPrefix, expr, table, cellXc) {
|
|
25443
|
+
// TODO: Ideally we'd want to make a real tokenizer, this simple approach won't work if for example the column name
|
|
25444
|
+
// contain # or , characters. But that's probably an edge case that we can ignore for now.
|
|
25445
|
+
const parts = expr.split(",").map((part) => part.trim());
|
|
25412
25446
|
const tableZone = toZone(table.ref);
|
|
25413
|
-
const
|
|
25414
|
-
|
|
25415
|
-
|
|
25416
|
-
|
|
25417
|
-
|
|
25418
|
-
|
|
25419
|
-
|
|
25420
|
-
|
|
25421
|
-
|
|
25422
|
-
|
|
25423
|
-
|
|
25447
|
+
const colIndexes = [];
|
|
25448
|
+
const rowIndexes = [];
|
|
25449
|
+
const foundKeywords = [];
|
|
25450
|
+
for (const part of parts) {
|
|
25451
|
+
if (removeBrackets(part).startsWith("#")) {
|
|
25452
|
+
const keyWord = removeBrackets(part);
|
|
25453
|
+
foundKeywords.push(keyWord);
|
|
25454
|
+
switch (keyWord) {
|
|
25455
|
+
case "#All":
|
|
25456
|
+
rowIndexes.push(tableZone.top, tableZone.bottom);
|
|
25457
|
+
break;
|
|
25458
|
+
case "#Data":
|
|
25459
|
+
const top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
|
|
25460
|
+
const bottom = table.totalsRowCount
|
|
25461
|
+
? tableZone.bottom - table.totalsRowCount
|
|
25462
|
+
: tableZone.bottom;
|
|
25463
|
+
rowIndexes.push(top, bottom);
|
|
25464
|
+
break;
|
|
25465
|
+
case "#This Row":
|
|
25466
|
+
rowIndexes.push(toCartesian(cellXc).row);
|
|
25467
|
+
break;
|
|
25468
|
+
case "#Headers":
|
|
25469
|
+
if (!table.headerRowCount) {
|
|
25470
|
+
return CellErrorType.InvalidReference;
|
|
25471
|
+
}
|
|
25472
|
+
rowIndexes.push(tableZone.top);
|
|
25473
|
+
break;
|
|
25474
|
+
case "#Totals":
|
|
25475
|
+
if (!table.totalsRowCount) {
|
|
25476
|
+
return CellErrorType.InvalidReference;
|
|
25477
|
+
}
|
|
25478
|
+
rowIndexes.push(tableZone.bottom);
|
|
25479
|
+
break;
|
|
25480
|
+
}
|
|
25424
25481
|
}
|
|
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;
|
|
25482
|
+
else {
|
|
25483
|
+
const columns = part
|
|
25484
|
+
.split(":")
|
|
25485
|
+
.map((part) => part.trim())
|
|
25486
|
+
.map(removeBrackets);
|
|
25487
|
+
if (colIndexes.length) {
|
|
25488
|
+
return CellErrorType.InvalidReference;
|
|
25489
|
+
}
|
|
25490
|
+
const colRelativeIndex = table.cols.findIndex((col) => col.name === columns[0]);
|
|
25491
|
+
if (colRelativeIndex === -1) {
|
|
25492
|
+
return CellErrorType.InvalidReference;
|
|
25493
|
+
}
|
|
25494
|
+
colIndexes.push(colRelativeIndex + tableZone.left);
|
|
25495
|
+
if (columns[1]) {
|
|
25496
|
+
const colRelativeIndex2 = table.cols.findIndex((col) => col.name === columns[1]);
|
|
25497
|
+
if (colRelativeIndex2 === -1) {
|
|
25498
|
+
return CellErrorType.InvalidReference;
|
|
25450
25499
|
}
|
|
25451
|
-
|
|
25500
|
+
colIndexes.push(colRelativeIndex2 + tableZone.left);
|
|
25501
|
+
}
|
|
25452
25502
|
}
|
|
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
25503
|
}
|
|
25457
|
-
if (!
|
|
25504
|
+
if (!areKeywordsCompatible(foundKeywords)) {
|
|
25458
25505
|
return CellErrorType.InvalidReference;
|
|
25459
25506
|
}
|
|
25460
|
-
|
|
25507
|
+
if (rowIndexes.length === 0) {
|
|
25508
|
+
const top = table.headerRowCount ? tableZone.top + table.headerRowCount : tableZone.top;
|
|
25509
|
+
const bottom = table.totalsRowCount
|
|
25510
|
+
? tableZone.bottom - table.totalsRowCount
|
|
25511
|
+
: tableZone.bottom;
|
|
25512
|
+
rowIndexes.push(top, bottom);
|
|
25513
|
+
}
|
|
25514
|
+
if (colIndexes.length === 0) {
|
|
25515
|
+
colIndexes.push(tableZone.left, tableZone.right);
|
|
25516
|
+
}
|
|
25517
|
+
const refZone = {
|
|
25518
|
+
top: Math.min(...rowIndexes),
|
|
25519
|
+
left: Math.min(...colIndexes),
|
|
25520
|
+
bottom: Math.max(...rowIndexes),
|
|
25521
|
+
right: Math.max(...colIndexes),
|
|
25522
|
+
};
|
|
25523
|
+
return sheetPrefix + zoneToXc(refZone);
|
|
25524
|
+
}
|
|
25525
|
+
function removeBrackets(str) {
|
|
25526
|
+
return str.startsWith("[") && str.endsWith("]") ? str.slice(1, str.length - 1) : str;
|
|
25527
|
+
}
|
|
25528
|
+
function areKeywordsCompatible(keywords) {
|
|
25529
|
+
if (keywords.length < 2) {
|
|
25530
|
+
return true;
|
|
25531
|
+
}
|
|
25532
|
+
else if (keywords.length > 2) {
|
|
25533
|
+
return false;
|
|
25534
|
+
}
|
|
25535
|
+
else if (keywords.includes("#Data") && keywords.includes("#Totals")) {
|
|
25536
|
+
return true;
|
|
25537
|
+
}
|
|
25538
|
+
else if (keywords.includes("#Headers") && keywords.includes("#Data")) {
|
|
25539
|
+
return true;
|
|
25540
|
+
}
|
|
25541
|
+
return false;
|
|
25461
25542
|
}
|
|
25462
25543
|
|
|
25463
25544
|
// -------------------------------------
|
|
@@ -28625,11 +28706,12 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
28625
28706
|
}
|
|
28626
28707
|
let missingTimeAdapterAlreadyWarned = false;
|
|
28627
28708
|
function isLuxonTimeAdapterInstalled() {
|
|
28628
|
-
|
|
28709
|
+
const Chart = getChartJSConstructor();
|
|
28710
|
+
if (!Chart) {
|
|
28629
28711
|
return false;
|
|
28630
28712
|
}
|
|
28631
28713
|
// @ts-ignore
|
|
28632
|
-
const adapter = new
|
|
28714
|
+
const adapter = new Chart._adapters._date({});
|
|
28633
28715
|
const isInstalled = adapter._id === "luxon";
|
|
28634
28716
|
if (!isInstalled && !missingTimeAdapterAlreadyWarned) {
|
|
28635
28717
|
missingTimeAdapterAlreadyWarned = true;
|
|
@@ -32326,10 +32408,6 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
32326
32408
|
this.currentDisplayValue = newDisplay;
|
|
32327
32409
|
if (!anchor)
|
|
32328
32410
|
return;
|
|
32329
|
-
el.style.top = "";
|
|
32330
|
-
el.style.left = "";
|
|
32331
|
-
el.style["max-height"] = "";
|
|
32332
|
-
el.style["max-width"] = "";
|
|
32333
32411
|
const propsMaxSize = { width: this.props.maxWidth, height: this.props.maxHeight };
|
|
32334
32412
|
let elDims = {
|
|
32335
32413
|
width: el.getBoundingClientRect().width,
|
|
@@ -33867,6 +33945,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
33867
33945
|
drawScoreChart: drawScoreChart,
|
|
33868
33946
|
formatChartDatasetValue: formatChartDatasetValue,
|
|
33869
33947
|
formatTickValue: formatTickValue,
|
|
33948
|
+
getChartJSConstructor: getChartJSConstructor,
|
|
33870
33949
|
getChartPositionAtCenterOfViewport: getChartPositionAtCenterOfViewport,
|
|
33871
33950
|
getDefinedAxis: getDefinedAxis,
|
|
33872
33951
|
getPieColors: getPieColors,
|
|
@@ -37750,6 +37829,9 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
37750
37829
|
this.state.datasetDispatchResult = this.props.updateChart(this.props.figureId, {
|
|
37751
37830
|
dataSets: this.dataSeriesRanges,
|
|
37752
37831
|
});
|
|
37832
|
+
if (this.state.datasetDispatchResult.isSuccessful) {
|
|
37833
|
+
this.dataSeriesRanges = this.env.model.getters.getChartDefinition(this.props.figureId).dataSets;
|
|
37834
|
+
}
|
|
37753
37835
|
}
|
|
37754
37836
|
getDataSeriesRanges() {
|
|
37755
37837
|
return this.dataSeriesRanges;
|
|
@@ -40389,8 +40471,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
40389
40471
|
}
|
|
40390
40472
|
|
|
40391
40473
|
.o-composer-assistant {
|
|
40392
|
-
|
|
40393
|
-
margin: 1px 4px;
|
|
40474
|
+
margin-top: 1px;
|
|
40394
40475
|
|
|
40395
40476
|
.o-semi-bold {
|
|
40396
40477
|
/* FIXME: to remove in favor of Bootstrap
|
|
@@ -40441,10 +40522,11 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
40441
40522
|
});
|
|
40442
40523
|
compositionActive = false;
|
|
40443
40524
|
spreadsheetRect = useSpreadsheetRect();
|
|
40444
|
-
get
|
|
40525
|
+
get assistantStyleProperties() {
|
|
40445
40526
|
const composerRect = this.composerRef.el.getBoundingClientRect();
|
|
40446
40527
|
const assistantStyle = {};
|
|
40447
|
-
|
|
40528
|
+
const minWidth = Math.min(this.props.rect?.width || Infinity, ASSISTANT_WIDTH);
|
|
40529
|
+
assistantStyle["min-width"] = `${minWidth}px`;
|
|
40448
40530
|
const proposals = this.autoCompleteState.provider?.proposals;
|
|
40449
40531
|
const proposalsHaveDescription = proposals?.some((proposal) => proposal.description);
|
|
40450
40532
|
if (this.functionDescriptionState.showDescription || proposalsHaveDescription) {
|
|
@@ -40468,13 +40550,29 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
40468
40550
|
}
|
|
40469
40551
|
}
|
|
40470
40552
|
else {
|
|
40471
|
-
assistantStyle["max-height"] = `${this.spreadsheetRect.height - composerRect.bottom}px`;
|
|
40553
|
+
assistantStyle["max-height"] = `${this.spreadsheetRect.height - composerRect.bottom - 1}px`; // -1: margin
|
|
40472
40554
|
if (composerRect.left + ASSISTANT_WIDTH + SCROLLBAR_WIDTH + CLOSE_ICON_RADIUS >
|
|
40473
40555
|
this.spreadsheetRect.width) {
|
|
40474
40556
|
assistantStyle.right = `${CLOSE_ICON_RADIUS}px`;
|
|
40475
40557
|
}
|
|
40476
40558
|
}
|
|
40477
|
-
return
|
|
40559
|
+
return assistantStyle;
|
|
40560
|
+
}
|
|
40561
|
+
get assistantStyle() {
|
|
40562
|
+
const allProperties = this.assistantStyleProperties;
|
|
40563
|
+
return cssPropertiesToCss({
|
|
40564
|
+
"max-height": allProperties["max-height"],
|
|
40565
|
+
width: allProperties["width"],
|
|
40566
|
+
"min-width": allProperties["min-width"],
|
|
40567
|
+
});
|
|
40568
|
+
}
|
|
40569
|
+
get assistantContainerStyle() {
|
|
40570
|
+
const allProperties = this.assistantStyleProperties;
|
|
40571
|
+
return cssPropertiesToCss({
|
|
40572
|
+
top: allProperties["top"],
|
|
40573
|
+
right: allProperties["right"],
|
|
40574
|
+
transform: allProperties["transform"],
|
|
40575
|
+
});
|
|
40478
40576
|
}
|
|
40479
40577
|
// we can't allow input events to be triggered while we remove and add back the content of the composer in processContent
|
|
40480
40578
|
shouldProcessInputEvents = false;
|
|
@@ -46411,9 +46509,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
46411
46509
|
pivot: this.draft,
|
|
46412
46510
|
});
|
|
46413
46511
|
this.draft = null;
|
|
46414
|
-
if (!this.alreadyNotified &&
|
|
46415
|
-
!this.isDynamicPivotInViewport() &&
|
|
46416
|
-
this.isStaticPivotInViewport()) {
|
|
46512
|
+
if (!this.alreadyNotified && this.isUpdatedPivotVisibleInViewportOnlyAsStaticPivot()) {
|
|
46417
46513
|
const formulaId = this.getters.getPivotFormulaId(this.pivotId);
|
|
46418
46514
|
const pivotExample = `=PIVOT(${formulaId})`;
|
|
46419
46515
|
this.alreadyNotified = true;
|
|
@@ -46469,29 +46565,33 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
46469
46565
|
this.applyUpdate();
|
|
46470
46566
|
}
|
|
46471
46567
|
}
|
|
46472
|
-
|
|
46473
|
-
|
|
46474
|
-
|
|
46475
|
-
|
|
46476
|
-
|
|
46477
|
-
|
|
46478
|
-
|
|
46479
|
-
}
|
|
46480
|
-
}
|
|
46481
|
-
}
|
|
46482
|
-
return false;
|
|
46483
|
-
}
|
|
46484
|
-
isStaticPivotInViewport() {
|
|
46568
|
+
/**
|
|
46569
|
+
* @returns true if the updated pivot is visible in the viewport only as a
|
|
46570
|
+
* static pivot and not as a dynamic pivot
|
|
46571
|
+
*/
|
|
46572
|
+
isUpdatedPivotVisibleInViewportOnlyAsStaticPivot() {
|
|
46573
|
+
let staticPivotCount = 0;
|
|
46574
|
+
const updatedPivotFormulaId = this.getters.getPivotFormulaId(this.pivotId);
|
|
46485
46575
|
for (const position of this.getters.getVisibleCellPositions()) {
|
|
46486
46576
|
const cell = this.getters.getCell(position);
|
|
46487
46577
|
if (cell?.isFormula) {
|
|
46488
46578
|
const pivotFunction = getFirstPivotFunction(cell.compiledFormula.tokens);
|
|
46489
|
-
|
|
46490
|
-
|
|
46579
|
+
const pivotFormulaId = pivotFunction?.args[0]?.value;
|
|
46580
|
+
if (pivotFunction && updatedPivotFormulaId === pivotFormulaId.toString()) {
|
|
46581
|
+
if (pivotFunction.functionName === "PIVOT") {
|
|
46582
|
+
// if we have at least one dynamic pivot visible inserted the viewport
|
|
46583
|
+
// we return false
|
|
46584
|
+
return false;
|
|
46585
|
+
}
|
|
46586
|
+
else {
|
|
46587
|
+
staticPivotCount++;
|
|
46588
|
+
}
|
|
46491
46589
|
}
|
|
46492
46590
|
}
|
|
46493
46591
|
}
|
|
46494
|
-
return
|
|
46592
|
+
// we return true if there are only static pivots visible inserted the viewport,
|
|
46593
|
+
// otherwise false
|
|
46594
|
+
return staticPivotCount > 0;
|
|
46495
46595
|
}
|
|
46496
46596
|
addDefaultDateTimeGranularity(fields, definition) {
|
|
46497
46597
|
const { columns, rows } = definition;
|
|
@@ -60331,6 +60431,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
60331
60431
|
exportForExcel(data) {
|
|
60332
60432
|
for (const sheet of data.sheets) {
|
|
60333
60433
|
sheet.cellValues = {};
|
|
60434
|
+
sheet.formulaSpillRanges = {};
|
|
60334
60435
|
}
|
|
60335
60436
|
for (const position of this.evaluator.getEvaluatedPositions()) {
|
|
60336
60437
|
const evaluatedCell = this.evaluator.getEvaluatedCell(position);
|
|
@@ -60342,8 +60443,9 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
60342
60443
|
const exportedSheetData = data.sheets.find((sheet) => sheet.id === position.sheetId);
|
|
60343
60444
|
const formulaCell = this.getCorrespondingFormulaCell(position);
|
|
60344
60445
|
if (formulaCell) {
|
|
60446
|
+
const cell = this.getters.getCell(position);
|
|
60345
60447
|
isExported = isExportableToExcel(formulaCell.compiledFormula.tokens);
|
|
60346
|
-
isFormula = isExported;
|
|
60448
|
+
isFormula = isExported && cell?.content === formulaCell.content;
|
|
60347
60449
|
// If the cell contains a non-exported formula and that is evaluates to
|
|
60348
60450
|
// nothing* ,we don't export it.
|
|
60349
60451
|
// * non-falsy value are relevant and so are 0 and FALSE, which only leaves
|
|
@@ -60366,7 +60468,11 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
60366
60468
|
content = !isExported ? newContent : exportedCellData;
|
|
60367
60469
|
}
|
|
60368
60470
|
exportedSheetData.cells[xc] = content;
|
|
60369
|
-
exportedSheetData.cellValues[xc] = value;
|
|
60471
|
+
exportedSheetData.cellValues[xc] = evaluatedCell.type !== "error" ? value : undefined;
|
|
60472
|
+
const spillZone = this.getSpreadZone(position);
|
|
60473
|
+
if (spillZone) {
|
|
60474
|
+
exportedSheetData.formulaSpillRanges[xc] = this.getters.getRangeString(this.getters.getRangeFromZone(position.sheetId, spillZone), position.sheetId);
|
|
60475
|
+
}
|
|
60370
60476
|
}
|
|
60371
60477
|
}
|
|
60372
60478
|
/**
|
|
@@ -62573,7 +62679,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
62573
62679
|
getRule(cell, cells) {
|
|
62574
62680
|
const rules = autofillRulesRegistry.getAll().sort((a, b) => a.sequence - b.sequence);
|
|
62575
62681
|
const rule = rules.find((rule) => rule.condition(cell, cells));
|
|
62576
|
-
return rule && rule.generateRule(cell, cells);
|
|
62682
|
+
return rule && this.direction && rule.generateRule(cell, cells, this.direction);
|
|
62577
62683
|
}
|
|
62578
62684
|
/**
|
|
62579
62685
|
* Create the generator to be able to autofill the next cells.
|
|
@@ -73103,7 +73209,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
73103
73209
|
`;
|
|
73104
73210
|
}
|
|
73105
73211
|
|
|
73106
|
-
function addFormula(formula, value) {
|
|
73212
|
+
function addFormula(formula, value, formulaSpillRange) {
|
|
73107
73213
|
if (!formula) {
|
|
73108
73214
|
return { attrs: [], node: escapeXml `` };
|
|
73109
73215
|
}
|
|
@@ -73111,10 +73217,17 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
73111
73217
|
if (type === undefined) {
|
|
73112
73218
|
return { attrs: [], node: escapeXml `` };
|
|
73113
73219
|
}
|
|
73114
|
-
const attrs = [
|
|
73220
|
+
const attrs = [
|
|
73221
|
+
["cm", "1"],
|
|
73222
|
+
["t", type],
|
|
73223
|
+
];
|
|
73115
73224
|
const XlsxFormula = adaptFormulaToExcel(formula);
|
|
73116
73225
|
const exportedValue = adaptFormulaValueToExcel(value);
|
|
73117
|
-
|
|
73226
|
+
// We treat all formulas as array formulas (a simple formula
|
|
73227
|
+
// is an array formula that spills on only one cell) to avoid
|
|
73228
|
+
// trying to detect spilling sub-formulas which is not a trivial task.
|
|
73229
|
+
let node;
|
|
73230
|
+
node = escapeXml /*xml*/ `<f t="array" ref="${formulaSpillRange}">${XlsxFormula}</f><v>${exportedValue}</v>`;
|
|
73118
73231
|
return { attrs, node };
|
|
73119
73232
|
}
|
|
73120
73233
|
function addContent(content, sharedStrings, forceString = false) {
|
|
@@ -74104,7 +74217,7 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
74104
74217
|
let cellNode = escapeXml ``;
|
|
74105
74218
|
// Either formula or static value inside the cell
|
|
74106
74219
|
if (content?.startsWith("=") && value !== undefined) {
|
|
74107
|
-
const res = addFormula(content, value);
|
|
74220
|
+
const res = addFormula(content, value, sheet.formulaSpillRanges[xc] ?? xc);
|
|
74108
74221
|
if (!res) {
|
|
74109
74222
|
continue;
|
|
74110
74223
|
}
|
|
@@ -74390,6 +74503,30 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
74390
74503
|
`;
|
|
74391
74504
|
files.push(createXMLFile(parseXML(sheetXml), `xl/worksheets/sheet${sheetIndex}.xml`, "sheet"));
|
|
74392
74505
|
}
|
|
74506
|
+
const sheetMetadataXml = escapeXml /*xml*/ `
|
|
74507
|
+
<metadata xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:xda="http://schemas.microsoft.com/office/spreadsheetml/2017/dynamicarray">
|
|
74508
|
+
<metadataTypes count="1">
|
|
74509
|
+
<metadataType name="XLDAPR" minSupportedVersion="120000" copy="1" pasteAll="1"
|
|
74510
|
+
pasteValues="1" merge="1" splitFirst="1" rowColShift="1" clearFormats="1"
|
|
74511
|
+
clearComments="1" assign="1" coerce="1" cellMeta="1" />
|
|
74512
|
+
</metadataTypes>
|
|
74513
|
+
<futureMetadata name="XLDAPR" count="1">
|
|
74514
|
+
<bk>
|
|
74515
|
+
<extLst>
|
|
74516
|
+
<ext uri="{${ARRAY_FORMULA_URI}}">
|
|
74517
|
+
<xda:dynamicArrayProperties fDynamic="1" fCollapsed="0" />
|
|
74518
|
+
</ext>
|
|
74519
|
+
</extLst>
|
|
74520
|
+
</bk>
|
|
74521
|
+
</futureMetadata>
|
|
74522
|
+
<cellMetadata count="1">
|
|
74523
|
+
<bk>
|
|
74524
|
+
<rc t="1" v="0" />
|
|
74525
|
+
</bk>
|
|
74526
|
+
</cellMetadata>
|
|
74527
|
+
</metadata>
|
|
74528
|
+
`;
|
|
74529
|
+
files.push(createXMLFile(parseXML(sheetMetadataXml), "xl/metadata.xml", "metadata"));
|
|
74393
74530
|
addRelsToFile(construct.relsFiles, "xl/_rels/workbook.xml.rels", {
|
|
74394
74531
|
type: XLSX_RELATION_TYPE.sharedStrings,
|
|
74395
74532
|
target: "sharedStrings.xml",
|
|
@@ -74398,6 +74535,10 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
74398
74535
|
type: XLSX_RELATION_TYPE.styles,
|
|
74399
74536
|
target: "styles.xml",
|
|
74400
74537
|
});
|
|
74538
|
+
addRelsToFile(construct.relsFiles, "xl/_rels/workbook.xml.rels", {
|
|
74539
|
+
type: XLSX_RELATION_TYPE.metadata,
|
|
74540
|
+
target: "metadata.xml",
|
|
74541
|
+
});
|
|
74401
74542
|
return files;
|
|
74402
74543
|
}
|
|
74403
74544
|
/**
|
|
@@ -75326,9 +75467,9 @@ stores.inject(MyMetaStore, storeInstance);
|
|
|
75326
75467
|
exports.tokenize = tokenize;
|
|
75327
75468
|
|
|
75328
75469
|
|
|
75329
|
-
__info__.version = "18.1.
|
|
75330
|
-
__info__.date = "2025-
|
|
75331
|
-
__info__.hash = "
|
|
75470
|
+
__info__.version = "18.1.10";
|
|
75471
|
+
__info__.date = "2025-03-07T10:34:41.861Z";
|
|
75472
|
+
__info__.hash = "31e4526";
|
|
75332
75473
|
|
|
75333
75474
|
|
|
75334
75475
|
})(this.o_spreadsheet = this.o_spreadsheet || {}, owl);
|