reamkit 1.32.0 → 1.32.1

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.
@@ -25,6 +25,27 @@ var TYPE_OF_TAG = {
25
25
  "c:scatterChart": "scatter"
26
26
  };
27
27
  /**
28
+ * The most points a chart keeps, all its series together: as many as a sheet
29
+ * has rows, the longest range a series can name. A chart part states its own
30
+ * point counts — a cache's `c:ptCount`, each point's `idx`, the range of a
31
+ * `c:f` — and taken at their word, a count of two billion in a part a few
32
+ * hundred bytes long asked for a dense array that ran the process out of
33
+ * memory, and one past 2³² − 1 threw.
34
+ */
35
+ var MOST_CHART_POINTS = 1048576;
36
+ /**
37
+ * How many points each of a chart's `seriesCount` series keeps: an even share
38
+ * of {@link MOST_CHART_POINTS}, so that the bound holds for the chart and not
39
+ * series by series — two thousand series, each declaring a sheet's worth of
40
+ * points, are two billion. A series' points past its share are not read.
41
+ *
42
+ * @param seriesCount How many series the chart has.
43
+ * @returns The points each keeps, at least one.
44
+ */
45
+ function pointsPerSeries(seriesCount) {
46
+ return Math.max(1, Math.floor(MOST_CHART_POINTS / Math.max(1, seriesCount)));
47
+ }
48
+ /**
28
49
  * Parse a DrawingML chart part (chart1.xml) into a {@link Chart}, reading the
29
50
  * CACHED data (`c:numCache` / `c:strCache`) rather than the embedded spreadsheet
30
51
  * — the cache holds the last-computed categories and values, exactly what Word
@@ -48,6 +69,7 @@ function parseChart(chartXml, resolveColor, resolveImage) {
48
69
  const type = group ? TYPE_OF_TAG[poTag(group)] ?? "unknown" : "unknown";
49
70
  const groupAxIds = (g) => poChildren(g).filter((c) => poIs(c, "c:axId")).map((c) => poAttr(c, "val") ?? "");
50
71
  const primaryAxIds = new Set(group ? groupAxIds(group) : []);
72
+ const most = pointsPerSeries(groups.reduce((n, g) => n + poChildren(g).filter((c) => poIs(c, "c:ser")).length, 0));
51
73
  const serNodes = [];
52
74
  const series = [];
53
75
  let secondaryValAxId;
@@ -58,7 +80,7 @@ function parseChart(chartXml, resolveColor, resolveImage) {
58
80
  for (const s of poChildren(g).filter((c) => poIs(c, "c:ser"))) {
59
81
  serNodes.push(s);
60
82
  series.push({
61
- ...parseSeries(s, resolveColor),
83
+ ...parseSeries(s, resolveColor, most),
62
84
  ...groupType === type ? {} : { type: groupType },
63
85
  ...secondary ? { secondaryAxis: true } : {}
64
86
  });
@@ -75,8 +97,8 @@ function parseChart(chartXml, resolveColor, resolveImage) {
75
97
  for (const s of serNodes) {
76
98
  const cat = poChildren(s).find((c) => poIs(c, "c:cat"));
77
99
  if (cat) {
78
- const levels = multiLevelCategories(cat);
79
- categories = levels ? levels.categories : denseStrings(cat);
100
+ const levels = multiLevelCategories(cat, most);
101
+ categories = levels ? levels.categories : denseStrings(cat, most);
80
102
  categoryGroups = levels?.groups;
81
103
  categoriesRef ??= refFormula(cat);
82
104
  break;
@@ -165,11 +187,11 @@ function parseChart(chartXml, resolveColor, resolveImage) {
165
187
  ...numberFormat ? { numberFormat } : {}
166
188
  };
167
189
  }
168
- function parseSeries(ser, resolveColor) {
190
+ function parseSeries(ser, resolveColor, most) {
169
191
  const valNode = poChildren(ser).find((c) => poIs(c, "c:val")) ?? poChildren(ser).find((c) => poIs(c, "c:yVal"));
170
- const values = valNode ? denseNumbers(valNode) : [];
192
+ const values = valNode ? denseNumbers(valNode, most) : [];
171
193
  const xValNode = poChildren(ser).find((c) => poIs(c, "c:xVal"));
172
- const xValues = xValNode ? denseNumbers(xValNode) : void 0;
194
+ const xValues = xValNode ? denseNumbers(xValNode, most) : void 0;
173
195
  const name = seriesName(ser);
174
196
  const colorHex = fillColorOf(poChildren(ser).find((c) => poIs(c, "c:spPr")), resolveColor);
175
197
  const pointColors = dataPointColors(ser, resolveColor);
@@ -375,7 +397,7 @@ function cachedTitleText(title) {
375
397
  const tx = poChildren(title).find((c) => poIs(c, "c:tx"));
376
398
  const ref = tx ? poChildren(tx).find((c) => poIs(c, "c:strRef")) : void 0;
377
399
  if (!ref) return void 0;
378
- const cached = denseStrings(ref).filter((t) => t.length > 0);
400
+ const cached = denseStrings(ref, MOST_CHART_POINTS).filter((t) => t.length > 0);
379
401
  return cached.length > 0 ? cached.join(" ") : void 0;
380
402
  }
381
403
  function axisTitle(plotArea, axTag) {
@@ -509,24 +531,36 @@ function ptCountOf(container) {
509
531
  const pc = cache ? poChildren(cache).find((c) => poIs(c, "c:ptCount")) : void 0;
510
532
  return pc ? poIntAttr(pc, "val") ?? 0 : 0;
511
533
  }
512
- function denseLength(container, pts) {
513
- let max = ptCountOf(container);
514
- for (const p of pts) max = Math.max(max, p.idx + 1);
515
- return max;
534
+ /** Whether `n` can index a point: a whole number from zero. */
535
+ var isPointIndex = (n) => Number.isInteger(n) && n >= 0;
536
+ /**
537
+ * How many slots `count` points and those at `indexes` take — the count, or
538
+ * past it the last of them, and never more than `most`. A count or an index
539
+ * that is no whole number (2.5) asked for an array of no possible length and
540
+ * threw; it counts for nothing.
541
+ */
542
+ function denseLength(count, indexes, most) {
543
+ let length = isPointIndex(count) ? count : 0;
544
+ for (const idx of indexes) if (isPointIndex(idx)) length = Math.max(length, idx + 1);
545
+ return Math.min(length, most);
516
546
  }
517
- function denseNumbers(container) {
547
+ /** A cache's values by point index, gaps 0, at most `most` of them. */
548
+ function denseNumbers(container, most) {
518
549
  const pts = readPts(container);
519
- const arr = new Array(denseLength(container, pts)).fill(0);
550
+ const length = denseLength(ptCountOf(container), pts.map((p) => p.idx), most);
551
+ const arr = new Array(length).fill(0);
520
552
  for (const p of pts) {
521
553
  const n = Number(p.v);
522
- if (Number.isFinite(n)) arr[p.idx] = n;
554
+ if (Number.isFinite(n) && isPointIndex(p.idx) && p.idx < length) arr[p.idx] = n;
523
555
  }
524
556
  return arr;
525
557
  }
526
- function denseStrings(container) {
558
+ /** A cache's texts by point index, gaps empty, at most `most` of them. */
559
+ function denseStrings(container, most) {
527
560
  const pts = readPts(container);
528
- const arr = new Array(denseLength(container, pts)).fill("");
529
- for (const p of pts) arr[p.idx] = p.v;
561
+ const length = denseLength(ptCountOf(container), pts.map((p) => p.idx), most);
562
+ const arr = new Array(length).fill("");
563
+ for (const p of pts) if (isPointIndex(p.idx) && p.idx < length) arr[p.idx] = p.v;
530
564
  return arr;
531
565
  }
532
566
  /**
@@ -537,11 +571,12 @@ function denseStrings(container) {
537
571
  * names, both columns of them in turn: WithChartSheet.xlsx's six bars stood in
538
572
  * the first six of sixteen slots under a jumble of years and measure names.
539
573
  *
540
- * @param cat The `c:cat` element.
574
+ * @param cat The `c:cat` element.
575
+ * @param most The most categories it keeps (see {@link pointsPerSeries}).
541
576
  * @returns The innermost labels and the outer levels' groups, or undefined
542
577
  * for a category axis of one level.
543
578
  */
544
- function multiLevelCategories(cat) {
579
+ function multiLevelCategories(cat, most) {
545
580
  const cache = poFindDescendant(cat, "c:multiLvlStrCache");
546
581
  if (!cache) return void 0;
547
582
  const count = poIntAttr(poChildren(cache).find((c) => poIs(c, "c:ptCount")) ?? cache, "val") ?? 0;
@@ -553,11 +588,13 @@ function multiLevelCategories(cat) {
553
588
  };
554
589
  }));
555
590
  if (!inner) return void 0;
556
- const categories = new Array(Math.max(count, ...inner.map((p) => p.start + 1))).fill("");
557
- for (const p of inner) categories[p.start] = p.label;
591
+ const length = denseLength(count, inner.map((p) => p.start), most);
592
+ const categories = new Array(length).fill("");
593
+ const kept = (p) => isPointIndex(p.start) && p.start < length;
594
+ for (const p of inner) if (kept(p)) categories[p.start] = p.label;
558
595
  return {
559
596
  categories,
560
- groups: outer.map((level) => [...level].sort((a, b) => a.start - b.start))
597
+ groups: outer.map((level) => level.filter(kept).sort((a, b) => a.start - b.start))
561
598
  };
562
599
  }
563
600
  function isLegendPos(v) {
@@ -636,4 +673,4 @@ var SCATTER_STYLES = new Set([
636
673
  ]);
637
674
  var REL_CHART_COLOR_STYLE = "http://schemas.microsoft.com/office/2011/relationships/chartColorStyle";
638
675
  //#endregion
639
- export { parseChart, withChartColorStyle };
676
+ export { parseChart, pointsPerSeries, withChartColorStyle };
@@ -50,25 +50,50 @@ export declare function excelSerialToDate(serial: number, date1904: boolean): Da
50
50
  */
51
51
  export declare function excelSerialFromUtcParts(year: number, month0: number, day: number, date1904: boolean): number;
52
52
  /**
53
- * A General number rounded to the decimals that fit `maxChars` characters.
53
+ * A General number in its column: the most precise of the ways General may
54
+ * write it ({@link generalRenderings}) that `fits`.
54
55
  *
55
- * General is not a fixed format: a spreadsheet shows as many decimal places as
56
- * the column has room for and ROUNDS to that, so 4.3900875881221957 in a
57
- * column eight characters wide reads 4.390088. We rendered every stored digit
58
- * and let the cell clip it, which turns the same value into 4.390087 — off by
59
- * one in the last place shown, with nothing to say a digit was cut.
56
+ * General is not a fixed format: a spreadsheet shows as many places as the
57
+ * column has room for and ROUNDS to that, so 4.3900875881221957 in a column
58
+ * eight characters wide reads 4.390088. We rendered every stored digit and let
59
+ * the cell clip it, which turns the same value into 4.390087 — off by one in
60
+ * the last place shown, with nothing to say a digit was cut. A number too wide
61
+ * for every way of writing it comes back in its finest, and the cell fills
62
+ * with `#` (CellProperties.hashOnOverflow); one under 1 always fits as `0` or
63
+ * `-0`, which is what Excel prints in a column too narrow for anything else.
60
64
  *
61
- * The integer part is never dropped: a number too wide even without decimals
62
- * keeps them all and the cell says so its own way (see hashOnOverflow).
63
- *
64
- * `fits` decides, and it must measure in the face this will be DRAWN in rather
65
- * than count characters against the column's width in the document's unit.
66
- * Counted that way we produced nine digits for a column that holds nine of
67
- * Excel's and eight of ours, and the layout clipped the ninth — the very
68
- * truncation this exists to prevent. Fewer digits than the reference shows is a
69
- * coarser number; a clipped one is a different number.
65
+ * `fits` decides whether a rendering has room. The print model asks it in the
66
+ * document's own terms — the column's width less the padding it carries, in
67
+ * the font's widest digits — because that is the room Excel gives up places
68
+ * against, whatever face the page is drawn in.
70
69
  *
71
70
  * @param rawValue The cell's stored value.
72
71
  * @param fits Whether a rendering will fit the cell.
73
72
  */
74
73
  export declare function generalToWidth(rawValue: string, fits: (text: string) => boolean): string;
74
+ /**
75
+ * Every way General may write `value` in `chars` characters, the most precise
76
+ * first. One rule, measured in Excel's own PDF on chart axes, which give it
77
+ * nine characters with the sign among them, and in cells, eleven with the
78
+ * sign apart (2026-10-01/02):
79
+ *
80
+ * - the number as it is, its first 15 significant digits, where that fits:
81
+ * 0.000012345, 0.0000003, 12345678901;
82
+ * - else rounded to fewer places or written in scientific notation, whichever
83
+ * keeps the finer last digit — 0.123456789, not 1.23457E-01; 1.23457E-10,
84
+ * not 0; 4E-04, not 0, in a column five characters wide — and of two as
85
+ * fine, the plain one down to 0.0001 and the scientific one below it: 0.0001
86
+ * for 0.000123456 but 1E-05 for 1.00049E-05;
87
+ * - a whole part longer than the room only in scientific notation, and never
88
+ * with the zeros a mantissa or a fraction ends in: 1E+10, 2.5E+12.
89
+ *
90
+ * The digits are the 15 of {@link decimalOf}, rounded half away from zero, so
91
+ * 1.2345E-05 is 1.235E-05, where `toExponential` rounded the binary double
92
+ * down to 1.234E-05.
93
+ *
94
+ * @param value The number.
95
+ * @param chars How many characters General has.
96
+ * @param signCounts Whether a minus sign takes one of them.
97
+ * @returns The renderings, sign included, finest first.
98
+ */
99
+ export declare function generalRenderings(value: number, chars: number, signCounts?: boolean): Generator<string, void, undefined>;