reamkit 1.31.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.
Files changed (98) hide show
  1. package/README.md +10 -0
  2. package/dist/esm/core/bidi/index.d.ts +10 -0
  3. package/dist/esm/core/bidi/index.js +18 -2
  4. package/dist/esm/core/converter/facade.js +10 -5
  5. package/dist/esm/core/converter/project.js +1 -0
  6. package/dist/esm/core/converter/ream.js +17 -6
  7. package/dist/esm/core/converter/sheet-images.d.ts +17 -0
  8. package/dist/esm/core/converter/sheet-images.js +33 -0
  9. package/dist/esm/core/crypto/offcrypto.js +56 -12
  10. package/dist/esm/core/document-model/index.d.ts +1 -1
  11. package/dist/esm/core/document-model/types.d.ts +112 -0
  12. package/dist/esm/core/drawingml/chart-geometry.d.ts +36 -13
  13. package/dist/esm/core/drawingml/chart-geometry.js +372 -146
  14. package/dist/esm/core/drawingml/chart-parser.d.ts +19 -0
  15. package/dist/esm/core/drawingml/chart-parser.js +126 -18
  16. package/dist/esm/core/drawingml/colors.d.ts +6 -0
  17. package/dist/esm/core/drawingml/colors.js +23 -3
  18. package/dist/esm/core/drawingml/shape-render.d.ts +3 -1
  19. package/dist/esm/core/drawingml/shape-render.js +4 -2
  20. package/dist/esm/core/font/measure.d.ts +9 -0
  21. package/dist/esm/core/font/measure.js +9 -1
  22. package/dist/esm/core/fonts/families.js +4 -2
  23. package/dist/esm/core/fonts/scripts.js +4 -2
  24. package/dist/esm/core/ir/flow.d.ts +6 -0
  25. package/dist/esm/core/ir/sheet.d.ts +43 -0
  26. package/dist/esm/core/number-format.d.ts +40 -15
  27. package/dist/esm/core/number-format.js +295 -83
  28. package/dist/esm/core/opc/tag-scan.d.ts +15 -0
  29. package/dist/esm/core/opc/tag-scan.js +41 -0
  30. package/dist/esm/core/opc/xml-entities.js +39 -2
  31. package/dist/esm/core/spreadsheet-model/index.d.ts +1 -1
  32. package/dist/esm/core/spreadsheet-model/types.d.ts +96 -0
  33. package/dist/esm/core/style-cascade/resolver.js +4 -2
  34. package/dist/esm/excel/column-bands.d.ts +39 -6
  35. package/dist/esm/excel/column-bands.js +41 -17
  36. package/dist/esm/excel/comments-parser.js +19 -0
  37. package/dist/esm/excel/conditional-format.d.ts +7 -3
  38. package/dist/esm/excel/conditional-format.js +77 -39
  39. package/dist/esm/excel/formula/context.d.ts +18 -0
  40. package/dist/esm/excel/formula/eval.js +51 -0
  41. package/dist/esm/excel/formula/functions.js +37 -14
  42. package/dist/esm/excel/formula/index.d.ts +2 -2
  43. package/dist/esm/excel/formula/lexer.d.ts +1 -1
  44. package/dist/esm/excel/formula/lexer.js +41 -0
  45. package/dist/esm/excel/formula/parser.d.ts +25 -0
  46. package/dist/esm/excel/formula/parser.js +95 -0
  47. package/dist/esm/excel/print-model.d.ts +38 -1
  48. package/dist/esm/excel/print-model.js +536 -83
  49. package/dist/esm/excel/shared-strings-parser.d.ts +9 -0
  50. package/dist/esm/excel/shared-strings-parser.js +11 -1
  51. package/dist/esm/excel/sheet-drawing.d.ts +38 -2
  52. package/dist/esm/excel/sheet-drawing.js +47 -13
  53. package/dist/esm/excel/sheet-shape-parser.d.ts +6 -2
  54. package/dist/esm/excel/sheet-shape-parser.js +18 -6
  55. package/dist/esm/excel/sheet-to-flow.d.ts +17 -0
  56. package/dist/esm/excel/sheet-to-flow.js +434 -47
  57. package/dist/esm/excel/styles-parser.d.ts +9 -0
  58. package/dist/esm/excel/styles-parser.js +80 -26
  59. package/dist/esm/excel/table-parser.js +5 -0
  60. package/dist/esm/excel/table-style-presets.d.ts +26 -0
  61. package/dist/esm/excel/table-style-presets.js +1555 -0
  62. package/dist/esm/excel/table-style.d.ts +13 -0
  63. package/dist/esm/excel/table-style.js +75 -0
  64. package/dist/esm/excel/tint.d.ts +10 -0
  65. package/dist/esm/excel/tint.js +75 -0
  66. package/dist/esm/excel/vml-drawing.d.ts +41 -3
  67. package/dist/esm/excel/vml-drawing.js +144 -11
  68. package/dist/esm/excel/worksheet-parser.js +89 -15
  69. package/dist/esm/excel/xls/biff-reader.js +10 -2
  70. package/dist/esm/excel/xlsx-reader.js +234 -19
  71. package/dist/esm/excel/xlsx-writer.js +8 -7
  72. package/dist/esm/html/html-writer.js +202 -20
  73. package/dist/esm/layout/fit-pages.d.ts +11 -0
  74. package/dist/esm/layout/fit-pages.js +86 -0
  75. package/dist/esm/layout/line-paint.d.ts +58 -0
  76. package/dist/esm/layout/line-paint.js +107 -0
  77. package/dist/esm/layout/page-doc.d.ts +51 -0
  78. package/dist/esm/layout/page-doc.js +141 -1
  79. package/dist/esm/layout/styled-layout.d.ts +7 -0
  80. package/dist/esm/layout/styled-layout.js +275 -52
  81. package/dist/esm/layout/turned-section.js +14 -2
  82. package/dist/esm/markdown/markdown-writer.js +75 -13
  83. package/dist/esm/pdf/styled-page-emitter.js +49 -71
  84. package/dist/esm/pdf/styled-page-renderer.js +1 -1
  85. package/dist/esm/pdf-reader/annot-draw.js +2 -2
  86. package/dist/esm/pdf-reader/annots.js +3 -1
  87. package/dist/esm/pdf-reader/font.js +4 -4
  88. package/dist/esm/pdf-reader/layout.js +1 -1
  89. package/dist/esm/svg/svg-text.d.ts +58 -0
  90. package/dist/esm/svg/svg-text.js +322 -0
  91. package/dist/esm/svg/svg-writer.js +135 -71
  92. package/dist/esm/word/document-parser.js +1 -1
  93. package/dist/esm/word/docx-reader.js +13 -8
  94. package/dist/esm/word/docx-to-pdf.js +3 -3
  95. package/dist/esm/word/docx-writer.js +11 -2
  96. package/dist/esm/word/font-table.js +89 -10
  97. package/dist/esm/word/numbering-parser.js +1 -1
  98. package/package.json +7 -3
@@ -1,4 +1,4 @@
1
- import { applyNumberFormat } from "../number-format.js";
1
+ import { applyNumberFormat, generalRenderings } from "../number-format.js";
2
2
  var AXIS_COLOR = "595959";
3
3
  var GRID_COLOR = "D9D9D9";
4
4
  var LABEL_COLOR = "595959";
@@ -23,85 +23,238 @@ var SERIES_COLORS = [
23
23
  * @returns An RRGGBB hex string.
24
24
  */
25
25
  var seriesColor = (s, i, cycle) => s.colorHex ?? (cycle && cycle.length > 0 ? cycle[i % cycle.length] : SERIES_COLORS[i % SERIES_COLORS.length]);
26
- function niceNum(range, round) {
27
- const exp = Math.floor(Math.log10(range));
28
- const f = range / 10 ** exp;
29
- let nf;
30
- if (round) nf = f < 1.5 ? 1 : f < 3 ? 2 : f < 7 ? 5 : 10;
31
- else nf = f <= 1 ? 1 : f <= 2 ? 2 : f <= 5 ? 5 : 10;
32
- return nf * 10 ** exp;
33
- }
34
26
  /**
35
- * Compute a human-friendly axis {@link Scale} for the data range using
36
- * Heckbert's "nice numbers" algorithm: rounded endpoints and a 1/2/5·10ⁿ step
37
- * that yields about `maxTicks` ticks. A degenerate range (`dataMin === dataMax`)
38
- * is widened by 1 so the axis is non-empty.
27
+ * The scale Excel gives a value axis it is left to choose, measured against
28
+ * Excel's own PDF of its column charts (2026-10-01 and -02):
29
+ *
30
+ * - the ends: values all of one sign put the axis's near end at zero —
31
+ * unless their spread is under a sixth of the largest of them, when it
32
+ * stops half a spread short of the nearest (93…97 runs from 91) — and the
33
+ * far end takes 5% of its reach from that near end before it is rounded
34
+ * out to the step. 70…96 reaches 100.8 and runs 0…120 by 20, where 5% of
35
+ * the data's own spread stopped at 97.3 and ran it 0…100 by 10. Both ends
36
+ * of a range across zero take 5% of the spread;
37
+ * - the step: the smallest 1, 2 or 5 × 10ⁿ that leaves at most ten intervals
38
+ * (`maxIntervals`), whatever the chart's height — 3 750 runs 0…4 000 by 500
39
+ * on a 150pt chart as on a 400pt one. Only an axis too short to hold the
40
+ * labels asks for fewer, and the caller says so.
41
+ *
42
+ * Every case of the probes lands where Excel puts it: −1…1.2 on −1.5…1.5 by
43
+ * 0.5, 0.3…4.7 on 0…5 by 0.5, 120…950 on 0…1 000 by 100, 0.012…0.047 on
44
+ * 0…0.05 by 0.005, 1E-150…3E-150 on 0…3.5E-150. Heckbert's nice numbers with
45
+ * a budget of ticks by height stepped the budget chart by 1 000 and started
46
+ * 93…97 at zero.
39
47
  *
40
- * @param dataMin The smallest data value to cover.
41
- * @param dataMax The largest data value to cover.
42
- * @param maxTicks Target upper bound on tick count (default 6).
48
+ * @param dataMin The smallest value to cover.
49
+ * @param dataMax The largest value to cover.
50
+ * @param maxIntervals At most this many steps between the ends (default 10).
43
51
  * @returns The rounded min/max and tick step.
44
52
  */
45
- function niceScale(dataMin, dataMax, maxTicks = 6) {
46
- const lo = Math.min(dataMin, dataMax);
47
- let hi = Math.max(dataMin, dataMax);
48
- if (lo === hi) hi = lo + 1;
49
- const step = niceNum((hi - lo) / Math.max(1, maxTicks - 1), true);
50
- const headroom = hi > 0 ? hi * 1.05 : hi;
53
+ function niceScale(dataMin, dataMax, maxIntervals = 10) {
54
+ const end = paddedEnds(dataMin, dataMax);
55
+ const step = stepFor(end.lo, end.hi, maxIntervals);
51
56
  return {
52
- min: Math.floor(lo / step) * step,
53
- max: Math.ceil(headroom / step) * step,
57
+ min: below(end.lo, step),
58
+ max: above(end.hi, step),
54
59
  step
55
60
  };
56
61
  }
57
62
  /**
58
- * Format an axis tick value, choosing decimal places from the tick `step` so
59
- * `0.25`-spaced ticks read `0.25` while integer steps drop the fraction.
63
+ * The ends {@link niceScale} rounds out to its step: the data's, padded as
64
+ * Excel pads them. Never past the largest double, which the padding of values
65
+ * out at ±1.7E+308 would run them to.
66
+ */
67
+ function paddedEnds(dataMin, dataMax) {
68
+ let lo = Math.min(dataMin, dataMax);
69
+ let hi = Math.max(dataMin, dataMax);
70
+ if (lo === hi) if (hi > 0) lo = 0;
71
+ else if (lo < 0) hi = 0;
72
+ else hi = 1;
73
+ const spread = hi - lo;
74
+ if (lo >= 0) {
75
+ const from = spread < hi / 6 ? lo - spread / 2 : 0;
76
+ return {
77
+ lo: from,
78
+ hi: finite(hi + (hi - from) * .05)
79
+ };
80
+ }
81
+ if (hi <= 0) {
82
+ const to = spread < -lo / 6 ? hi + spread / 2 : 0;
83
+ return {
84
+ lo: finite(lo - (to - lo) * .05),
85
+ hi: to
86
+ };
87
+ }
88
+ return {
89
+ lo: finite(lo - spread * .05),
90
+ hi: finite(hi + spread * .05)
91
+ };
92
+ }
93
+ /** `v`, or the largest double of its sign where it ran past it. */
94
+ var finite = (v) => Math.min(Number.MAX_VALUE, Math.max(-Number.MAX_VALUE, v));
95
+ /** `v` rounded down to a multiple of `step`, short of the largest double. */
96
+ var below = (v, step) => finite(Math.floor(v / step + 1e-9) * step);
97
+ /** `v` rounded up to a multiple of `step`, short of the largest double. */
98
+ var above = (v, step) => finite(Math.ceil(v / step - 1e-9) * step);
99
+ /**
100
+ * The smallest 1, 2 or 5 × 10ⁿ that cuts `lo`…`hi` into at most
101
+ * `maxIntervals` steps. The share of the span each interval takes is reckoned
102
+ * so that a span past the largest double has one: −1.7E+308…1.7E+308 spans
103
+ * Infinity, and the search for its step never ended.
104
+ */
105
+ function stepFor(lo, hi, maxIntervals) {
106
+ const most = Math.max(1, maxIntervals);
107
+ const span = hi - lo;
108
+ const share = Math.abs(Number.isFinite(span) ? span / most : hi / most - lo / most);
109
+ if (!(share > 0 && share < Infinity)) return 1;
110
+ let base = Math.max(Number.MIN_VALUE, 10 ** Math.floor(Math.log10(share)));
111
+ for (;;) {
112
+ for (const m of [
113
+ 1,
114
+ 2,
115
+ 5
116
+ ]) if (share <= m * base * 1.000000001) return Math.min(m * base, Number.MAX_VALUE);
117
+ base *= 10;
118
+ }
119
+ }
120
+ /** How many steps lie between a scale's ends — in halves where the span overflows. */
121
+ function intervalsOf(s) {
122
+ const span = s.max - s.min;
123
+ return Number.isFinite(span) ? span / s.step : (s.max / 2 - s.min / 2) / (s.step / 2);
124
+ }
125
+ /**
126
+ * How far along `s` the value `v` stands: 0 at its min, 1 at its max —
127
+ * reckoned in halves where a difference overflows, as it does between ends
128
+ * out at ±1.7E+308.
129
+ */
130
+ function fractionOf(v, s) {
131
+ const span = s.max - s.min;
132
+ const from = v - s.min;
133
+ return Number.isFinite(span) && Number.isFinite(from) ? from / span : (v / 2 - s.min / 2) / (s.max / 2 - s.min / 2);
134
+ }
135
+ /**
136
+ * The most intervals Excel cuts a value axis into. A finer step than that is
137
+ * widened to a 500th of the axis: its own PDF ran 0…26.25 by 0.0525 for steps
138
+ * of 0.05, 0.01 and 0.001 alike, and kept 0.0525 itself (2026-10-02).
139
+ */
140
+ var MOST_INTERVALS = 500;
141
+ /** The most characters Excel gives a value-axis label under General, its sign among them. */
142
+ var AXIS_LABEL_CHARS = 9;
143
+ /**
144
+ * A value-axis tick as Excel labels it under General. An axis is not a cell:
145
+ * its General gives every label nine characters, the sign among them, on an
146
+ * axis 150pt or 500pt long, upright or lying alike — Excel's own PDF of 44
147
+ * axes (2026-10-01 and -02). Within them it is the cells' rule
148
+ * ({@link generalRenderings}): the number as it is where it fits (0.25 — we
149
+ * wrote 0.3 on an axis stepped by 0.25 — 500000000, −50000000, 0.0000003),
150
+ * else the finest of it rounded or in scientific notation (0.1234568,
151
+ * −0.123457, 123456771, 1E+09, 1.235E+09, −1.23E+09, 1.235E-05, 5E-151).
152
+ * Places counted from the step and written by `toFixed` threw past a hundred
153
+ * of them, which a step of 1E-150 asked for.
60
154
  *
61
- * @param v The tick value.
62
- * @param step The tick spacing (from {@link niceScale}).
155
+ * @param v The tick value.
63
156
  * @returns The label text.
64
157
  */
65
- function formatTick(v, step) {
66
- if (Number.isInteger(step) && Number.isInteger(v)) return String(v);
67
- const decimals = Math.max(0, -Math.floor(Math.log10(step)));
68
- return v.toFixed(decimals).replace(/\.?0+$/, (m) => m.includes(".") ? "" : m);
158
+ function formatTick(v) {
159
+ return generalRenderings(v, AXIS_LABEL_CHARS, true).next().value ?? String(v);
69
160
  }
161
+ /**
162
+ * The values a scale labels: its min and every step up to its max. Counted,
163
+ * not summed: the sum drifted, and its tolerance was absolute, so a scale of
164
+ * 1E-150 ran on until it passed 1E-9 and asked for more ticks than an array
165
+ * holds. Never more than Excel's most intervals and one past them — rounding
166
+ * the ends out to a widened step can add that one.
167
+ */
70
168
  var ticks = (s) => {
169
+ const count = Math.min(Math.floor(intervalsOf(s) + 1e-9), MOST_INTERVALS + 1);
71
170
  const out = [];
72
- for (let v = s.min; v <= s.max + 1e-9; v += s.step) out.push(Math.abs(v) < 1e-9 ? 0 : v);
171
+ for (let i = 0; i <= count; i++) {
172
+ const sum = s.min + i * s.step;
173
+ const v = Number.isFinite(sum) ? sum : 2 * (s.min / 2 + i * (s.step / 2));
174
+ out.push(Math.abs(v) < s.step * 1e-9 ? 0 : v);
175
+ }
73
176
  return out;
74
177
  };
75
178
  /**
76
179
  * The value axis's {@link Scale}: the ends the author fixed where they fixed
77
- * them (§21.2.2.157 `c:scaling/c:min|c:max`), the data's own range where they
78
- * did not. A fixed end is exact — nice-rounding it would move a number the
79
- * author chose — so only the tick step comes from the rounding pass.
180
+ * them (§21.2.2.157 `c:scaling/c:min|c:max`), the data's, padded as
181
+ * {@link niceScale} pads them, where they did not. A fixed end is exact —
182
+ * nice-rounding it would move a number the author chose.
80
183
  *
81
- * @param chart The chart (for its fixed ends).
82
- * @param dataMin The smallest value to cover.
83
- * @param dataMax The largest value to cover.
84
- * @param hPt The chart's height — how many ticks fit is a question about
85
- * the plot, not about the numbers.
184
+ * The step is the author's (§21.2.2.98 `c:majorUnit`), else the 1-2-5 step
185
+ * of the span between the ends — with both fixed, of the span they fix:
186
+ * −1E+300…1E+300 runs by 2E+299 in Excel, where padding the ends first
187
+ * stepped it by 5E+299. The automatic ends round out to an author's step
188
+ * from where the padding left them, not from where a step of our own would
189
+ * have: 10…25 by 0.1 runs 0…26.3, as in Excel, not 0…30. And no step cuts
190
+ * the axis finer than {@link MOST_INTERVALS}: Excel widens one that would to
191
+ * a 500th of the span and rounds the automatic ends out to the widened step
192
+ * (−10…25 by 0.01 runs −11.781…26.796 by 0.077). A step of 0.00001 asked for
193
+ * three million labels and ran out of stack.
194
+ *
195
+ * @param chart The chart (for its fixed ends and step).
196
+ * @param dataMin The smallest value to cover.
197
+ * @param dataMax The largest value to cover.
198
+ * @param extentPt The axis's length — how many ticks fit is a question
199
+ * about the plot, not about the numbers.
200
+ * @param horizontal Whether the axis lies along the foot.
86
201
  * @returns The axis min/max and tick step.
87
202
  */
88
- function axisScale(chart, dataMin, dataMax, hPt) {
89
- const rounded = niceScale(chart.valAxisMin ?? dataMin, chart.valAxisMax ?? dataMax, tickBudget(hPt));
90
- return {
91
- min: chart.valAxisMin ?? rounded.min,
92
- max: chart.valAxisMax ?? rounded.max,
93
- step: rounded.step
94
- };
203
+ function axisScale(chart, dataMin, dataMax, extentPt, horizontal = false) {
204
+ const fixedMin = chart.valAxisMin;
205
+ const fixedMax = chart.valAxisMax;
206
+ const padded = paddedEnds(fixedMin ?? dataMin, fixedMax ?? dataMax);
207
+ const lo = fixedMin ?? padded.lo;
208
+ const hi = fixedMax ?? padded.hi;
209
+ const rounded = (step) => ({
210
+ min: fixedMin ?? below(lo, step),
211
+ max: fixedMax ?? above(hi, step),
212
+ step
213
+ });
214
+ const scale = rounded(chart.valAxisMajorUnit ?? stepFor(lo, hi, intervalsThatFit(extentPt, horizontal)));
215
+ if (intervalsOf(scale) <= MOST_INTERVALS) return scale;
216
+ const span = hi - lo;
217
+ return rounded(Number.isFinite(span) ? span / MOST_INTERVALS : hi / MOST_INTERVALS - lo / MOST_INTERVALS);
95
218
  }
96
- function pushChartTitle(labels, chart, wPt, hPt) {
97
- if (!chart.title) return;
98
- labels.push({
99
- text: chart.title,
100
- x: wPt / 2,
101
- y: hPt - 4 - 13,
102
- sizePt: 13,
103
- colorHex: TITLE_COLOR,
104
- align: "center"
219
+ /**
220
+ * The chart title's lines. A title longer than the chart is wide wraps at its
221
+ * spaces, as Excel's and Calc's do, rather than running out past both sides
222
+ * of the frame: dataValidationTableRange.xlsx's "Ranking of Washington
223
+ * Counties on Days per Patient (ALOS) in 2015" crossed the cells beside its
224
+ * chart. A line holds at most four fifths of the chart's width.
225
+ *
226
+ * @param chart The chart.
227
+ * @param wPt The chart's width.
228
+ * @param measure The text measurer.
229
+ * @returns The lines, top first; none for a chart without a title.
230
+ */
231
+ function titleLines(chart, wPt, measure) {
232
+ if (!chart.title) return [];
233
+ const room = wPt * .8;
234
+ const lines = [];
235
+ let line = "";
236
+ for (const word of chart.title.split(/\s+/).filter((w) => w.length > 0)) {
237
+ const longer = line ? `${line} ${word}` : word;
238
+ if (line && measure(longer, 13) > room) {
239
+ lines.push(line);
240
+ line = word;
241
+ } else line = longer;
242
+ }
243
+ if (line) lines.push(line);
244
+ return lines;
245
+ }
246
+ /** The band a title of `lines` takes at the top of the chart. */
247
+ var titleHeight = (lines) => lines.length === 0 ? 0 : 13 * (1.6 + 1.2 * (lines.length - 1));
248
+ function pushChartTitle(labels, lines, wPt, hPt) {
249
+ lines.forEach((text, i) => {
250
+ labels.push({
251
+ text,
252
+ x: wPt / 2,
253
+ y: hPt - 4 - 13 * (1 + 1.2 * i),
254
+ sizePt: 13,
255
+ colorHex: TITLE_COLOR,
256
+ align: "center"
257
+ });
105
258
  });
106
259
  }
107
260
  function buildLegendBlock(chart, wPt, hPt, measure) {
@@ -113,7 +266,7 @@ function buildLegendBlock(chart, wPt, hPt, measure) {
113
266
  const marker = chart.type === "line" || chart.type === "scatter" ? "line" : "box";
114
267
  return layoutLegend(legendEntries, chart.hasLegend, chart.legendPos ?? "b", wPt, hPt, measure, marker);
115
268
  }
116
- function pushGridTicks(gridlines, labels, tickVals, fmt, axis, at, x0, y0, plotW, plotH, grid) {
269
+ function pushGridTicks(gridlines, labels, tickVals, fmt, axis, at, x0, y0, plotW, plotH, grid, atEnd = false) {
117
270
  for (const v of tickVals) if (axis === "x") {
118
271
  const gx = at(v);
119
272
  gridlines.push({
@@ -124,7 +277,7 @@ function pushGridTicks(gridlines, labels, tickVals, fmt, axis, at, x0, y0, plotW
124
277
  labels.push({
125
278
  text: fmt(v),
126
279
  x: gx,
127
- y: y0 - 9,
280
+ y: atEnd ? y0 + plotH + 9 * .4 : y0 - 9,
128
281
  sizePt: 9,
129
282
  colorHex: LABEL_COLOR,
130
283
  align: "center"
@@ -138,11 +291,11 @@ function pushGridTicks(gridlines, labels, tickVals, fmt, axis, at, x0, y0, plotW
138
291
  });
139
292
  labels.push({
140
293
  text: fmt(v),
141
- x: x0 - 3,
294
+ x: atEnd ? x0 + plotW + 3 : x0 - 3,
142
295
  y: gy - 9 / 3,
143
296
  sizePt: 9,
144
297
  colorHex: LABEL_COLOR,
145
- align: "right"
298
+ align: atEnd ? "left" : "right"
146
299
  });
147
300
  }
148
301
  }
@@ -159,16 +312,24 @@ function axisStroke(style) {
159
312
  widthPt: style?.widthPt ?? 1
160
313
  };
161
314
  }
162
- function pushAxisLines(polylines, x0, y0, plotW, plotH, chart) {
315
+ /**
316
+ * The two axis lines: the upright one (in the left axis's style) and the one
317
+ * lying along (in the bottom axis's). The category axis lies where it crosses
318
+ * the value axis — `cross.y` up a column chart, `cross.x` along a bar chart —
319
+ * and the other stands at the plot's edge.
320
+ */
321
+ function pushAxisLines(polylines, x0, y0, plotW, plotH, chart, cross = {}) {
163
322
  const val = axisStroke(chart.valAxisLine);
323
+ const x = x0 + (cross.x ?? 0);
324
+ const y = y0 + (cross.y ?? 0);
164
325
  if (val) polylines.push({
165
- points: [[x0, y0], [x0, y0 + plotH]],
326
+ points: [[x, y0], [x, y0 + plotH]],
166
327
  strokeHex: val.hex,
167
328
  widthPt: val.widthPt
168
329
  });
169
330
  const cat = axisStroke(chart.catAxisLine);
170
331
  if (cat) polylines.push({
171
- points: [[x0, y0], [x0 + plotW, y0]],
332
+ points: [[x0, y], [x0 + plotW, y]],
172
333
  strokeHex: cat.hex,
173
334
  widthPt: cat.widthPt
174
335
  });
@@ -178,30 +339,47 @@ function buildFrame(chart, wPt, hPt, measure, horizontal, opts = {}) {
178
339
  const polylines = [];
179
340
  const gridlines = [];
180
341
  const labels = [];
181
- const nCats = Math.max(chart.categories.length, ...chart.series.map((s) => s.values.length), 1);
342
+ const nCats = catCount(chart);
182
343
  const onSecondary = chart.series.filter((s) => s.secondaryAxis);
183
344
  const allVals = (onSecondary.length > 0 ? chart.series.filter((s) => !s.secondaryAxis) : chart.series).flatMap((s) => s.values.slice(0, nCats));
184
- const [dataMin, dataMax] = opts.dataRange ?? [Math.min(0, ...allVals), Math.max(0, ...allVals)];
185
- const scale = axisScale(chart, dataMin, dataMax, hPt);
186
- const fmtVal = opts.formatValue ?? ((v) => formatTick(v, scale.step));
345
+ const [dataMin, dataMax] = opts.dataRange ?? extentOf(allVals) ?? [0, 1];
346
+ const scale = opts.exactRange ? {
347
+ min: dataMin,
348
+ max: dataMax,
349
+ step: niceScale(dataMin, dataMax, intervalsThatFit(horizontal ? wPt : hPt, horizontal)).step
350
+ } : axisScale(chart, dataMin, dataMax, horizontal ? wPt : hPt, horizontal);
351
+ const fmtVal = opts.formatValue ?? formatTick;
187
352
  const tickVals = ticks(scale);
188
- const vals2 = onSecondary.flatMap((s) => s.values.slice(0, nCats));
189
- const scale2 = vals2.length > 0 ? niceScale(Math.min(0, ...vals2), Math.max(0, ...vals2), tickBudget(hPt)) : void 0;
353
+ const range2 = extentOf(onSecondary.flatMap((s) => s.values.slice(0, nCats)));
354
+ const scale2 = range2 ? niceScale(range2[0], range2[1], intervalsThatFit(hPt, false)) : void 0;
190
355
  const tickVals2 = scale2 ? ticks(scale2) : [];
191
- let top = 4;
192
- if (chart.title) top += 13 * 1.6;
356
+ const crosses = chart.catAxisCrosses;
357
+ const crossValue = Math.min(Math.max(crosses === "min" ? scale.min : crosses === "max" ? scale.max : crosses ?? 0, scale.min), scale.max);
358
+ const labelsAt = chart.catTickLabelPos === "none" ? "none" : chart.catTickLabelPos === "low" ? "start" : chart.catTickLabelPos === "high" ? "end" : crossValue <= scale.min ? "start" : crossValue >= scale.max ? "end" : "cross";
359
+ const catText = (c) => chart.categories[c] ?? String(c + 1);
360
+ let widestCat = 0;
361
+ for (let c = 0; c < nCats; c++) widestCat = Math.max(widestCat, measure(catText(c), 9));
362
+ const catBand = 9 * 1.6;
363
+ const groupLevels = horizontal ? [] : chart.categoryGroups ?? [];
364
+ const valAtEnd = chart.catAxisReversed === true ? chart.valAxisCrosses !== "max" : chart.valAxisCrosses === "max";
365
+ const title = titleLines(chart, wPt, measure);
366
+ const top = 4 + titleHeight(title) + (!horizontal && labelsAt === "end" ? catBand : 0) + (horizontal && valAtEnd ? catBand : 0);
193
367
  const legend = buildLegendBlock(chart, wPt, hPt, measure);
194
- const tick2W = scale2 && !horizontal ? Math.max(0, ...tickVals2.map((v) => measure(formatTick(v, scale2.step), 9))) + 4 + (chart.secondaryValAxisTitle ? 9 * 1.5 : 0) : 0;
195
- const plotRight = wPt - 4 - legend.rightWidth - tick2W;
368
+ const tick2W = scale2 && !horizontal ? Math.max(0, ...tickVals2.map((v) => measure(formatTick(v), 9))) + 4 + (chart.secondaryValAxisTitle ? 9 * 1.5 : 0) : 0;
369
+ const tickHalf = (v) => horizontal && v !== void 0 ? measure(fmtVal(v), 9) / 2 : 0;
370
+ const lastTickHalf = tickHalf(tickVals[tickVals.length - 1]);
371
+ const catLabelW = Math.min(wPt * .4, widestCat + 6);
196
372
  const tickLabelW = Math.max(0, ...tickVals.map((v) => measure(fmtVal(v), 9))) + 4;
197
- const catLabelH = 9 * 1.6;
198
- const catTitleH = chart.catAxisTitle ? 9 * 1.5 : 0;
199
- const x0 = 4 + (chart.valAxisTitle ? 9 * 1.5 : 0) + tickLabelW;
200
- const y0 = 4 + legend.bottomHeight + catTitleH + catLabelH;
373
+ const plotRight = wPt - 4 - legend.rightWidth - tick2W - (horizontal && labelsAt === "end" ? catLabelW : lastTickHalf) - (!horizontal && valAtEnd ? tickLabelW : 0);
374
+ const axisTitleBand = 9 * 1.5;
375
+ const leftTitle = horizontal ? chart.catAxisTitle : chart.valAxisTitle;
376
+ const footTitle = horizontal ? chart.valAxisTitle : chart.catAxisTitle;
377
+ const x0 = 4 + (leftTitle ? axisTitleBand : 0) + (horizontal ? labelsAt === "start" ? catLabelW : tickHalf(tickVals[0]) : valAtEnd ? 0 : tickLabelW);
378
+ const y0 = 4 + legend.bottomHeight + (footTitle ? axisTitleBand : 0) + ((horizontal ? !valAtEnd : labelsAt === "start") ? catBand : 0) + (!horizontal && labelsAt === "start" ? groupLevels.length * catBand : 0);
201
379
  const plotW = Math.max(1, plotRight - x0);
202
380
  const plotH = Math.max(1, hPt - top - y0);
203
- const valueOffset = (v) => (v - scale.min) / (scale.max - scale.min) * (horizontal ? plotW : plotH);
204
- const zeroOffset = valueOffset(0);
381
+ const valueOffset = (v) => fractionOf(v, scale) * (horizontal ? plotW : plotH);
382
+ const zeroOffset = valueOffset(crossValue);
205
383
  const plotBackground = chart.plotFillHex ?? chart.plotLine?.colorHex ? {
206
384
  x: x0,
207
385
  y: y0,
@@ -214,9 +392,9 @@ function buildFrame(chart, wPt, hPt, measure, horizontal, opts = {}) {
214
392
  ...chart.plotLine.dash ? { strokeDash: chart.plotLine.dash } : {}
215
393
  } : {}
216
394
  } : void 0;
217
- pushChartTitle(labels, chart, wPt, hPt);
218
- if (chart.valAxisTitle) labels.push({
219
- text: chart.valAxisTitle,
395
+ pushChartTitle(labels, title, wPt, hPt);
396
+ if (leftTitle) labels.push({
397
+ text: leftTitle,
220
398
  x: 12.1,
221
399
  y: y0 + plotH / 2,
222
400
  sizePt: 9,
@@ -224,20 +402,20 @@ function buildFrame(chart, wPt, hPt, measure, horizontal, opts = {}) {
224
402
  align: "center",
225
403
  rotationDeg: 90
226
404
  });
227
- if (chart.catAxisTitle) labels.push({
228
- text: chart.catAxisTitle,
405
+ if (footTitle) labels.push({
406
+ text: footTitle,
229
407
  x: x0 + plotW / 2,
230
408
  y: legend.bottomHeight + 2,
231
409
  sizePt: 9,
232
410
  colorHex: LABEL_COLOR,
233
411
  align: "center"
234
412
  });
235
- if (horizontal) pushGridTicks(gridlines, labels, tickVals, fmtVal, "x", (v) => x0 + valueOffset(v), x0, y0, plotW, plotH, chart.gridLine);
236
- else pushGridTicks(gridlines, labels, tickVals, fmtVal, "y", (v) => y0 + valueOffset(v), x0, y0, plotW, plotH, chart.gridLine);
237
- const valueOffset2 = scale2 && !horizontal ? (v) => (v - scale2.min) / (scale2.max - scale2.min) * plotH : void 0;
413
+ if (horizontal) pushGridTicks(gridlines, labels, tickVals, fmtVal, "x", (v) => x0 + valueOffset(v), x0, y0, plotW, plotH, chart.gridLine, valAtEnd);
414
+ else pushGridTicks(gridlines, labels, tickVals, fmtVal, "y", (v) => y0 + valueOffset(v), x0, y0, plotW, plotH, chart.gridLine, valAtEnd);
415
+ const valueOffset2 = scale2 && !horizontal ? (v) => fractionOf(v, scale2) * plotH : void 0;
238
416
  if (scale2 && valueOffset2) {
239
417
  for (const v of tickVals2) labels.push({
240
- text: formatTick(v, scale2.step),
418
+ text: formatTick(v),
241
419
  x: x0 + plotW + 3,
242
420
  y: y0 + valueOffset2(v) - 9 / 3,
243
421
  sizePt: 9,
@@ -261,30 +439,60 @@ function buildFrame(chart, wPt, hPt, measure, horizontal, opts = {}) {
261
439
  });
262
440
  }
263
441
  const slot = (horizontal ? plotH : plotW) / nCats;
264
- const need = horizontal ? 9 * 1.4 : Math.max(0, ...chart.categories.map((t) => measure(t, 9))) + 4;
442
+ const need = horizontal ? 9 * 1.4 : widestCat + 4;
265
443
  const step = Math.max(1, Math.ceil(need / Math.max(slot, .01)));
266
- for (let c = 0; c < nCats; c += step) {
267
- const cat = chart.categories[c] ?? String(c + 1);
444
+ const across = labelsAt === "start" ? 0 : labelsAt === "end" ? horizontal ? plotW : plotH : zeroOffset;
445
+ const labelled = [];
446
+ if (labelsAt !== "none") if (chart.catAxisReversed === true) for (let c = nCats - 1; c >= 0; c -= step) labelled.push(c);
447
+ else for (let c = 0; c < nCats; c += step) labelled.push(c);
448
+ for (const c of labelled) {
449
+ const cat = catText(c);
268
450
  if (!cat) continue;
269
451
  const center = (horizontal ? y0 : x0) + c * slot + slot / 2;
270
452
  if (horizontal) labels.push({
271
453
  text: cat,
272
- x: x0 - 3,
454
+ x: labelsAt === "end" ? x0 + across + 3 : x0 + across - 3,
273
455
  y: center - 9 / 3,
274
456
  sizePt: 9,
275
457
  colorHex: LABEL_COLOR,
276
- align: "right"
458
+ align: labelsAt === "end" ? "left" : "right"
277
459
  });
278
460
  else labels.push({
279
461
  text: cat,
280
462
  x: center,
281
- y: y0 - 9,
463
+ y: labelsAt === "end" ? y0 + across + 9 * .4 : y0 + across - 9,
282
464
  sizePt: 9,
283
465
  colorHex: LABEL_COLOR,
284
466
  align: "center"
285
467
  });
286
468
  }
287
- pushAxisLines(polylines, x0, y0, plotW, plotH, chart);
469
+ if (labelsAt !== "none" && labelsAt !== "end") groupLevels.forEach((groups, level) => {
470
+ const rowY = y0 + across - 9 - (level + 1) * catBand;
471
+ groups.forEach((group, i) => {
472
+ const end = Math.min(nCats, groups[i + 1]?.start ?? nCats);
473
+ if (end <= group.start) return;
474
+ labels.push({
475
+ text: group.label,
476
+ x: x0 + (group.start + end) / 2 * slot,
477
+ y: rowY,
478
+ sizePt: 9,
479
+ colorHex: LABEL_COLOR,
480
+ align: "center"
481
+ });
482
+ if (i > 0) polylines.push({
483
+ points: [[x0 + group.start * slot, y0 + across], [x0 + group.start * slot, rowY - 9 * .4]],
484
+ strokeHex: GRID_COLOR,
485
+ widthPt: .75
486
+ });
487
+ });
488
+ });
489
+ pushAxisLines(polylines, x0, y0, plotW, plotH, chart, horizontal ? {
490
+ x: zeroOffset,
491
+ ...valAtEnd ? { y: plotH } : {}
492
+ } : {
493
+ y: zeroOffset,
494
+ ...valAtEnd ? { x: plotW } : {}
495
+ });
288
496
  legend.emit(rects, labels);
289
497
  return {
290
498
  x0,
@@ -306,8 +514,29 @@ function buildFrame(chart, wPt, hPt, measure, horizontal, opts = {}) {
306
514
  }
307
515
  var pctLabel = (v) => `${Math.round(v * 100)}%`;
308
516
  var fmtDataLabel = (chart, v) => chartValueFormatter(chart)?.(v) ?? (Number.isInteger(v) ? String(v) : String(Math.round(v * 100) / 100));
517
+ /**
518
+ * How many category slots a chart has: its labels, or its longest series —
519
+ * counted in a loop, as spreading a file's series into one call overflows
520
+ * the stack.
521
+ */
309
522
  function catCount(chart) {
310
- return Math.max(chart.categories.length, ...chart.series.map((s) => s.values.length), 1);
523
+ let n = Math.max(chart.categories.length, 1);
524
+ for (const s of chart.series) n = Math.max(n, s.values.length);
525
+ return n;
526
+ }
527
+ /**
528
+ * The least and the greatest of `values`, or undefined where none is a
529
+ * number. Read in a loop: `Math.min(...values)` throws on a series of 200 000
530
+ * points, as a call takes no more arguments than the stack holds.
531
+ */
532
+ function extentOf(values) {
533
+ let lo = Infinity;
534
+ let hi = -Infinity;
535
+ for (const v of values) {
536
+ if (v < lo) lo = v;
537
+ if (v > hi) hi = v;
538
+ }
539
+ return lo <= hi ? [lo, hi] : void 0;
311
540
  }
312
541
  function stackedTotals(chart, nCats) {
313
542
  let max = 0;
@@ -332,6 +561,7 @@ function groupingFrameOpts(chart, nCats) {
332
561
  const g = chart.grouping ?? "clustered";
333
562
  if (g === "percentStacked") return {
334
563
  dataRange: [0, 1],
564
+ exactRange: true,
335
565
  formatValue: pctLabel
336
566
  };
337
567
  const format = chartValueFormatter(chart);
@@ -655,22 +885,24 @@ function buildScatterScene(chart, wPt, hPt, measure) {
655
885
  wedges: [],
656
886
  labels
657
887
  };
658
- const xScale = niceScale(...autoRange(xs), tickBudget(wPt / 2));
659
- const yScale = niceScale(...autoRange(ys), tickBudget(hPt));
888
+ const [xLo, xHi] = extentOf(xs) ?? [0, 1];
889
+ const [yLo, yHi] = extentOf(ys) ?? [0, 1];
890
+ const xScale = niceScale(xLo, xHi, intervalsThatFit(wPt, true));
891
+ const yScale = niceScale(yLo, yHi, intervalsThatFit(hPt, false));
660
892
  const xTicks = ticks(xScale);
661
893
  const yTicks = ticks(yScale);
662
- let top = 4;
663
- if (chart.title) top += 13 * 1.6;
894
+ const title = titleLines(chart, wPt, measure);
895
+ const top = 4 + titleHeight(title);
664
896
  const legend = buildLegendBlock(chart, wPt, hPt, measure);
665
- const x0 = 4 + (Math.max(0, ...yTicks.map((v) => measure(formatTick(v, yScale.step), 9))) + 4);
897
+ const x0 = 4 + (Math.max(0, ...yTicks.map((v) => measure(formatTick(v), 9))) + 4);
666
898
  const y0 = 4 + legend.bottomHeight + 9 * 1.6;
667
899
  const plotW = Math.max(1, wPt - 4 - legend.rightWidth - x0);
668
900
  const plotH = Math.max(1, hPt - top - y0);
669
- const xAt = (v) => x0 + (v - xScale.min) / (xScale.max - xScale.min) * plotW;
670
- const yAt = (v) => y0 + (v - yScale.min) / (yScale.max - yScale.min) * plotH;
671
- pushChartTitle(labels, chart, wPt, hPt);
672
- pushGridTicks(gridlines, labels, yTicks, (v) => formatTick(v, yScale.step), "y", yAt, x0, y0, plotW, plotH, chart.gridLine);
673
- pushGridTicks(gridlines, labels, xTicks, (v) => formatTick(v, xScale.step), "x", xAt, x0, y0, plotW, plotH, chart.gridLine);
901
+ const xAt = (v) => x0 + fractionOf(v, xScale) * plotW;
902
+ const yAt = (v) => y0 + fractionOf(v, yScale) * plotH;
903
+ pushChartTitle(labels, title, wPt, hPt);
904
+ pushGridTicks(gridlines, labels, yTicks, formatTick, "y", yAt, x0, y0, plotW, plotH, chart.gridLine);
905
+ pushGridTicks(gridlines, labels, xTicks, formatTick, "x", xAt, x0, y0, plotW, plotH, chart.gridLine);
674
906
  pushAxisLines(polylines, x0, y0, plotW, plotH, chart);
675
907
  const style = chart.scatterStyle ?? "marker";
676
908
  const joins = style === "line" || style === "lineMarker" || style.startsWith("smooth");
@@ -702,27 +934,13 @@ function buildScatterScene(chart, wPt, hPt, measure) {
702
934
  };
703
935
  }
704
936
  /**
705
- * The data range an automatic axis covers: its own extent, except that data
706
- * starting within 5/6 of the top reads as data that belongs against a zero
707
- * baseline — Excel's own rule for when an automatic minimum stays at 0.
937
+ * How many intervals an axis `extentPt` long has room to label: ten, Excel's
938
+ * own most, unless its labels would run into each other first — one label
939
+ * line apiece stacked up a vertical axis, a few digits' width apiece along a
940
+ * horizontal one. Excel crams nine labels up a 150pt column chart rather than
941
+ * step it more coarsely, so the room is all that limits it.
708
942
  */
709
- function autoRange(vals) {
710
- const lo = Math.min(...vals);
711
- const hi = Math.max(...vals);
712
- return [lo > 0 && lo < 5 / 6 * hi ? 0 : lo, hi];
713
- }
714
- /**
715
- * How many ticks an axis `extentPt` long carries (mirrors {@link axisScale}).
716
- *
717
- * A tall axis takes more of them — a fixed six drew 0/100/200/300 down a plot
718
- * where both references fit 0/50/…/300 — but the appetite is milder than it
719
- * looks: asked for one every 32pt, a 427pt chart wanted ten, and Excel labels
720
- * that chart's 0…5 data 0/1/…/6 where we drew half-steps (chart-texture-bg
721
- * .pptx). One per ~48pt and never more than eight satisfies both, now that
722
- * {@link niceScale} measures its step against the range the data spans rather
723
- * than against Heckbert's rounded-up one.
724
- */
725
- var tickBudget = (extentPt) => Math.min(8, Math.max(4, Math.round(extentPt / 48)));
943
+ var intervalsThatFit = (extentPt, horizontal) => Math.min(10, Math.max(2, Math.floor(extentPt * .75 / (9 * (horizontal ? 3.5 : 1.4)))));
726
944
  /** Side (points) of the square stamped for a series that names no symbol. */
727
945
  var DEFAULT_MARKER_PT = 4;
728
946
  /**
@@ -772,10 +990,7 @@ function pushMarker(rects, wedges, marker, x, y, color) {
772
990
  * @returns The positioned scene primitives.
773
991
  */
774
992
  function buildLineScene(chart, wPt, hPt, measure) {
775
- const allVals = chart.series.flatMap((s) => s.values);
776
- const lo = allVals.length > 0 ? Math.min(...allVals) : 0;
777
- const hi = allVals.length > 0 ? Math.max(...allVals) : 1;
778
- const range = lo > 0 && lo > 5 / 6 * hi ? [lo, hi] : [Math.min(0, lo), Math.max(0, hi)];
993
+ const range = extentOf(chart.series.flatMap((s) => s.values)) ?? [0, 1];
779
994
  const lineFormat = chartValueFormatter(chart);
780
995
  const f = buildFrame(chart, wPt, hPt, measure, false, {
781
996
  dataRange: range,
@@ -846,8 +1061,8 @@ function buildPieScene(chart, wPt, hPt, measure) {
846
1061
  wedges,
847
1062
  labels
848
1063
  };
849
- let top = 4;
850
- if (chart.title) top += 13 * 1.6;
1064
+ const title = titleLines(chart, wPt, measure);
1065
+ const top = 4 + titleHeight(title);
851
1066
  const legend = layoutLegend(values.map((_, i) => ({
852
1067
  name: chart.categories[i] ?? String(i + 1),
853
1068
  colorHex: sliceColor(series, i, chart.seriesColorCycle)
@@ -876,7 +1091,7 @@ function buildPieScene(chart, wPt, hPt, measure) {
876
1091
  const mid = ang + sweep / 2;
877
1092
  const pct = Math.round(v / total * 100);
878
1093
  const own = chart.showValues ? fmtDataLabel(chart, v) : `${pct}%`;
879
- const custom = series.pointLabels?.find((l) => l.idx === i)?.text;
1094
+ const custom = atPoint(series.pointLabels, i)?.text;
880
1095
  if (custom !== void 0 || chart.showValues || pct >= 5) labels.push({
881
1096
  text: custom ?? own,
882
1097
  x: cx + Math.cos(mid) * labelR,
@@ -895,14 +1110,7 @@ function buildPieScene(chart, wPt, hPt, measure) {
895
1110
  sweepRad: -2 * Math.PI,
896
1111
  fillHex: "FFFFFF"
897
1112
  });
898
- if (chart.title) labels.push({
899
- text: chart.title,
900
- x: wPt / 2,
901
- y: hPt - 4 - 13,
902
- sizePt: 13,
903
- colorHex: TITLE_COLOR,
904
- align: "center"
905
- });
1113
+ pushChartTitle(labels, title, wPt, hPt);
906
1114
  legend.emit(rects, labels);
907
1115
  return {
908
1116
  rects,
@@ -912,7 +1120,25 @@ function buildPieScene(chart, wPt, hPt, measure) {
912
1120
  };
913
1121
  }
914
1122
  function pointColor(series, idx) {
915
- return series.pointColors?.find((p) => p.idx === idx)?.colorHex;
1123
+ return atPoint(series.pointColors, idx)?.colorHex;
1124
+ }
1125
+ /** Each override list's entries by point index, built on first use. */
1126
+ var overridesByPoint = /* @__PURE__ */ new WeakMap();
1127
+ /**
1128
+ * The first of `overrides` for the point at `idx` — looked up, not searched
1129
+ * for: a series that overrides many of its points, searched through for every
1130
+ * one of them, took the square of their number.
1131
+ */
1132
+ function atPoint(overrides, idx) {
1133
+ if (!overrides) return void 0;
1134
+ let byPoint = overridesByPoint.get(overrides);
1135
+ if (!byPoint) {
1136
+ const index = /* @__PURE__ */ new Map();
1137
+ for (const o of overrides) if (!index.has(o.idx)) index.set(o.idx, o);
1138
+ overridesByPoint.set(overrides, index);
1139
+ byPoint = index;
1140
+ }
1141
+ return byPoint.get(idx);
916
1142
  }
917
1143
  function layoutLegend(entries, hasLegend, pos, wPt, hPt, measure, marker = "box") {
918
1144
  if (!hasLegend || entries.length === 0) return {
@@ -939,7 +1165,7 @@ function layoutLegend(entries, hasLegend, pos, wPt, hPt, measure, marker = "box"
939
1165
  };
940
1166
  const entryW = (e) => keyW + 3 + measure(e.name, 9) + gap * 2;
941
1167
  if (pos === "r" || pos === "l") {
942
- const colW = Math.max(...entries.map(entryW));
1168
+ const colW = entries.reduce((w, e) => Math.max(w, entryW(e)), 0);
943
1169
  return {
944
1170
  rightWidth: pos === "r" ? colW : 0,
945
1171
  bottomHeight: 0,
@@ -1010,11 +1236,11 @@ function buildChartScene(chart, wPt, hPt, measure) {
1010
1236
  * in reverse.
1011
1237
  */
1012
1238
  function withReversedCategories(chart) {
1013
- const n = Math.max(chart.categories.length, ...chart.series.map((s) => s.values.length), 1);
1239
+ const n = catCount(chart);
1014
1240
  const flip = (i) => n - 1 - i;
1015
1241
  return {
1016
1242
  ...chart,
1017
- categories: chart.categories.length > 0 ? Array.from({ length: n }, (_, i) => chart.categories[flip(i)] ?? "") : chart.categories,
1243
+ categories: Array.from({ length: n }, (_, i) => chart.categories.length > 0 ? chart.categories[flip(i)] ?? "" : String(flip(i) + 1)),
1018
1244
  series: chart.series.map((s) => ({
1019
1245
  ...s,
1020
1246
  values: Array.from({ length: n }, (_, i) => s.values[flip(i)] ?? 0),