@casualoffice/sheets 0.18.0 → 0.20.0
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/{api-DJhuCjhn.d.cts → api-cyO4EA4_.d.cts} +1 -1
- package/dist/{api-DJhuCjhn.d.ts → api-cyO4EA4_.d.ts} +1 -1
- package/dist/{attachCollab-BmCRT4V_.d.ts → attachCollab-BT4fVzxP.d.ts} +1 -1
- package/dist/{attachCollab-DIUQCCrq.d.cts → attachCollab-DHjb5XYK.d.cts} +1 -1
- package/dist/chrome.cjs +60 -3
- package/dist/chrome.cjs.map +1 -1
- package/dist/chrome.d.cts +3 -3
- package/dist/chrome.d.ts +3 -3
- package/dist/chrome.js +60 -3
- package/dist/chrome.js.map +1 -1
- package/dist/collab.d.cts +2 -2
- package/dist/collab.d.ts +2 -2
- package/dist/embed/embed-runtime.js +205 -161
- package/dist/{extensions-DZnqTEN2.d.ts → extensions-Cni7mbEm.d.ts} +1 -1
- package/dist/{extensions-FFqdLpk_.d.cts → extensions-DMmPnIYB.d.cts} +1 -1
- package/dist/index.cjs +6424 -1559
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +6409 -1530
- package/dist/index.js.map +1 -1
- package/dist/sheets.cjs +5426 -561
- package/dist/sheets.cjs.map +1 -1
- package/dist/sheets.d.cts +5 -5
- package/dist/sheets.d.ts +5 -5
- package/dist/sheets.js +5415 -536
- package/dist/sheets.js.map +1 -1
- package/package.json +8 -7
- package/src/charts/ChartContextMenu.tsx +264 -0
- package/src/charts/ChartLayer.tsx +333 -0
- package/src/charts/ChartOverlay.tsx +293 -0
- package/src/charts/ChartsPanel.tsx +232 -0
- package/src/charts/FormatChartDialog.tsx +419 -0
- package/src/charts/InsertChartDialog.tsx +478 -0
- package/src/charts/build-option.ts +476 -0
- package/src/charts/charts-context.tsx +205 -0
- package/src/charts/echarts-init.ts +58 -0
- package/src/charts/hit-test.ts +130 -0
- package/src/charts/insert-chart.ts +106 -0
- package/src/charts/naming.ts +38 -0
- package/src/charts/render-to-png.ts +117 -0
- package/src/charts/resources.ts +108 -0
- package/src/charts/types.ts +239 -0
- package/src/charts/univer-dom.ts +102 -0
- package/src/chrome/CommentsPanel.tsx +418 -0
- package/src/chrome/HistoryPanel.tsx +304 -0
- package/src/chrome/InsertPivotDialog.tsx +74 -2
- package/src/chrome/PanelHost.tsx +56 -0
- package/src/chrome/PanelRail.tsx +90 -0
- package/src/chrome/PivotFieldsPanel.tsx +1021 -0
- package/src/chrome/TablesPanel.tsx +283 -0
- package/src/chrome/Toolbar.tsx +7 -1
- package/src/chrome/panel-context.tsx +55 -0
- package/src/chrome/panel-registry.ts +48 -0
- package/src/chrome/panel-shell.tsx +151 -0
- package/src/pivots/apply.ts +139 -0
- package/src/pivots/compute.ts +604 -0
- package/src/pivots/fields-model.ts +267 -0
- package/src/pivots/types.ts +137 -0
- package/src/sheets/CasualSheets.tsx +61 -35
- package/src/sheets/api.ts +37 -2
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2026 Casual Office
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { use } from 'echarts/core';
|
|
18
|
+
import { CanvasRenderer } from 'echarts/renderers';
|
|
19
|
+
import { BarChart, LineChart, PieChart, ScatterChart } from 'echarts/charts';
|
|
20
|
+
import {
|
|
21
|
+
GridComponent,
|
|
22
|
+
LegendComponent,
|
|
23
|
+
TitleComponent,
|
|
24
|
+
TooltipComponent,
|
|
25
|
+
DatasetComponent,
|
|
26
|
+
} from 'echarts/components';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Tree-shaken ECharts wiring. We pay only for the chart types + canvas
|
|
30
|
+
* renderer we actually use. Pipeline Stage 4 already ships each lazy
|
|
31
|
+
* group as its own chunk; this module is the chart-group entry point —
|
|
32
|
+
* importing anything from `'echarts'` directly elsewhere would pull
|
|
33
|
+
* the full library back in and undo the bundle savings.
|
|
34
|
+
*
|
|
35
|
+
* P0 ships Bar (the demo type). P3 layers Line / Pie / Scatter /
|
|
36
|
+
* Area / Combo / Stacked variants on top — those use the same
|
|
37
|
+
* Chart/Component registrations registered here, so future types
|
|
38
|
+
* just import their chart constructor and add to `use([…])`.
|
|
39
|
+
*/
|
|
40
|
+
// `use` here is ECharts' renderer/component registry, not a React hook —
|
|
41
|
+
// silence rules-of-hooks for the module-level call.
|
|
42
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
43
|
+
use([
|
|
44
|
+
CanvasRenderer,
|
|
45
|
+
BarChart,
|
|
46
|
+
LineChart,
|
|
47
|
+
PieChart,
|
|
48
|
+
ScatterChart,
|
|
49
|
+
GridComponent,
|
|
50
|
+
LegendComponent,
|
|
51
|
+
TitleComponent,
|
|
52
|
+
TooltipComponent,
|
|
53
|
+
DatasetComponent,
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
export { init } from 'echarts/core';
|
|
57
|
+
export type { EChartsType } from 'echarts/core';
|
|
58
|
+
export type { EChartsOption } from 'echarts';
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2026 Casual Office
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Pixel ↔ cell conversion for chart drag / resize. The Univer facade
|
|
19
|
+
* doesn't ship a `pixelToCell` for our coordinate space, so we walk
|
|
20
|
+
* row heights / column widths via `getCellRect` until the cumulative
|
|
21
|
+
* extent contains the target coordinate.
|
|
22
|
+
*
|
|
23
|
+
* Inputs are in canvas-local PRE-scroll coordinates — the same frame
|
|
24
|
+
* `getCellRect` itself returns. Callers must add the viewport scroll
|
|
25
|
+
* before invoking these.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
29
|
+
type Sheet = any;
|
|
30
|
+
|
|
31
|
+
const MAX_SCAN = 2048;
|
|
32
|
+
|
|
33
|
+
/** Find the row whose [top, bottom) range contains `y`. Clamped to MAX_SCAN. */
|
|
34
|
+
export function rowAtY(sheet: Sheet, y: number, hintRow = 0): number {
|
|
35
|
+
if (y < 0) return 0;
|
|
36
|
+
// Walk forward from the hint. Charts only move a few cells per drag
|
|
37
|
+
// frame, so the hint puts us within a handful of iterations.
|
|
38
|
+
let r = Math.max(0, hintRow);
|
|
39
|
+
for (let i = 0; i < MAX_SCAN; i++) {
|
|
40
|
+
let rect: { top: number; bottom: number } | null = null;
|
|
41
|
+
try {
|
|
42
|
+
rect = sheet.getRange(r, 0).getCellRect();
|
|
43
|
+
} catch {
|
|
44
|
+
return Math.max(0, r - 1);
|
|
45
|
+
}
|
|
46
|
+
if (!rect) return Math.max(0, r - 1);
|
|
47
|
+
if (rect.top > y) {
|
|
48
|
+
// Overshot — walk back row by row.
|
|
49
|
+
while (r > 0) {
|
|
50
|
+
r--;
|
|
51
|
+
try {
|
|
52
|
+
const back = sheet.getRange(r, 0).getCellRect();
|
|
53
|
+
if (back && back.top <= y && back.bottom > y) return r;
|
|
54
|
+
if (back && back.top <= y) return r;
|
|
55
|
+
} catch {
|
|
56
|
+
return r;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return 0;
|
|
60
|
+
}
|
|
61
|
+
if (rect.bottom > y) return r;
|
|
62
|
+
r++;
|
|
63
|
+
}
|
|
64
|
+
return r;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Find the column whose [left, right) range contains `x`. Clamped to MAX_SCAN. */
|
|
68
|
+
export function colAtX(sheet: Sheet, x: number, hintCol = 0): number {
|
|
69
|
+
if (x < 0) return 0;
|
|
70
|
+
let c = Math.max(0, hintCol);
|
|
71
|
+
for (let i = 0; i < MAX_SCAN; i++) {
|
|
72
|
+
let rect: { left: number; right: number } | null = null;
|
|
73
|
+
try {
|
|
74
|
+
rect = sheet.getRange(0, c).getCellRect();
|
|
75
|
+
} catch {
|
|
76
|
+
return Math.max(0, c - 1);
|
|
77
|
+
}
|
|
78
|
+
if (!rect) return Math.max(0, c - 1);
|
|
79
|
+
if (rect.left > x) {
|
|
80
|
+
while (c > 0) {
|
|
81
|
+
c--;
|
|
82
|
+
try {
|
|
83
|
+
const back = sheet.getRange(0, c).getCellRect();
|
|
84
|
+
if (back && back.left <= x && back.right > x) return c;
|
|
85
|
+
if (back && back.left <= x) return c;
|
|
86
|
+
} catch {
|
|
87
|
+
return c;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return 0;
|
|
91
|
+
}
|
|
92
|
+
if (rect.right > x) return c;
|
|
93
|
+
c++;
|
|
94
|
+
}
|
|
95
|
+
return c;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Convert a chart's screen-local pixel rect to cell-coordinate position.
|
|
100
|
+
* `screenRect` is the chart's host-local box (left/top/width/height).
|
|
101
|
+
* `host`-local pixels are translated to canvas-local pre-scroll coords
|
|
102
|
+
* using `canvasOffset` (canvas vs. host offset) and `scroll` (the
|
|
103
|
+
* worksheet's current scroll offset). We then snap the resulting
|
|
104
|
+
* top-left and bottom-right cells.
|
|
105
|
+
*/
|
|
106
|
+
export function rectToCellPos(
|
|
107
|
+
sheet: Sheet,
|
|
108
|
+
screenRect: { left: number; top: number; width: number; height: number },
|
|
109
|
+
canvasOffset: { x: number; y: number },
|
|
110
|
+
scroll: { x: number; y: number },
|
|
111
|
+
hint?: { startRow: number; startColumn: number; endRow: number; endColumn: number },
|
|
112
|
+
): { startRow: number; endRow: number; startColumn: number; endColumn: number } | null {
|
|
113
|
+
const preLeft = screenRect.left - canvasOffset.x + scroll.x;
|
|
114
|
+
const preTop = screenRect.top - canvasOffset.y + scroll.y;
|
|
115
|
+
const preRight = preLeft + screenRect.width;
|
|
116
|
+
const preBottom = preTop + screenRect.height;
|
|
117
|
+
|
|
118
|
+
const startRow = rowAtY(sheet, preTop, hint?.startRow);
|
|
119
|
+
const startColumn = colAtX(sheet, preLeft, hint?.startColumn);
|
|
120
|
+
// For the bottom-right, snap to the cell BEFORE the bottom-right
|
|
121
|
+
// pixel (so the chart includes that cell, matching Excel's "anchor
|
|
122
|
+
// to bottom-right cell" semantics). Subtract 1 from preRight/Bottom
|
|
123
|
+
// to land in the inclusive last cell.
|
|
124
|
+
const endRow = Math.max(startRow, rowAtY(sheet, preBottom - 1, hint?.endRow ?? startRow));
|
|
125
|
+
const endColumn = Math.max(
|
|
126
|
+
startColumn,
|
|
127
|
+
colAtX(sheet, preRight - 1, hint?.endColumn ?? startColumn),
|
|
128
|
+
);
|
|
129
|
+
return { startRow, endRow, startColumn, endColumn };
|
|
130
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2026 Casual Office
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { FUniver } from '@univerjs/core/facade';
|
|
18
|
+
import type { ChartType, ChartModel } from './types';
|
|
19
|
+
import { newChartId } from './types';
|
|
20
|
+
|
|
21
|
+
type Range = { startRow: number; endRow: number; startColumn: number; endColumn: number };
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Build a ChartModel for `source` on the active sheet. The chart anchors
|
|
25
|
+
* to a 10-row × 8-col block placed two rows below the source so it doesn't
|
|
26
|
+
* cover its own data. Mirrors Excel's Insert > Chart defaults — source =
|
|
27
|
+
* the dialog's selection, output = roughly the right size, dropped below.
|
|
28
|
+
*
|
|
29
|
+
* Returns null when there's no active workbook/sheet, or the range has
|
|
30
|
+
* fewer than 2 rows × 2 cols (no header + data row, or no label + value
|
|
31
|
+
* column).
|
|
32
|
+
*/
|
|
33
|
+
export function buildChartModelForRange(
|
|
34
|
+
api: FUniver,
|
|
35
|
+
source: Range,
|
|
36
|
+
type: ChartType,
|
|
37
|
+
): ChartModel | null {
|
|
38
|
+
const wb = api.getActiveWorkbook();
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
40
|
+
const ws = wb?.getActiveSheet() as any;
|
|
41
|
+
if (!wb || !ws) return null;
|
|
42
|
+
|
|
43
|
+
const rows = source.endRow - source.startRow + 1;
|
|
44
|
+
const cols = source.endColumn - source.startColumn + 1;
|
|
45
|
+
if (rows < 2 || cols < 2) return null;
|
|
46
|
+
|
|
47
|
+
const chartTop = source.endRow + 2;
|
|
48
|
+
const chartLeft = source.startColumn;
|
|
49
|
+
return {
|
|
50
|
+
id: newChartId(),
|
|
51
|
+
sheetId: ws.getSheetId(),
|
|
52
|
+
source,
|
|
53
|
+
pos: {
|
|
54
|
+
startRow: chartTop,
|
|
55
|
+
endRow: chartTop + 9,
|
|
56
|
+
startColumn: chartLeft,
|
|
57
|
+
endColumn: chartLeft + 7,
|
|
58
|
+
},
|
|
59
|
+
type,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Read the active selection on the active sheet. Returns null if no
|
|
65
|
+
* workbook / sheet / selection is available — the menu item is enabled
|
|
66
|
+
* unconditionally, so the caller still has to handle the empty case.
|
|
67
|
+
*/
|
|
68
|
+
export function getActiveSelectionRange(api: FUniver): Range | null {
|
|
69
|
+
const wb = api.getActiveWorkbook();
|
|
70
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
71
|
+
const ws = wb?.getActiveSheet() as any;
|
|
72
|
+
const range = ws?.getActiveRange();
|
|
73
|
+
if (!wb || !ws || !range) return null;
|
|
74
|
+
const r = range.getRange();
|
|
75
|
+
return {
|
|
76
|
+
startRow: r.startRow,
|
|
77
|
+
endRow: r.endRow,
|
|
78
|
+
startColumn: r.startColumn,
|
|
79
|
+
endColumn: r.endColumn,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Format a cell range as an A1-style reference (e.g. `A1:C4`). Used by
|
|
85
|
+
* the insert dialog to pre-fill its source-range input from the current
|
|
86
|
+
* selection so the common case is two clicks.
|
|
87
|
+
*/
|
|
88
|
+
export function rangeToA1(range: Range): string {
|
|
89
|
+
const tl = `${colIndexToA1(range.startColumn)}${range.startRow + 1}`;
|
|
90
|
+
if (range.startRow === range.endRow && range.startColumn === range.endColumn) {
|
|
91
|
+
return tl;
|
|
92
|
+
}
|
|
93
|
+
const br = `${colIndexToA1(range.endColumn)}${range.endRow + 1}`;
|
|
94
|
+
return `${tl}:${br}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function colIndexToA1(c: number): string {
|
|
98
|
+
let n = c + 1;
|
|
99
|
+
let s = '';
|
|
100
|
+
while (n > 0) {
|
|
101
|
+
const rem = (n - 1) % 26;
|
|
102
|
+
s = String.fromCharCode(65 + rem) + s;
|
|
103
|
+
n = Math.floor((n - 1) / 26);
|
|
104
|
+
}
|
|
105
|
+
return s;
|
|
106
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2026 Casual Office
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { ChartModel } from './types';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Excel auto-names charts as "Chart 1", "Chart 2", ..., picking the
|
|
21
|
+
* lowest unused suffix even if intermediate charts have been deleted
|
|
22
|
+
* (so deleting Chart 2 then inserting again gives you Chart 2, not
|
|
23
|
+
* Chart 4). Match that.
|
|
24
|
+
*
|
|
25
|
+
* Operates on `ChartModel.title` — see the Chart Selection Pane for
|
|
26
|
+
* inline rename.
|
|
27
|
+
*/
|
|
28
|
+
export function nextChartName(existing: ChartModel[]): string {
|
|
29
|
+
const used = new Set<number>();
|
|
30
|
+
for (const c of existing) {
|
|
31
|
+
if (!c.title) continue;
|
|
32
|
+
const m = /^Chart (\d+)$/.exec(c.title);
|
|
33
|
+
if (m) used.add(Number(m[1]));
|
|
34
|
+
}
|
|
35
|
+
let n = 1;
|
|
36
|
+
while (used.has(n)) n++;
|
|
37
|
+
return `Chart ${n}`;
|
|
38
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2026 Casual Office
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { FUniver } from '@univerjs/core/facade';
|
|
18
|
+
import { init } from './echarts-init';
|
|
19
|
+
import { buildEChartsOption } from './build-option';
|
|
20
|
+
import type { ChartModel } from './types';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Render a chart model to a PNG ArrayBuffer using a detached ECharts
|
|
24
|
+
* instance. Used by the xlsx exporter (Charts P5b) so the chart shows
|
|
25
|
+
* up as a real image when the file is opened in Excel — the live
|
|
26
|
+
* interactive ECharts canvas can't ship; an embedded image can.
|
|
27
|
+
*
|
|
28
|
+
* Constraints:
|
|
29
|
+
* - Must run on the main thread (ECharts needs a DOM container).
|
|
30
|
+
* - The container is created inside `document.body` with
|
|
31
|
+
* `visibility: hidden` + `pointer-events: none` so it never
|
|
32
|
+
* interferes with the actual UI even momentarily.
|
|
33
|
+
* - We choose pixel dimensions based on the chart's cell-anchored
|
|
34
|
+
* position so the embedded PNG roughly matches what the user
|
|
35
|
+
* sees in the app.
|
|
36
|
+
*
|
|
37
|
+
* Returns null when there's no source data (empty range etc.) so the
|
|
38
|
+
* exporter can skip the chart cleanly rather than embed a 1×1 blank.
|
|
39
|
+
*/
|
|
40
|
+
export async function renderChartToPng(
|
|
41
|
+
api: FUniver,
|
|
42
|
+
model: ChartModel,
|
|
43
|
+
pxWidth: number,
|
|
44
|
+
pxHeight: number,
|
|
45
|
+
): Promise<ArrayBuffer | null> {
|
|
46
|
+
const opt = buildEChartsOption(api, model);
|
|
47
|
+
if (!opt) return null;
|
|
48
|
+
|
|
49
|
+
const host = document.createElement('div');
|
|
50
|
+
host.style.position = 'absolute';
|
|
51
|
+
host.style.left = '-99999px';
|
|
52
|
+
host.style.top = '0';
|
|
53
|
+
host.style.width = `${pxWidth}px`;
|
|
54
|
+
host.style.height = `${pxHeight}px`;
|
|
55
|
+
host.style.visibility = 'hidden';
|
|
56
|
+
host.style.pointerEvents = 'none';
|
|
57
|
+
document.body.appendChild(host);
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const inst = init(host, undefined, { renderer: 'canvas' });
|
|
61
|
+
// Apply the same option ChartOverlay uses so the snapshot matches
|
|
62
|
+
// what the user sees in-app. notMerge: true to fully override any
|
|
63
|
+
// ECharts defaults left over from the previous setOption.
|
|
64
|
+
inst.setOption(opt, true);
|
|
65
|
+
// ECharts renders on the next animation frame; force a synchronous
|
|
66
|
+
// resize so the canvas is filled before we read it.
|
|
67
|
+
inst.resize();
|
|
68
|
+
const dataUrl = inst.getDataURL({
|
|
69
|
+
type: 'png',
|
|
70
|
+
pixelRatio: 2,
|
|
71
|
+
backgroundColor: '#ffffff',
|
|
72
|
+
});
|
|
73
|
+
inst.dispose();
|
|
74
|
+
return dataUrlToArrayBuffer(dataUrl);
|
|
75
|
+
} finally {
|
|
76
|
+
host.remove();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function dataUrlToArrayBuffer(dataUrl: string): ArrayBuffer {
|
|
81
|
+
const comma = dataUrl.indexOf(',');
|
|
82
|
+
if (comma < 0) return new ArrayBuffer(0);
|
|
83
|
+
const b64 = dataUrl.slice(comma + 1);
|
|
84
|
+
const binary = atob(b64);
|
|
85
|
+
const bytes = new Uint8Array(binary.length);
|
|
86
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
87
|
+
return bytes.buffer;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Resolve the chart's pixel size from its cell-anchored position by
|
|
92
|
+
* walking the sheet's column widths / row heights. Falls back to a
|
|
93
|
+
* sensible default (480×320) when the snapshot doesn't have width/height
|
|
94
|
+
* data for the spanned cells.
|
|
95
|
+
*/
|
|
96
|
+
export function pixelsForChart(api: FUniver, model: ChartModel): { width: number; height: number } {
|
|
97
|
+
const wb = api.getActiveWorkbook();
|
|
98
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
99
|
+
const sheets = wb?.getSheets() as any[] | undefined;
|
|
100
|
+
const ws = sheets?.find((s) => s.getSheetId?.() === model.sheetId);
|
|
101
|
+
if (!ws) return { width: 480, height: 320 };
|
|
102
|
+
|
|
103
|
+
let width = 0;
|
|
104
|
+
for (let c = model.pos.startColumn; c <= model.pos.endColumn; c++) {
|
|
105
|
+
const w = Number(ws.getColumnWidth?.(c) ?? 0);
|
|
106
|
+
width += w > 0 ? w : 88;
|
|
107
|
+
}
|
|
108
|
+
let height = 0;
|
|
109
|
+
for (let r = model.pos.startRow; r <= model.pos.endRow; r++) {
|
|
110
|
+
const h = Number(ws.getRowHeight?.(r) ?? 0);
|
|
111
|
+
height += h > 0 ? h : 24;
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
width: Math.max(120, Math.round(width)),
|
|
115
|
+
height: Math.max(80, Math.round(height)),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2026 Casual Office
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { IWorkbookData } from '@univerjs/core';
|
|
18
|
+
import {
|
|
19
|
+
CHARTS_RESOURCE_NAME,
|
|
20
|
+
type ChartModel,
|
|
21
|
+
type ChartsResourceV1,
|
|
22
|
+
type ChartType,
|
|
23
|
+
} from './types';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Round-trip helpers for the charts plugin resource. The resource lives on
|
|
27
|
+
* `IWorkbookData.resources` and is carried through xlsx via the hidden
|
|
28
|
+
* `__casual_sheets_resources__` sheet we already use for outline groups,
|
|
29
|
+
* and through collab via Univer's snapshot-load path.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
const VALID_TYPES: ChartType[] = [
|
|
33
|
+
'column',
|
|
34
|
+
'column-stacked',
|
|
35
|
+
'column-stacked-100',
|
|
36
|
+
'bar',
|
|
37
|
+
'bar-stacked',
|
|
38
|
+
'bar-stacked-100',
|
|
39
|
+
'line',
|
|
40
|
+
'line-stacked',
|
|
41
|
+
'area',
|
|
42
|
+
'area-stacked',
|
|
43
|
+
'pie',
|
|
44
|
+
'doughnut',
|
|
45
|
+
'scatter',
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The pre-P3 store used `'bar'` for what we now (correctly) call
|
|
50
|
+
* `'column'` — vertical bars. Migrate on read so existing workbooks
|
|
51
|
+
* keep rendering after the rename.
|
|
52
|
+
*/
|
|
53
|
+
function migrateType(raw: unknown): ChartType | null {
|
|
54
|
+
if (typeof raw !== 'string') return null;
|
|
55
|
+
// Existing 'bar' string is ambiguous between old "vertical column"
|
|
56
|
+
// and new "horizontal bar". P0/P1/P2 saved 'bar' meaning column;
|
|
57
|
+
// we have no horizontal-bar charts in the wild yet, so 'bar' from
|
|
58
|
+
// before P3 means column.
|
|
59
|
+
if (raw === 'bar') return 'column';
|
|
60
|
+
return VALID_TYPES.includes(raw as ChartType) ? (raw as ChartType) : null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isValidChart(c: unknown): c is ChartModel {
|
|
64
|
+
if (!c || typeof c !== 'object') return false;
|
|
65
|
+
const r = c as Record<string, unknown>;
|
|
66
|
+
if (typeof r.id !== 'string' || typeof r.sheetId !== 'string') return false;
|
|
67
|
+
const migrated = migrateType(r.type);
|
|
68
|
+
if (!migrated) return false;
|
|
69
|
+
r.type = migrated; // mutate so the rest of the app sees the new value
|
|
70
|
+
const src = r.source as Record<string, unknown> | undefined;
|
|
71
|
+
const pos = r.pos as Record<string, unknown> | undefined;
|
|
72
|
+
if (!src || !pos) return false;
|
|
73
|
+
for (const k of ['startRow', 'endRow', 'startColumn', 'endColumn'] as const) {
|
|
74
|
+
if (typeof src[k] !== 'number' || typeof pos[k] !== 'number') return false;
|
|
75
|
+
}
|
|
76
|
+
// `format` is optional and freely shaped — defer validation to
|
|
77
|
+
// `mergeFormat`, which fills missing fields with defaults. Anything
|
|
78
|
+
// we don't recognise is ignored at render time.
|
|
79
|
+
if (r.format != null && typeof r.format !== 'object') return false;
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Read chart models out of a snapshot. Tolerant of older / missing payloads. */
|
|
84
|
+
export function readChartsFromSnapshot(data: IWorkbookData | undefined): ChartModel[] {
|
|
85
|
+
if (!data?.resources?.length) return [];
|
|
86
|
+
const entry = data.resources.find((r) => r.name === CHARTS_RESOURCE_NAME);
|
|
87
|
+
if (!entry?.data) return [];
|
|
88
|
+
try {
|
|
89
|
+
const parsed = JSON.parse(entry.data) as Partial<ChartsResourceV1>;
|
|
90
|
+
if (parsed?.v !== 1 || !Array.isArray(parsed.charts)) return [];
|
|
91
|
+
return parsed.charts.filter(isValidChart);
|
|
92
|
+
} catch {
|
|
93
|
+
/* corrupt payload — drop silently, the workbook still opens fine */
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Merge chart models INTO `data.resources` for export. Mutates in place. */
|
|
99
|
+
export function writeChartsIntoSnapshot(data: IWorkbookData, charts: ChartModel[]): void {
|
|
100
|
+
const existing = data.resources ?? [];
|
|
101
|
+
const filtered = existing.filter((r) => r.name !== CHARTS_RESOURCE_NAME);
|
|
102
|
+
if (charts.length === 0) {
|
|
103
|
+
data.resources = filtered;
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const payload: ChartsResourceV1 = { v: 1, charts };
|
|
107
|
+
data.resources = [...filtered, { name: CHARTS_RESOURCE_NAME, data: JSON.stringify(payload) }];
|
|
108
|
+
}
|