@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.
@@ -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
- this.parser.Walk(parse_result.expression, (unit) => {
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
- if (is_range(translated)) {
280
- arrays.push(ShiftRange(translated, -1, -1));
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 array of arrays) {
321
- if (InRange(array, shifted) && (shifted.row !== array.from.row || shifted.col !== array.from.col)) {
322
- calculated_type = type;
323
- calculated_value = value;
324
- value = undefined;
325
- type = 'undefined'; // ValueType.undefined;
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
- Unknown = 0, Column, Bar, Line, Scatter, Donut, Pie, Bubble, Box
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.Unknown
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
- result.title = '"' + nodes.join('') + '"';
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
- if (ex_series.every(test => test.a$?.layoutId === 'boxWhisker')) {
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);