@trebco/treb 32.5.0 → 32.6.6
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/treb-spreadsheet.mjs +12 -12
- package/package.json +1 -1
- package/treb-base-types/src/import.ts +14 -10
- package/treb-base-types/src/value-type.ts +6 -7
- package/treb-calculator/src/calculator.ts +5 -0
- package/treb-calculator/src/functions/base-functions.ts +5 -5
- package/treb-calculator/src/functions/statistics-functions.ts +1 -1
- package/treb-charts/src/chart-types.ts +2 -0
- package/treb-charts/src/chart-utils.ts +45 -4
- package/treb-charts/src/default-chart-renderer.ts +84 -24
- package/treb-data-model/src/data_model.ts +8 -1
- package/treb-data-model/src/named.ts +5 -2
- package/treb-data-model/src/sheet.ts +9 -0
- package/treb-embed/style/tab-bar.scss +6 -0
- package/treb-export/src/import.ts +147 -13
- package/treb-export/src/metadata.ts +187 -0
- package/treb-export/src/workbook.ts +151 -5
|
@@ -26,7 +26,7 @@ import Base64JS from 'base64-js';
|
|
|
26
26
|
|
|
27
27
|
import type { AnchoredChartDescription, AnchoredImageDescription, AnchoredTextBoxDescription} from './workbook';
|
|
28
28
|
import { ChartType, ConditionalFormatOperators, Workbook } from './workbook';
|
|
29
|
-
import type { ParseResult } from 'treb-parser';
|
|
29
|
+
import type { ExpressionUnit, ParseResult, UnitCall } from 'treb-parser';
|
|
30
30
|
import { DecimalMarkType, Parser } from 'treb-parser';
|
|
31
31
|
import type { RangeType, AddressType, HyperlinkType } from './address-type';
|
|
32
32
|
import { is_range, ShiftRange, InRange, is_address } from './address-type';
|
|
@@ -42,6 +42,7 @@ import { ColumnWidthToPixels } from './column-width';
|
|
|
42
42
|
import type { DataValidation, AnnotationType } from 'treb-data-model';
|
|
43
43
|
import { ZipWrapper } from './zip-wrapper';
|
|
44
44
|
import type { ConditionalFormat } from 'treb-data-model';
|
|
45
|
+
import { LookupMetadata, type MetadataFlags } from './metadata';
|
|
45
46
|
|
|
46
47
|
interface SharedFormula {
|
|
47
48
|
row: number;
|
|
@@ -57,6 +58,7 @@ interface CellElementType {
|
|
|
57
58
|
r?: string;
|
|
58
59
|
t?: string;
|
|
59
60
|
s?: string;
|
|
61
|
+
cm?: string;
|
|
60
62
|
};
|
|
61
63
|
v?: string|number|{
|
|
62
64
|
t$: string;
|
|
@@ -147,6 +149,7 @@ export class Importer {
|
|
|
147
149
|
element: CellElementType,
|
|
148
150
|
shared_formulae: SharedFormulaMap,
|
|
149
151
|
arrays: RangeType[],
|
|
152
|
+
dynamic_arrays: RangeType[],
|
|
150
153
|
merges: RangeType[],
|
|
151
154
|
links: HyperlinkType[],
|
|
152
155
|
// validations: Array<{ address: ICellAddress, validation: DataValidation }>,
|
|
@@ -165,6 +168,15 @@ export class Importer {
|
|
|
165
168
|
return undefined;
|
|
166
169
|
}
|
|
167
170
|
|
|
171
|
+
// new (to us) metadata
|
|
172
|
+
let metadata_flags: MetadataFlags = {};
|
|
173
|
+
if (element.a$?.cm) {
|
|
174
|
+
const cm_index = Number(element.a$?.cm);
|
|
175
|
+
if (this.workbook?.metadata) {
|
|
176
|
+
metadata_flags = LookupMetadata(this.workbook.metadata, 'cell', cm_index).flags;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
168
180
|
// console.info(element);
|
|
169
181
|
|
|
170
182
|
let value: undefined | number | boolean | string;
|
|
@@ -213,6 +225,8 @@ export class Importer {
|
|
|
213
225
|
|
|
214
226
|
if (formula) {
|
|
215
227
|
|
|
228
|
+
// console.info("F", formula);
|
|
229
|
+
|
|
216
230
|
// doing it like this is sloppy (also does not work properly).
|
|
217
231
|
value = '=' + formula.replace(/^_xll\./g, '');
|
|
218
232
|
|
|
@@ -221,10 +235,66 @@ export class Importer {
|
|
|
221
235
|
value = formula;
|
|
222
236
|
}
|
|
223
237
|
else {
|
|
238
|
+
|
|
224
239
|
const parse_result = this.parser.Parse(formula); // l10n?
|
|
225
240
|
if (parse_result.expression) {
|
|
226
|
-
|
|
241
|
+
|
|
242
|
+
const TrimPrefixes = (name: string) => {
|
|
243
|
+
|
|
244
|
+
if (/^_xll\./.test(name)) {
|
|
245
|
+
name = name.substring(5);
|
|
246
|
+
}
|
|
247
|
+
if (/^_xlfn\./.test(name)) {
|
|
248
|
+
console.info("xlfn:", name);
|
|
249
|
+
name = name.substring(6);
|
|
250
|
+
}
|
|
251
|
+
if (/^_xlws\./.test(name)) {
|
|
252
|
+
console.info("xlws:", name);
|
|
253
|
+
name = name.substring(6);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return name;
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const TreeWalker = (unit: ExpressionUnit) => {
|
|
260
|
+
|
|
227
261
|
if (unit.type === 'call') {
|
|
262
|
+
|
|
263
|
+
// if we see _xlfn.SINGLE, translate that into
|
|
264
|
+
// @ + the name of the first parameter...
|
|
265
|
+
//
|
|
266
|
+
// this is solving the case where single appears, but it
|
|
267
|
+
// doesn't solve the case where it does not appear -- they
|
|
268
|
+
// may be using the array flag of the cell as an indicator?
|
|
269
|
+
// but how then do they know it _should_ be an array? not
|
|
270
|
+
// sure, and I don't see any other indication.
|
|
271
|
+
|
|
272
|
+
if (/^_xlfn\.single/i.test(unit.name)) {
|
|
273
|
+
|
|
274
|
+
const first = unit.args[0];
|
|
275
|
+
if (first.type === 'call') {
|
|
276
|
+
|
|
277
|
+
// we could do this in place, we don't need to copy...
|
|
278
|
+
// although it seems like a good idea. also watch out,
|
|
279
|
+
// these SINGLEs could be nested.
|
|
280
|
+
|
|
281
|
+
const replacement: UnitCall = JSON.parse(JSON.stringify(first));
|
|
282
|
+
replacement.name = '@' + TrimPrefixes(replacement.name);
|
|
283
|
+
|
|
284
|
+
for (let i = 0; i < replacement.args.length; i++) {
|
|
285
|
+
replacement.args[i] = this.parser.Walk2(replacement.args[i], TreeWalker);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return replacement;
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
console.info("_xlfn.SINGLE unexpected argument", unit.args[0]);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
unit.name = TrimPrefixes(unit.name);
|
|
296
|
+
|
|
297
|
+
/*
|
|
228
298
|
if (/^_xll\./.test(unit.name)) {
|
|
229
299
|
unit.name = unit.name.substring(5);
|
|
230
300
|
}
|
|
@@ -236,9 +306,15 @@ export class Importer {
|
|
|
236
306
|
console.info("xlws:", unit.name);
|
|
237
307
|
unit.name = unit.name.substring(6);
|
|
238
308
|
}
|
|
309
|
+
*/
|
|
310
|
+
|
|
239
311
|
}
|
|
240
312
|
return true;
|
|
241
|
-
|
|
313
|
+
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
parse_result.expression = this.parser.Walk2(parse_result.expression, TreeWalker);
|
|
317
|
+
|
|
242
318
|
value = '=' + this.parser.Render(parse_result.expression, { missing: '' });
|
|
243
319
|
}
|
|
244
320
|
}
|
|
@@ -274,10 +350,38 @@ export class Importer {
|
|
|
274
350
|
}
|
|
275
351
|
}
|
|
276
352
|
|
|
353
|
+
//
|
|
354
|
+
// arrays and spill/dynamic arrays
|
|
355
|
+
//
|
|
356
|
+
|
|
277
357
|
if (typeof element.f !== 'string' && element.f.a$?.t === 'array') {
|
|
278
358
|
const translated = sheet.TranslateAddress(element.f.a$.ref || '');
|
|
279
|
-
|
|
280
|
-
|
|
359
|
+
|
|
360
|
+
// why are we checking "is_range" here? this should be valid
|
|
361
|
+
// even if the ref attribute is one cell, if it explicitly
|
|
362
|
+
// says t="array"
|
|
363
|
+
|
|
364
|
+
// we will need to adjust it, though? yes, because the lists
|
|
365
|
+
// only accept ranges. note that range type has a superfluous
|
|
366
|
+
// sheet parameter? ...
|
|
367
|
+
|
|
368
|
+
let range = translated;
|
|
369
|
+
if (!is_range(range)) {
|
|
370
|
+
range = {
|
|
371
|
+
to: { ...range },
|
|
372
|
+
from: { ...range },
|
|
373
|
+
sheet: range.sheet,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// if (is_range(translated))
|
|
378
|
+
{
|
|
379
|
+
if (metadata_flags['dynamic-array']) {
|
|
380
|
+
dynamic_arrays.push(ShiftRange(range, -1, -1));
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
arrays.push(ShiftRange(range, -1, -1));
|
|
384
|
+
}
|
|
281
385
|
}
|
|
282
386
|
}
|
|
283
387
|
|
|
@@ -317,12 +421,14 @@ export class Importer {
|
|
|
317
421
|
// but perhaps we should check that... although at this point we have
|
|
318
422
|
// already added the array so we need to check for root
|
|
319
423
|
|
|
320
|
-
for (const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
424
|
+
for (const set of [arrays, dynamic_arrays]) {
|
|
425
|
+
for (const array of set) {
|
|
426
|
+
if (InRange(array, shifted) && (shifted.row !== array.from.row || shifted.col !== array.from.col)) {
|
|
427
|
+
calculated_type = type;
|
|
428
|
+
calculated_value = value;
|
|
429
|
+
value = undefined;
|
|
430
|
+
type = 'undefined'; // ValueType.undefined;
|
|
431
|
+
}
|
|
326
432
|
}
|
|
327
433
|
}
|
|
328
434
|
|
|
@@ -369,6 +475,20 @@ export class Importer {
|
|
|
369
475
|
}
|
|
370
476
|
}
|
|
371
477
|
|
|
478
|
+
for (const range of dynamic_arrays) {
|
|
479
|
+
if (InRange(range, shifted)) {
|
|
480
|
+
result.spill = {
|
|
481
|
+
start: {
|
|
482
|
+
row: range.from.row,
|
|
483
|
+
column: range.from.col,
|
|
484
|
+
}, end: {
|
|
485
|
+
row: range.to.row,
|
|
486
|
+
column: range.to.col,
|
|
487
|
+
},
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
372
492
|
for (const range of arrays) {
|
|
373
493
|
if (InRange(range, shifted)) {
|
|
374
494
|
result.area = {
|
|
@@ -675,6 +795,7 @@ export class Importer {
|
|
|
675
795
|
const data: CellParseResult[] = [];
|
|
676
796
|
const shared_formulae: {[index: string]: SharedFormula} = {};
|
|
677
797
|
const arrays: RangeType[] = [];
|
|
798
|
+
const dynamic_arrays: RangeType[] = [];
|
|
678
799
|
const merges: RangeType[] = [];
|
|
679
800
|
const conditional_formats: ConditionalFormat[] = [];
|
|
680
801
|
const links: HyperlinkType[] = [];
|
|
@@ -967,7 +1088,7 @@ export class Importer {
|
|
|
967
1088
|
const cells = row.c ? Array.isArray(row.c) ? row.c : [row.c] : [];
|
|
968
1089
|
|
|
969
1090
|
for (const element of cells) {
|
|
970
|
-
const cell = this.ParseCell(sheet, element as unknown as CellElementType, shared_formulae, arrays, merges, links); // , validations);
|
|
1091
|
+
const cell = this.ParseCell(sheet, element as unknown as CellElementType, shared_formulae, arrays, dynamic_arrays, merges, links); // , validations);
|
|
971
1092
|
if (cell) {
|
|
972
1093
|
data.push(cell);
|
|
973
1094
|
}
|
|
@@ -1254,12 +1375,21 @@ export class Importer {
|
|
|
1254
1375
|
|
|
1255
1376
|
break;
|
|
1256
1377
|
|
|
1378
|
+
case ChartType.Histogram:
|
|
1379
|
+
type = 'treb-chart';
|
|
1380
|
+
func = 'Histogram.Plot';
|
|
1381
|
+
if (series?.length) {
|
|
1382
|
+
// ...
|
|
1383
|
+
}
|
|
1384
|
+
args[1] = descriptor.chart.title;
|
|
1385
|
+
break;
|
|
1386
|
+
|
|
1257
1387
|
case ChartType.Box:
|
|
1258
1388
|
type = 'treb-chart';
|
|
1259
1389
|
func = 'Box.Plot';
|
|
1260
1390
|
if (series?.length) {
|
|
1261
1391
|
args[0] = `Group(${series.map(s => `Series(${s.title || ''},,${s.values||''})`).join(', ')})`;
|
|
1262
|
-
console.info("S?", {series}, args[0])
|
|
1392
|
+
// console.info("S?", {series}, args[0])
|
|
1263
1393
|
}
|
|
1264
1394
|
args[1] = descriptor.chart.title;
|
|
1265
1395
|
break;
|
|
@@ -1317,6 +1447,10 @@ export class Importer {
|
|
|
1317
1447
|
args[1] = series[0]?.categories || '';
|
|
1318
1448
|
}
|
|
1319
1449
|
|
|
1450
|
+
if (descriptor.chart.type === ChartType.Column && descriptor.chart.flags?.includes('stacked')) {
|
|
1451
|
+
args[3] = '"stacked"';
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1320
1454
|
break;
|
|
1321
1455
|
}
|
|
1322
1456
|
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { XMLUtils } from './xml-utils';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* https://learn.microsoft.com/en-us/dotnet/api/documentformat.openxml.spreadsheet.metadatarecord?view=openxml-3.0.1
|
|
5
|
+
*
|
|
6
|
+
* ah, mixing 0-based and 1-based indexes. that's great. not at all confusing.
|
|
7
|
+
*/
|
|
8
|
+
export interface MetadataRecord {
|
|
9
|
+
|
|
10
|
+
//
|
|
11
|
+
// A 1-based index to the metadata record type in metadataTypes.
|
|
12
|
+
//
|
|
13
|
+
t: number;
|
|
14
|
+
|
|
15
|
+
//
|
|
16
|
+
// A zero based index to a specific metadata record. If the corresponding
|
|
17
|
+
// metadataType has name="XLMDX", then this is an index to a record in
|
|
18
|
+
// mdxMetadata, otherwise this is an index to a record in the futureMetadata
|
|
19
|
+
// section whose name matches the name of the metadataType.
|
|
20
|
+
//
|
|
21
|
+
v: number;
|
|
22
|
+
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* https://learn.microsoft.com/en-us/dotnet/api/documentformat.openxml.spreadsheet.metadatatype?view=openxml-3.0.1
|
|
27
|
+
*
|
|
28
|
+
* punting on all these attributes for now, except the ones we specifically
|
|
29
|
+
* want.
|
|
30
|
+
*/
|
|
31
|
+
export interface MetadataType {
|
|
32
|
+
|
|
33
|
+
//
|
|
34
|
+
// Represents the name of this particular metadata type. This name shall be
|
|
35
|
+
// unique amongst all other metadataTypes.
|
|
36
|
+
//
|
|
37
|
+
name: string;
|
|
38
|
+
|
|
39
|
+
//
|
|
40
|
+
// A Boolean flag indicating whether metadata is cell metadata. True when
|
|
41
|
+
// the metadata is cell metadata, false otherwise - in the false case it
|
|
42
|
+
// is considered to be value metadata.
|
|
43
|
+
//
|
|
44
|
+
cell_meta?: boolean;
|
|
45
|
+
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* the type `Future Feature Data Storage Area` is not actually defined? not
|
|
50
|
+
* sure. maybe it's just a standard extension type and I need to look that up?
|
|
51
|
+
*
|
|
52
|
+
* for the time being we'll use a list of flags...
|
|
53
|
+
*/
|
|
54
|
+
export interface MetadataFlags {
|
|
55
|
+
'dynamic-array'?: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* https://learn.microsoft.com/en-us/dotnet/api/documentformat.openxml.spreadsheet.futuremetadata?view=openxml-3.0.1
|
|
60
|
+
* https://learn.microsoft.com/en-us/dotnet/api/documentformat.openxml.spreadsheet.futuremetadatablock?view=openxml-3.0.1
|
|
61
|
+
*
|
|
62
|
+
* these are named, matching the names in "MetadataType"; we'll use those
|
|
63
|
+
* as indexes, and not store them in the object.
|
|
64
|
+
*
|
|
65
|
+
*/
|
|
66
|
+
export interface FutureMetadata {
|
|
67
|
+
flags: MetadataFlags;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* we're just reflecting the actual data at this point, not simplifying
|
|
72
|
+
* (although we should simplify). will require some lookups.
|
|
73
|
+
*/
|
|
74
|
+
export interface Metadata {
|
|
75
|
+
cell_metadata: MetadataRecord[];
|
|
76
|
+
metadata_types: MetadataType[];
|
|
77
|
+
|
|
78
|
+
// TODO
|
|
79
|
+
// value_metadata: MetadataRecord[];
|
|
80
|
+
|
|
81
|
+
future_metadata: Record<string, FutureMetadata[]>;
|
|
82
|
+
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const LookupMetadata = (source: Metadata, type: 'cell'|'value', index: number): FutureMetadata => {
|
|
86
|
+
|
|
87
|
+
if (type === 'cell') {
|
|
88
|
+
|
|
89
|
+
//
|
|
90
|
+
// the docs say "zero based", but this looks to be one based -- there's
|
|
91
|
+
// only one entry when we create a doc, but the cm index in cells is "1".
|
|
92
|
+
//
|
|
93
|
+
// https://learn.microsoft.com/en-us/dotnet/api/documentformat.openxml.spreadsheet.cell?view=openxml-3.0.1&redirectedfrom=MSDN
|
|
94
|
+
//
|
|
95
|
+
|
|
96
|
+
const metadata_type = source.cell_metadata[index - 1];
|
|
97
|
+
|
|
98
|
+
if (metadata_type) {
|
|
99
|
+
const record_type = source.metadata_types[metadata_type.t - 1]; // 1-based
|
|
100
|
+
if (record_type) {
|
|
101
|
+
if (record_type.name === 'XLMDX') {
|
|
102
|
+
// ...
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
const future_metadata_list = source.future_metadata[record_type.name];
|
|
106
|
+
if (future_metadata_list) {
|
|
107
|
+
return future_metadata_list[metadata_type.v] || {}; // 0-based
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
console.warn('value metadata not implemented')
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {flags: {}}; // null, essentially
|
|
118
|
+
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
122
|
+
export const ParseMetadataXML = (xml: any): Metadata => {
|
|
123
|
+
|
|
124
|
+
const metadata: Metadata = {
|
|
125
|
+
metadata_types: [],
|
|
126
|
+
cell_metadata: [],
|
|
127
|
+
future_metadata: {},
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const metadata_types = XMLUtils.FindAll(xml, 'metadata/metadataTypes/metadataType');
|
|
131
|
+
for (const entry of metadata_types) {
|
|
132
|
+
|
|
133
|
+
const name: string = entry.a$?.name || '';
|
|
134
|
+
const value = entry.a$?.cellMeta;
|
|
135
|
+
const cell_meta: boolean = (value === '1' || value === 'true');
|
|
136
|
+
|
|
137
|
+
metadata.metadata_types.push({
|
|
138
|
+
name, cell_meta
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const future_metadata_blocks = XMLUtils.FindAll(xml, 'metadata/futureMetadata');
|
|
144
|
+
for (const entry of future_metadata_blocks) {
|
|
145
|
+
|
|
146
|
+
const name: string = entry.a$?.name || '';
|
|
147
|
+
if (name) {
|
|
148
|
+
|
|
149
|
+
const future_metadata_list: FutureMetadata[] = [];
|
|
150
|
+
|
|
151
|
+
// `extLst` entries can be inside of `bk` entries, but aparently not
|
|
152
|
+
// required? what's the case where they are _not_ in `bk` elements?
|
|
153
|
+
|
|
154
|
+
for (const block of XMLUtils.FindAll(entry, 'bk')) {
|
|
155
|
+
|
|
156
|
+
const future_metadata: FutureMetadata = { flags: {} };
|
|
157
|
+
|
|
158
|
+
// I guess metadata attributes are elements? we'll probably
|
|
159
|
+
// have to look them up individually
|
|
160
|
+
|
|
161
|
+
for (const child of XMLUtils.FindAll(block, 'extLst/ext/xda:dynamicArrayProperties')) {
|
|
162
|
+
if (child?.a$.fDynamic === '1') {
|
|
163
|
+
future_metadata.flags['dynamic-array'] = true;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
future_metadata_list.push(future_metadata);
|
|
168
|
+
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
metadata.future_metadata[name] = future_metadata_list;
|
|
172
|
+
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
for (const entry of XMLUtils.FindAll(xml, 'metadata/cellMetadata/bk/rc')) {
|
|
177
|
+
metadata.cell_metadata.push({
|
|
178
|
+
t: Number(entry.a$?.t || -1),
|
|
179
|
+
v: Number(entry.a$?.v || -1),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// console.info({metadata});
|
|
184
|
+
|
|
185
|
+
return metadata;
|
|
186
|
+
|
|
187
|
+
}
|
|
@@ -38,6 +38,7 @@ import type { RelationshipMap } from './relationship';
|
|
|
38
38
|
import { ZipWrapper } from './zip-wrapper';
|
|
39
39
|
import type { CellStyle, ThemeColor } from 'treb-base-types';
|
|
40
40
|
import type { SerializedNamed } from 'treb-data-model';
|
|
41
|
+
import { type Metadata, ParseMetadataXML } from './metadata';
|
|
41
42
|
|
|
42
43
|
/**
|
|
43
44
|
* @privateRemarks -- FIXME: not sure about the equal/equals thing. need to check.
|
|
@@ -53,20 +54,42 @@ export const ConditionalFormatOperators: Record<string, string> = {
|
|
|
53
54
|
notEqual: '<>',
|
|
54
55
|
};
|
|
55
56
|
|
|
57
|
+
//
|
|
58
|
+
// enums? really? in 2025? FIXME
|
|
59
|
+
//
|
|
56
60
|
export enum ChartType {
|
|
57
|
-
|
|
61
|
+
Null = 0,
|
|
62
|
+
Column,
|
|
63
|
+
Bar,
|
|
64
|
+
Line,
|
|
65
|
+
Scatter,
|
|
66
|
+
Donut,
|
|
67
|
+
Pie,
|
|
68
|
+
Bubble,
|
|
69
|
+
Box,
|
|
70
|
+
Histogram,
|
|
71
|
+
Unknown
|
|
58
72
|
}
|
|
59
73
|
|
|
60
74
|
export interface ChartSeries {
|
|
61
75
|
values?: string;
|
|
62
76
|
categories?: string;
|
|
63
77
|
bubble_size?: string;
|
|
78
|
+
|
|
79
|
+
/** special for histogram */
|
|
80
|
+
bin_count?: number;
|
|
81
|
+
|
|
64
82
|
title?: string;
|
|
65
83
|
}
|
|
66
84
|
|
|
85
|
+
type ChartFlags =
|
|
86
|
+
'stacked'
|
|
87
|
+
;
|
|
88
|
+
|
|
67
89
|
export interface ChartDescription {
|
|
68
90
|
title?: string;
|
|
69
91
|
type: ChartType;
|
|
92
|
+
flags?: ChartFlags[];
|
|
70
93
|
series?: ChartSeries[];
|
|
71
94
|
}
|
|
72
95
|
|
|
@@ -151,6 +174,9 @@ export class Workbook {
|
|
|
151
174
|
/** the workbook "rels" */
|
|
152
175
|
public rels: RelationshipMap = {};
|
|
153
176
|
|
|
177
|
+
/** metadata reference; new and WIP */
|
|
178
|
+
public metadata?: Metadata;
|
|
179
|
+
|
|
154
180
|
public sheets: Sheet[] = [];
|
|
155
181
|
|
|
156
182
|
public active_tab = 0;
|
|
@@ -200,6 +226,13 @@ export class Workbook {
|
|
|
200
226
|
let xml = xmlparser2.parse(data || '');
|
|
201
227
|
this.shared_strings.FromXML(xml);
|
|
202
228
|
|
|
229
|
+
// new(ish) metadata
|
|
230
|
+
if (this.zip.Has('xl/metadata.xml')) {
|
|
231
|
+
data = this.zip.Get('xl/metadata.xml');
|
|
232
|
+
xml = xmlparser2.parse(data);
|
|
233
|
+
this.metadata = ParseMetadataXML(xml);
|
|
234
|
+
}
|
|
235
|
+
|
|
203
236
|
// theme
|
|
204
237
|
data = this.zip.Get('xl/theme/theme1.xml');
|
|
205
238
|
xml = xmlparser2.parse(data);
|
|
@@ -542,7 +575,7 @@ export class Workbook {
|
|
|
542
575
|
const xml = xmlparser2.parse(data);
|
|
543
576
|
|
|
544
577
|
const result: ChartDescription = {
|
|
545
|
-
type: ChartType.
|
|
578
|
+
type: ChartType.Null
|
|
546
579
|
};
|
|
547
580
|
|
|
548
581
|
// console.info("RC", xml);
|
|
@@ -562,8 +595,29 @@ export class Workbook {
|
|
|
562
595
|
}
|
|
563
596
|
}
|
|
564
597
|
else {
|
|
598
|
+
|
|
599
|
+
// there's a bug in FindAll -- seems to have to do with the nodes
|
|
600
|
+
// here being strings
|
|
601
|
+
|
|
602
|
+
const nodes: (string | { t$: string })[] = [];
|
|
603
|
+
const parents = XMLUtils.FindAll(title_node, 'c:tx/c:rich/a:p/a:r');
|
|
604
|
+
for (const entry of parents) {
|
|
605
|
+
if (entry['a:t']) {
|
|
606
|
+
nodes.push(entry['a:t'])
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/*
|
|
611
|
+
const xx = XMLUtils.FindAll(title_node, 'c:tx/c:rich/a:p/a:r');
|
|
612
|
+
const yy = XMLUtils.FindAll(title_node, 'c:tx/c:rich/a:p/a:r/a:t');
|
|
613
|
+
console.info({xx, yy});
|
|
614
|
+
|
|
565
615
|
const nodes = XMLUtils.FindAll(title_node, 'c:tx/c:rich/a:p/a:r/a:t');
|
|
566
|
-
|
|
616
|
+
*/
|
|
617
|
+
|
|
618
|
+
result.title = '"' + nodes.map(node => {
|
|
619
|
+
return typeof node === 'string' ? node : (node.t$ || '');
|
|
620
|
+
}).join('') + '"';
|
|
567
621
|
}
|
|
568
622
|
|
|
569
623
|
}
|
|
@@ -653,6 +707,14 @@ export class Workbook {
|
|
|
653
707
|
if (node['c:barDir']) {
|
|
654
708
|
if (node['c:barDir'].a$?.val === 'col') {
|
|
655
709
|
result.type = ChartType.Column;
|
|
710
|
+
|
|
711
|
+
if (node['c:grouping']?.a$?.val === 'stacked') {
|
|
712
|
+
if (!result.flags) {
|
|
713
|
+
result.flags = [];
|
|
714
|
+
}
|
|
715
|
+
result.flags.push('stacked');
|
|
716
|
+
}
|
|
717
|
+
|
|
656
718
|
}
|
|
657
719
|
}
|
|
658
720
|
|
|
@@ -705,9 +767,92 @@ export class Workbook {
|
|
|
705
767
|
// box plot uses "extended chart" which is totally different... but we
|
|
706
768
|
// might need it again later? for the time being it's just inlined
|
|
707
769
|
|
|
770
|
+
// hmmm also used for histogram... histograms aren't named, they are
|
|
771
|
+
// clustered column type with a binning element, which has some attributes
|
|
772
|
+
|
|
708
773
|
const ex_series = XMLUtils.FindAll(xml, 'cx:chartSpace/cx:chart/cx:plotArea/cx:plotAreaRegion/cx:series');
|
|
709
774
|
if (ex_series?.length) {
|
|
710
|
-
|
|
775
|
+
|
|
776
|
+
// testing seems to require looping, so let's try to merge loops
|
|
777
|
+
|
|
778
|
+
let clustered_column = true;
|
|
779
|
+
let histogram = true;
|
|
780
|
+
let box_whisker = true;
|
|
781
|
+
|
|
782
|
+
for (const series of ex_series) {
|
|
783
|
+
|
|
784
|
+
const layout = series.a$?.layoutId;
|
|
785
|
+
|
|
786
|
+
if (clustered_column && layout !== 'clusteredColumn') {
|
|
787
|
+
clustered_column = false;
|
|
788
|
+
}
|
|
789
|
+
if (box_whisker && layout !== 'boxWhisker') {
|
|
790
|
+
box_whisker = false;
|
|
791
|
+
}
|
|
792
|
+
if (clustered_column && histogram) {
|
|
793
|
+
const binning = XMLUtils.FindAll(series, `cx:layoutPr/cx:binning`);
|
|
794
|
+
if (!binning.length) {
|
|
795
|
+
histogram = false;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// ok that's what we know so far...
|
|
802
|
+
|
|
803
|
+
if (histogram) {
|
|
804
|
+
result.type = ChartType.Histogram;
|
|
805
|
+
result.series = [];
|
|
806
|
+
|
|
807
|
+
for (const series_entry of ex_series) {
|
|
808
|
+
|
|
809
|
+
if (series_entry.a$?.hidden === '1') {
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const series: ChartSeries = {};
|
|
814
|
+
|
|
815
|
+
const data = XMLUtils.FindAll(xml, 'cx:chartSpace/cx:chartData/cx:data');
|
|
816
|
+
|
|
817
|
+
// so there are multiple series, and multiple datasets,
|
|
818
|
+
// but they are all merged together? no idea how this design
|
|
819
|
+
// works
|
|
820
|
+
|
|
821
|
+
const values_list: string[] = [];
|
|
822
|
+
for (const data_series of data) {
|
|
823
|
+
values_list.push(data_series['cx:numDim']?.['cx:f'] || '');
|
|
824
|
+
}
|
|
825
|
+
series.values = values_list.join(',');
|
|
826
|
+
|
|
827
|
+
const bin_count = XMLUtils.FindAll(series_entry, `cx:layoutPr/cx:binning/cx:binCount`);
|
|
828
|
+
if (bin_count[0]) {
|
|
829
|
+
const count = Number(bin_count[0].a$?.val || 0);
|
|
830
|
+
if (count) {
|
|
831
|
+
series.bin_count = count;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
result.series.push(series);
|
|
836
|
+
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
const title = XMLUtils.FindAll(xml, 'cx:chartSpace/cx:chart/cx:title/cx:tx/cx:txData');
|
|
840
|
+
if (title) {
|
|
841
|
+
if (title[0]?.['cx:f']) {
|
|
842
|
+
result.title = title[0]['cx:f'];
|
|
843
|
+
}
|
|
844
|
+
else if (title[0]?.['cx:v']) {
|
|
845
|
+
result.title = '"' + title[0]['cx:v'] + '"';
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
console.info("histogram", result);
|
|
850
|
+
return result;
|
|
851
|
+
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
if (box_whisker) {
|
|
855
|
+
|
|
711
856
|
result.type = ChartType.Box;
|
|
712
857
|
result.series = [];
|
|
713
858
|
const data = XMLUtils.FindAll(xml, 'cx:chartSpace/cx:chartData/cx:data'); // /cx:data/cx:numDim/cx:f');
|
|
@@ -758,7 +903,8 @@ export class Workbook {
|
|
|
758
903
|
}
|
|
759
904
|
|
|
760
905
|
if (!node) {
|
|
761
|
-
console.info("Chart type not handled");
|
|
906
|
+
console.info("Chart type not handled", {xml});
|
|
907
|
+
result.type = ChartType.Unknown;
|
|
762
908
|
}
|
|
763
909
|
|
|
764
910
|
// console.info("RX?", result);
|