@node-projects/excelforge 2.4.0 → 3.0.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/.github/FUNDING.yml +4 -0
- package/MISSING.md +326 -0
- package/README.md +484 -12
- package/dist/core/SharedStrings.js +6 -2
- package/dist/core/SharedStrings.js.map +1 -1
- package/dist/core/Workbook.d.ts +41 -1
- package/dist/core/Workbook.js +773 -57
- package/dist/core/Workbook.js.map +1 -1
- package/dist/core/WorkbookReader.d.ts +18 -4
- package/dist/core/WorkbookReader.js +1386 -20
- package/dist/core/WorkbookReader.js.map +1 -1
- package/dist/core/Worksheet.d.ts +130 -2
- package/dist/core/Worksheet.js +792 -63
- package/dist/core/Worksheet.js.map +1 -1
- package/dist/core/types.d.ts +287 -5
- package/dist/core/types.js +12 -1
- package/dist/core/types.js.map +1 -1
- package/dist/features/ChartBuilder.d.ts +9 -1
- package/dist/features/ChartBuilder.js +140 -14
- package/dist/features/ChartBuilder.js.map +1 -1
- package/dist/features/CsvModule.d.ts +11 -0
- package/dist/features/CsvModule.js +137 -0
- package/dist/features/CsvModule.js.map +1 -0
- package/dist/features/Encryption.d.ts +6 -0
- package/dist/features/Encryption.js +806 -0
- package/dist/features/Encryption.js.map +1 -0
- package/dist/features/FormControlBuilder.d.ts +6 -0
- package/dist/features/FormControlBuilder.js +135 -0
- package/dist/features/FormControlBuilder.js.map +1 -0
- package/dist/features/FormulaEngine.d.ts +22 -0
- package/dist/features/FormulaEngine.js +498 -0
- package/dist/features/FormulaEngine.js.map +1 -0
- package/dist/features/HtmlModule.d.ts +21 -0
- package/dist/features/HtmlModule.js +1417 -0
- package/dist/features/HtmlModule.js.map +1 -0
- package/dist/features/JsonModule.d.ts +10 -0
- package/dist/features/JsonModule.js +76 -0
- package/dist/features/JsonModule.js.map +1 -0
- package/dist/features/PivotTableBuilder.d.ts +7 -0
- package/dist/features/PivotTableBuilder.js +170 -0
- package/dist/features/PivotTableBuilder.js.map +1 -0
- package/dist/features/Signing.d.ts +12 -0
- package/dist/features/Signing.js +318 -0
- package/dist/features/Signing.js.map +1 -0
- package/dist/features/TableBuilder.js +2 -2
- package/dist/features/TableBuilder.js.map +1 -1
- package/dist/index-min.js +579 -144
- package/dist/index.d.ts +17 -1
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -1
- package/dist/styles/StyleRegistry.d.ts +14 -0
- package/dist/styles/StyleRegistry.js +95 -30
- package/dist/styles/StyleRegistry.js.map +1 -1
- package/dist/utils/helpers.d.ts +4 -0
- package/dist/utils/helpers.js +64 -14
- package/dist/utils/helpers.js.map +1 -1
- package/dist/utils/zip.js +145 -73
- package/dist/utils/zip.js.map +1 -1
- package/dist/vba/VbaProject.d.ts +19 -0
- package/dist/vba/VbaProject.js +281 -0
- package/dist/vba/VbaProject.js.map +1 -0
- package/dist/vba/cfb.d.ts +7 -0
- package/dist/vba/cfb.js +352 -0
- package/dist/vba/cfb.js.map +1 -0
- package/dist/vba/ovba.d.ts +2 -0
- package/dist/vba/ovba.js +137 -0
- package/dist/vba/ovba.js.map +1 -0
- package/package.json +4 -3
- package/validator.cs +0 -155
- package/validatorEpplus.cs +0 -27
- package/validatorReadData.cs +0 -111
|
@@ -0,0 +1,1417 @@
|
|
|
1
|
+
import { escapeXml, colIndexToLetter } from '../utils/helpers.js';
|
|
2
|
+
const THEME_COLORS = [
|
|
3
|
+
'#000000', '#FFFFFF', '#44546A', '#E7E6E6', '#4472C4', '#ED7D31',
|
|
4
|
+
'#A5A5A5', '#FFC000', '#5B9BD5', '#70AD47',
|
|
5
|
+
];
|
|
6
|
+
function colorToCSS(c) {
|
|
7
|
+
if (!c)
|
|
8
|
+
return '';
|
|
9
|
+
if (c.startsWith('#'))
|
|
10
|
+
return c;
|
|
11
|
+
if (c.startsWith('theme:')) {
|
|
12
|
+
const idx = parseInt(c.slice(6), 10);
|
|
13
|
+
return THEME_COLORS[idx] ?? '#000';
|
|
14
|
+
}
|
|
15
|
+
if (c.length === 8 && !c.startsWith('#'))
|
|
16
|
+
return '#' + c.slice(2);
|
|
17
|
+
return '#' + c;
|
|
18
|
+
}
|
|
19
|
+
function parseColor(c) {
|
|
20
|
+
const hex = c.replace('#', '');
|
|
21
|
+
return [parseInt(hex.slice(0, 2), 16), parseInt(hex.slice(2, 4), 16), parseInt(hex.slice(4, 6), 16)];
|
|
22
|
+
}
|
|
23
|
+
function interpolateColor(c1, c2, t) {
|
|
24
|
+
const [r1, g1, b1] = parseColor(colorToCSS(c1) || '#FFFFFF');
|
|
25
|
+
const [r2, g2, b2] = parseColor(colorToCSS(c2) || '#000000');
|
|
26
|
+
const r = Math.round(r1 + (r2 - r1) * t);
|
|
27
|
+
const g = Math.round(g1 + (g2 - g1) * t);
|
|
28
|
+
const b = Math.round(b1 + (b2 - b1) * t);
|
|
29
|
+
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
|
|
30
|
+
}
|
|
31
|
+
function fontToCSS(f) {
|
|
32
|
+
const parts = [];
|
|
33
|
+
if (f.bold)
|
|
34
|
+
parts.push('font-weight:bold');
|
|
35
|
+
if (f.italic)
|
|
36
|
+
parts.push('font-style:italic');
|
|
37
|
+
const decs = [];
|
|
38
|
+
if (f.underline && f.underline !== 'none')
|
|
39
|
+
decs.push('underline');
|
|
40
|
+
if (f.strike)
|
|
41
|
+
decs.push('line-through');
|
|
42
|
+
if (decs.length)
|
|
43
|
+
parts.push(`text-decoration:${decs.join(' ')}`);
|
|
44
|
+
if (f.size)
|
|
45
|
+
parts.push(`font-size:${f.size}pt`);
|
|
46
|
+
if (f.color)
|
|
47
|
+
parts.push(`color:${colorToCSS(f.color)}`);
|
|
48
|
+
if (f.name)
|
|
49
|
+
parts.push(`font-family:'${f.name}',sans-serif`);
|
|
50
|
+
if (f.vertAlign === 'superscript')
|
|
51
|
+
parts.push('vertical-align:super;font-size:smaller');
|
|
52
|
+
else if (f.vertAlign === 'subscript')
|
|
53
|
+
parts.push('vertical-align:sub;font-size:smaller');
|
|
54
|
+
return parts.join(';');
|
|
55
|
+
}
|
|
56
|
+
function fillToCSS(fill) {
|
|
57
|
+
if (fill.type === 'pattern') {
|
|
58
|
+
const pf = fill;
|
|
59
|
+
if (pf.pattern === 'solid' && pf.fgColor)
|
|
60
|
+
return `background-color:${colorToCSS(pf.fgColor)}`;
|
|
61
|
+
}
|
|
62
|
+
if (fill.type === 'gradient') {
|
|
63
|
+
const gf = fill;
|
|
64
|
+
if (gf.stops && gf.stops.length >= 2) {
|
|
65
|
+
const stops = gf.stops.map(s => `${colorToCSS(s.color)} ${Math.round(s.position * 100)}%`).join(',');
|
|
66
|
+
const deg = gf.degree ?? 0;
|
|
67
|
+
return `background:linear-gradient(${deg}deg,${stops})`;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return '';
|
|
71
|
+
}
|
|
72
|
+
function borderSideCSS(side) {
|
|
73
|
+
if (!side || !side.style)
|
|
74
|
+
return '';
|
|
75
|
+
const widthMap = {
|
|
76
|
+
thin: '1px', medium: '2px', thick: '3px', dashed: '1px', dotted: '1px',
|
|
77
|
+
double: '3px', hair: '1px', mediumDashed: '2px', dashDot: '1px',
|
|
78
|
+
mediumDashDot: '2px', dashDotDot: '1px', mediumDashDotDot: '2px', slantDashDot: '2px',
|
|
79
|
+
};
|
|
80
|
+
const styleMap = {
|
|
81
|
+
thin: 'solid', medium: 'solid', thick: 'solid', dashed: 'dashed', dotted: 'dotted',
|
|
82
|
+
double: 'double', hair: 'solid', mediumDashed: 'dashed', dashDot: 'dashed',
|
|
83
|
+
mediumDashDot: 'dashed', dashDotDot: 'dotted', mediumDashDotDot: 'dotted', slantDashDot: 'dashed',
|
|
84
|
+
};
|
|
85
|
+
const w = widthMap[side.style] ?? '1px';
|
|
86
|
+
const s = styleMap[side.style] ?? 'solid';
|
|
87
|
+
const c = side.color ? colorToCSS(side.color) : '#000';
|
|
88
|
+
return `${w} ${s} ${c}`;
|
|
89
|
+
}
|
|
90
|
+
function alignmentCSS(a) {
|
|
91
|
+
const parts = [];
|
|
92
|
+
if (a.horizontal) {
|
|
93
|
+
const hMap = { left: 'left', center: 'center', right: 'right', fill: 'justify', justify: 'justify', distributed: 'justify' };
|
|
94
|
+
parts.push(`text-align:${hMap[a.horizontal] ?? a.horizontal}`);
|
|
95
|
+
}
|
|
96
|
+
if (a.vertical) {
|
|
97
|
+
const vMap = { top: 'top', center: 'middle', bottom: 'bottom', distributed: 'middle' };
|
|
98
|
+
parts.push(`vertical-align:${vMap[a.vertical] ?? 'bottom'}`);
|
|
99
|
+
}
|
|
100
|
+
if (a.wrapText)
|
|
101
|
+
parts.push('white-space:normal;word-wrap:break-word');
|
|
102
|
+
if (a.textRotation)
|
|
103
|
+
parts.push(`transform:rotate(-${a.textRotation}deg)`);
|
|
104
|
+
if (a.indent)
|
|
105
|
+
parts.push(`padding-left:${a.indent * 8}px`);
|
|
106
|
+
return parts.join(';');
|
|
107
|
+
}
|
|
108
|
+
function styleToCSS(s) {
|
|
109
|
+
const parts = [];
|
|
110
|
+
if (s.font)
|
|
111
|
+
parts.push(fontToCSS(s.font));
|
|
112
|
+
if (s.fill)
|
|
113
|
+
parts.push(fillToCSS(s.fill));
|
|
114
|
+
if (s.alignment)
|
|
115
|
+
parts.push(alignmentCSS(s.alignment));
|
|
116
|
+
if (s.border) {
|
|
117
|
+
if (s.border.top)
|
|
118
|
+
parts.push(`border-top:${borderSideCSS(s.border.top)}`);
|
|
119
|
+
if (s.border.bottom)
|
|
120
|
+
parts.push(`border-bottom:${borderSideCSS(s.border.bottom)}`);
|
|
121
|
+
if (s.border.left)
|
|
122
|
+
parts.push(`border-left:${borderSideCSS(s.border.left)}`);
|
|
123
|
+
if (s.border.right)
|
|
124
|
+
parts.push(`border-right:${borderSideCSS(s.border.right)}`);
|
|
125
|
+
}
|
|
126
|
+
return parts.filter(Boolean).join(';');
|
|
127
|
+
}
|
|
128
|
+
function formatNumber(value, fmt) {
|
|
129
|
+
if (value == null)
|
|
130
|
+
return '';
|
|
131
|
+
if (!fmt || fmt === 'General')
|
|
132
|
+
return String(value);
|
|
133
|
+
const num = typeof value === 'number' ? value : parseFloat(String(value));
|
|
134
|
+
if (isNaN(num))
|
|
135
|
+
return String(value);
|
|
136
|
+
if (fmt.includes('%')) {
|
|
137
|
+
const decimals = (fmt.match(/0\.(0+)%/) ?? [])[1]?.length ?? 0;
|
|
138
|
+
return (num * 100).toFixed(decimals) + '%';
|
|
139
|
+
}
|
|
140
|
+
const currMatch = fmt.match(/[$€£¥]|"CHF"/);
|
|
141
|
+
if (currMatch) {
|
|
142
|
+
const sym = currMatch[0].replace(/"/g, '');
|
|
143
|
+
const decimals = (fmt.match(/\.(0+)/) ?? [])[1]?.length ?? 2;
|
|
144
|
+
const formatted = Math.abs(num).toFixed(decimals).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
145
|
+
if (fmt.indexOf(currMatch[0]) < fmt.indexOf('0')) {
|
|
146
|
+
return (num < 0 ? '-' : '') + sym + formatted;
|
|
147
|
+
}
|
|
148
|
+
return (num < 0 ? '-' : '') + formatted + ' ' + sym;
|
|
149
|
+
}
|
|
150
|
+
if (fmt.includes('#,##0') || fmt.includes('#,###')) {
|
|
151
|
+
const decimals = (fmt.match(/\.(0+)/) ?? [])[1]?.length ?? 0;
|
|
152
|
+
return num.toFixed(decimals).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
153
|
+
}
|
|
154
|
+
const fixedMatch = fmt.match(/^0\.(0+)$/);
|
|
155
|
+
if (fixedMatch)
|
|
156
|
+
return num.toFixed(fixedMatch[1].length);
|
|
157
|
+
if (/[ymdh]/i.test(fmt))
|
|
158
|
+
return formatDate(num, fmt);
|
|
159
|
+
if (fmt.includes('?/?') || fmt.includes('??/??'))
|
|
160
|
+
return formatFraction(num);
|
|
161
|
+
if (/0\.0+E\+0+/i.test(fmt)) {
|
|
162
|
+
const decimals = (fmt.match(/0\.(0+)/) ?? [])[1]?.length ?? 2;
|
|
163
|
+
return num.toExponential(decimals).toUpperCase();
|
|
164
|
+
}
|
|
165
|
+
return String(value);
|
|
166
|
+
}
|
|
167
|
+
function formatDate(serial, fmt) {
|
|
168
|
+
const epoch = new Date(1899, 11, 30);
|
|
169
|
+
const d = new Date(epoch.getTime() + serial * 86400000);
|
|
170
|
+
const Y = d.getFullYear(), M = d.getMonth() + 1, D = d.getDate();
|
|
171
|
+
const h = d.getHours(), m = d.getMinutes(), s = d.getSeconds();
|
|
172
|
+
return fmt
|
|
173
|
+
.replace(/yyyy/gi, String(Y))
|
|
174
|
+
.replace(/yy/gi, String(Y).slice(-2))
|
|
175
|
+
.replace(/mmmm/gi, d.toLocaleDateString('en', { month: 'long' }))
|
|
176
|
+
.replace(/mmm/gi, d.toLocaleDateString('en', { month: 'short' }))
|
|
177
|
+
.replace(/mm/gi, String(M).padStart(2, '0'))
|
|
178
|
+
.replace(/m/gi, String(M))
|
|
179
|
+
.replace(/dd/gi, String(D).padStart(2, '0'))
|
|
180
|
+
.replace(/d/gi, String(D))
|
|
181
|
+
.replace(/hh/gi, String(h).padStart(2, '0'))
|
|
182
|
+
.replace(/h/gi, String(h))
|
|
183
|
+
.replace(/ss/gi, String(s).padStart(2, '0'))
|
|
184
|
+
.replace(/nn|MM/g, String(m).padStart(2, '0'));
|
|
185
|
+
}
|
|
186
|
+
function formatFraction(num) {
|
|
187
|
+
const whole = Math.floor(Math.abs(num));
|
|
188
|
+
const frac = Math.abs(num) - whole;
|
|
189
|
+
if (frac < 0.0001)
|
|
190
|
+
return String(num < 0 ? -whole : whole);
|
|
191
|
+
let bestN = 0, bestD = 1, bestErr = 1;
|
|
192
|
+
for (let d = 1; d <= 100; d++) {
|
|
193
|
+
const n = Math.round(frac * d);
|
|
194
|
+
const err = Math.abs(frac - n / d);
|
|
195
|
+
if (err < bestErr) {
|
|
196
|
+
bestN = n;
|
|
197
|
+
bestD = d;
|
|
198
|
+
bestErr = err;
|
|
199
|
+
}
|
|
200
|
+
if (err < 0.0001)
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
const sign = num < 0 ? '-' : '';
|
|
204
|
+
return whole > 0 ? `${sign}${whole} ${bestN}/${bestD}` : `${sign}${bestN}/${bestD}`;
|
|
205
|
+
}
|
|
206
|
+
function evaluateConditionalFormats(cf, value, allValues) {
|
|
207
|
+
if (cf.colorScale) {
|
|
208
|
+
const sorted = [...allValues].sort((a, b) => a - b);
|
|
209
|
+
const min = sorted[0], max = sorted[sorted.length - 1];
|
|
210
|
+
const range = max - min || 1;
|
|
211
|
+
const t = (value - min) / range;
|
|
212
|
+
const cs = cf.colorScale;
|
|
213
|
+
if (cs.color.length === 2)
|
|
214
|
+
return `background-color:${interpolateColor(cs.color[0], cs.color[1], t)}`;
|
|
215
|
+
if (cs.color.length >= 3) {
|
|
216
|
+
if (t <= 0.5)
|
|
217
|
+
return `background-color:${interpolateColor(cs.color[0], cs.color[1], t * 2)}`;
|
|
218
|
+
return `background-color:${interpolateColor(cs.color[1], cs.color[2], (t - 0.5) * 2)}`;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (cf.dataBar) {
|
|
222
|
+
const sorted = [...allValues].sort((a, b) => a - b);
|
|
223
|
+
const min = sorted[0], max = sorted[sorted.length - 1];
|
|
224
|
+
const pct = Math.round(((value - min) / (max - min || 1)) * 100);
|
|
225
|
+
const color = colorToCSS(cf.dataBar.color) || '#638EC6';
|
|
226
|
+
return `background:linear-gradient(90deg,${color} ${pct}%,transparent ${pct}%)`;
|
|
227
|
+
}
|
|
228
|
+
if (cf.iconSet) {
|
|
229
|
+
const sorted = [...allValues].sort((a, b) => a - b);
|
|
230
|
+
const min = sorted[0], max = sorted[sorted.length - 1];
|
|
231
|
+
const t = (value - min) / (max - min || 1);
|
|
232
|
+
const ICON_MAP = {
|
|
233
|
+
'3Arrows': ['↓', '→', '↑'], '3ArrowsGray': ['⇩', '⇨', '⇧'],
|
|
234
|
+
'3TrafficLights1': ['🔴', '🟡', '🟢'], '3TrafficLights2': ['🔴', '🟡', '🟢'],
|
|
235
|
+
'3Signs': ['⛔', '⚠️', '✅'], '3Symbols': ['✖', '!', '✔'],
|
|
236
|
+
'3Symbols2': ['✖', '!', '✔'], '3Flags': ['🏴', '🏳', '🏁'],
|
|
237
|
+
'3Stars': ['☆', '★', '★'], '4Arrows': ['↓', '↘', '↗', '↑'],
|
|
238
|
+
'4ArrowsGray': ['⇩', '⇘', '⇗', '⇧'], '4Rating': ['◔', '◑', '◕', '●'],
|
|
239
|
+
'4RedToBlack': ['⬤', '⬤', '⬤', '⬤'], '4TrafficLights': ['⬤', '⬤', '⬤', '⬤'],
|
|
240
|
+
'5Arrows': ['↓', '↘', '→', '↗', '↑'], '5ArrowsGray': ['⇩', '⇘', '⇨', '⇗', '⇧'],
|
|
241
|
+
'5Quarters': ['○', '◔', '◑', '◕', '●'], '5Rating': ['◔', '◔', '◑', '◕', '●'],
|
|
242
|
+
};
|
|
243
|
+
const icons = ICON_MAP[cf.iconSet.iconSet ?? '3TrafficLights1'] ?? ['🔴', '🟡', '🟢'];
|
|
244
|
+
const idx = Math.min(Math.floor(t * icons.length), icons.length - 1);
|
|
245
|
+
return `data-icon="${icons[idx]}"`;
|
|
246
|
+
}
|
|
247
|
+
return '';
|
|
248
|
+
}
|
|
249
|
+
function sparklineToSvg(sparkline, values) {
|
|
250
|
+
if (!values.length)
|
|
251
|
+
return '';
|
|
252
|
+
const W = 100, H = 20;
|
|
253
|
+
const min = Math.min(...values), max = Math.max(...values);
|
|
254
|
+
const range = max - min || 1;
|
|
255
|
+
const color = colorToCSS(sparkline.color) || '#4472C4';
|
|
256
|
+
const maxIdx = values.indexOf(Math.max(...values));
|
|
257
|
+
const minIdx = values.indexOf(Math.min(...values));
|
|
258
|
+
if (sparkline.type === 'bar' || sparkline.type === 'stacked') {
|
|
259
|
+
const bw = W / values.length;
|
|
260
|
+
const bars = values.map((v, i) => {
|
|
261
|
+
const barH = sparkline.type === 'stacked'
|
|
262
|
+
? (v >= 0 ? H / 2 : H / 2)
|
|
263
|
+
: Math.max(1, ((v - min) / range) * H);
|
|
264
|
+
const y = sparkline.type === 'stacked'
|
|
265
|
+
? (v >= 0 ? H / 2 - barH : H / 2)
|
|
266
|
+
: H - barH;
|
|
267
|
+
let fill = color;
|
|
268
|
+
if (v < 0 && sparkline.negativeColor)
|
|
269
|
+
fill = colorToCSS(sparkline.negativeColor);
|
|
270
|
+
else if (sparkline.showHigh && i === maxIdx && sparkline.highColor)
|
|
271
|
+
fill = colorToCSS(sparkline.highColor);
|
|
272
|
+
else if (sparkline.showLow && i === minIdx && sparkline.lowColor)
|
|
273
|
+
fill = colorToCSS(sparkline.lowColor);
|
|
274
|
+
else if (sparkline.showFirst && i === 0 && sparkline.firstColor)
|
|
275
|
+
fill = colorToCSS(sparkline.firstColor);
|
|
276
|
+
else if (sparkline.showLast && i === values.length - 1 && sparkline.lastColor)
|
|
277
|
+
fill = colorToCSS(sparkline.lastColor);
|
|
278
|
+
return `<rect x="${i * bw + bw * 0.1}" y="${y}" width="${bw * 0.8}" height="${barH}" fill="${fill}" rx="1"/>`;
|
|
279
|
+
}).join('');
|
|
280
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" style="display:inline-block;vertical-align:middle">${bars}</svg>`;
|
|
281
|
+
}
|
|
282
|
+
const strokeW = sparkline.lineWidth ?? 1.5;
|
|
283
|
+
const pts = values.map((v, i) => `${(i / (values.length - 1 || 1)) * W},${H - ((v - min) / range) * H}`).join(' ');
|
|
284
|
+
let markers = '';
|
|
285
|
+
if (sparkline.showMarkers) {
|
|
286
|
+
markers = values.map((v, i) => {
|
|
287
|
+
const x = (i / (values.length - 1 || 1)) * W;
|
|
288
|
+
const y = H - ((v - min) / range) * H;
|
|
289
|
+
return `<circle cx="${x}" cy="${y}" r="1.5" fill="${colorToCSS(sparkline.markersColor) || color}"/>`;
|
|
290
|
+
}).join('');
|
|
291
|
+
}
|
|
292
|
+
const specialMarkers = [];
|
|
293
|
+
const addMarker = (idx, clr) => {
|
|
294
|
+
const x = (idx / (values.length - 1 || 1)) * W;
|
|
295
|
+
const y = H - ((values[idx] - min) / range) * H;
|
|
296
|
+
specialMarkers.push(`<circle cx="${x}" cy="${y}" r="2.5" fill="${clr}" stroke="white" stroke-width="0.5"/>`);
|
|
297
|
+
};
|
|
298
|
+
if (sparkline.showHigh && sparkline.highColor)
|
|
299
|
+
addMarker(maxIdx, colorToCSS(sparkline.highColor));
|
|
300
|
+
if (sparkline.showLow && sparkline.lowColor)
|
|
301
|
+
addMarker(minIdx, colorToCSS(sparkline.lowColor));
|
|
302
|
+
if (sparkline.showFirst && sparkline.firstColor)
|
|
303
|
+
addMarker(0, colorToCSS(sparkline.firstColor));
|
|
304
|
+
if (sparkline.showLast && sparkline.lastColor)
|
|
305
|
+
addMarker(values.length - 1, colorToCSS(sparkline.lastColor));
|
|
306
|
+
if (sparkline.showNegative && sparkline.negativeColor) {
|
|
307
|
+
values.forEach((v, i) => {
|
|
308
|
+
if (v < 0) {
|
|
309
|
+
const x = (i / (values.length - 1 || 1)) * W;
|
|
310
|
+
const y = H - ((v - min) / range) * H;
|
|
311
|
+
specialMarkers.push(`<circle cx="${x}" cy="${y}" r="2" fill="${colorToCSS(sparkline.negativeColor)}"/>`);
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" style="display:inline-block;vertical-align:middle"><polyline points="${pts}" fill="none" stroke="${color}" stroke-width="${strokeW}"/>${markers}${specialMarkers.join('')}</svg>`;
|
|
316
|
+
}
|
|
317
|
+
const CHART_PALETTE = ['#4472C4', '#ED7D31', '#A5A5A5', '#FFC000', '#5B9BD5', '#70AD47', '#264478', '#9B57A0', '#636363', '#EB7E3A'];
|
|
318
|
+
function resolveChartSeriesData(ws, ref) {
|
|
319
|
+
const vals = [];
|
|
320
|
+
const part = ref.includes('!') ? ref.split('!')[1] : ref;
|
|
321
|
+
const clean = part.replace(/\$/g, '');
|
|
322
|
+
const m = clean.match(/^([A-Z]+)(\d+):([A-Z]+)(\d+)$/);
|
|
323
|
+
if (!m)
|
|
324
|
+
return vals;
|
|
325
|
+
const c1 = colLetterToIdx(m[1]), r1 = parseInt(m[2], 10);
|
|
326
|
+
const c2 = colLetterToIdx(m[3]), r2 = parseInt(m[4], 10);
|
|
327
|
+
for (let r = r1; r <= r2; r++) {
|
|
328
|
+
for (let c = c1; c <= c2; c++) {
|
|
329
|
+
const cell = ws.getCell(r, c);
|
|
330
|
+
vals.push(typeof cell.value === 'number' ? cell.value : null);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return vals;
|
|
334
|
+
}
|
|
335
|
+
function resolveChartCategories(ws, ref) {
|
|
336
|
+
const cats = [];
|
|
337
|
+
const part = ref.includes('!') ? ref.split('!')[1] : ref;
|
|
338
|
+
const clean = part.replace(/\$/g, '');
|
|
339
|
+
const m = clean.match(/^([A-Z]+)(\d+):([A-Z]+)(\d+)$/);
|
|
340
|
+
if (!m)
|
|
341
|
+
return cats;
|
|
342
|
+
const c1 = colLetterToIdx(m[1]), r1 = parseInt(m[2], 10);
|
|
343
|
+
const c2 = colLetterToIdx(m[3]), r2 = parseInt(m[4], 10);
|
|
344
|
+
for (let r = r1; r <= r2; r++) {
|
|
345
|
+
for (let c = c1; c <= c2; c++) {
|
|
346
|
+
const cell = ws.getCell(r, c);
|
|
347
|
+
cats.push(cell.value != null ? String(cell.value) : '');
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return cats;
|
|
351
|
+
}
|
|
352
|
+
function chartToSvg(chart, ws) {
|
|
353
|
+
const W = 520, H = 340;
|
|
354
|
+
const PAD_T = 45, PAD_B = 55, PAD_L = 65, PAD_R = 20;
|
|
355
|
+
const plotW = W - PAD_L - PAD_R, plotH = H - PAD_T - PAD_B;
|
|
356
|
+
const title = chart.title ?? '';
|
|
357
|
+
const type = chart.type;
|
|
358
|
+
const allSeries = [];
|
|
359
|
+
let categories = [];
|
|
360
|
+
for (let si = 0; si < chart.series.length; si++) {
|
|
361
|
+
const s = chart.series[si];
|
|
362
|
+
const rawVals = resolveChartSeriesData(ws, s.values);
|
|
363
|
+
const vals = rawVals.map(v => v ?? 0);
|
|
364
|
+
if (s.categories && !categories.length)
|
|
365
|
+
categories = resolveChartCategories(ws, s.categories);
|
|
366
|
+
allSeries.push({
|
|
367
|
+
name: s.name ?? `Series ${si + 1}`,
|
|
368
|
+
values: vals,
|
|
369
|
+
color: s.color ? colorToCSS(s.color) : CHART_PALETTE[si % CHART_PALETTE.length],
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
if (!allSeries.length || !allSeries[0].values.length) {
|
|
373
|
+
return `<div style="display:inline-block;width:${W}px;height:${H}px;border:1px solid #ccc;background:#f9f9f9;text-align:center;line-height:${H}px;color:#666;font-size:14px" data-chart-type="${type}">[Chart: ${escapeXml(title || type)} — no data]</div>`;
|
|
374
|
+
}
|
|
375
|
+
const numCats = Math.max(...allSeries.map(s => s.values.length));
|
|
376
|
+
if (!categories.length)
|
|
377
|
+
categories = Array.from({ length: numCats }, (_, i) => String(i + 1));
|
|
378
|
+
if (type === 'pie' || type === 'doughnut')
|
|
379
|
+
return pieChartSvg(chart, allSeries, categories, W, H);
|
|
380
|
+
if (type === 'radar' || type === 'radarFilled')
|
|
381
|
+
return radarChartSvg(chart, allSeries, categories, W, H);
|
|
382
|
+
if (type === 'scatter' || type === 'scatterSmooth' || type === 'bubble')
|
|
383
|
+
return scatterChartSvg(chart, allSeries, ws, W, H);
|
|
384
|
+
const allVals = allSeries.flatMap(s => s.values);
|
|
385
|
+
let yMin = chart.yAxis?.min ?? Math.min(0, ...allVals);
|
|
386
|
+
let yMax = chart.yAxis?.max ?? Math.max(...allVals);
|
|
387
|
+
if (yMax === yMin)
|
|
388
|
+
yMax = yMin + 1;
|
|
389
|
+
const yRange = yMax - yMin;
|
|
390
|
+
const numTicks = 5;
|
|
391
|
+
const gridLines = [];
|
|
392
|
+
const yLabels = [];
|
|
393
|
+
for (let i = 0; i <= numTicks; i++) {
|
|
394
|
+
const val = yMin + (yRange * i / numTicks);
|
|
395
|
+
const y = PAD_T + plotH - (plotH * (val - yMin) / yRange);
|
|
396
|
+
gridLines.push(`<line x1="${PAD_L}" y1="${y}" x2="${W - PAD_R}" y2="${y}" stroke="#e0e0e0" stroke-width="1"/>`);
|
|
397
|
+
const label = Math.abs(val) >= 1000 ? (val / 1000).toFixed(1) + 'k' : Number.isInteger(val) ? String(val) : val.toFixed(1);
|
|
398
|
+
yLabels.push(`<text x="${PAD_L - 8}" y="${y + 4}" text-anchor="end" font-size="10" fill="#666">${label}</text>`);
|
|
399
|
+
}
|
|
400
|
+
const zeroY = PAD_T + plotH - (plotH * (0 - yMin) / yRange);
|
|
401
|
+
let dataSvg = '';
|
|
402
|
+
const isBar = type === 'bar' || type === 'barStacked' || type === 'barStacked100';
|
|
403
|
+
const isColumn = type === 'column' || type === 'columnStacked' || type === 'columnStacked100';
|
|
404
|
+
const isArea = type === 'area' || type === 'areaStacked';
|
|
405
|
+
const isLine = type === 'line' || type === 'lineStacked' || type === 'lineMarker';
|
|
406
|
+
const isStacked = type.includes('Stacked');
|
|
407
|
+
const is100 = type.includes('100');
|
|
408
|
+
if (isBar) {
|
|
409
|
+
const barGroupH = plotH / numCats;
|
|
410
|
+
const barH = isStacked ? barGroupH * 0.7 : (barGroupH * 0.7) / allSeries.length;
|
|
411
|
+
for (let ci = 0; ci < numCats; ci++) {
|
|
412
|
+
if (isStacked) {
|
|
413
|
+
let xAcc = 0;
|
|
414
|
+
const total = is100 ? allSeries.reduce((s, ser) => s + Math.abs(ser.values[ci] ?? 0), 0) || 1 : 1;
|
|
415
|
+
for (let si = 0; si < allSeries.length; si++) {
|
|
416
|
+
let val = allSeries[si].values[ci] ?? 0;
|
|
417
|
+
if (is100)
|
|
418
|
+
val = (val / total) * (yMax - yMin);
|
|
419
|
+
const barW = (Math.abs(val) / yRange) * plotW;
|
|
420
|
+
const x = PAD_L + (xAcc / yRange) * plotW;
|
|
421
|
+
const y = PAD_T + ci * barGroupH + barGroupH * 0.15;
|
|
422
|
+
dataSvg += `<rect x="${x}" y="${y}" width="${barW}" height="${barH}" fill="${allSeries[si].color}" rx="2"><title>${allSeries[si].name}: ${allSeries[si].values[ci] ?? 0}</title></rect>`;
|
|
423
|
+
xAcc += Math.abs(val);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
for (let si = 0; si < allSeries.length; si++) {
|
|
428
|
+
const val = allSeries[si].values[ci] ?? 0;
|
|
429
|
+
const barW = (Math.abs(val - yMin) / yRange) * plotW;
|
|
430
|
+
const y = PAD_T + ci * barGroupH + barGroupH * 0.15 + si * barH;
|
|
431
|
+
dataSvg += `<rect x="${PAD_L}" y="${y}" width="${barW}" height="${barH}" fill="${allSeries[si].color}" rx="2"><title>${allSeries[si].name}: ${val}</title></rect>`;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
for (let ci = 0; ci < numCats; ci++) {
|
|
436
|
+
const y = PAD_T + ci * barGroupH + barGroupH / 2 + 4;
|
|
437
|
+
yLabels.push(`<text x="${PAD_L - 8}" y="${y}" text-anchor="end" font-size="10" fill="#666">${escapeXml(categories[ci] ?? '')}</text>`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
else if (isColumn || (!isLine && !isArea)) {
|
|
441
|
+
const groupW = plotW / numCats;
|
|
442
|
+
const barW = isStacked ? groupW * 0.6 : (groupW * 0.6) / allSeries.length;
|
|
443
|
+
for (let ci = 0; ci < numCats; ci++) {
|
|
444
|
+
if (isStacked) {
|
|
445
|
+
let yAcc = 0;
|
|
446
|
+
const total = is100 ? allSeries.reduce((s, ser) => s + Math.abs(ser.values[ci] ?? 0), 0) || 1 : 1;
|
|
447
|
+
for (let si = 0; si < allSeries.length; si++) {
|
|
448
|
+
let val = allSeries[si].values[ci] ?? 0;
|
|
449
|
+
if (is100)
|
|
450
|
+
val = (val / total) * yRange;
|
|
451
|
+
const bh = (Math.abs(val) / yRange) * plotH;
|
|
452
|
+
const x = PAD_L + ci * groupW + groupW * 0.2;
|
|
453
|
+
const y = zeroY - yAcc - bh;
|
|
454
|
+
dataSvg += `<rect x="${x}" y="${y}" width="${barW}" height="${bh}" fill="${allSeries[si].color}" rx="2"><title>${allSeries[si].name}: ${allSeries[si].values[ci] ?? 0}</title></rect>`;
|
|
455
|
+
yAcc += bh;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
else {
|
|
459
|
+
for (let si = 0; si < allSeries.length; si++) {
|
|
460
|
+
const val = allSeries[si].values[ci] ?? 0;
|
|
461
|
+
const bh = (Math.abs(val - yMin) / yRange) * plotH;
|
|
462
|
+
const x = PAD_L + ci * groupW + groupW * 0.2 + si * barW;
|
|
463
|
+
const y = PAD_T + plotH - bh;
|
|
464
|
+
dataSvg += `<rect x="${x}" y="${y}" width="${barW}" height="${bh}" fill="${allSeries[si].color}" rx="2"><title>${allSeries[si].name}: ${val}</title></rect>`;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (isArea) {
|
|
470
|
+
for (let si = 0; si < allSeries.length; si++) {
|
|
471
|
+
const pts = [];
|
|
472
|
+
for (let ci = 0; ci < numCats; ci++) {
|
|
473
|
+
const val = allSeries[si].values[ci] ?? 0;
|
|
474
|
+
const x = PAD_L + (ci / (numCats - 1 || 1)) * plotW;
|
|
475
|
+
const y = PAD_T + plotH - ((val - yMin) / yRange) * plotH;
|
|
476
|
+
pts.push(`${x},${y}`);
|
|
477
|
+
}
|
|
478
|
+
const firstX = PAD_L, lastX = PAD_L + ((numCats - 1) / (numCats - 1 || 1)) * plotW;
|
|
479
|
+
const areaPath = `M${firstX},${zeroY} L${pts.join(' L')} L${lastX},${zeroY} Z`;
|
|
480
|
+
dataSvg += `<path d="${areaPath}" fill="${allSeries[si].color}" fill-opacity="0.3"/>`;
|
|
481
|
+
dataSvg += `<polyline points="${pts.join(' ')}" fill="none" stroke="${allSeries[si].color}" stroke-width="2"/>`;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
if (isLine) {
|
|
485
|
+
for (let si = 0; si < allSeries.length; si++) {
|
|
486
|
+
const pts = [];
|
|
487
|
+
for (let ci = 0; ci < numCats; ci++) {
|
|
488
|
+
const val = allSeries[si].values[ci] ?? 0;
|
|
489
|
+
const x = PAD_L + (ci / (numCats - 1 || 1)) * plotW;
|
|
490
|
+
const y = PAD_T + plotH - ((val - yMin) / yRange) * plotH;
|
|
491
|
+
pts.push(`${x},${y}`);
|
|
492
|
+
}
|
|
493
|
+
dataSvg += `<polyline points="${pts.join(' ')}" fill="none" stroke="${allSeries[si].color}" stroke-width="2.5"/>`;
|
|
494
|
+
if (type === 'lineMarker' || allSeries[si].values.length <= 20) {
|
|
495
|
+
for (let ci = 0; ci < numCats; ci++) {
|
|
496
|
+
const val = allSeries[si].values[ci] ?? 0;
|
|
497
|
+
const x = PAD_L + (ci / (numCats - 1 || 1)) * plotW;
|
|
498
|
+
const y = PAD_T + plotH - ((val - yMin) / yRange) * plotH;
|
|
499
|
+
dataSvg += `<circle cx="${x}" cy="${y}" r="3.5" fill="${allSeries[si].color}" stroke="white" stroke-width="1.5"><title>${allSeries[si].name}: ${val}</title></circle>`;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
let catLabels = '';
|
|
505
|
+
if (!isBar) {
|
|
506
|
+
const step = numCats > 20 ? Math.ceil(numCats / 15) : 1;
|
|
507
|
+
for (let ci = 0; ci < numCats; ci += step) {
|
|
508
|
+
const x = isColumn || (!isLine && !isArea)
|
|
509
|
+
? PAD_L + ci * (plotW / numCats) + (plotW / numCats) / 2
|
|
510
|
+
: PAD_L + (ci / (numCats - 1 || 1)) * plotW;
|
|
511
|
+
catLabels += `<text x="${x}" y="${PAD_T + plotH + 18}" text-anchor="middle" font-size="10" fill="#666" transform="rotate(-30 ${x} ${PAD_T + plotH + 18})">${escapeXml((categories[ci] ?? '').slice(0, 12))}</text>`;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
let legendSvg = '';
|
|
515
|
+
if (chart.legend !== false && allSeries.length > 1) {
|
|
516
|
+
const ly = H - 12;
|
|
517
|
+
const totalWidth = allSeries.reduce((s, ser) => s + ser.name.length * 7 + 25, 0);
|
|
518
|
+
let lx = (W - totalWidth) / 2;
|
|
519
|
+
for (const ser of allSeries) {
|
|
520
|
+
legendSvg += `<rect x="${lx}" y="${ly - 8}" width="12" height="12" rx="2" fill="${ser.color}"/>`;
|
|
521
|
+
legendSvg += `<text x="${lx + 16}" y="${ly + 2}" font-size="10" fill="#444">${escapeXml(ser.name)}</text>`;
|
|
522
|
+
lx += ser.name.length * 7 + 25;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
let axisTitles = '';
|
|
526
|
+
if (chart.xAxis?.title) {
|
|
527
|
+
axisTitles += `<text x="${W / 2}" y="${H - 2}" text-anchor="middle" font-size="11" fill="#444">${escapeXml(chart.xAxis.title)}</text>`;
|
|
528
|
+
}
|
|
529
|
+
if (chart.yAxis?.title) {
|
|
530
|
+
axisTitles += `<text x="14" y="${PAD_T + plotH / 2}" text-anchor="middle" font-size="11" fill="#444" transform="rotate(-90 14 ${PAD_T + plotH / 2})">${escapeXml(chart.yAxis.title)}</text>`;
|
|
531
|
+
}
|
|
532
|
+
const titleSvg = title ? `<text x="${W / 2}" y="22" text-anchor="middle" font-size="14" font-weight="600" fill="#333">${escapeXml(title)}</text>` : '';
|
|
533
|
+
const plotBorder = `<rect x="${PAD_L}" y="${PAD_T}" width="${plotW}" height="${plotH}" fill="none" stroke="#ccc" stroke-width="0.5"/>`;
|
|
534
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" style="background:white;border:1px solid #e0e0e0;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,.08);margin:4px">
|
|
535
|
+
${titleSvg}
|
|
536
|
+
${gridLines.join('\n')}
|
|
537
|
+
${plotBorder}
|
|
538
|
+
${dataSvg}
|
|
539
|
+
${catLabels}
|
|
540
|
+
${yLabels.join('\n')}
|
|
541
|
+
${legendSvg}
|
|
542
|
+
${axisTitles}
|
|
543
|
+
</svg>`;
|
|
544
|
+
}
|
|
545
|
+
function pieChartSvg(chart, allSeries, categories, W, H) {
|
|
546
|
+
const cx = W / 2, cy = H / 2 + 10;
|
|
547
|
+
const outerR = Math.min(W, H) / 2 - 40;
|
|
548
|
+
const innerR = chart.type === 'doughnut' ? outerR * 0.5 : 0;
|
|
549
|
+
const vals = allSeries[0].values;
|
|
550
|
+
const total = vals.reduce((s, v) => s + Math.abs(v), 0) || 1;
|
|
551
|
+
const title = chart.title ?? '';
|
|
552
|
+
let angle = -Math.PI / 2;
|
|
553
|
+
const slices = [];
|
|
554
|
+
const labels = [];
|
|
555
|
+
for (let i = 0; i < vals.length; i++) {
|
|
556
|
+
const pct = Math.abs(vals[i]) / total;
|
|
557
|
+
const sweep = pct * Math.PI * 2;
|
|
558
|
+
const midAngle = angle + sweep / 2;
|
|
559
|
+
const large = sweep > Math.PI ? 1 : 0;
|
|
560
|
+
const x1o = cx + outerR * Math.cos(angle);
|
|
561
|
+
const y1o = cy + outerR * Math.sin(angle);
|
|
562
|
+
const x2o = cx + outerR * Math.cos(angle + sweep);
|
|
563
|
+
const y2o = cy + outerR * Math.sin(angle + sweep);
|
|
564
|
+
let path;
|
|
565
|
+
if (innerR > 0) {
|
|
566
|
+
const x1i = cx + innerR * Math.cos(angle);
|
|
567
|
+
const y1i = cy + innerR * Math.sin(angle);
|
|
568
|
+
const x2i = cx + innerR * Math.cos(angle + sweep);
|
|
569
|
+
const y2i = cy + innerR * Math.sin(angle + sweep);
|
|
570
|
+
path = `M${x1o},${y1o} A${outerR},${outerR} 0 ${large} 1 ${x2o},${y2o} L${x2i},${y2i} A${innerR},${innerR} 0 ${large} 0 ${x1i},${y1i} Z`;
|
|
571
|
+
}
|
|
572
|
+
else {
|
|
573
|
+
path = vals.length === 1
|
|
574
|
+
? `M${cx - outerR},${cy} A${outerR},${outerR} 0 1 1 ${cx + outerR},${cy} A${outerR},${outerR} 0 1 1 ${cx - outerR},${cy} Z`
|
|
575
|
+
: `M${cx},${cy} L${x1o},${y1o} A${outerR},${outerR} 0 ${large} 1 ${x2o},${y2o} Z`;
|
|
576
|
+
}
|
|
577
|
+
const color = CHART_PALETTE[i % CHART_PALETTE.length];
|
|
578
|
+
slices.push(`<path d="${path}" fill="${color}" stroke="white" stroke-width="1.5"><title>${escapeXml(categories[i] ?? '')}: ${vals[i]} (${(pct * 100).toFixed(1)}%)</title></path>`);
|
|
579
|
+
if (pct > 0.04) {
|
|
580
|
+
const lr = outerR + 16;
|
|
581
|
+
const lx = cx + lr * Math.cos(midAngle);
|
|
582
|
+
const ly = cy + lr * Math.sin(midAngle);
|
|
583
|
+
const anchor = midAngle > Math.PI / 2 && midAngle < Math.PI * 1.5 ? 'end' : 'start';
|
|
584
|
+
labels.push(`<text x="${lx}" y="${ly + 4}" text-anchor="${anchor}" font-size="10" fill="#444">${escapeXml((categories[i] ?? '').slice(0, 10))} ${(pct * 100).toFixed(0)}%</text>`);
|
|
585
|
+
}
|
|
586
|
+
angle += sweep;
|
|
587
|
+
}
|
|
588
|
+
let legendSvg = '';
|
|
589
|
+
if (chart.legend !== false) {
|
|
590
|
+
const lx = W - 10;
|
|
591
|
+
for (let i = 0; i < Math.min(vals.length, 10); i++) {
|
|
592
|
+
const ly = 40 + i * 18;
|
|
593
|
+
legendSvg += `<rect x="${lx - 80}" y="${ly - 8}" width="10" height="10" rx="2" fill="${CHART_PALETTE[i % CHART_PALETTE.length]}"/>`;
|
|
594
|
+
legendSvg += `<text x="${lx - 65}" y="${ly + 2}" font-size="10" fill="#444">${escapeXml((categories[i] ?? '').slice(0, 10))}</text>`;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
const titleSvg = title ? `<text x="${W / 2}" y="22" text-anchor="middle" font-size="14" font-weight="600" fill="#333">${escapeXml(title)}</text>` : '';
|
|
598
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" style="background:white;border:1px solid #e0e0e0;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,.08);margin:4px">
|
|
599
|
+
${titleSvg}
|
|
600
|
+
${slices.join('\n')}
|
|
601
|
+
${labels.join('\n')}
|
|
602
|
+
${legendSvg}
|
|
603
|
+
</svg>`;
|
|
604
|
+
}
|
|
605
|
+
function radarChartSvg(chart, allSeries, categories, W, H) {
|
|
606
|
+
const cx = W / 2, cy = H / 2 + 10;
|
|
607
|
+
const R = Math.min(W, H) / 2 - 50;
|
|
608
|
+
const n = categories.length || 1;
|
|
609
|
+
const isFilled = chart.type === 'radarFilled';
|
|
610
|
+
const allVals = allSeries.flatMap(s => s.values);
|
|
611
|
+
const maxVal = Math.max(...allVals, 1);
|
|
612
|
+
const rings = [];
|
|
613
|
+
for (let ring = 1; ring <= 4; ring++) {
|
|
614
|
+
const r = R * ring / 4;
|
|
615
|
+
const pts = Array.from({ length: n }, (_, i) => {
|
|
616
|
+
const a = -Math.PI / 2 + (2 * Math.PI * i / n);
|
|
617
|
+
return `${cx + r * Math.cos(a)},${cy + r * Math.sin(a)}`;
|
|
618
|
+
});
|
|
619
|
+
rings.push(`<polygon points="${pts.join(' ')}" fill="none" stroke="#e0e0e0" stroke-width="0.5"/>`);
|
|
620
|
+
}
|
|
621
|
+
const axes = [];
|
|
622
|
+
for (let i = 0; i < n; i++) {
|
|
623
|
+
const a = -Math.PI / 2 + (2 * Math.PI * i / n);
|
|
624
|
+
const x = cx + R * Math.cos(a);
|
|
625
|
+
const y = cy + R * Math.sin(a);
|
|
626
|
+
axes.push(`<line x1="${cx}" y1="${cy}" x2="${x}" y2="${y}" stroke="#ccc" stroke-width="0.5"/>`);
|
|
627
|
+
const lx = cx + (R + 14) * Math.cos(a);
|
|
628
|
+
const ly = cy + (R + 14) * Math.sin(a);
|
|
629
|
+
axes.push(`<text x="${lx}" y="${ly + 4}" text-anchor="middle" font-size="9" fill="#666">${escapeXml((categories[i] ?? '').slice(0, 8))}</text>`);
|
|
630
|
+
}
|
|
631
|
+
const seriesSvg = [];
|
|
632
|
+
for (const ser of allSeries) {
|
|
633
|
+
const pts = ser.values.map((v, i) => {
|
|
634
|
+
const a = -Math.PI / 2 + (2 * Math.PI * i / n);
|
|
635
|
+
const r = (v / maxVal) * R;
|
|
636
|
+
return `${cx + r * Math.cos(a)},${cy + r * Math.sin(a)}`;
|
|
637
|
+
});
|
|
638
|
+
if (isFilled) {
|
|
639
|
+
seriesSvg.push(`<polygon points="${pts.join(' ')}" fill="${ser.color}" fill-opacity="0.2" stroke="${ser.color}" stroke-width="2"/>`);
|
|
640
|
+
}
|
|
641
|
+
else {
|
|
642
|
+
seriesSvg.push(`<polygon points="${pts.join(' ')}" fill="none" stroke="${ser.color}" stroke-width="2"/>`);
|
|
643
|
+
}
|
|
644
|
+
ser.values.forEach((v, i) => {
|
|
645
|
+
const a = -Math.PI / 2 + (2 * Math.PI * i / n);
|
|
646
|
+
const r = (v / maxVal) * R;
|
|
647
|
+
seriesSvg.push(`<circle cx="${cx + r * Math.cos(a)}" cy="${cy + r * Math.sin(a)}" r="3" fill="${ser.color}" stroke="white" stroke-width="1"><title>${ser.name}: ${v}</title></circle>`);
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
const titleSvg = chart.title ? `<text x="${W / 2}" y="20" text-anchor="middle" font-size="14" font-weight="600" fill="#333">${escapeXml(chart.title)}</text>` : '';
|
|
651
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" style="background:white;border:1px solid #e0e0e0;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,.08);margin:4px">
|
|
652
|
+
${titleSvg}
|
|
653
|
+
${rings.join('\n')}
|
|
654
|
+
${axes.join('\n')}
|
|
655
|
+
${seriesSvg.join('\n')}
|
|
656
|
+
</svg>`;
|
|
657
|
+
}
|
|
658
|
+
function scatterChartSvg(chart, allSeries, ws, W, H) {
|
|
659
|
+
const PAD_T = 45, PAD_B = 40, PAD_L = 60, PAD_R = 20;
|
|
660
|
+
const plotW = W - PAD_L - PAD_R, plotH = H - PAD_T - PAD_B;
|
|
661
|
+
const catSeries = chart.series[0]?.categories ? resolveChartSeriesData(ws, chart.series[0].categories) : [];
|
|
662
|
+
const points = [];
|
|
663
|
+
for (const ser of allSeries) {
|
|
664
|
+
for (let i = 0; i < ser.values.length; i++) {
|
|
665
|
+
points.push({ x: catSeries[i] ?? i, y: ser.values[i], name: ser.name, color: ser.color });
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
if (!points.length)
|
|
669
|
+
return '';
|
|
670
|
+
const xMin = Math.min(...points.map(p => p.x)), xMax = Math.max(...points.map(p => p.x));
|
|
671
|
+
const yMin = Math.min(0, ...points.map(p => p.y)), yMax = Math.max(...points.map(p => p.y));
|
|
672
|
+
const xRange = xMax - xMin || 1, yRange = yMax - yMin || 1;
|
|
673
|
+
const gridLines = [];
|
|
674
|
+
for (let i = 0; i <= 4; i++) {
|
|
675
|
+
const y = PAD_T + plotH - (plotH * i / 4);
|
|
676
|
+
gridLines.push(`<line x1="${PAD_L}" y1="${y}" x2="${W - PAD_R}" y2="${y}" stroke="#e0e0e0" stroke-width="0.5"/>`);
|
|
677
|
+
const val = yMin + yRange * i / 4;
|
|
678
|
+
gridLines.push(`<text x="${PAD_L - 8}" y="${y + 4}" text-anchor="end" font-size="10" fill="#666">${val.toFixed(0)}</text>`);
|
|
679
|
+
}
|
|
680
|
+
const dotsSvg = points.map(p => {
|
|
681
|
+
const x = PAD_L + ((p.x - xMin) / xRange) * plotW;
|
|
682
|
+
const y = PAD_T + plotH - ((p.y - yMin) / yRange) * plotH;
|
|
683
|
+
const r = chart.type === 'bubble' ? Math.max(4, Math.min(20, Math.sqrt(Math.abs(p.y)) * 2)) : 4;
|
|
684
|
+
return `<circle cx="${x}" cy="${y}" r="${r}" fill="${p.color}" fill-opacity="${chart.type === 'bubble' ? '0.6' : '1'}" stroke="white" stroke-width="1"><title>${p.name}: (${p.x}, ${p.y})</title></circle>`;
|
|
685
|
+
}).join('\n');
|
|
686
|
+
let lineSvg = '';
|
|
687
|
+
if (chart.type === 'scatterSmooth') {
|
|
688
|
+
for (const ser of allSeries) {
|
|
689
|
+
const pts = ser.values.map((v, i) => {
|
|
690
|
+
const xVal = catSeries[i] ?? i;
|
|
691
|
+
const x = PAD_L + ((xVal - xMin) / xRange) * plotW;
|
|
692
|
+
const y = PAD_T + plotH - ((v - yMin) / yRange) * plotH;
|
|
693
|
+
return `${x},${y}`;
|
|
694
|
+
});
|
|
695
|
+
lineSvg += `<polyline points="${pts.join(' ')}" fill="none" stroke="${ser.color}" stroke-width="2"/>`;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
const titleSvg = chart.title ? `<text x="${W / 2}" y="22" text-anchor="middle" font-size="14" font-weight="600" fill="#333">${escapeXml(chart.title)}</text>` : '';
|
|
699
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" style="background:white;border:1px solid #e0e0e0;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,.08);margin:4px">
|
|
700
|
+
${titleSvg}
|
|
701
|
+
${gridLines.join('\n')}
|
|
702
|
+
<rect x="${PAD_L}" y="${PAD_T}" width="${plotW}" height="${plotH}" fill="none" stroke="#ccc" stroke-width="0.5"/>
|
|
703
|
+
${lineSvg}
|
|
704
|
+
${dotsSvg}
|
|
705
|
+
</svg>`;
|
|
706
|
+
}
|
|
707
|
+
function chartToHtml(chart, ws) {
|
|
708
|
+
return `<div class="xl-chart" data-from-col="${chart.from.col}" data-from-row="${chart.from.row}" data-to-col="${chart.to.col}" data-to-row="${chart.to.row}" style="position:absolute;z-index:4">${chartToSvg(chart, ws)}</div>`;
|
|
709
|
+
}
|
|
710
|
+
function shapeSvgPath(type, w, h) {
|
|
711
|
+
switch (type) {
|
|
712
|
+
case 'rect': return `<rect x="0" y="0" width="${w}" height="${h}"/>`;
|
|
713
|
+
case 'roundRect': return `<rect x="0" y="0" width="${w}" height="${h}" rx="${Math.min(w, h) * 0.15}"/>`;
|
|
714
|
+
case 'ellipse': return `<ellipse cx="${w / 2}" cy="${h / 2}" rx="${w / 2}" ry="${h / 2}"/>`;
|
|
715
|
+
case 'triangle': return `<polygon points="${w / 2},0 ${w},${h} 0,${h}"/>`;
|
|
716
|
+
case 'diamond': return `<polygon points="${w / 2},0 ${w},${h / 2} ${w / 2},${h} 0,${h / 2}"/>`;
|
|
717
|
+
case 'pentagon': {
|
|
718
|
+
const pts = Array.from({ length: 5 }, (_, i) => {
|
|
719
|
+
const a = -Math.PI / 2 + (2 * Math.PI * i / 5);
|
|
720
|
+
return `${w / 2 + w / 2 * Math.cos(a)},${h / 2 + h / 2 * Math.sin(a)}`;
|
|
721
|
+
});
|
|
722
|
+
return `<polygon points="${pts.join(' ')}"/>`;
|
|
723
|
+
}
|
|
724
|
+
case 'hexagon': {
|
|
725
|
+
const pts = Array.from({ length: 6 }, (_, i) => {
|
|
726
|
+
const a = -Math.PI / 6 + (2 * Math.PI * i / 6);
|
|
727
|
+
return `${w / 2 + w / 2 * Math.cos(a)},${h / 2 + h / 2 * Math.sin(a)}`;
|
|
728
|
+
});
|
|
729
|
+
return `<polygon points="${pts.join(' ')}"/>`;
|
|
730
|
+
}
|
|
731
|
+
case 'octagon': {
|
|
732
|
+
const pts = Array.from({ length: 8 }, (_, i) => {
|
|
733
|
+
const a = -Math.PI / 8 + (2 * Math.PI * i / 8);
|
|
734
|
+
return `${w / 2 + w / 2 * Math.cos(a)},${h / 2 + h / 2 * Math.sin(a)}`;
|
|
735
|
+
});
|
|
736
|
+
return `<polygon points="${pts.join(' ')}"/>`;
|
|
737
|
+
}
|
|
738
|
+
case 'star5':
|
|
739
|
+
case 'star6': {
|
|
740
|
+
const n = type === 'star5' ? 5 : 6;
|
|
741
|
+
const pts = [];
|
|
742
|
+
for (let i = 0; i < n * 2; i++) {
|
|
743
|
+
const a = -Math.PI / 2 + (Math.PI * i / n);
|
|
744
|
+
const r = i % 2 === 0 ? Math.min(w, h) / 2 : Math.min(w, h) / 4.5;
|
|
745
|
+
pts.push(`${w / 2 + r * Math.cos(a)},${h / 2 + r * Math.sin(a)}`);
|
|
746
|
+
}
|
|
747
|
+
return `<polygon points="${pts.join(' ')}"/>`;
|
|
748
|
+
}
|
|
749
|
+
case 'rightArrow':
|
|
750
|
+
return `<polygon points="0,${h * 0.25} ${w * 0.65},${h * 0.25} ${w * 0.65},0 ${w},${h / 2} ${w * 0.65},${h} ${w * 0.65},${h * 0.75} 0,${h * 0.75}"/>`;
|
|
751
|
+
case 'leftArrow':
|
|
752
|
+
return `<polygon points="${w},${h * 0.25} ${w * 0.35},${h * 0.25} ${w * 0.35},0 0,${h / 2} ${w * 0.35},${h} ${w * 0.35},${h * 0.75} ${w},${h * 0.75}"/>`;
|
|
753
|
+
case 'upArrow':
|
|
754
|
+
return `<polygon points="${w * 0.25},${h} ${w * 0.25},${h * 0.35} 0,${h * 0.35} ${w / 2},0 ${w},${h * 0.35} ${w * 0.75},${h * 0.35} ${w * 0.75},${h}"/>`;
|
|
755
|
+
case 'downArrow':
|
|
756
|
+
return `<polygon points="${w * 0.25},0 ${w * 0.75},0 ${w * 0.75},${h * 0.65} ${w},${h * 0.65} ${w / 2},${h} 0,${h * 0.65} ${w * 0.25},${h * 0.65}"/>`;
|
|
757
|
+
case 'heart': {
|
|
758
|
+
const hw = w / 2, hh = h;
|
|
759
|
+
return `<path d="M${hw},${hh * 0.35} C${hw},${hh * 0.15} ${hw * 0.5},0 ${hw * 0.25},0 C0,0 0,${hh * 0.35} 0,${hh * 0.35} C0,${hh * 0.6} ${hw * 0.5},${hh * 0.8} ${hw},${hh} C${hw * 1.5},${hh * 0.8} ${w},${hh * 0.6} ${w},${hh * 0.35} C${w},${hh * 0.35} ${w},0 ${hw * 1.75},0 C${hw * 1.5},0 ${hw},${hh * 0.15} ${hw},${hh * 0.35} Z"/>`;
|
|
760
|
+
}
|
|
761
|
+
case 'lightningBolt':
|
|
762
|
+
return `<polygon points="${w * 0.55},0 ${w * 0.2},${h * 0.45} ${w * 0.45},${h * 0.45} ${w * 0.15},${h} ${w * 0.8},${h * 0.4} ${w * 0.55},${h * 0.4}"/>`;
|
|
763
|
+
case 'sun': return `<circle cx="${w / 2}" cy="${h / 2}" r="${Math.min(w, h) * 0.3}"/>`;
|
|
764
|
+
case 'moon':
|
|
765
|
+
return `<path d="M${w * 0.6},${h * 0.1} A${w * 0.4},${h * 0.4} 0 1 0 ${w * 0.6},${h * 0.9} A${w * 0.3},${h * 0.35} 0 1 1 ${w * 0.6},${h * 0.1} Z"/>`;
|
|
766
|
+
case 'smileyFace':
|
|
767
|
+
return `<circle cx="${w / 2}" cy="${h / 2}" r="${Math.min(w, h) * 0.45}"/>`
|
|
768
|
+
+ `<circle cx="${w * 0.35}" cy="${h * 0.38}" r="${Math.min(w, h) * 0.04}" fill="white"/>`
|
|
769
|
+
+ `<circle cx="${w * 0.65}" cy="${h * 0.38}" r="${Math.min(w, h) * 0.04}" fill="white"/>`
|
|
770
|
+
+ `<path d="M${w * 0.3},${h * 0.58} Q${w / 2},${h * 0.78} ${w * 0.7},${h * 0.58}" fill="none" stroke="white" stroke-width="2"/>`;
|
|
771
|
+
case 'cloud':
|
|
772
|
+
return `<ellipse cx="${w * 0.35}" cy="${h * 0.55}" rx="${w * 0.25}" ry="${h * 0.25}"/>`
|
|
773
|
+
+ `<ellipse cx="${w * 0.55}" cy="${h * 0.35}" rx="${w * 0.22}" ry="${h * 0.22}"/>`
|
|
774
|
+
+ `<ellipse cx="${w * 0.7}" cy="${h * 0.5}" rx="${w * 0.2}" ry="${h * 0.2}"/>`
|
|
775
|
+
+ `<rect x="${w * 0.15}" y="${h * 0.5}" width="${w * 0.7}" height="${h * 0.25}" rx="4"/>`;
|
|
776
|
+
case 'callout1':
|
|
777
|
+
return `<path d="M0,0 L${w},0 L${w},${h * 0.7} L${w * 0.4},${h * 0.7} L${w * 0.25},${h} L${w * 0.3},${h * 0.7} L0,${h * 0.7} Z"/>`;
|
|
778
|
+
case 'callout2':
|
|
779
|
+
return `<path d="M${w * 0.1},0 L${w * 0.9},0 Q${w},0 ${w},${h * 0.1} L${w},${h * 0.6} Q${w},${h * 0.7} ${w * 0.9},${h * 0.7} L${w * 0.4},${h * 0.7} L${w * 0.25},${h} L${w * 0.3},${h * 0.7} L${w * 0.1},${h * 0.7} Q0,${h * 0.7} 0,${h * 0.6} L0,${h * 0.1} Q0,0 ${w * 0.1},0 Z"/>`;
|
|
780
|
+
case 'flowChartProcess':
|
|
781
|
+
return `<rect x="0" y="0" width="${w}" height="${h}" rx="2"/>`;
|
|
782
|
+
case 'flowChartDecision':
|
|
783
|
+
return `<polygon points="${w / 2},0 ${w},${h / 2} ${w / 2},${h} 0,${h / 2}"/>`;
|
|
784
|
+
case 'flowChartTerminator':
|
|
785
|
+
return `<rect x="0" y="0" width="${w}" height="${h}" rx="${h / 2}"/>`;
|
|
786
|
+
case 'flowChartDocument':
|
|
787
|
+
return `<path d="M0,0 L${w},0 L${w},${h * 0.8} Q${w * 0.75},${h * 0.65} ${w * 0.5},${h * 0.8} Q${w * 0.25},${h * 0.95} 0,${h * 0.8} Z"/>`;
|
|
788
|
+
case 'line':
|
|
789
|
+
return `<line x1="0" y1="${h / 2}" x2="${w}" y2="${h / 2}" stroke-width="2"/>`;
|
|
790
|
+
case 'curvedConnector3':
|
|
791
|
+
return `<path d="M0,${h / 2} C${w * 0.3},${h * 0.1} ${w * 0.7},${h * 0.9} ${w},${h / 2}" fill="none" stroke-width="2"/>`;
|
|
792
|
+
default:
|
|
793
|
+
return `<rect x="0" y="0" width="${w}" height="${h}" rx="4"/>`;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
function shapeToHtml(shape) {
|
|
797
|
+
const toHex = (c) => { let h = c.replace(/^#/, ''); if (h.length === 8)
|
|
798
|
+
h = h.substring(2); return '#' + h; };
|
|
799
|
+
const bg = shape.fillColor ? toHex(shape.fillColor) : '#4472C4';
|
|
800
|
+
const border = shape.lineColor ? toHex(shape.lineColor) : '#2F5496';
|
|
801
|
+
const lw = shape.lineWidth ?? 2;
|
|
802
|
+
const w = 160, h = 80;
|
|
803
|
+
const rotation = shape.rotation ? ` transform="rotate(${shape.rotation} ${w / 2} ${h / 2})"` : '';
|
|
804
|
+
const textEl = shape.text ? `<text x="${w / 2}" y="${h / 2 + 5}" text-anchor="middle" fill="white" font-size="13" font-weight="600"${shape.font?.name ? ` font-family="'${escapeXml(shape.font.name)}'"` : ''}>${escapeXml(shape.text)}</text>` : '';
|
|
805
|
+
const isLine = shape.type === 'line' || shape.type === 'curvedConnector3';
|
|
806
|
+
const fillAttr = isLine ? `fill="none" stroke="${bg}"` : `fill="${bg}" stroke="${border}" stroke-width="${lw}"`;
|
|
807
|
+
return `<div class="xl-shape" data-from-col="${shape.from.col}" data-from-row="${shape.from.row}" data-to-col="${shape.to.col}" data-to-row="${shape.to.row}" style="position:absolute;z-index:2">
|
|
808
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}"${rotation}>
|
|
809
|
+
<g ${fillAttr}>${shapeSvgPath(shape.type, w, h)}</g>
|
|
810
|
+
${textEl}
|
|
811
|
+
</svg></div>`;
|
|
812
|
+
}
|
|
813
|
+
function wordArtToHtml(wa) {
|
|
814
|
+
const toHex = (c) => { let h = c.replace(/^#/, ''); if (h.length === 8)
|
|
815
|
+
h = h.substring(2); return '#' + h; };
|
|
816
|
+
const color = wa.fillColor ? toHex(wa.fillColor) : '#333';
|
|
817
|
+
const outline = wa.outlineColor ? toHex(wa.outlineColor) : '';
|
|
818
|
+
const family = wa.font?.name ?? 'Impact';
|
|
819
|
+
const size = wa.font?.size ?? 36;
|
|
820
|
+
const bold = wa.font?.bold !== false ? 'font-weight:bold;' : '';
|
|
821
|
+
const italic = wa.font?.italic ? 'font-style:italic;' : '';
|
|
822
|
+
const textStroke = outline ? `-webkit-text-stroke:1px ${outline};paint-order:stroke fill;` : '';
|
|
823
|
+
const presetStyle = wordArtPresetCSS(wa.preset);
|
|
824
|
+
return `<div class="xl-wordart" data-from-col="${wa.from.col}" data-from-row="${wa.from.row}" data-to-col="${wa.to.col}" data-to-row="${wa.to.row}" style="position:absolute;z-index:2;font-family:'${escapeXml(family)}',sans-serif;font-size:${size}px;${bold}${italic}color:${color};${textStroke}text-shadow:2px 2px 4px rgba(0,0,0,.3);${presetStyle}padding:8px 16px;white-space:nowrap;line-height:1.2">${escapeXml(wa.text)}</div>`;
|
|
825
|
+
}
|
|
826
|
+
function wordArtPresetCSS(preset) {
|
|
827
|
+
if (!preset || preset === 'textPlain')
|
|
828
|
+
return '';
|
|
829
|
+
const presets = {
|
|
830
|
+
textArchUp: 'letter-spacing:4px;',
|
|
831
|
+
textArchDown: 'letter-spacing:4px;transform:scaleY(-1);',
|
|
832
|
+
textCircle: 'letter-spacing:6px;',
|
|
833
|
+
textWave1: 'letter-spacing:2px;font-style:italic;transform:skewX(-5deg);',
|
|
834
|
+
textWave2: 'letter-spacing:2px;font-style:italic;transform:skewX(5deg);',
|
|
835
|
+
textInflate: 'letter-spacing:3px;transform:scaleY(1.3);',
|
|
836
|
+
textDeflate: 'letter-spacing:1px;transform:scaleY(0.7);',
|
|
837
|
+
textSlantUp: 'transform:perspective(300px) rotateY(-8deg) rotateX(3deg);',
|
|
838
|
+
textSlantDown: 'transform:perspective(300px) rotateY(8deg) rotateX(-3deg);',
|
|
839
|
+
textFadeUp: 'transform:perspective(200px) rotateX(-8deg);',
|
|
840
|
+
textFadeDown: 'transform:perspective(200px) rotateX(8deg);',
|
|
841
|
+
textFadeLeft: 'transform:perspective(200px) rotateY(8deg);',
|
|
842
|
+
textFadeRight: 'transform:perspective(200px) rotateY(-8deg);',
|
|
843
|
+
textCascadeUp: 'letter-spacing:3px;transform:rotate(-5deg) scaleX(1.1);',
|
|
844
|
+
textCascadeDown: 'letter-spacing:3px;transform:rotate(5deg) scaleX(1.1);',
|
|
845
|
+
textChevron: 'letter-spacing:5px;transform:scaleX(1.15);',
|
|
846
|
+
textRingInside: 'letter-spacing:8px;',
|
|
847
|
+
textRingOutside: 'letter-spacing:6px;',
|
|
848
|
+
textStop: 'letter-spacing:2px;transform:scaleY(0.85) scaleX(1.1);',
|
|
849
|
+
};
|
|
850
|
+
return presets[preset] ?? '';
|
|
851
|
+
}
|
|
852
|
+
function toBase64(bytes) {
|
|
853
|
+
let b64 = '';
|
|
854
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
855
|
+
const b0 = bytes[i], b1 = bytes[i + 1] ?? 0, b2 = bytes[i + 2] ?? 0;
|
|
856
|
+
const n = (b0 << 16) | (b1 << 8) | b2;
|
|
857
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
858
|
+
b64 += chars[(n >> 18) & 63] + chars[(n >> 12) & 63];
|
|
859
|
+
b64 += i + 1 < bytes.length ? chars[(n >> 6) & 63] : '=';
|
|
860
|
+
b64 += i + 2 < bytes.length ? chars[n & 63] : '=';
|
|
861
|
+
}
|
|
862
|
+
return b64;
|
|
863
|
+
}
|
|
864
|
+
function imageDataUri(data, format) {
|
|
865
|
+
const mime = formatToMime(format);
|
|
866
|
+
const b64 = typeof data === 'string' ? data : toBase64(data);
|
|
867
|
+
return `data:${mime};base64,${b64}`;
|
|
868
|
+
}
|
|
869
|
+
function imageToPositionedHtml(img) {
|
|
870
|
+
const src = imageDataUri(img.data, img.format);
|
|
871
|
+
const alt = img.altText ? ` alt="${escapeXml(img.altText)}"` : '';
|
|
872
|
+
const w = img.width ? `width:${img.width}px;` : 'max-width:400px;';
|
|
873
|
+
const h = img.height ? `height:${img.height}px;` : 'max-height:300px;';
|
|
874
|
+
const fromCol = img.from?.col ?? 0;
|
|
875
|
+
const fromRow = img.from?.row ?? 0;
|
|
876
|
+
return `<img src="${src}"${alt} class="xl-img" data-from-col="${fromCol}" data-from-row="${fromRow}" style="${w}${h}border:1px solid #ddd;border-radius:4px"/>`;
|
|
877
|
+
}
|
|
878
|
+
function cellImageToHtml(ci) {
|
|
879
|
+
const src = imageDataUri(ci.data, ci.format);
|
|
880
|
+
const alt = ci.altText ? ` alt="${escapeXml(ci.altText)}"` : '';
|
|
881
|
+
return `<img src="${src}"${alt} style="max-width:100%;max-height:100%;object-fit:contain"/>`;
|
|
882
|
+
}
|
|
883
|
+
function formatToMime(fmt) {
|
|
884
|
+
switch (fmt) {
|
|
885
|
+
case 'jpeg':
|
|
886
|
+
case 'jpg': return 'image/jpeg';
|
|
887
|
+
case 'gif': return 'image/gif';
|
|
888
|
+
case 'svg': return 'image/svg+xml';
|
|
889
|
+
case 'webp': return 'image/webp';
|
|
890
|
+
case 'bmp': return 'image/bmp';
|
|
891
|
+
default: return 'image/png';
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
function mathElementToMathML(el) {
|
|
895
|
+
switch (el.type) {
|
|
896
|
+
case 'text': {
|
|
897
|
+
const text = escapeXml(el.text ?? '');
|
|
898
|
+
if (el.font === 'normal' || /^[a-zA-Z]{2,}$/.test(el.text ?? ''))
|
|
899
|
+
return `<mi mathvariant="normal">${text}</mi>`;
|
|
900
|
+
if (/^[0-9.]+$/.test(el.text ?? ''))
|
|
901
|
+
return `<mn>${text}</mn>`;
|
|
902
|
+
if (el.text && el.text.length === 1 && /[+\-*/=<>±×÷≤≥≠∞∈∉∪∩⊂⊃∧∨¬→←↔∀∃∑∏∫]/.test(el.text))
|
|
903
|
+
return `<mo>${text}</mo>`;
|
|
904
|
+
return `<mi>${text}</mi>`;
|
|
905
|
+
}
|
|
906
|
+
case 'frac':
|
|
907
|
+
return `<mfrac><mrow>${(el.base ?? []).map(mathElementToMathML).join('')}</mrow><mrow>${(el.argument ?? []).map(mathElementToMathML).join('')}</mrow></mfrac>`;
|
|
908
|
+
case 'sup':
|
|
909
|
+
return `<msup><mrow>${(el.base ?? []).map(mathElementToMathML).join('')}</mrow><mrow>${(el.argument ?? []).map(mathElementToMathML).join('')}</mrow></msup>`;
|
|
910
|
+
case 'sub':
|
|
911
|
+
return `<msub><mrow>${(el.base ?? []).map(mathElementToMathML).join('')}</mrow><mrow>${(el.argument ?? []).map(mathElementToMathML).join('')}</mrow></msub>`;
|
|
912
|
+
case 'subSup':
|
|
913
|
+
return `<msubsup><mrow>${(el.base ?? []).map(mathElementToMathML).join('')}</mrow><mrow>${(el.subscript ?? []).map(mathElementToMathML).join('')}</mrow><mrow>${(el.superscript ?? []).map(mathElementToMathML).join('')}</mrow></msubsup>`;
|
|
914
|
+
case 'nary':
|
|
915
|
+
return `<munderover><mo>${escapeXml(el.operator ?? '∑')}</mo><mrow>${(el.lower ?? []).map(mathElementToMathML).join('')}</mrow><mrow>${(el.upper ?? []).map(mathElementToMathML).join('')}</mrow></munderover><mrow>${(el.body ?? []).map(mathElementToMathML).join('')}</mrow>`;
|
|
916
|
+
case 'rad':
|
|
917
|
+
if (!el.hideDegree && el.degree?.length)
|
|
918
|
+
return `<mroot><mrow>${(el.body ?? []).map(mathElementToMathML).join('')}</mrow><mrow>${el.degree.map(mathElementToMathML).join('')}</mrow></mroot>`;
|
|
919
|
+
return `<msqrt><mrow>${(el.body ?? []).map(mathElementToMathML).join('')}</mrow></msqrt>`;
|
|
920
|
+
case 'delim':
|
|
921
|
+
return `<mrow><mo>${escapeXml(el.open ?? '(')}</mo>${(el.body ?? []).map(mathElementToMathML).join('')}<mo>${escapeXml(el.close ?? ')')}</mo></mrow>`;
|
|
922
|
+
case 'func':
|
|
923
|
+
return `<mrow><mi mathvariant="normal">${(el.base ?? []).map(e => escapeXml(e.text ?? '')).join('')}</mi><mo>⁡</mo>${(el.argument ?? []).map(mathElementToMathML).join('')}</mrow>`;
|
|
924
|
+
case 'groupChar':
|
|
925
|
+
return `<munder><mrow>${(el.base ?? []).map(mathElementToMathML).join('')}</mrow><mo>${escapeXml(el.operator ?? '⏟')}</mo></munder>`;
|
|
926
|
+
case 'accent':
|
|
927
|
+
return `<mover accent="true"><mrow>${(el.base ?? []).map(mathElementToMathML).join('')}</mrow><mo>${escapeXml(el.operator ?? '̂')}</mo></mover>`;
|
|
928
|
+
case 'bar':
|
|
929
|
+
return `<mover><mrow>${(el.base ?? []).map(mathElementToMathML).join('')}</mrow><mo>¯</mo></mover>`;
|
|
930
|
+
case 'limLow':
|
|
931
|
+
return `<munder><mrow>${(el.base ?? []).map(mathElementToMathML).join('')}</mrow><mrow>${(el.argument ?? []).map(mathElementToMathML).join('')}</mrow></munder>`;
|
|
932
|
+
case 'limUpp':
|
|
933
|
+
return `<mover><mrow>${(el.base ?? []).map(mathElementToMathML).join('')}</mrow><mrow>${(el.argument ?? []).map(mathElementToMathML).join('')}</mrow></mover>`;
|
|
934
|
+
case 'eqArr':
|
|
935
|
+
return `<mtable>${(el.rows ?? []).map(row => `<mtr><mtd>${row.map(mathElementToMathML).join('')}</mtd></mtr>`).join('')}</mtable>`;
|
|
936
|
+
case 'matrix':
|
|
937
|
+
return `<mrow><mo>(</mo><mtable>${(el.rows ?? []).map(row => `<mtr>${row.map(c => `<mtd>${mathElementToMathML(c)}</mtd>`).join('')}</mtr>`).join('')}</mtable><mo>)</mo></mrow>`;
|
|
938
|
+
default:
|
|
939
|
+
return el.text ? `<mi>${escapeXml(el.text)}</mi>` : '<mrow></mrow>';
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
function mathEquationToMathML(eq) {
|
|
943
|
+
const size = eq.fontSize ?? 11;
|
|
944
|
+
const font = eq.fontName ?? 'Cambria Math';
|
|
945
|
+
return `<div class="xl-math" data-from-col="${eq.from.col}" data-from-row="${eq.from.row}" style="position:absolute;z-index:2;font-family:'${escapeXml(font)}',serif;font-size:${size}pt;padding:4px;background:white"><math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><mrow>${eq.elements.map(mathElementToMathML).join('')}</mrow></math></div>`;
|
|
946
|
+
}
|
|
947
|
+
function formControlToPositionedHtml(fc) {
|
|
948
|
+
const fromCol = fc.from.col;
|
|
949
|
+
const fromRow = fc.from.row;
|
|
950
|
+
const toCol = fc.to?.col;
|
|
951
|
+
const toRow = fc.to?.row;
|
|
952
|
+
const toAttrs = toCol != null && toRow != null ? ` data-to-col="${toCol}" data-to-row="${toRow}"` : '';
|
|
953
|
+
const linked = fc.linkedCell ? ` data-linked-cell="${escapeXml(fc.linkedCell)}"` : '';
|
|
954
|
+
const inputRange = fc.inputRange ? ` data-input-range="${escapeXml(fc.inputRange)}"` : '';
|
|
955
|
+
const macro = fc.macro ? ` data-macro="${escapeXml(fc.macro)}"` : '';
|
|
956
|
+
let inner = '';
|
|
957
|
+
switch (fc.type) {
|
|
958
|
+
case 'button':
|
|
959
|
+
case 'dialog':
|
|
960
|
+
inner = `<button style="width:100%;height:100%;padding:4px 12px;font-size:13px;border:1px outset #ccc;background:linear-gradient(180deg,#f8f8f8,#e0e0e0);cursor:pointer;border-radius:3px;white-space:nowrap"${macro}>${escapeXml(fc.text ?? 'Button')}</button>`;
|
|
961
|
+
break;
|
|
962
|
+
case 'checkBox': {
|
|
963
|
+
const checked = fc.checked === 'checked' ? ' checked' : '';
|
|
964
|
+
const indeterminate = fc.checked === 'mixed' ? ' data-indeterminate="true"' : '';
|
|
965
|
+
inner = `<label style="font-size:13px;display:inline-flex;align-items:center;gap:4px;width:100%;height:100%;padding:2px 4px;cursor:pointer"><input type="checkbox"${checked}${indeterminate}${linked}/> ${escapeXml(fc.text ?? 'Checkbox')}</label>`;
|
|
966
|
+
break;
|
|
967
|
+
}
|
|
968
|
+
case 'optionButton': {
|
|
969
|
+
const checked = fc.checked === 'checked' ? ' checked' : '';
|
|
970
|
+
inner = `<label style="font-size:13px;display:inline-flex;align-items:center;gap:4px;width:100%;height:100%;padding:2px 4px;cursor:pointer"><input type="radio" name="group"${checked}${linked}/> ${escapeXml(fc.text ?? 'Option')}</label>`;
|
|
971
|
+
break;
|
|
972
|
+
}
|
|
973
|
+
case 'comboBox': {
|
|
974
|
+
const lines = fc.dropLines ?? 8;
|
|
975
|
+
inner = `<select style="width:100%;height:100%;padding:2px 4px;font-size:13px;border:1px solid #aaa;background:white"${linked}${inputRange} size="1" data-drop-lines="${lines}"><option>${escapeXml(fc.text ?? 'Select...')}</option></select>`;
|
|
976
|
+
break;
|
|
977
|
+
}
|
|
978
|
+
case 'listBox': {
|
|
979
|
+
const size = fc.dropLines ?? 5;
|
|
980
|
+
const sel = fc.selType ?? 'single';
|
|
981
|
+
const multi = sel === 'multi' || sel === 'extend' ? ' multiple' : '';
|
|
982
|
+
inner = `<select style="width:100%;height:100%;padding:2px;font-size:13px;border:1px solid #aaa;background:white"${linked}${inputRange} size="${size}"${multi}><option>${escapeXml(fc.text ?? 'Item')}</option></select>`;
|
|
983
|
+
break;
|
|
984
|
+
}
|
|
985
|
+
case 'spinner': {
|
|
986
|
+
const min = fc.min ?? 0;
|
|
987
|
+
const max = fc.max ?? 100;
|
|
988
|
+
const step = fc.inc ?? 1;
|
|
989
|
+
const val = fc.val ?? min;
|
|
990
|
+
inner = `<input type="number" value="${val}" min="${min}" max="${max}" step="${step}" style="width:100%;height:100%;padding:2px 4px;font-size:13px;border:1px solid #aaa"${linked}/>`;
|
|
991
|
+
break;
|
|
992
|
+
}
|
|
993
|
+
case 'scrollBar': {
|
|
994
|
+
const min = fc.min ?? 0;
|
|
995
|
+
const max = fc.max ?? 100;
|
|
996
|
+
const step = fc.inc ?? 1;
|
|
997
|
+
const val = fc.val ?? min;
|
|
998
|
+
inner = `<input type="range" value="${val}" min="${min}" max="${max}" step="${step}" style="width:100%;height:100%"${linked}/>`;
|
|
999
|
+
break;
|
|
1000
|
+
}
|
|
1001
|
+
case 'label':
|
|
1002
|
+
inner = `<span style="font-size:13px;display:flex;align-items:center;width:100%;height:100%;padding:2px 4px">${escapeXml(fc.text ?? 'Label')}</span>`;
|
|
1003
|
+
break;
|
|
1004
|
+
case 'groupBox':
|
|
1005
|
+
inner = `<fieldset style="width:100%;height:100%;padding:8px;border:1px solid #999;font-size:13px;margin:0;box-sizing:border-box"><legend>${escapeXml(fc.text ?? 'Group')}</legend></fieldset>`;
|
|
1006
|
+
break;
|
|
1007
|
+
default:
|
|
1008
|
+
inner = `<span style="font-size:13px">[${escapeXml(fc.type)}]</span>`;
|
|
1009
|
+
}
|
|
1010
|
+
return `<div class="xl-fc" data-from-col="${fromCol}" data-from-row="${fromRow}"${toAttrs} style="position:absolute;overflow:hidden">${inner}</div>`;
|
|
1011
|
+
}
|
|
1012
|
+
function colLetterToIdx(letter) {
|
|
1013
|
+
let idx = 0;
|
|
1014
|
+
for (let i = 0; i < letter.length; i++) {
|
|
1015
|
+
idx = idx * 26 + (letter.charCodeAt(i) - 64);
|
|
1016
|
+
}
|
|
1017
|
+
return idx;
|
|
1018
|
+
}
|
|
1019
|
+
function resolveSparklineData(ws, dataRange) {
|
|
1020
|
+
const vals = [];
|
|
1021
|
+
const ref = dataRange.includes('!') ? dataRange.split('!')[1] : dataRange;
|
|
1022
|
+
const m = ref.match(/^([A-Z]+)(\d+):([A-Z]+)(\d+)$/);
|
|
1023
|
+
if (m) {
|
|
1024
|
+
const c1 = colLetterToIdx(m[1]), r1 = parseInt(m[2], 10);
|
|
1025
|
+
const c2 = colLetterToIdx(m[3]), r2 = parseInt(m[4], 10);
|
|
1026
|
+
for (let r = r1; r <= r2; r++) {
|
|
1027
|
+
for (let c = c1; c <= c2; c++) {
|
|
1028
|
+
const cell = ws.getCell(r, c);
|
|
1029
|
+
if (typeof cell.value === 'number')
|
|
1030
|
+
vals.push(cell.value);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
return vals;
|
|
1035
|
+
}
|
|
1036
|
+
export function worksheetToHtml(ws, options = {}) {
|
|
1037
|
+
const range = ws.getUsedRange();
|
|
1038
|
+
if (!range) {
|
|
1039
|
+
return options.fullDocument !== false
|
|
1040
|
+
? `<!DOCTYPE html><html><head><title>${escapeXml(options.title ?? '')}</title></head><body><p>Empty worksheet</p></body></html>`
|
|
1041
|
+
: '<table></table>';
|
|
1042
|
+
}
|
|
1043
|
+
let { startRow, startCol, endRow, endCol } = range;
|
|
1044
|
+
if (options.printAreaOnly && ws.printArea) {
|
|
1045
|
+
const pa = ws.printArea;
|
|
1046
|
+
const m = pa.match(/^([A-Z]+)(\d+):([A-Z]+)(\d+)$/);
|
|
1047
|
+
if (m) {
|
|
1048
|
+
startCol = colLetterToIdx(m[1]);
|
|
1049
|
+
startRow = parseInt(m[2], 10);
|
|
1050
|
+
endCol = colLetterToIdx(m[3]);
|
|
1051
|
+
endRow = parseInt(m[4], 10);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
const merges = ws.getMerges();
|
|
1055
|
+
const conditionalFormats = options.includeConditionalFormatting !== false ? ws.getConditionalFormats() : [];
|
|
1056
|
+
const sparklines = options.includeSparklines !== false ? ws.getSparklines() : [];
|
|
1057
|
+
const sparklineMap = new Map();
|
|
1058
|
+
for (const sp of sparklines) {
|
|
1059
|
+
const m = sp.location.match(/^([A-Z]+)(\d+)$/);
|
|
1060
|
+
if (m)
|
|
1061
|
+
sparklineMap.set(`${parseInt(m[2], 10)},${colLetterToIdx(m[1])}`, sp);
|
|
1062
|
+
}
|
|
1063
|
+
const cfValueMap = new Map();
|
|
1064
|
+
for (const cf of conditionalFormats) {
|
|
1065
|
+
if (!cf.colorScale && !cf.dataBar && !cf.iconSet)
|
|
1066
|
+
continue;
|
|
1067
|
+
const vals = [];
|
|
1068
|
+
const refs = cf.sqref.split(' ');
|
|
1069
|
+
for (const ref of refs) {
|
|
1070
|
+
const rm = ref.match(/^([A-Z]+)(\d+):([A-Z]+)(\d+)$/);
|
|
1071
|
+
if (rm) {
|
|
1072
|
+
for (let r = parseInt(rm[2], 10); r <= parseInt(rm[4], 10); r++) {
|
|
1073
|
+
for (let c = colLetterToIdx(rm[1]); c <= colLetterToIdx(rm[3]); c++) {
|
|
1074
|
+
const cell = ws.getCell(r, c);
|
|
1075
|
+
if (typeof cell.value === 'number')
|
|
1076
|
+
vals.push(cell.value);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
cfValueMap.set(cf, vals);
|
|
1082
|
+
}
|
|
1083
|
+
const mergeMap = new Map();
|
|
1084
|
+
for (const m of merges) {
|
|
1085
|
+
const rs = m.endRow - m.startRow + 1;
|
|
1086
|
+
const cs = m.endCol - m.startCol + 1;
|
|
1087
|
+
mergeMap.set(`${m.startRow},${m.startCol}`, { rowSpan: rs, colSpan: cs });
|
|
1088
|
+
for (let r = m.startRow; r <= m.endRow; r++) {
|
|
1089
|
+
for (let c = m.startCol; c <= m.endCol; c++) {
|
|
1090
|
+
if (r !== m.startRow || c !== m.startCol)
|
|
1091
|
+
mergeMap.set(`${r},${c}`, 'skip');
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
const colWidths = [];
|
|
1096
|
+
if (options.includeColumnWidths !== false) {
|
|
1097
|
+
for (let c = startCol; c <= endCol; c++) {
|
|
1098
|
+
const def = ws.getColumn(c);
|
|
1099
|
+
if (options.skipHidden && def?.hidden)
|
|
1100
|
+
continue;
|
|
1101
|
+
const w = def?.width ? Math.round(def.width * 7.5) : undefined;
|
|
1102
|
+
colWidths.push(w ? `<col style="width:${w}px">` : '<col>');
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
const cellImageMap = new Map();
|
|
1106
|
+
const cellImages = ws.getCellImages?.();
|
|
1107
|
+
if (cellImages) {
|
|
1108
|
+
for (const ci of cellImages)
|
|
1109
|
+
cellImageMap.set(ci.cell, ci);
|
|
1110
|
+
}
|
|
1111
|
+
const rows = [];
|
|
1112
|
+
for (let r = startRow; r <= endRow; r++) {
|
|
1113
|
+
const rowDef = ws.getRow(r);
|
|
1114
|
+
if (options.skipHidden && rowDef?.hidden)
|
|
1115
|
+
continue;
|
|
1116
|
+
const rowStyle = rowDef?.height ? ` style="height:${rowDef.height}px"` : '';
|
|
1117
|
+
const cells = [];
|
|
1118
|
+
for (let c = startCol; c <= endCol; c++) {
|
|
1119
|
+
const colDef = ws.getColumn(c);
|
|
1120
|
+
if (options.skipHidden && colDef?.hidden)
|
|
1121
|
+
continue;
|
|
1122
|
+
const key = `${r},${c}`;
|
|
1123
|
+
const merge = mergeMap.get(key);
|
|
1124
|
+
if (merge === 'skip')
|
|
1125
|
+
continue;
|
|
1126
|
+
const cell = ws.getCell(r, c);
|
|
1127
|
+
let val = '';
|
|
1128
|
+
const cellRef = `${colIndexToLetter(c)}${r}`;
|
|
1129
|
+
const ci = cellImageMap.get(cellRef);
|
|
1130
|
+
if (ci) {
|
|
1131
|
+
val = cellImageToHtml(ci);
|
|
1132
|
+
}
|
|
1133
|
+
else if (cell.richText) {
|
|
1134
|
+
val = cell.richText.map(run => {
|
|
1135
|
+
const s = run.font ? fontToCSS(run.font) : '';
|
|
1136
|
+
return s ? `<span style="${s}">${escapeXml(run.text)}</span>` : escapeXml(run.text);
|
|
1137
|
+
}).join('');
|
|
1138
|
+
}
|
|
1139
|
+
else if (cell.value != null) {
|
|
1140
|
+
const formatted = cell.style?.numberFormat
|
|
1141
|
+
? formatNumber(cell.value, cell.style.numberFormat.formatCode)
|
|
1142
|
+
: String(cell.value);
|
|
1143
|
+
val = escapeXml(formatted);
|
|
1144
|
+
}
|
|
1145
|
+
if (cell.hyperlink) {
|
|
1146
|
+
const href = escapeXml(cell.hyperlink.href ?? '');
|
|
1147
|
+
const tip = cell.hyperlink.tooltip ? ` title="${escapeXml(cell.hyperlink.tooltip)}"` : '';
|
|
1148
|
+
val = `<a href="${href}"${tip} style="color:#0563C1;text-decoration:underline">${val}</a>`;
|
|
1149
|
+
}
|
|
1150
|
+
if (cell.comment) {
|
|
1151
|
+
const commentText = cell.comment.richText
|
|
1152
|
+
? cell.comment.richText.map(run => run.text).join('')
|
|
1153
|
+
: cell.comment.text;
|
|
1154
|
+
val = `<span title="${escapeXml(commentText)}" style="cursor:help">${val}</span>`;
|
|
1155
|
+
}
|
|
1156
|
+
const attrs = [];
|
|
1157
|
+
if (merge && typeof merge !== 'string') {
|
|
1158
|
+
if (merge.rowSpan > 1)
|
|
1159
|
+
attrs.push(`rowspan="${merge.rowSpan}"`);
|
|
1160
|
+
if (merge.colSpan > 1)
|
|
1161
|
+
attrs.push(`colspan="${merge.colSpan}"`);
|
|
1162
|
+
}
|
|
1163
|
+
const cssParts = [];
|
|
1164
|
+
if (options.includeStyles !== false && cell.style)
|
|
1165
|
+
cssParts.push(styleToCSS(cell.style));
|
|
1166
|
+
let iconAttr = '';
|
|
1167
|
+
if (typeof cell.value === 'number') {
|
|
1168
|
+
for (const cf of conditionalFormats) {
|
|
1169
|
+
const allVals = cfValueMap.get(cf);
|
|
1170
|
+
if (!allVals)
|
|
1171
|
+
continue;
|
|
1172
|
+
const result = evaluateConditionalFormats(cf, cell.value, allVals);
|
|
1173
|
+
if (result.startsWith('data-icon=')) {
|
|
1174
|
+
iconAttr = ` ${result}`;
|
|
1175
|
+
}
|
|
1176
|
+
else if (result) {
|
|
1177
|
+
cssParts.push(result);
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
const css = cssParts.filter(Boolean).join(';');
|
|
1182
|
+
if (css)
|
|
1183
|
+
attrs.push(`style="${css}"`);
|
|
1184
|
+
attrs.push(`data-cell="${colIndexToLetter(c)}${r}"`);
|
|
1185
|
+
const sp = sparklineMap.get(key);
|
|
1186
|
+
if (sp)
|
|
1187
|
+
val += sparklineToSvg(sp, resolveSparklineData(ws, sp.dataRange));
|
|
1188
|
+
const attrStr = attrs.length ? ' ' + attrs.join(' ') : '';
|
|
1189
|
+
cells.push(`<td${attrStr}${iconAttr}>${val}</td>`);
|
|
1190
|
+
}
|
|
1191
|
+
rows.push(`<tr${rowStyle}>${cells.join('')}</tr>`);
|
|
1192
|
+
}
|
|
1193
|
+
const colGroup = colWidths.length ? `<colgroup>${colWidths.join('')}</colgroup>` : '';
|
|
1194
|
+
const tableHtml = `<div class="xl-sheet-wrapper" style="position:relative;display:inline-block"><table border="0" cellpadding="4" cellspacing="0">\n${colGroup}\n${rows.join('\n')}\n</table>`;
|
|
1195
|
+
let chartsHtml = '';
|
|
1196
|
+
if (options.includeCharts !== false) {
|
|
1197
|
+
const charts = ws.getCharts();
|
|
1198
|
+
if (charts.length)
|
|
1199
|
+
chartsHtml = '\n<div class="xl-charts">' + charts.map(ch => chartToHtml(ch, ws)).join('\n') + '</div>';
|
|
1200
|
+
}
|
|
1201
|
+
let imagesHtml = '';
|
|
1202
|
+
const images = ws.getImages?.();
|
|
1203
|
+
if (images?.length)
|
|
1204
|
+
imagesHtml = '\n<div class="xl-images">' + images.map(imageToPositionedHtml).join('\n') + '</div>';
|
|
1205
|
+
let shapesHtml = '';
|
|
1206
|
+
const shapes = ws.getShapes?.();
|
|
1207
|
+
if (shapes?.length)
|
|
1208
|
+
shapesHtml = '\n<div class="xl-shapes">' + shapes.map(shapeToHtml).join('\n') + '</div>';
|
|
1209
|
+
let wordArtHtml = '';
|
|
1210
|
+
const wordArts = ws.getWordArt?.();
|
|
1211
|
+
if (wordArts?.length)
|
|
1212
|
+
wordArtHtml = '\n<div class="xl-wordarts">' + wordArts.map(wordArtToHtml).join('\n') + '</div>';
|
|
1213
|
+
let mathHtml = '';
|
|
1214
|
+
const mathEqs = ws.getMathEquations?.();
|
|
1215
|
+
if (mathEqs?.length)
|
|
1216
|
+
mathHtml = '\n<div class="xl-math-equations">' + mathEqs.map(mathEquationToMathML).join('\n') + '</div>';
|
|
1217
|
+
let formControlsHtml = '';
|
|
1218
|
+
const fcs = ws.getFormControls?.();
|
|
1219
|
+
if (fcs?.length)
|
|
1220
|
+
formControlsHtml = '\n<div class="xl-form-controls">' + fcs.map(formControlToPositionedHtml).join('\n') + '</div>';
|
|
1221
|
+
const extraHtml = chartsHtml + imagesHtml + shapesHtml + wordArtHtml + mathHtml + formControlsHtml;
|
|
1222
|
+
const wrapperClose = '</div>';
|
|
1223
|
+
if (options.fullDocument === false)
|
|
1224
|
+
return tableHtml + extraHtml + wrapperClose;
|
|
1225
|
+
const title = escapeXml(options.title ?? options.sheetName ?? 'Export');
|
|
1226
|
+
const css = `<style>
|
|
1227
|
+
* { box-sizing: border-box; }
|
|
1228
|
+
body { font-family: 'Segoe UI', Calibri, sans-serif; margin: 20px; background: #f5f6fa; }
|
|
1229
|
+
.xl-sheet-wrapper { position: relative; display: inline-block; }
|
|
1230
|
+
table { border-collapse: collapse; background: white; box-shadow: 0 1px 4px rgba(0,0,0,.1); }
|
|
1231
|
+
td { padding: 4px 8px; border: 1px solid #d4d4d4; vertical-align: bottom; }
|
|
1232
|
+
td[data-icon]::before { content: attr(data-icon); margin-right: 4px; }
|
|
1233
|
+
.xl-images { position: absolute; top: 0; left: 0; pointer-events: none; }
|
|
1234
|
+
.xl-images .xl-img { pointer-events: auto; position: absolute; z-index: 2; }
|
|
1235
|
+
.xl-charts { position: absolute; top: 0; left: 0; pointer-events: none; }
|
|
1236
|
+
.xl-charts .xl-chart { pointer-events: auto; }
|
|
1237
|
+
.xl-shapes { position: absolute; top: 0; left: 0; pointer-events: none; }
|
|
1238
|
+
.xl-shapes .xl-shape { pointer-events: auto; }
|
|
1239
|
+
.xl-wordarts { position: absolute; top: 0; left: 0; pointer-events: none; }
|
|
1240
|
+
.xl-wordarts .xl-wordart { pointer-events: auto; }
|
|
1241
|
+
.xl-math-equations { position: absolute; top: 0; left: 0; pointer-events: none; }
|
|
1242
|
+
.xl-math-equations .xl-math { pointer-events: auto; }
|
|
1243
|
+
.xl-form-controls { position: absolute; top: 0; left: 0; pointer-events: none; }
|
|
1244
|
+
.xl-form-controls .xl-fc { pointer-events: auto; z-index: 3; }
|
|
1245
|
+
a { color: #0563C1; }
|
|
1246
|
+
</style>`;
|
|
1247
|
+
const positionScript = `<script>
|
|
1248
|
+
(function(){
|
|
1249
|
+
document.querySelectorAll('.xl-sheet-wrapper').forEach(function(wrapper){
|
|
1250
|
+
var table = wrapper.querySelector('table');
|
|
1251
|
+
if (!table) return;
|
|
1252
|
+
var startRow = parseInt((table.rows[0] && table.rows[0].cells[0] || {}).getAttribute && table.rows[0].cells[0].getAttribute('data-cell') ? table.rows[0].cells[0].getAttribute('data-cell').replace(/[A-Z]+/,'') : '1', 10) || 1;
|
|
1253
|
+
var startCol = 1;
|
|
1254
|
+
var firstCell = table.rows[0] && table.rows[0].cells[0] ? table.rows[0].cells[0].getAttribute('data-cell') : null;
|
|
1255
|
+
if (firstCell) {
|
|
1256
|
+
var colStr = firstCell.replace(/[0-9]+/g,'');
|
|
1257
|
+
startCol = 0;
|
|
1258
|
+
for (var ci = 0; ci < colStr.length; ci++) startCol = startCol * 26 + (colStr.charCodeAt(ci) - 64);
|
|
1259
|
+
}
|
|
1260
|
+
function cellRect(sheetRow, sheetCol) {
|
|
1261
|
+
var ri = sheetRow - startRow;
|
|
1262
|
+
var colIdx = sheetCol - startCol;
|
|
1263
|
+
if (ri < 0) ri = 0;
|
|
1264
|
+
if (colIdx < 0) colIdx = 0;
|
|
1265
|
+
var tr = table.rows[ri];
|
|
1266
|
+
if (!tr) tr = table.rows[table.rows.length - 1] || table.rows[0];
|
|
1267
|
+
if (!tr) return {x:0, y:0, w:0, h:0};
|
|
1268
|
+
var td = tr.cells[colIdx];
|
|
1269
|
+
if (!td) td = tr.cells[tr.cells.length - 1] || tr.cells[0];
|
|
1270
|
+
if (!td) return {x:0, y:0, w:0, h:0};
|
|
1271
|
+
return {x: td.offsetLeft, y: td.offsetTop, w: td.offsetWidth, h: td.offsetHeight};
|
|
1272
|
+
}
|
|
1273
|
+
wrapper.querySelectorAll('[data-from-col][data-from-row]').forEach(function(el){
|
|
1274
|
+
var fc = parseInt(el.getAttribute('data-from-col'),10);
|
|
1275
|
+
var fr = parseInt(el.getAttribute('data-from-row'),10);
|
|
1276
|
+
var from = cellRect(fr, fc);
|
|
1277
|
+
el.style.left = from.x + 'px';
|
|
1278
|
+
el.style.top = from.y + 'px';
|
|
1279
|
+
var tc = el.getAttribute('data-to-col');
|
|
1280
|
+
var tr2 = el.getAttribute('data-to-row');
|
|
1281
|
+
if (tc !== null && tr2 !== null) {
|
|
1282
|
+
var to = cellRect(parseInt(tr2,10), parseInt(tc,10));
|
|
1283
|
+
var w = to.x + to.w - from.x;
|
|
1284
|
+
var h = to.y + to.h - from.y;
|
|
1285
|
+
if (w > 0) el.style.width = w + 'px';
|
|
1286
|
+
if (h > 0) el.style.height = h + 'px';
|
|
1287
|
+
}
|
|
1288
|
+
});
|
|
1289
|
+
});
|
|
1290
|
+
})();
|
|
1291
|
+
</script>`;
|
|
1292
|
+
return `<!DOCTYPE html>
|
|
1293
|
+
<html lang="en">
|
|
1294
|
+
<head>
|
|
1295
|
+
<meta charset="UTF-8">
|
|
1296
|
+
<title>${title}</title>
|
|
1297
|
+
${css}
|
|
1298
|
+
</head>
|
|
1299
|
+
<body>
|
|
1300
|
+
${tableHtml}${extraHtml}${wrapperClose}
|
|
1301
|
+
${positionScript}
|
|
1302
|
+
</body>
|
|
1303
|
+
</html>`;
|
|
1304
|
+
}
|
|
1305
|
+
export function workbookToHtml(wb, options = {}) {
|
|
1306
|
+
const sheets = wb.getSheets();
|
|
1307
|
+
const names = wb.getSheetNames();
|
|
1308
|
+
const selected = options.sheets ?? names;
|
|
1309
|
+
const includeTabs = options.includeTabs !== false;
|
|
1310
|
+
const sheetHtmls = [];
|
|
1311
|
+
for (let i = 0; i < sheets.length; i++) {
|
|
1312
|
+
if (!selected.includes(names[i]))
|
|
1313
|
+
continue;
|
|
1314
|
+
if (sheets[i]._isChartSheet || sheets[i]._isDialogSheet)
|
|
1315
|
+
continue;
|
|
1316
|
+
const html = worksheetToHtml(sheets[i], { ...options, fullDocument: false, sheetName: names[i] });
|
|
1317
|
+
sheetHtmls.push({ name: names[i], html });
|
|
1318
|
+
}
|
|
1319
|
+
if (sheetHtmls.length === 1 && !includeTabs) {
|
|
1320
|
+
return worksheetToHtml(sheets[0], options);
|
|
1321
|
+
}
|
|
1322
|
+
const title = escapeXml(options.title ?? 'Workbook Export');
|
|
1323
|
+
const tabs = sheetHtmls.map((s, i) => `<button class="tab${i === 0 ? ' active' : ''}" onclick="switchTab(${i})">${escapeXml(s.name)}</button>`).join('');
|
|
1324
|
+
const panels = sheetHtmls.map((s, i) => `<div class="panel${i === 0 ? ' active' : ''}" id="panel-${i}">${s.html}</div>`).join('\n');
|
|
1325
|
+
return `<!DOCTYPE html>
|
|
1326
|
+
<html lang="en">
|
|
1327
|
+
<head>
|
|
1328
|
+
<meta charset="UTF-8">
|
|
1329
|
+
<title>${title}</title>
|
|
1330
|
+
<style>
|
|
1331
|
+
* { box-sizing: border-box; }
|
|
1332
|
+
body { font-family: 'Segoe UI', Calibri, sans-serif; margin: 0; background: #f5f6fa; }
|
|
1333
|
+
.tab-bar { display: flex; background: #2b579a; padding: 0 16px; gap: 2px; position: sticky; top: 0; z-index: 10; }
|
|
1334
|
+
.tab { padding: 8px 20px; border: none; background: rgba(255,255,255,.15); color: white; cursor: pointer;
|
|
1335
|
+
font-size: 13px; border-radius: 4px 4px 0 0; margin-top: 4px; transition: background .15s; }
|
|
1336
|
+
.tab:hover { background: rgba(255,255,255,.3); }
|
|
1337
|
+
.tab.active { background: white; color: #2b579a; font-weight: 600; }
|
|
1338
|
+
.panel { display: none; padding: 20px; overflow: auto; }
|
|
1339
|
+
.panel.active { display: block; }
|
|
1340
|
+
.xl-sheet-wrapper { position: relative; display: inline-block; }
|
|
1341
|
+
table { border-collapse: collapse; background: white; box-shadow: 0 1px 4px rgba(0,0,0,.1); }
|
|
1342
|
+
td { padding: 4px 8px; border: 1px solid #d4d4d4; vertical-align: bottom; }
|
|
1343
|
+
td[data-icon]::before { content: attr(data-icon); margin-right: 4px; }
|
|
1344
|
+
.xl-images { position: absolute; top: 0; left: 0; pointer-events: none; }
|
|
1345
|
+
.xl-images .xl-img { pointer-events: auto; position: absolute; z-index: 2; }
|
|
1346
|
+
.xl-charts { position: absolute; top: 0; left: 0; pointer-events: none; }
|
|
1347
|
+
.xl-charts .xl-chart { pointer-events: auto; }
|
|
1348
|
+
.xl-shapes { position: absolute; top: 0; left: 0; pointer-events: none; }
|
|
1349
|
+
.xl-shapes .xl-shape { pointer-events: auto; }
|
|
1350
|
+
.xl-wordarts { position: absolute; top: 0; left: 0; pointer-events: none; }
|
|
1351
|
+
.xl-wordarts .xl-wordart { pointer-events: auto; }
|
|
1352
|
+
.xl-math-equations { position: absolute; top: 0; left: 0; pointer-events: none; }
|
|
1353
|
+
.xl-math-equations .xl-math { pointer-events: auto; }
|
|
1354
|
+
.xl-form-controls { position: absolute; top: 0; left: 0; pointer-events: none; }
|
|
1355
|
+
.xl-form-controls .xl-fc { pointer-events: auto; z-index: 3; }
|
|
1356
|
+
a { color: #0563C1; }
|
|
1357
|
+
</style>
|
|
1358
|
+
</head>
|
|
1359
|
+
<body>
|
|
1360
|
+
${includeTabs ? `<div class="tab-bar">${tabs}</div>` : ''}
|
|
1361
|
+
${panels}
|
|
1362
|
+
<script>
|
|
1363
|
+
function switchTab(idx) {
|
|
1364
|
+
document.querySelectorAll('.tab').forEach((t,i) => t.classList.toggle('active', i===idx));
|
|
1365
|
+
document.querySelectorAll('.panel').forEach((p,i) => p.classList.toggle('active', i===idx));
|
|
1366
|
+
// Re-run positioning after tab switch since hidden panels may have 0 dimensions
|
|
1367
|
+
setTimeout(positionOverlays, 50);
|
|
1368
|
+
}
|
|
1369
|
+
function positionOverlays() {
|
|
1370
|
+
document.querySelectorAll('.xl-sheet-wrapper').forEach(function(wrapper){
|
|
1371
|
+
var table = wrapper.querySelector('table');
|
|
1372
|
+
if (!table) return;
|
|
1373
|
+
var startRow = 1, startCol = 1;
|
|
1374
|
+
var firstCell = table.rows[0] && table.rows[0].cells[0] ? table.rows[0].cells[0].getAttribute('data-cell') : null;
|
|
1375
|
+
if (firstCell) {
|
|
1376
|
+
startRow = parseInt(firstCell.replace(/[A-Z]+/,''), 10) || 1;
|
|
1377
|
+
var colStr = firstCell.replace(/[0-9]+/g,'');
|
|
1378
|
+
startCol = 0;
|
|
1379
|
+
for (var ci = 0; ci < colStr.length; ci++) startCol = startCol * 26 + (colStr.charCodeAt(ci) - 64);
|
|
1380
|
+
}
|
|
1381
|
+
function cellRect(sheetRow, sheetCol) {
|
|
1382
|
+
var ri = sheetRow - startRow;
|
|
1383
|
+
var colIdx = sheetCol - startCol;
|
|
1384
|
+
if (ri < 0) ri = 0;
|
|
1385
|
+
if (colIdx < 0) colIdx = 0;
|
|
1386
|
+
var tr = table.rows[ri];
|
|
1387
|
+
if (!tr) tr = table.rows[table.rows.length - 1] || table.rows[0];
|
|
1388
|
+
if (!tr) return {x:0, y:0, w:0, h:0};
|
|
1389
|
+
var td = tr.cells[colIdx];
|
|
1390
|
+
if (!td) td = tr.cells[tr.cells.length - 1] || tr.cells[0];
|
|
1391
|
+
if (!td) return {x:0, y:0, w:0, h:0};
|
|
1392
|
+
return {x: td.offsetLeft, y: td.offsetTop, w: td.offsetWidth, h: td.offsetHeight};
|
|
1393
|
+
}
|
|
1394
|
+
wrapper.querySelectorAll('[data-from-col][data-from-row]').forEach(function(el){
|
|
1395
|
+
var fc = parseInt(el.getAttribute('data-from-col'),10);
|
|
1396
|
+
var fr = parseInt(el.getAttribute('data-from-row'),10);
|
|
1397
|
+
var from = cellRect(fr, fc);
|
|
1398
|
+
el.style.left = from.x + 'px';
|
|
1399
|
+
el.style.top = from.y + 'px';
|
|
1400
|
+
var tc = el.getAttribute('data-to-col');
|
|
1401
|
+
var tr2 = el.getAttribute('data-to-row');
|
|
1402
|
+
if (tc !== null && tr2 !== null) {
|
|
1403
|
+
var to = cellRect(parseInt(tr2,10), parseInt(tc,10));
|
|
1404
|
+
var w = to.x + to.w - from.x;
|
|
1405
|
+
var h = to.y + to.h - from.y;
|
|
1406
|
+
if (w > 0) el.style.width = w + 'px';
|
|
1407
|
+
if (h > 0) el.style.height = h + 'px';
|
|
1408
|
+
}
|
|
1409
|
+
});
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
positionOverlays();
|
|
1413
|
+
</script>
|
|
1414
|
+
</body>
|
|
1415
|
+
</html>`;
|
|
1416
|
+
}
|
|
1417
|
+
//# sourceMappingURL=HtmlModule.js.map
|