reamkit 1.31.0 → 1.32.0

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 (83) 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/document-model/index.d.ts +1 -1
  10. package/dist/esm/core/document-model/types.d.ts +112 -0
  11. package/dist/esm/core/drawingml/chart-geometry.d.ts +21 -8
  12. package/dist/esm/core/drawingml/chart-geometry.js +198 -108
  13. package/dist/esm/core/drawingml/chart-parser.js +73 -2
  14. package/dist/esm/core/drawingml/colors.d.ts +6 -0
  15. package/dist/esm/core/drawingml/colors.js +23 -3
  16. package/dist/esm/core/drawingml/shape-render.d.ts +3 -1
  17. package/dist/esm/core/drawingml/shape-render.js +4 -2
  18. package/dist/esm/core/font/measure.d.ts +9 -0
  19. package/dist/esm/core/font/measure.js +9 -1
  20. package/dist/esm/core/fonts/families.js +4 -2
  21. package/dist/esm/core/fonts/scripts.js +4 -2
  22. package/dist/esm/core/ir/flow.d.ts +6 -0
  23. package/dist/esm/core/ir/sheet.d.ts +43 -0
  24. package/dist/esm/core/number-format.js +16 -11
  25. package/dist/esm/core/spreadsheet-model/index.d.ts +1 -1
  26. package/dist/esm/core/spreadsheet-model/types.d.ts +96 -0
  27. package/dist/esm/core/style-cascade/resolver.js +4 -2
  28. package/dist/esm/excel/column-bands.d.ts +39 -6
  29. package/dist/esm/excel/column-bands.js +41 -17
  30. package/dist/esm/excel/comments-parser.js +19 -0
  31. package/dist/esm/excel/conditional-format.d.ts +7 -3
  32. package/dist/esm/excel/conditional-format.js +77 -39
  33. package/dist/esm/excel/formula/context.d.ts +18 -0
  34. package/dist/esm/excel/formula/eval.js +51 -0
  35. package/dist/esm/excel/formula/index.d.ts +2 -2
  36. package/dist/esm/excel/formula/lexer.d.ts +1 -1
  37. package/dist/esm/excel/formula/lexer.js +41 -0
  38. package/dist/esm/excel/formula/parser.d.ts +25 -0
  39. package/dist/esm/excel/formula/parser.js +95 -0
  40. package/dist/esm/excel/print-model.d.ts +38 -1
  41. package/dist/esm/excel/print-model.js +522 -80
  42. package/dist/esm/excel/shared-strings-parser.d.ts +9 -0
  43. package/dist/esm/excel/shared-strings-parser.js +11 -1
  44. package/dist/esm/excel/sheet-drawing.d.ts +38 -2
  45. package/dist/esm/excel/sheet-drawing.js +47 -13
  46. package/dist/esm/excel/sheet-shape-parser.d.ts +6 -2
  47. package/dist/esm/excel/sheet-shape-parser.js +18 -6
  48. package/dist/esm/excel/sheet-to-flow.d.ts +17 -0
  49. package/dist/esm/excel/sheet-to-flow.js +434 -47
  50. package/dist/esm/excel/styles-parser.d.ts +9 -0
  51. package/dist/esm/excel/styles-parser.js +80 -26
  52. package/dist/esm/excel/table-parser.js +5 -0
  53. package/dist/esm/excel/table-style-presets.d.ts +26 -0
  54. package/dist/esm/excel/table-style-presets.js +1555 -0
  55. package/dist/esm/excel/table-style.d.ts +13 -0
  56. package/dist/esm/excel/table-style.js +75 -0
  57. package/dist/esm/excel/tint.d.ts +10 -0
  58. package/dist/esm/excel/tint.js +75 -0
  59. package/dist/esm/excel/vml-drawing.d.ts +41 -3
  60. package/dist/esm/excel/vml-drawing.js +143 -10
  61. package/dist/esm/excel/worksheet-parser.js +89 -15
  62. package/dist/esm/excel/xls/biff-reader.js +10 -2
  63. package/dist/esm/excel/xlsx-reader.js +195 -14
  64. package/dist/esm/excel/xlsx-writer.js +8 -7
  65. package/dist/esm/html/html-writer.js +202 -20
  66. package/dist/esm/layout/fit-pages.d.ts +11 -0
  67. package/dist/esm/layout/fit-pages.js +86 -0
  68. package/dist/esm/layout/line-paint.d.ts +58 -0
  69. package/dist/esm/layout/line-paint.js +107 -0
  70. package/dist/esm/layout/page-doc.d.ts +51 -0
  71. package/dist/esm/layout/page-doc.js +141 -1
  72. package/dist/esm/layout/styled-layout.d.ts +7 -0
  73. package/dist/esm/layout/styled-layout.js +274 -51
  74. package/dist/esm/layout/turned-section.js +14 -2
  75. package/dist/esm/markdown/markdown-writer.js +17 -6
  76. package/dist/esm/pdf/styled-page-emitter.js +49 -71
  77. package/dist/esm/pdf/styled-page-renderer.js +1 -1
  78. package/dist/esm/pdf-reader/font.js +3 -3
  79. package/dist/esm/svg/svg-text.d.ts +58 -0
  80. package/dist/esm/svg/svg-text.js +322 -0
  81. package/dist/esm/svg/svg-writer.js +135 -71
  82. package/dist/esm/word/docx-writer.js +11 -2
  83. package/package.json +5 -1
@@ -23,37 +23,63 @@ 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 eight column charts (2026-10-01):
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, like both ends of a range across zero, takes 5% of the spread
34
+ * beyond the data before it is rounded out to the step;
35
+ * - the step: the smallest 1, 2 or 5 × 10ⁿ that leaves at most ten intervals
36
+ * (`maxIntervals`), whatever the chart's height — 3 750 runs 0…4 000 by 500
37
+ * on a 150pt chart as on a 400pt one. Only an axis too short to hold the
38
+ * labels asks for fewer, and the caller says so.
39
39
  *
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).
40
+ * Every case of the probe lands where Excel puts it: −1…1.2 on −1.5…1.5 by
41
+ * 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
42
+ * 0…0.05 by 0.005. Heckbert's nice numbers with a budget of ticks by height
43
+ * stepped the budget chart by 1 000 and started 93…97 at zero.
44
+ *
45
+ * @param dataMin The smallest value to cover.
46
+ * @param dataMax The largest value to cover.
47
+ * @param maxIntervals At most this many steps between the ends (default 10).
43
48
  * @returns The rounded min/max and tick step.
44
49
  */
45
- function niceScale(dataMin, dataMax, maxTicks = 6) {
46
- const lo = Math.min(dataMin, dataMax);
50
+ function niceScale(dataMin, dataMax, maxIntervals = 10) {
51
+ let lo = Math.min(dataMin, dataMax);
47
52
  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
+ if (lo === hi) if (hi > 0) lo = 0;
54
+ else if (lo < 0) hi = 0;
55
+ else hi = 1;
56
+ const spread = hi - lo;
57
+ const end = {
58
+ lo: lo - spread * .05,
59
+ hi: hi + spread * .05
60
+ };
61
+ if (lo >= 0) end.lo = spread < hi / 6 ? lo - spread / 2 : 0;
62
+ else if (hi <= 0) end.hi = spread < -lo / 6 ? hi + spread / 2 : 0;
63
+ const step = stepFor(end.hi - end.lo, maxIntervals);
51
64
  return {
52
- min: Math.floor(lo / step) * step,
53
- max: Math.ceil(headroom / step) * step,
65
+ min: Math.floor(end.lo / step + 1e-9) * step,
66
+ max: Math.ceil(end.hi / step - 1e-9) * step,
54
67
  step
55
68
  };
56
69
  }
70
+ /** The smallest 1, 2 or 5 × 10ⁿ that cuts `span` into at most `maxIntervals` steps. */
71
+ function stepFor(span, maxIntervals) {
72
+ const most = Math.max(1, maxIntervals);
73
+ let base = 10 ** Math.floor(Math.log10(span / most));
74
+ for (;;) {
75
+ for (const m of [
76
+ 1,
77
+ 2,
78
+ 5
79
+ ]) if (span / (m * base) <= most + 1e-9) return m * base;
80
+ base *= 10;
81
+ }
82
+ }
57
83
  /**
58
84
  * Format an axis tick value, choosing decimal places from the tick `step` so
59
85
  * `0.25`-spaced ticks read `0.25` while integer steps drop the fraction.
@@ -65,7 +91,8 @@ function niceScale(dataMin, dataMax, maxTicks = 6) {
65
91
  function formatTick(v, step) {
66
92
  if (Number.isInteger(step) && Number.isInteger(v)) return String(v);
67
93
  const decimals = Math.max(0, -Math.floor(Math.log10(step)));
68
- return v.toFixed(decimals).replace(/\.?0+$/, (m) => m.includes(".") ? "" : m);
94
+ const fixed = v.toFixed(decimals);
95
+ return fixed.includes(".") ? fixed.replace(/0+$/, "").replace(/\.$/, "") : fixed;
69
96
  }
70
97
  var ticks = (s) => {
71
98
  const out = [];
@@ -85,23 +112,54 @@ var ticks = (s) => {
85
112
  * the plot, not about the numbers.
86
113
  * @returns The axis min/max and tick step.
87
114
  */
88
- function axisScale(chart, dataMin, dataMax, hPt) {
89
- const rounded = niceScale(chart.valAxisMin ?? dataMin, chart.valAxisMax ?? dataMax, tickBudget(hPt));
115
+ function axisScale(chart, dataMin, dataMax, extentPt, horizontal = false) {
116
+ const rounded = niceScale(chart.valAxisMin ?? dataMin, chart.valAxisMax ?? dataMax, intervalsThatFit(extentPt, horizontal));
117
+ const step = chart.valAxisMajorUnit ?? rounded.step;
90
118
  return {
91
- min: chart.valAxisMin ?? rounded.min,
92
- max: chart.valAxisMax ?? rounded.max,
93
- step: rounded.step
119
+ min: chart.valAxisMin ?? (step === rounded.step ? rounded.min : Math.floor(rounded.min / step) * step),
120
+ max: chart.valAxisMax ?? (step === rounded.step ? rounded.max : Math.ceil(rounded.max / step) * step),
121
+ step
94
122
  };
95
123
  }
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"
124
+ /**
125
+ * The chart title's lines. A title longer than the chart is wide wraps at its
126
+ * spaces, as Excel's and Calc's do, rather than running out past both sides
127
+ * of the frame: dataValidationTableRange.xlsx's "Ranking of Washington
128
+ * Counties on Days per Patient (ALOS) in 2015" crossed the cells beside its
129
+ * chart. A line holds at most four fifths of the chart's width.
130
+ *
131
+ * @param chart The chart.
132
+ * @param wPt The chart's width.
133
+ * @param measure The text measurer.
134
+ * @returns The lines, top first; none for a chart without a title.
135
+ */
136
+ function titleLines(chart, wPt, measure) {
137
+ if (!chart.title) return [];
138
+ const room = wPt * .8;
139
+ const lines = [];
140
+ let line = "";
141
+ for (const word of chart.title.split(/\s+/).filter((w) => w.length > 0)) {
142
+ const longer = line ? `${line} ${word}` : word;
143
+ if (line && measure(longer, 13) > room) {
144
+ lines.push(line);
145
+ line = word;
146
+ } else line = longer;
147
+ }
148
+ if (line) lines.push(line);
149
+ return lines;
150
+ }
151
+ /** The band a title of `lines` takes at the top of the chart. */
152
+ var titleHeight = (lines) => lines.length === 0 ? 0 : 13 * (1.6 + 1.2 * (lines.length - 1));
153
+ function pushChartTitle(labels, lines, wPt, hPt) {
154
+ lines.forEach((text, i) => {
155
+ labels.push({
156
+ text,
157
+ x: wPt / 2,
158
+ y: hPt - 4 - 13 * (1 + 1.2 * i),
159
+ sizePt: 13,
160
+ colorHex: TITLE_COLOR,
161
+ align: "center"
162
+ });
105
163
  });
106
164
  }
107
165
  function buildLegendBlock(chart, wPt, hPt, measure) {
@@ -113,7 +171,7 @@ function buildLegendBlock(chart, wPt, hPt, measure) {
113
171
  const marker = chart.type === "line" || chart.type === "scatter" ? "line" : "box";
114
172
  return layoutLegend(legendEntries, chart.hasLegend, chart.legendPos ?? "b", wPt, hPt, measure, marker);
115
173
  }
116
- function pushGridTicks(gridlines, labels, tickVals, fmt, axis, at, x0, y0, plotW, plotH, grid) {
174
+ function pushGridTicks(gridlines, labels, tickVals, fmt, axis, at, x0, y0, plotW, plotH, grid, atEnd = false) {
117
175
  for (const v of tickVals) if (axis === "x") {
118
176
  const gx = at(v);
119
177
  gridlines.push({
@@ -124,7 +182,7 @@ function pushGridTicks(gridlines, labels, tickVals, fmt, axis, at, x0, y0, plotW
124
182
  labels.push({
125
183
  text: fmt(v),
126
184
  x: gx,
127
- y: y0 - 9,
185
+ y: atEnd ? y0 + plotH + 9 * .4 : y0 - 9,
128
186
  sizePt: 9,
129
187
  colorHex: LABEL_COLOR,
130
188
  align: "center"
@@ -138,11 +196,11 @@ function pushGridTicks(gridlines, labels, tickVals, fmt, axis, at, x0, y0, plotW
138
196
  });
139
197
  labels.push({
140
198
  text: fmt(v),
141
- x: x0 - 3,
199
+ x: atEnd ? x0 + plotW + 3 : x0 - 3,
142
200
  y: gy - 9 / 3,
143
201
  sizePt: 9,
144
202
  colorHex: LABEL_COLOR,
145
- align: "right"
203
+ align: atEnd ? "left" : "right"
146
204
  });
147
205
  }
148
206
  }
@@ -159,16 +217,24 @@ function axisStroke(style) {
159
217
  widthPt: style?.widthPt ?? 1
160
218
  };
161
219
  }
162
- function pushAxisLines(polylines, x0, y0, plotW, plotH, chart) {
220
+ /**
221
+ * The two axis lines: the upright one (in the left axis's style) and the one
222
+ * lying along (in the bottom axis's). The category axis lies where it crosses
223
+ * the value axis — `cross.y` up a column chart, `cross.x` along a bar chart —
224
+ * and the other stands at the plot's edge.
225
+ */
226
+ function pushAxisLines(polylines, x0, y0, plotW, plotH, chart, cross = {}) {
163
227
  const val = axisStroke(chart.valAxisLine);
228
+ const x = x0 + (cross.x ?? 0);
229
+ const y = y0 + (cross.y ?? 0);
164
230
  if (val) polylines.push({
165
- points: [[x0, y0], [x0, y0 + plotH]],
231
+ points: [[x, y0], [x, y0 + plotH]],
166
232
  strokeHex: val.hex,
167
233
  widthPt: val.widthPt
168
234
  });
169
235
  const cat = axisStroke(chart.catAxisLine);
170
236
  if (cat) polylines.push({
171
- points: [[x0, y0], [x0 + plotW, y0]],
237
+ points: [[x0, y], [x0 + plotW, y]],
172
238
  strokeHex: cat.hex,
173
239
  widthPt: cat.widthPt
174
240
  });
@@ -181,27 +247,43 @@ function buildFrame(chart, wPt, hPt, measure, horizontal, opts = {}) {
181
247
  const nCats = Math.max(chart.categories.length, ...chart.series.map((s) => s.values.length), 1);
182
248
  const onSecondary = chart.series.filter((s) => s.secondaryAxis);
183
249
  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);
250
+ const [dataMin, dataMax] = opts.dataRange ?? (allVals.length > 0 ? [Math.min(...allVals), Math.max(...allVals)] : [0, 1]);
251
+ const scale = opts.exactRange ? {
252
+ min: dataMin,
253
+ max: dataMax,
254
+ step: niceScale(dataMin, dataMax, intervalsThatFit(horizontal ? wPt : hPt, horizontal)).step
255
+ } : axisScale(chart, dataMin, dataMax, horizontal ? wPt : hPt, horizontal);
186
256
  const fmtVal = opts.formatValue ?? ((v) => formatTick(v, scale.step));
187
257
  const tickVals = ticks(scale);
188
258
  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;
259
+ const scale2 = vals2.length > 0 ? niceScale(Math.min(...vals2), Math.max(...vals2), intervalsThatFit(hPt, false)) : void 0;
190
260
  const tickVals2 = scale2 ? ticks(scale2) : [];
191
- let top = 4;
192
- if (chart.title) top += 13 * 1.6;
261
+ const crosses = chart.catAxisCrosses;
262
+ const crossValue = Math.min(Math.max(crosses === "min" ? scale.min : crosses === "max" ? scale.max : crosses ?? 0, scale.min), scale.max);
263
+ const labelsAt = chart.catTickLabelPos === "none" ? "none" : chart.catTickLabelPos === "low" ? "start" : chart.catTickLabelPos === "high" ? "end" : crossValue <= scale.min ? "start" : crossValue >= scale.max ? "end" : "cross";
264
+ const catText = (c) => chart.categories[c] ?? String(c + 1);
265
+ const widestCat = Math.max(0, ...Array.from({ length: nCats }, (_, c) => measure(catText(c), 9)));
266
+ const catBand = 9 * 1.6;
267
+ const groupLevels = horizontal ? [] : chart.categoryGroups ?? [];
268
+ const valAtEnd = chart.catAxisReversed === true ? chart.valAxisCrosses !== "max" : chart.valAxisCrosses === "max";
269
+ const title = titleLines(chart, wPt, measure);
270
+ const top = 4 + titleHeight(title) + (!horizontal && labelsAt === "end" ? catBand : 0) + (horizontal && valAtEnd ? catBand : 0);
193
271
  const legend = buildLegendBlock(chart, wPt, hPt, measure);
194
272
  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;
273
+ const tickHalf = (v) => horizontal && v !== void 0 ? measure(fmtVal(v), 9) / 2 : 0;
274
+ const lastTickHalf = tickHalf(tickVals[tickVals.length - 1]);
275
+ const catLabelW = Math.min(wPt * .4, widestCat + 6);
196
276
  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;
277
+ const plotRight = wPt - 4 - legend.rightWidth - tick2W - (horizontal && labelsAt === "end" ? catLabelW : lastTickHalf) - (!horizontal && valAtEnd ? tickLabelW : 0);
278
+ const axisTitleBand = 9 * 1.5;
279
+ const leftTitle = horizontal ? chart.catAxisTitle : chart.valAxisTitle;
280
+ const footTitle = horizontal ? chart.valAxisTitle : chart.catAxisTitle;
281
+ const x0 = 4 + (leftTitle ? axisTitleBand : 0) + (horizontal ? labelsAt === "start" ? catLabelW : tickHalf(tickVals[0]) : valAtEnd ? 0 : tickLabelW);
282
+ const y0 = 4 + legend.bottomHeight + (footTitle ? axisTitleBand : 0) + ((horizontal ? !valAtEnd : labelsAt === "start") ? catBand : 0) + (!horizontal && labelsAt === "start" ? groupLevels.length * catBand : 0);
201
283
  const plotW = Math.max(1, plotRight - x0);
202
284
  const plotH = Math.max(1, hPt - top - y0);
203
285
  const valueOffset = (v) => (v - scale.min) / (scale.max - scale.min) * (horizontal ? plotW : plotH);
204
- const zeroOffset = valueOffset(0);
286
+ const zeroOffset = valueOffset(crossValue);
205
287
  const plotBackground = chart.plotFillHex ?? chart.plotLine?.colorHex ? {
206
288
  x: x0,
207
289
  y: y0,
@@ -214,9 +296,9 @@ function buildFrame(chart, wPt, hPt, measure, horizontal, opts = {}) {
214
296
  ...chart.plotLine.dash ? { strokeDash: chart.plotLine.dash } : {}
215
297
  } : {}
216
298
  } : void 0;
217
- pushChartTitle(labels, chart, wPt, hPt);
218
- if (chart.valAxisTitle) labels.push({
219
- text: chart.valAxisTitle,
299
+ pushChartTitle(labels, title, wPt, hPt);
300
+ if (leftTitle) labels.push({
301
+ text: leftTitle,
220
302
  x: 12.1,
221
303
  y: y0 + plotH / 2,
222
304
  sizePt: 9,
@@ -224,16 +306,16 @@ function buildFrame(chart, wPt, hPt, measure, horizontal, opts = {}) {
224
306
  align: "center",
225
307
  rotationDeg: 90
226
308
  });
227
- if (chart.catAxisTitle) labels.push({
228
- text: chart.catAxisTitle,
309
+ if (footTitle) labels.push({
310
+ text: footTitle,
229
311
  x: x0 + plotW / 2,
230
312
  y: legend.bottomHeight + 2,
231
313
  sizePt: 9,
232
314
  colorHex: LABEL_COLOR,
233
315
  align: "center"
234
316
  });
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);
317
+ if (horizontal) pushGridTicks(gridlines, labels, tickVals, fmtVal, "x", (v) => x0 + valueOffset(v), x0, y0, plotW, plotH, chart.gridLine, valAtEnd);
318
+ else pushGridTicks(gridlines, labels, tickVals, fmtVal, "y", (v) => y0 + valueOffset(v), x0, y0, plotW, plotH, chart.gridLine, valAtEnd);
237
319
  const valueOffset2 = scale2 && !horizontal ? (v) => (v - scale2.min) / (scale2.max - scale2.min) * plotH : void 0;
238
320
  if (scale2 && valueOffset2) {
239
321
  for (const v of tickVals2) labels.push({
@@ -261,30 +343,60 @@ function buildFrame(chart, wPt, hPt, measure, horizontal, opts = {}) {
261
343
  });
262
344
  }
263
345
  const slot = (horizontal ? plotH : plotW) / nCats;
264
- const need = horizontal ? 9 * 1.4 : Math.max(0, ...chart.categories.map((t) => measure(t, 9))) + 4;
346
+ const need = horizontal ? 9 * 1.4 : widestCat + 4;
265
347
  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);
348
+ const across = labelsAt === "start" ? 0 : labelsAt === "end" ? horizontal ? plotW : plotH : zeroOffset;
349
+ const labelled = [];
350
+ if (labelsAt !== "none") if (chart.catAxisReversed === true) for (let c = nCats - 1; c >= 0; c -= step) labelled.push(c);
351
+ else for (let c = 0; c < nCats; c += step) labelled.push(c);
352
+ for (const c of labelled) {
353
+ const cat = catText(c);
268
354
  if (!cat) continue;
269
355
  const center = (horizontal ? y0 : x0) + c * slot + slot / 2;
270
356
  if (horizontal) labels.push({
271
357
  text: cat,
272
- x: x0 - 3,
358
+ x: labelsAt === "end" ? x0 + across + 3 : x0 + across - 3,
273
359
  y: center - 9 / 3,
274
360
  sizePt: 9,
275
361
  colorHex: LABEL_COLOR,
276
- align: "right"
362
+ align: labelsAt === "end" ? "left" : "right"
277
363
  });
278
364
  else labels.push({
279
365
  text: cat,
280
366
  x: center,
281
- y: y0 - 9,
367
+ y: labelsAt === "end" ? y0 + across + 9 * .4 : y0 + across - 9,
282
368
  sizePt: 9,
283
369
  colorHex: LABEL_COLOR,
284
370
  align: "center"
285
371
  });
286
372
  }
287
- pushAxisLines(polylines, x0, y0, plotW, plotH, chart);
373
+ if (labelsAt !== "none" && labelsAt !== "end") groupLevels.forEach((groups, level) => {
374
+ const rowY = y0 + across - 9 - (level + 1) * catBand;
375
+ groups.forEach((group, i) => {
376
+ const end = Math.min(nCats, groups[i + 1]?.start ?? nCats);
377
+ if (end <= group.start) return;
378
+ labels.push({
379
+ text: group.label,
380
+ x: x0 + (group.start + end) / 2 * slot,
381
+ y: rowY,
382
+ sizePt: 9,
383
+ colorHex: LABEL_COLOR,
384
+ align: "center"
385
+ });
386
+ if (i > 0) polylines.push({
387
+ points: [[x0 + group.start * slot, y0 + across], [x0 + group.start * slot, rowY - 9 * .4]],
388
+ strokeHex: GRID_COLOR,
389
+ widthPt: .75
390
+ });
391
+ });
392
+ });
393
+ pushAxisLines(polylines, x0, y0, plotW, plotH, chart, horizontal ? {
394
+ x: zeroOffset,
395
+ ...valAtEnd ? { y: plotH } : {}
396
+ } : {
397
+ y: zeroOffset,
398
+ ...valAtEnd ? { x: plotW } : {}
399
+ });
288
400
  legend.emit(rects, labels);
289
401
  return {
290
402
  x0,
@@ -332,6 +444,7 @@ function groupingFrameOpts(chart, nCats) {
332
444
  const g = chart.grouping ?? "clustered";
333
445
  if (g === "percentStacked") return {
334
446
  dataRange: [0, 1],
447
+ exactRange: true,
335
448
  formatValue: pctLabel
336
449
  };
337
450
  const format = chartValueFormatter(chart);
@@ -655,12 +768,12 @@ function buildScatterScene(chart, wPt, hPt, measure) {
655
768
  wedges: [],
656
769
  labels
657
770
  };
658
- const xScale = niceScale(...autoRange(xs), tickBudget(wPt / 2));
659
- const yScale = niceScale(...autoRange(ys), tickBudget(hPt));
771
+ const xScale = niceScale(Math.min(...xs), Math.max(...xs), intervalsThatFit(wPt, true));
772
+ const yScale = niceScale(Math.min(...ys), Math.max(...ys), intervalsThatFit(hPt, false));
660
773
  const xTicks = ticks(xScale);
661
774
  const yTicks = ticks(yScale);
662
- let top = 4;
663
- if (chart.title) top += 13 * 1.6;
775
+ const title = titleLines(chart, wPt, measure);
776
+ const top = 4 + titleHeight(title);
664
777
  const legend = buildLegendBlock(chart, wPt, hPt, measure);
665
778
  const x0 = 4 + (Math.max(0, ...yTicks.map((v) => measure(formatTick(v, yScale.step), 9))) + 4);
666
779
  const y0 = 4 + legend.bottomHeight + 9 * 1.6;
@@ -668,7 +781,7 @@ function buildScatterScene(chart, wPt, hPt, measure) {
668
781
  const plotH = Math.max(1, hPt - top - y0);
669
782
  const xAt = (v) => x0 + (v - xScale.min) / (xScale.max - xScale.min) * plotW;
670
783
  const yAt = (v) => y0 + (v - yScale.min) / (yScale.max - yScale.min) * plotH;
671
- pushChartTitle(labels, chart, wPt, hPt);
784
+ pushChartTitle(labels, title, wPt, hPt);
672
785
  pushGridTicks(gridlines, labels, yTicks, (v) => formatTick(v, yScale.step), "y", yAt, x0, y0, plotW, plotH, chart.gridLine);
673
786
  pushGridTicks(gridlines, labels, xTicks, (v) => formatTick(v, xScale.step), "x", xAt, x0, y0, plotW, plotH, chart.gridLine);
674
787
  pushAxisLines(polylines, x0, y0, plotW, plotH, chart);
@@ -702,27 +815,13 @@ function buildScatterScene(chart, wPt, hPt, measure) {
702
815
  };
703
816
  }
704
817
  /**
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.
818
+ * How many intervals an axis `extentPt` long has room to label: ten, Excel's
819
+ * own most, unless its labels would run into each other first — one label
820
+ * line apiece stacked up a vertical axis, a few digits' width apiece along a
821
+ * horizontal one. Excel crams nine labels up a 150pt column chart rather than
822
+ * step it more coarsely, so the room is all that limits it.
708
823
  */
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)));
824
+ var intervalsThatFit = (extentPt, horizontal) => Math.min(10, Math.max(2, Math.floor(extentPt * .75 / (9 * (horizontal ? 3.5 : 1.4)))));
726
825
  /** Side (points) of the square stamped for a series that names no symbol. */
727
826
  var DEFAULT_MARKER_PT = 4;
728
827
  /**
@@ -773,9 +872,7 @@ function pushMarker(rects, wedges, marker, x, y, color) {
773
872
  */
774
873
  function buildLineScene(chart, wPt, hPt, measure) {
775
874
  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)];
875
+ const range = [allVals.length > 0 ? Math.min(...allVals) : 0, allVals.length > 0 ? Math.max(...allVals) : 1];
779
876
  const lineFormat = chartValueFormatter(chart);
780
877
  const f = buildFrame(chart, wPt, hPt, measure, false, {
781
878
  dataRange: range,
@@ -846,8 +943,8 @@ function buildPieScene(chart, wPt, hPt, measure) {
846
943
  wedges,
847
944
  labels
848
945
  };
849
- let top = 4;
850
- if (chart.title) top += 13 * 1.6;
946
+ const title = titleLines(chart, wPt, measure);
947
+ const top = 4 + titleHeight(title);
851
948
  const legend = layoutLegend(values.map((_, i) => ({
852
949
  name: chart.categories[i] ?? String(i + 1),
853
950
  colorHex: sliceColor(series, i, chart.seriesColorCycle)
@@ -895,14 +992,7 @@ function buildPieScene(chart, wPt, hPt, measure) {
895
992
  sweepRad: -2 * Math.PI,
896
993
  fillHex: "FFFFFF"
897
994
  });
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
- });
995
+ pushChartTitle(labels, title, wPt, hPt);
906
996
  legend.emit(rects, labels);
907
997
  return {
908
998
  rects,
@@ -1014,7 +1104,7 @@ function withReversedCategories(chart) {
1014
1104
  const flip = (i) => n - 1 - i;
1015
1105
  return {
1016
1106
  ...chart,
1017
- categories: chart.categories.length > 0 ? Array.from({ length: n }, (_, i) => chart.categories[flip(i)] ?? "") : chart.categories,
1107
+ categories: Array.from({ length: n }, (_, i) => chart.categories.length > 0 ? chart.categories[flip(i)] ?? "" : String(flip(i) + 1)),
1018
1108
  series: chart.series.map((s) => ({
1019
1109
  ...s,
1020
1110
  values: Array.from({ length: n }, (_, i) => s.values[flip(i)] ?? 0),
@@ -71,10 +71,13 @@ function parseChart(chartXml, resolveColor, resolveImage) {
71
71
  const secondaryValAxisTitle = secondaryTitleNode ? collectAT(secondaryTitleNode) || "Axis Title" : void 0;
72
72
  let categories = [];
73
73
  let categoriesRef;
74
+ let categoryGroups;
74
75
  for (const s of serNodes) {
75
76
  const cat = poChildren(s).find((c) => poIs(c, "c:cat"));
76
77
  if (cat) {
77
- categories = denseStrings(cat);
78
+ const levels = multiLevelCategories(cat);
79
+ categories = levels ? levels.categories : denseStrings(cat);
80
+ categoryGroups = levels?.groups;
78
81
  categoriesRef ??= refFormula(cat);
79
82
  break;
80
83
  }
@@ -99,7 +102,12 @@ function parseChart(chartXml, resolveColor, resolveImage) {
99
102
  const gridLine = lineStyleOf(leftAxNode ? poChildren(leftAxNode).find((c) => poIs(c, "c:majorGridlines")) : void 0, resolveColor);
100
103
  const valAxisMin = axisScaling(plotArea, "c:min");
101
104
  const valAxisMax = axisScaling(plotArea, "c:max");
105
+ const valAxisMajorUnit = majorUnitOf(plotArea);
102
106
  const catAxisReversed = axisOrientation(catAxNode) === "maxMin";
107
+ const catAxisCrosses = axisCrossing(catAxNode);
108
+ const valAxisCrosses = axisCrossing(valAxNode);
109
+ const tickLblPos = catAxNode ? poVal(poChildren(catAxNode).find((c) => poIs(c, "c:tickLblPos"))) : void 0;
110
+ const catTickLabelPos = tickLblPos === "low" || tickLblPos === "high" || tickLblPos === "none" ? tickLblPos : void 0;
103
111
  const chartSpace = tree.find((c) => poIs(c, "c:chartSpace"));
104
112
  const spaceSpPr = poChildren(chartSpace).find((c) => poIs(c, "c:spPr"));
105
113
  const frameLine = spaceSpPr ? poChildren(spaceSpPr).find((c) => poIs(c, "a:ln")) : void 0;
@@ -123,6 +131,7 @@ function parseChart(chartXml, resolveColor, resolveImage) {
123
131
  ...Number.isFinite(gapPercent) && gapPercent >= 0 ? { gapPercent } : {},
124
132
  categories,
125
133
  ...categoriesRef ? { categoriesRef } : {},
134
+ ...categoryGroups && categoryGroups.length > 0 ? { categoryGroups } : {},
126
135
  series,
127
136
  hasLegend: legend !== void 0,
128
137
  ...isLegendPos(legendPos) ? { legendPos } : {},
@@ -132,6 +141,9 @@ function parseChart(chartXml, resolveColor, resolveImage) {
132
141
  ...showValues ? { showValues: true } : {},
133
142
  ...catAxisTitle ? { catAxisTitle } : {},
134
143
  ...catAxisReversed ? { catAxisReversed } : {},
144
+ ...catAxisCrosses !== void 0 ? { catAxisCrosses } : {},
145
+ ...valAxisCrosses !== void 0 ? { valAxisCrosses } : {},
146
+ ...catTickLabelPos ? { catTickLabelPos } : {},
135
147
  ...valAxisTitle ? { valAxisTitle } : {},
136
148
  ...secondaryValAxisTitle ? { secondaryValAxisTitle } : {},
137
149
  ...scatterStyle ? { scatterStyle } : {},
@@ -142,6 +154,7 @@ function parseChart(chartXml, resolveColor, resolveImage) {
142
154
  ...gridLine ? { gridLine } : {},
143
155
  ...valAxisMin !== void 0 ? { valAxisMin } : {},
144
156
  ...valAxisMax !== void 0 ? { valAxisMax } : {},
157
+ ...valAxisMajorUnit !== void 0 ? { valAxisMajorUnit } : {},
145
158
  ...frameFillHex ? { frameFillHex } : {},
146
159
  ...frameFillImage ? { frameFillImage } : {},
147
160
  ...frameLineHex ? { frameLineHex } : {},
@@ -298,9 +311,16 @@ function fillColorOf(spPr, resolveColor) {
298
311
  const lnFill = ln ? poChildren(ln).find((c) => poIs(c, "a:solidFill")) : void 0;
299
312
  return lnFill ? colorFromSolidFill(lnFill, resolveColor) : void 0;
300
313
  }
314
+ /** The colour elements a chart's fill names a colour by (§20.1.2.3). */
315
+ var CHART_COLOR_NODES = [
316
+ "a:srgbClr",
317
+ "a:schemeClr",
318
+ "a:sysClr",
319
+ "a:prstClr"
320
+ ];
301
321
  function colorFromSolidFill(solid, resolveColor) {
302
322
  for (const c of poChildren(solid)) {
303
- if (!poIs(c, "a:srgbClr") && !poIs(c, "a:schemeClr")) continue;
323
+ if (!CHART_COLOR_NODES.some((name) => poIs(c, name))) continue;
304
324
  if (!poAttr(c, "val")) continue;
305
325
  return resolveColorNode(c, resolveColor);
306
326
  }
@@ -374,10 +394,30 @@ function axisTitle(plotArea, axTag) {
374
394
  * does not carry — those keep the plain numeric render.
375
395
  */
376
396
  /** §21.2.2.134 `c:scaling/c:orientation` — `minMax` (the default) or `maxMin`. */
397
+ /**
398
+ * §21.2.2.33/§21.2.2.34 — where an axis crosses the one it is drawn against:
399
+ * `crossesAt` a value, or `crosses` at that axis's minimum or maximum.
400
+ * undefined for `autoZero`, the default.
401
+ */
402
+ function axisCrossing(ax) {
403
+ if (!ax) return void 0;
404
+ const at = poChildren(ax).find((c) => poIs(c, "c:crossesAt"));
405
+ const value = at ? Number(poAttr(at, "val")) : NaN;
406
+ if (Number.isFinite(value)) return value;
407
+ const crosses = poVal(poChildren(ax).find((c) => poIs(c, "c:crosses")));
408
+ return crosses === "min" || crosses === "max" ? crosses : void 0;
409
+ }
377
410
  function axisOrientation(ax) {
378
411
  const scaling = ax ? poChildren(ax).find((c) => poIs(c, "c:scaling")) : void 0;
379
412
  return scaling ? poVal(poChildren(scaling).find((c) => poIs(c, "c:orientation"))) : void 0;
380
413
  }
414
+ /** §21.2.2.98 `c:valAx/c:majorUnit` — a positive step, or undefined for "auto". */
415
+ function majorUnitOf(plotArea) {
416
+ const ax = poChildren(plotArea).find((c) => poIs(c, "c:valAx"));
417
+ const node = ax ? poChildren(ax).find((c) => poIs(c, "c:majorUnit")) : void 0;
418
+ const v = node ? Number(poAttr(node, "val")) : NaN;
419
+ return Number.isFinite(v) && v > 0 ? v : void 0;
420
+ }
381
421
  function axisScaling(plotArea, tag) {
382
422
  const ax = poChildren(plotArea).find((c) => poIs(c, "c:valAx"));
383
423
  const scaling = ax ? poChildren(ax).find((c) => poIs(c, "c:scaling")) : void 0;
@@ -489,6 +529,37 @@ function denseStrings(container) {
489
529
  for (const p of pts) arr[p.idx] = p.v;
490
530
  return arr;
491
531
  }
532
+ /**
533
+ * §21.2.2.115 `c:multiLvlStrCache` — categories labelled on several levels:
534
+ * the first `c:lvl` labels each category, every later one groups them, a
535
+ * group's label standing at the category it starts at. Read as one flat cache
536
+ * it read as none, and an xlsx chart fell back to the cells its reference
537
+ * names, both columns of them in turn: WithChartSheet.xlsx's six bars stood in
538
+ * the first six of sixteen slots under a jumble of years and measure names.
539
+ *
540
+ * @param cat The `c:cat` element.
541
+ * @returns The innermost labels and the outer levels' groups, or undefined
542
+ * for a category axis of one level.
543
+ */
544
+ function multiLevelCategories(cat) {
545
+ const cache = poFindDescendant(cat, "c:multiLvlStrCache");
546
+ if (!cache) return void 0;
547
+ const count = poIntAttr(poChildren(cache).find((c) => poIs(c, "c:ptCount")) ?? cache, "val") ?? 0;
548
+ const [inner, ...outer] = poChildren(cache).filter((c) => poIs(c, "c:lvl")).map((lvl) => poChildren(lvl).filter((pt) => poIs(pt, "c:pt")).map((pt) => {
549
+ const v = poChildren(pt).find((c) => poIs(c, "c:v"));
550
+ return {
551
+ start: poIntAttr(pt, "idx") ?? 0,
552
+ label: v ? poText(v) : ""
553
+ };
554
+ }));
555
+ 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;
558
+ return {
559
+ categories,
560
+ groups: outer.map((level) => [...level].sort((a, b) => a.start - b.start))
561
+ };
562
+ }
492
563
  function isLegendPos(v) {
493
564
  return v === "r" || v === "l" || v === "t" || v === "b";
494
565
  }
@@ -40,6 +40,12 @@ export declare function applyColorMods(hex: string, mods: ReadonlyArray<ColorMod
40
40
  * scheme slots when a document carries no custom theme part.
41
41
  */
42
42
  export declare const DEFAULT_THEME_PALETTE: ReadonlyMap<string, string>;
43
+ /**
44
+ * The Office 2023 theme's palette — what Excel colours a workbook that carries
45
+ * no theme part with. Its own PDF of theme slots 0…9 in such a workbook
46
+ * (2026-10-01) paints exactly these: accent1 is 156082, not 2013's 4472C4.
47
+ */
48
+ export declare const OFFICE_2023_THEME_PALETTE: ReadonlyMap<string, string>;
43
49
  /**
44
50
  * Which theme slot each `schemeClr` name stands for. Only the text/background
45
51
  * aliases are ever remapped; anything absent resolves under its own name.