@trebco/treb 32.5.0 → 32.6.4
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-calculator/src/calculator.ts +5 -0
- 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/sheet.ts +9 -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
|
@@ -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);
|