reamkit 1.15.3 → 1.16.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 (95) hide show
  1. package/dist/esm/core/converter/facade.d.ts +5 -0
  2. package/dist/esm/core/converter/ream.d.ts +6 -0
  3. package/dist/esm/core/converter/ream.js +34 -8
  4. package/dist/esm/core/document-model/index.d.ts +1 -1
  5. package/dist/esm/core/document-model/types.d.ts +187 -1
  6. package/dist/esm/core/drawingml/chart-geometry.d.ts +28 -0
  7. package/dist/esm/core/drawingml/chart-geometry.js +371 -88
  8. package/dist/esm/core/drawingml/chart-parser.js +262 -11
  9. package/dist/esm/core/drawingml/colors.d.ts +6 -6
  10. package/dist/esm/core/drawingml/colors.js +58 -18
  11. package/dist/esm/core/drawingml/preset-geometry.js +7 -6
  12. package/dist/esm/core/drawingml/sparkline-geometry.js +6 -4
  13. package/dist/esm/core/drawingml/theme-parser.d.ts +41 -0
  14. package/dist/esm/core/drawingml/theme-parser.js +74 -1
  15. package/dist/esm/core/font/ttf-subset.js +26 -1
  16. package/dist/esm/core/images.js +172 -43
  17. package/dist/esm/core/indexed-colors.d.ts +11 -0
  18. package/dist/esm/core/indexed-colors.js +81 -0
  19. package/dist/esm/core/ir/sheet.d.ts +67 -3
  20. package/dist/esm/{excel → core}/number-format.d.ts +37 -0
  21. package/dist/esm/core/number-format.js +800 -0
  22. package/dist/esm/core/opc/alternate-content.d.ts +13 -0
  23. package/dist/esm/core/opc/alternate-content.js +72 -0
  24. package/dist/esm/core/opc/core-properties.js +1 -0
  25. package/dist/esm/core/opc/package.d.ts +14 -1
  26. package/dist/esm/core/opc/package.js +43 -4
  27. package/dist/esm/core/opc/relationships.js +1 -0
  28. package/dist/esm/core/opc/xml-entities.d.ts +12 -0
  29. package/dist/esm/core/opc/xml-entities.js +91 -0
  30. package/dist/esm/core/spreadsheet-model/index.d.ts +1 -1
  31. package/dist/esm/core/spreadsheet-model/types.d.ts +137 -0
  32. package/dist/esm/core/vector.d.ts +13 -0
  33. package/dist/esm/excel/activex-parser.d.ts +9 -0
  34. package/dist/esm/excel/activex-parser.js +16 -1
  35. package/dist/esm/excel/column-bands.d.ts +26 -1
  36. package/dist/esm/excel/column-bands.js +156 -20
  37. package/dist/esm/excel/comments-parser.js +5 -3
  38. package/dist/esm/excel/conditional-format.d.ts +25 -2
  39. package/dist/esm/excel/conditional-format.js +63 -19
  40. package/dist/esm/excel/escaped-text.d.ts +9 -0
  41. package/dist/esm/excel/escaped-text.js +20 -0
  42. package/dist/esm/excel/form-control-parser.js +1 -0
  43. package/dist/esm/excel/formula/dates.js +1 -1
  44. package/dist/esm/excel/header-footer.d.ts +9 -1
  45. package/dist/esm/excel/header-footer.js +162 -14
  46. package/dist/esm/excel/index.d.ts +1 -1
  47. package/dist/esm/excel/index.js +2 -2
  48. package/dist/esm/excel/pivot-table-parser.js +1 -0
  49. package/dist/esm/excel/print-model.d.ts +75 -0
  50. package/dist/esm/excel/print-model.js +902 -77
  51. package/dist/esm/excel/printer-settings.d.ts +16 -0
  52. package/dist/esm/excel/printer-settings.js +36 -0
  53. package/dist/esm/excel/rich-value.d.ts +13 -0
  54. package/dist/esm/excel/rich-value.js +120 -0
  55. package/dist/esm/excel/shared-strings-parser.js +11 -7
  56. package/dist/esm/excel/sheet-drawing.d.ts +11 -0
  57. package/dist/esm/excel/sheet-drawing.js +26 -3
  58. package/dist/esm/excel/sheet-shape-parser.d.ts +53 -1
  59. package/dist/esm/excel/sheet-shape-parser.js +461 -32
  60. package/dist/esm/excel/sheet-to-flow.d.ts +23 -2
  61. package/dist/esm/excel/sheet-to-flow.js +551 -36
  62. package/dist/esm/excel/slicer-parser.js +1 -0
  63. package/dist/esm/excel/styles-parser.d.ts +30 -2
  64. package/dist/esm/excel/styles-parser.js +177 -40
  65. package/dist/esm/excel/table-parser.js +1 -0
  66. package/dist/esm/excel/vml-drawing.d.ts +68 -0
  67. package/dist/esm/excel/vml-drawing.js +158 -0
  68. package/dist/esm/excel/workbook-parser.d.ts +5 -0
  69. package/dist/esm/excel/workbook-parser.js +5 -1
  70. package/dist/esm/excel/worksheet-parser.d.ts +2 -1
  71. package/dist/esm/excel/worksheet-parser.js +149 -28
  72. package/dist/esm/excel/xlsx-reader.js +271 -33
  73. package/dist/esm/excel/xlsx-to-pdf.js +12 -1
  74. package/dist/esm/html/html-writer.js +11 -2
  75. package/dist/esm/layout/page-doc.d.ts +26 -0
  76. package/dist/esm/layout/styled-layout.d.ts +2 -0
  77. package/dist/esm/layout/styled-layout.js +493 -64
  78. package/dist/esm/pdf/cid-font.js +20 -9
  79. package/dist/esm/pdf/objects.d.ts +13 -0
  80. package/dist/esm/pdf/objects.js +18 -1
  81. package/dist/esm/pdf/styled-page-emitter.js +164 -32
  82. package/dist/esm/pdf/text-page-renderer.js +1 -0
  83. package/dist/esm/pdf/vector-graphics.d.ts +3 -1
  84. package/dist/esm/pdf/vector-graphics.js +19 -2
  85. package/dist/esm/pptx/pptx-reader.js +1 -0
  86. package/dist/esm/pptx/slide-parser.js +12 -1
  87. package/dist/esm/svg/svg-writer.js +23 -4
  88. package/dist/esm/word/document-parser.js +1 -0
  89. package/dist/esm/word/drawing-parser.d.ts +34 -1
  90. package/dist/esm/word/drawing-parser.js +52 -2
  91. package/dist/esm/word/numbering-parser.js +1 -0
  92. package/dist/esm/word/settings-parser.js +1 -0
  93. package/dist/esm/word/styles-parser.js +1 -0
  94. package/package.json +7 -3
  95. package/dist/esm/excel/number-format.js +0 -476
@@ -1,3 +1,4 @@
1
+ import { applyNumberFormat } from "../number-format.js";
1
2
  var AXIS_COLOR = "595959";
2
3
  var GRID_COLOR = "D9D9D9";
3
4
  var LABEL_COLOR = "595959";
@@ -46,9 +47,10 @@ function niceScale(dataMin, dataMax, maxTicks = 6) {
46
47
  let hi = Math.max(dataMin, dataMax);
47
48
  if (lo === hi) hi = lo + 1;
48
49
  const step = niceNum(niceNum(hi - lo, false) / Math.max(1, maxTicks - 1), true);
50
+ const headroom = hi > 0 ? hi * 1.05 : hi;
49
51
  return {
50
52
  min: Math.floor(lo / step) * step,
51
- max: Math.ceil(hi / step) * step,
53
+ max: Math.ceil(headroom / step) * step,
52
54
  step
53
55
  };
54
56
  }
@@ -70,6 +72,27 @@ var ticks = (s) => {
70
72
  for (let v = s.min; v <= s.max + 1e-9; v += s.step) out.push(Math.abs(v) < 1e-9 ? 0 : v);
71
73
  return out;
72
74
  };
75
+ /**
76
+ * 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.
80
+ *
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.
86
+ * @returns The axis min/max and tick step.
87
+ */
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
+ };
95
+ }
73
96
  function pushChartTitle(labels, chart, wPt, hPt) {
74
97
  if (!chart.title) return;
75
98
  labels.push({
@@ -82,17 +105,20 @@ function pushChartTitle(labels, chart, wPt, hPt) {
82
105
  });
83
106
  }
84
107
  function buildLegendBlock(chart, wPt, hPt, measure) {
85
- return layoutLegend(chart.series.map((s, i) => ({
86
- name: s.name ?? "",
87
- colorHex: seriesColor(s, i, chart.seriesColorCycle)
88
- })).filter((e) => e.name !== ""), chart.hasLegend, chart.legendPos ?? "b", wPt, hPt, measure);
108
+ const legendEntries = chart.series.map((s, i) => ({
109
+ name: legendSeriesName(s, i),
110
+ colorHex: seriesColor(s, i, chart.seriesColorCycle),
111
+ ...s.line?.none ? { marker: "box" } : isLineLike(s) ? { marker: "line" } : {}
112
+ }));
113
+ const marker = chart.type === "line" || chart.type === "scatter" ? "line" : "box";
114
+ return layoutLegend(legendEntries, chart.hasLegend, chart.legendPos ?? "b", wPt, hPt, measure, marker);
89
115
  }
90
- function pushGridTicks(polylines, labels, tickVals, fmt, axis, at, x0, y0, plotW, plotH) {
116
+ function pushGridTicks(gridlines, labels, tickVals, fmt, axis, at, x0, y0, plotW, plotH, grid) {
91
117
  for (const v of tickVals) if (axis === "x") {
92
118
  const gx = at(v);
93
- polylines.push({
119
+ gridlines.push({
94
120
  points: [[gx, y0], [gx, y0 + plotH]],
95
- strokeHex: GRID_COLOR,
121
+ strokeHex: grid?.colorHex ?? GRID_COLOR,
96
122
  widthPt: .75
97
123
  });
98
124
  labels.push({
@@ -105,9 +131,9 @@ function pushGridTicks(polylines, labels, tickVals, fmt, axis, at, x0, y0, plotW
105
131
  });
106
132
  } else {
107
133
  const gy = at(v);
108
- polylines.push({
134
+ gridlines.push({
109
135
  points: [[x0, gy], [x0 + plotW, gy]],
110
- strokeHex: GRID_COLOR,
136
+ strokeHex: grid?.colorHex ?? GRID_COLOR,
111
137
  widthPt: .75
112
138
  });
113
139
  labels.push({
@@ -120,37 +146,57 @@ function pushGridTicks(polylines, labels, tickVals, fmt, axis, at, x0, y0, plotW
120
146
  });
121
147
  }
122
148
  }
123
- function pushAxisLines(polylines, x0, y0, plotW, plotH) {
124
- polylines.push({
149
+ /**
150
+ * §21.2.2.196 — an axis draws the rule its `c:spPr/a:ln` asks for. `<a:noFill/>`
151
+ * draws none at all, and both references honour it: 57362.xlsx gives its value
152
+ * axis a 0.75pt #D9D9D9 hairline where we drew a 1pt #595959 one, and hides its
153
+ * secondary axis's line entirely while keeping its labels.
154
+ */
155
+ function axisStroke(style) {
156
+ if (style?.none) return null;
157
+ return {
158
+ hex: style?.colorHex ?? AXIS_COLOR,
159
+ widthPt: style?.widthPt ?? 1
160
+ };
161
+ }
162
+ function pushAxisLines(polylines, x0, y0, plotW, plotH, chart) {
163
+ const val = axisStroke(chart.valAxisLine);
164
+ if (val) polylines.push({
125
165
  points: [[x0, y0], [x0, y0 + plotH]],
126
- strokeHex: AXIS_COLOR,
127
- widthPt: 1
166
+ strokeHex: val.hex,
167
+ widthPt: val.widthPt
128
168
  });
129
- polylines.push({
169
+ const cat = axisStroke(chart.catAxisLine);
170
+ if (cat) polylines.push({
130
171
  points: [[x0, y0], [x0 + plotW, y0]],
131
- strokeHex: AXIS_COLOR,
132
- widthPt: 1
172
+ strokeHex: cat.hex,
173
+ widthPt: cat.widthPt
133
174
  });
134
175
  }
135
176
  function buildFrame(chart, wPt, hPt, measure, horizontal, opts = {}) {
136
177
  const rects = [];
137
178
  const polylines = [];
179
+ const gridlines = [];
138
180
  const labels = [];
139
181
  const nCats = Math.max(chart.categories.length, ...chart.series.map((s) => s.values.length), 1);
140
- const allVals = chart.series.flatMap((s) => s.values.slice(0, nCats));
182
+ const onSecondary = chart.series.filter((s) => s.secondaryAxis);
183
+ const allVals = (onSecondary.length > 0 ? chart.series.filter((s) => !s.secondaryAxis) : chart.series).flatMap((s) => s.values.slice(0, nCats));
141
184
  const [dataMin, dataMax] = opts.dataRange ?? [Math.min(0, ...allVals), Math.max(0, ...allVals)];
142
- const scale = niceScale(dataMin, dataMax);
185
+ const scale = axisScale(chart, dataMin, dataMax, hPt);
143
186
  const fmtVal = opts.formatValue ?? ((v) => formatTick(v, scale.step));
144
187
  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), Math.min(10, Math.max(4, Math.round(hPt / 24)))) : void 0;
190
+ const tickVals2 = scale2 ? ticks(scale2) : [];
145
191
  let top = 4;
146
192
  if (chart.title) top += 13 * 1.6;
147
- if (chart.valAxisTitle) top += 9 * 1.4;
148
193
  const legend = buildLegendBlock(chart, wPt, hPt, measure);
149
- const plotRight = wPt - 4 - legend.rightWidth;
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;
150
196
  const tickLabelW = Math.max(0, ...tickVals.map((v) => measure(fmtVal(v), 9))) + 4;
151
197
  const catLabelH = 9 * 1.6;
152
198
  const catTitleH = chart.catAxisTitle ? 9 * 1.5 : 0;
153
- const x0 = 4 + tickLabelW;
199
+ const x0 = 4 + (chart.valAxisTitle ? 9 * 1.5 : 0) + tickLabelW;
154
200
  const y0 = 4 + legend.bottomHeight + catTitleH + catLabelH;
155
201
  const plotW = Math.max(1, plotRight - x0);
156
202
  const plotH = Math.max(1, hPt - top - y0);
@@ -159,11 +205,12 @@ function buildFrame(chart, wPt, hPt, measure, horizontal, opts = {}) {
159
205
  pushChartTitle(labels, chart, wPt, hPt);
160
206
  if (chart.valAxisTitle) labels.push({
161
207
  text: chart.valAxisTitle,
162
- x: x0,
163
- y: hPt - top + 3,
208
+ x: 12.1,
209
+ y: y0 + plotH / 2,
164
210
  sizePt: 9,
165
211
  colorHex: LABEL_COLOR,
166
- align: "left"
212
+ align: "center",
213
+ rotationDeg: 90
167
214
  });
168
215
  if (chart.catAxisTitle) labels.push({
169
216
  text: chart.catAxisTitle,
@@ -173,11 +220,39 @@ function buildFrame(chart, wPt, hPt, measure, horizontal, opts = {}) {
173
220
  colorHex: LABEL_COLOR,
174
221
  align: "center"
175
222
  });
176
- if (horizontal) pushGridTicks(polylines, labels, tickVals, fmtVal, "x", (v) => x0 + valueOffset(v), x0, y0, plotW, plotH);
177
- else pushGridTicks(polylines, labels, tickVals, fmtVal, "y", (v) => y0 + valueOffset(v), x0, y0, plotW, plotH);
223
+ if (horizontal) pushGridTicks(gridlines, labels, tickVals, fmtVal, "x", (v) => x0 + valueOffset(v), x0, y0, plotW, plotH, chart.gridLine);
224
+ else pushGridTicks(gridlines, labels, tickVals, fmtVal, "y", (v) => y0 + valueOffset(v), x0, y0, plotW, plotH, chart.gridLine);
225
+ const valueOffset2 = scale2 && !horizontal ? (v) => (v - scale2.min) / (scale2.max - scale2.min) * plotH : void 0;
226
+ if (scale2 && valueOffset2) {
227
+ for (const v of tickVals2) labels.push({
228
+ text: formatTick(v, scale2.step),
229
+ x: x0 + plotW + 3,
230
+ y: y0 + valueOffset2(v) - 9 / 3,
231
+ sizePt: 9,
232
+ colorHex: LABEL_COLOR,
233
+ align: "left"
234
+ });
235
+ const sec = axisStroke(chart.secondaryValAxisLine);
236
+ if (sec) polylines.push({
237
+ points: [[x0 + plotW, y0], [x0 + plotW, y0 + plotH]],
238
+ strokeHex: sec.hex,
239
+ widthPt: sec.widthPt
240
+ });
241
+ if (chart.secondaryValAxisTitle) labels.push({
242
+ text: chart.secondaryValAxisTitle,
243
+ x: wPt - 4,
244
+ y: y0 + plotH / 2,
245
+ sizePt: 9,
246
+ colorHex: LABEL_COLOR,
247
+ align: "center",
248
+ rotationDeg: 90
249
+ });
250
+ }
178
251
  const slot = (horizontal ? plotH : plotW) / nCats;
179
- for (let c = 0; c < nCats; c++) {
180
- const cat = chart.categories[c] ?? "";
252
+ const need = horizontal ? 9 * 1.4 : Math.max(0, ...chart.categories.map((t) => measure(t, 9))) + 4;
253
+ const step = Math.max(1, Math.ceil(need / Math.max(slot, .01)));
254
+ for (let c = 0; c < nCats; c += step) {
255
+ const cat = chart.categories[c] ?? String(c + 1);
181
256
  if (!cat) continue;
182
257
  const center = (horizontal ? y0 : x0) + c * slot + slot / 2;
183
258
  if (horizontal) labels.push({
@@ -197,7 +272,7 @@ function buildFrame(chart, wPt, hPt, measure, horizontal, opts = {}) {
197
272
  align: "center"
198
273
  });
199
274
  }
200
- pushAxisLines(polylines, x0, y0, plotW, plotH);
275
+ pushAxisLines(polylines, x0, y0, plotW, plotH, chart);
201
276
  legend.emit(rects, labels);
202
277
  return {
203
278
  x0,
@@ -209,13 +284,15 @@ function buildFrame(chart, wPt, hPt, measure, horizontal, opts = {}) {
209
284
  horizontal,
210
285
  zeroOffset,
211
286
  valueOffset,
287
+ ...valueOffset2 ? { valueOffset2 } : {},
212
288
  rects,
213
289
  polylines,
290
+ gridlines,
214
291
  labels
215
292
  };
216
293
  }
217
294
  var pctLabel = (v) => `${Math.round(v * 100)}%`;
218
- var fmtDataLabel = (v) => Number.isInteger(v) ? String(v) : String(Math.round(v * 100) / 100);
295
+ var fmtDataLabel = (chart, v) => chartValueFormatter(chart)?.(v) ?? (Number.isInteger(v) ? String(v) : String(Math.round(v * 100) / 100));
219
296
  function catCount(chart) {
220
297
  return Math.max(chart.categories.length, ...chart.series.map((s) => s.values.length), 1);
221
298
  }
@@ -244,11 +321,25 @@ function groupingFrameOpts(chart, nCats) {
244
321
  dataRange: [0, 1],
245
322
  formatValue: pctLabel
246
323
  };
324
+ const format = chartValueFormatter(chart);
247
325
  if (g === "stacked") {
248
326
  const t = stackedTotals(chart, nCats);
249
- return { dataRange: [Math.min(0, t.min), Math.max(0, t.max)] };
327
+ return {
328
+ dataRange: [Math.min(0, t.min), Math.max(0, t.max)],
329
+ ...format ? { formatValue: format } : {}
330
+ };
250
331
  }
251
- return {};
332
+ return format ? { formatValue: format } : {};
333
+ }
334
+ var CHART_FORMAT_ID = 1e6;
335
+ var formatterCache = /* @__PURE__ */ new WeakMap();
336
+ function chartValueFormatter(chart) {
337
+ const hit = formatterCache.get(chart);
338
+ if (hit !== void 0) return hit ?? void 0;
339
+ const code = chart.numberFormat;
340
+ const made = code === void 0 ? null : ((formats) => (v) => applyNumberFormat(String(v), CHART_FORMAT_ID, formats))(new Map([[CHART_FORMAT_ID, code]]));
341
+ formatterCache.set(chart, made);
342
+ return made ?? void 0;
252
343
  }
253
344
  /**
254
345
  * Lay out a bar/column {@link Chart} into a {@link ChartScene}. Honours
@@ -267,15 +358,18 @@ function buildBarScene(chart, wPt, hPt, measure) {
267
358
  const percent = g === "percentStacked";
268
359
  const horizontal = chart.barDir === "bar";
269
360
  const f = buildFrame(chart, wPt, hPt, measure, horizontal, groupingFrameOpts(chart, catCount(chart)));
361
+ const barIdx = [];
362
+ const lineIdx = [];
363
+ chart.series.forEach((s, i) => (isLineLike(s) ? lineIdx : barIdx).push(i));
270
364
  if (stacked) {
271
365
  const groupPad = f.slot * .15;
272
366
  const barW = f.slot - 2 * groupPad;
273
367
  for (let c = 0; c < f.nCats; c++) {
274
368
  const along = (horizontal ? f.y0 : f.x0) + c * f.slot + groupPad;
275
- const denom = percent ? chart.series.reduce((a, s) => a + Math.abs(s.values[c] ?? 0), 0) || 1 : 1;
369
+ const denom = percent ? barIdx.reduce((a, i) => a + Math.abs(chart.series[i].values[c] ?? 0), 0) || 1 : 1;
276
370
  let cumPos = 0;
277
371
  let cumNeg = 0;
278
- for (let s = 0; s < chart.series.length; s++) {
372
+ for (const s of barIdx) {
279
373
  const series = chart.series[s];
280
374
  const v = (series.values[c] ?? 0) / denom;
281
375
  const base = v >= 0 ? cumPos : cumNeg;
@@ -301,37 +395,41 @@ function buildBarScene(chart, wPt, hPt, measure) {
301
395
  });
302
396
  if (chart.showValues && span > 9) {
303
397
  const raw = series.values[c] ?? 0;
304
- if (horizontal) f.labels.push(centeredLabel(fmtDataLabel(raw), f.x0 + lo + span / 2, along + barW * .3));
305
- else f.labels.push(centeredLabel(fmtDataLabel(raw), along + barW / 2, f.y0 + lo + span / 2 - 3));
398
+ if (horizontal) f.labels.push(centeredLabel(fmtDataLabel(chart, raw), f.x0 + lo + span / 2, along + barW * .3));
399
+ else f.labels.push(centeredLabel(fmtDataLabel(chart, raw), along + barW / 2, f.y0 + lo + span / 2 - 3));
306
400
  }
307
401
  if (v >= 0) cumPos = top;
308
402
  else cumNeg = top;
309
403
  }
310
404
  }
405
+ pushComboLines(chart, f, lineIdx);
311
406
  return {
312
407
  rects: f.rects,
313
408
  polylines: f.polylines,
409
+ gridlines: f.gridlines,
314
410
  wedges: [],
315
411
  labels: f.labels
316
412
  };
317
413
  }
318
- const nSer = Math.max(1, chart.series.length);
319
- const groupPad = f.slot * .15;
320
- const barW = (f.slot - 2 * groupPad) / nSer;
414
+ const nSer = Math.max(1, barIdx.length);
415
+ const gap = (chart.gapPercent ?? 150) / 100;
416
+ const barW = f.slot / (nSer + gap);
417
+ const groupPad = (f.slot - barW * nSer) / 2;
321
418
  for (let c = 0; c < f.nCats; c++) {
322
419
  const slotStart = (horizontal ? f.y0 : f.x0) + c * f.slot + groupPad;
323
- for (let s = 0; s < chart.series.length; s++) {
420
+ for (let b = 0; b < barIdx.length; b++) {
421
+ const s = barIdx[b];
324
422
  const series = chart.series[s];
325
423
  const len = f.valueOffset(series.values[c] ?? 0) - f.zeroOffset;
326
424
  const color = pointColor(series, c) ?? seriesColor(series, s, chart.seriesColorCycle);
327
- const along = slotStart + s * barW;
425
+ const along = slotStart + b * barW;
328
426
  if (horizontal) {
329
427
  const bx = f.x0 + f.zeroOffset + Math.min(0, len);
330
428
  f.rects.push({
331
429
  x: bx,
332
430
  y: along,
333
431
  w: Math.abs(len),
334
- h: barW * .9,
432
+ h: barW,
335
433
  fillHex: color
336
434
  });
337
435
  } else {
@@ -339,13 +437,13 @@ function buildBarScene(chart, wPt, hPt, measure) {
339
437
  f.rects.push({
340
438
  x: along,
341
439
  y: by,
342
- w: barW * .9,
440
+ w: barW,
343
441
  h: Math.abs(len),
344
442
  fillHex: color
345
443
  });
346
444
  }
347
445
  if (chart.showValues) {
348
- const txt = fmtDataLabel(series.values[c] ?? 0);
446
+ const txt = fmtDataLabel(chart, series.values[c] ?? 0);
349
447
  if (horizontal) {
350
448
  const end = f.x0 + f.zeroOffset + len;
351
449
  f.labels.push({
@@ -363,13 +461,61 @@ function buildBarScene(chart, wPt, hPt, measure) {
363
461
  }
364
462
  }
365
463
  }
464
+ pushComboLines(chart, f, lineIdx);
366
465
  return {
367
466
  rects: f.rects,
368
467
  polylines: f.polylines,
468
+ gridlines: f.gridlines,
369
469
  wedges: [],
370
470
  labels: f.labels
371
471
  };
372
472
  }
473
+ /**
474
+ * The name a series shows in the legend: its own `c:tx`, or Excel's positional
475
+ * `SeriesN` when it has none. Exported because the SUBSET has to know it too —
476
+ * a name invented at draw time is a name no glyph collector ever saw, and
477
+ * 57362.xlsx drew its unnamed series as "eries1", the capital S appearing
478
+ * nowhere else on the page and so nowhere in the font.
479
+ *
480
+ * @param series The series.
481
+ * @param index Its zero-based index in the chart.
482
+ * @returns The legend text.
483
+ */
484
+ function legendSeriesName(series, index) {
485
+ return series.name && series.name.length > 0 ? series.name : `Series${index + 1}`;
486
+ }
487
+ /** Whether a series plots as a line rather than as a bar of its own cluster. */
488
+ function isLineLike(series) {
489
+ return series.type === "line" || series.type === "scatter";
490
+ }
491
+ /**
492
+ * Draw a combo's line-group series over an already-built cartesian frame, at the
493
+ * category slot centres the bars use (§21.2.2.145).
494
+ */
495
+ function pushComboLines(chart, f, lineIdx) {
496
+ for (const s of lineIdx) {
497
+ const series = chart.series[s];
498
+ const color = seriesColor(series, s, chart.seriesColorCycle);
499
+ const at = (series.secondaryAxis ? f.valueOffset2 : void 0) ?? f.valueOffset;
500
+ const pts = [];
501
+ for (let c = 0; c < f.nCats; c++) pts.push([f.x0 + c * f.slot + f.slot / 2, f.y0 + at(series.values[c] ?? 0)]);
502
+ if (pts.length >= 2) f.polylines.push({
503
+ points: pts,
504
+ strokeHex: color,
505
+ widthPt: 1.5
506
+ });
507
+ else if (pts.length === 1) {
508
+ const [px, py] = pts[0];
509
+ f.rects.push({
510
+ x: px - 1.5,
511
+ y: py - 1.5,
512
+ w: 3,
513
+ h: 3,
514
+ fillHex: color
515
+ });
516
+ }
517
+ }
518
+ }
373
519
  function centeredLabel(text, x, y) {
374
520
  return {
375
521
  text,
@@ -433,6 +579,7 @@ function buildAreaScene(chart, wPt, hPt, measure) {
433
579
  return {
434
580
  rects: f.rects,
435
581
  polylines: f.polylines,
582
+ gridlines: f.gridlines,
436
583
  wedges: [],
437
584
  labels: f.labels,
438
585
  polygons
@@ -452,6 +599,7 @@ function buildAreaScene(chart, wPt, hPt, measure) {
452
599
  function buildScatterScene(chart, wPt, hPt, measure) {
453
600
  const rects = [];
454
601
  const polylines = [];
602
+ const gridlines = [];
455
603
  const labels = [];
456
604
  const xs = [];
457
605
  const ys = [];
@@ -462,11 +610,12 @@ function buildScatterScene(chart, wPt, hPt, measure) {
462
610
  if (xs.length === 0) return {
463
611
  rects,
464
612
  polylines,
613
+ gridlines,
465
614
  wedges: [],
466
615
  labels
467
616
  };
468
- const xScale = niceScale(Math.min(...xs), Math.max(...xs));
469
- const yScale = niceScale(Math.min(...ys), Math.max(...ys));
617
+ const xScale = niceScale(...autoRange(xs), tickBudget(wPt / 2));
618
+ const yScale = niceScale(...autoRange(ys), tickBudget(hPt));
470
619
  const xTicks = ticks(xScale);
471
620
  const yTicks = ticks(yScale);
472
621
  let top = 4;
@@ -479,33 +628,88 @@ function buildScatterScene(chart, wPt, hPt, measure) {
479
628
  const xAt = (v) => x0 + (v - xScale.min) / (xScale.max - xScale.min) * plotW;
480
629
  const yAt = (v) => y0 + (v - yScale.min) / (yScale.max - yScale.min) * plotH;
481
630
  pushChartTitle(labels, chart, wPt, hPt);
482
- pushGridTicks(polylines, labels, yTicks, (v) => formatTick(v, yScale.step), "y", yAt, x0, y0, plotW, plotH);
483
- pushGridTicks(polylines, labels, xTicks, (v) => formatTick(v, xScale.step), "x", xAt, x0, y0, plotW, plotH);
484
- pushAxisLines(polylines, x0, y0, plotW, plotH);
631
+ pushGridTicks(gridlines, labels, yTicks, (v) => formatTick(v, yScale.step), "y", yAt, x0, y0, plotW, plotH, chart.gridLine);
632
+ pushGridTicks(gridlines, labels, xTicks, (v) => formatTick(v, xScale.step), "x", xAt, x0, y0, plotW, plotH, chart.gridLine);
633
+ pushAxisLines(polylines, x0, y0, plotW, plotH, chart);
634
+ const style = chart.scatterStyle ?? "marker";
635
+ const joins = style === "line" || style === "lineMarker" || style.startsWith("smooth");
636
+ const marks = style === "marker" || style === "lineMarker" || style === "smoothMarker";
637
+ const wedges = [];
485
638
  for (let s = 0; s < chart.series.length; s++) {
486
639
  const series = chart.series[s];
487
640
  const color = seriesColor(series, s, chart.seriesColorCycle);
641
+ const pts = [];
488
642
  for (let i = 0; i < series.values.length; i++) {
489
643
  const px = xAt(series.xValues?.[i] ?? i);
490
644
  const py = yAt(series.values[i] ?? 0);
491
- rects.push({
492
- x: px - 2,
493
- y: py - 2,
494
- w: 4,
495
- h: 4,
496
- fillHex: color
497
- });
645
+ pts.push([px, py]);
646
+ if (marks) pushMarker(rects, wedges, series.marker, px, py, color);
498
647
  }
648
+ if (joins && !series.line?.none && pts.length >= 2) polylines.push({
649
+ points: pts,
650
+ strokeHex: color,
651
+ widthPt: series.line?.widthPt ?? 1.5
652
+ });
499
653
  }
500
654
  legend.emit(rects, labels);
501
655
  return {
502
656
  rects,
503
657
  polylines,
504
- wedges: [],
658
+ gridlines,
659
+ wedges,
505
660
  labels
506
661
  };
507
662
  }
508
663
  /**
664
+ * The data range an automatic axis covers: its own extent, except that data
665
+ * starting within 5/6 of the top reads as data that belongs against a zero
666
+ * baseline — Excel's own rule for when an automatic minimum stays at 0.
667
+ */
668
+ function autoRange(vals) {
669
+ const lo = Math.min(...vals);
670
+ const hi = Math.max(...vals);
671
+ return [lo > 0 && lo < 5 / 6 * hi ? 0 : lo, hi];
672
+ }
673
+ /** Ticks that fit along an axis `extentPt` long (mirrors {@link axisScale}). */
674
+ var tickBudget = (extentPt) => Math.min(10, Math.max(4, Math.round(extentPt / 24)));
675
+ /** Side (points) of the square stamped for a series that names no symbol. */
676
+ var DEFAULT_MARKER_PT = 4;
677
+ /**
678
+ * Stamp one data point's marker. §21.2.2.107: `none` draws nothing at all even
679
+ * when the scatter style marks its points, a round symbol is a disc (a full
680
+ * wedge — drawn last, so it sits over the line joining the points), and every
681
+ * other symbol keeps the square we have always drawn.
682
+ *
683
+ * @param rects Collects a square marker.
684
+ * @param wedges Collects a round marker.
685
+ * @param marker The series' `c:marker`, if it declared one.
686
+ * @param x Point centre, scene x.
687
+ * @param y Point centre, scene y.
688
+ * @param color The series colour.
689
+ */
690
+ function pushMarker(rects, wedges, marker, x, y, color) {
691
+ if (marker?.symbol === "none") return;
692
+ const size = marker?.sizePt ?? DEFAULT_MARKER_PT;
693
+ if (marker?.symbol === "circle" || marker?.symbol === "dot") {
694
+ wedges.push({
695
+ cx: x,
696
+ cy: y,
697
+ r: size / 2,
698
+ startRad: 0,
699
+ sweepRad: -2 * Math.PI,
700
+ fillHex: color
701
+ });
702
+ return;
703
+ }
704
+ rects.push({
705
+ x: x - size / 2,
706
+ y: y - size / 2,
707
+ w: size,
708
+ h: size,
709
+ fillHex: color
710
+ });
711
+ }
712
+ /**
509
713
  * Lay out a line {@link Chart} into a {@link ChartScene}: each series becomes a
510
714
  * stroked polyline across the category slots, over the shared cartesian frame.
511
715
  * Unlike bars/areas the value axis auto-mins (it need not include 0).
@@ -518,7 +722,13 @@ function buildScatterScene(chart, wPt, hPt, measure) {
518
722
  */
519
723
  function buildLineScene(chart, wPt, hPt, measure) {
520
724
  const allVals = chart.series.flatMap((s) => s.values);
521
- const f = buildFrame(chart, wPt, hPt, measure, false, { dataRange: allVals.length > 0 ? [Math.min(...allVals), Math.max(...allVals)] : [0, 1] });
725
+ const range = allVals.length > 0 ? [Math.min(...allVals), Math.max(...allVals)] : [0, 1];
726
+ const lineFormat = chartValueFormatter(chart);
727
+ const f = buildFrame(chart, wPt, hPt, measure, false, {
728
+ dataRange: range,
729
+ ...lineFormat ? { formatValue: lineFormat } : {}
730
+ });
731
+ const wedges = [];
522
732
  for (let s = 0; s < chart.series.length; s++) {
523
733
  const series = chart.series[s];
524
734
  const color = seriesColor(series, s, chart.seriesColorCycle);
@@ -527,7 +737,8 @@ function buildLineScene(chart, wPt, hPt, measure) {
527
737
  const x = f.x0 + c * f.slot + f.slot / 2;
528
738
  const y = f.y0 + f.valueOffset(series.values[c] ?? 0);
529
739
  pts.push([x, y]);
530
- if (chart.showValues) f.labels.push(centeredLabel(fmtDataLabel(series.values[c] ?? 0), x, y + 3));
740
+ if (chart.lineMarkers) pushMarker(f.rects, wedges, series.marker, x, y, color);
741
+ if (chart.showValues) f.labels.push(centeredLabel(fmtDataLabel(chart, series.values[c] ?? 0), x, y + 3));
531
742
  }
532
743
  if (pts.length >= 2) f.polylines.push({
533
744
  points: pts,
@@ -548,11 +759,15 @@ function buildLineScene(chart, wPt, hPt, measure) {
548
759
  return {
549
760
  rects: f.rects,
550
761
  polylines: f.polylines,
551
- wedges: [],
762
+ gridlines: f.gridlines,
763
+ wedges,
552
764
  labels: f.labels
553
765
  };
554
766
  }
555
- var sliceColor = (series, i) => pointColor(series, i) ?? SERIES_COLORS[i % SERIES_COLORS.length];
767
+ var sliceColor = (series, i, cycle) => {
768
+ const palette = cycle && cycle.length > 0 ? cycle : SERIES_COLORS;
769
+ return pointColor(series, i) ?? palette[i % palette.length];
770
+ };
556
771
  /**
557
772
  * Lay out a pie/doughnut {@link Chart} into a {@link ChartScene}: the first
558
773
  * series' values become proportional wedges (a centre hole for doughnut),
@@ -579,9 +794,9 @@ function buildPieScene(chart, wPt, hPt, measure) {
579
794
  };
580
795
  let top = 4;
581
796
  if (chart.title) top += 13 * 1.6;
582
- const legend = layoutLegend(chart.categories.map((c, i) => ({
583
- name: c,
584
- colorHex: sliceColor(series, i)
797
+ const legend = layoutLegend(values.map((_, i) => ({
798
+ name: chart.categories[i] ?? String(i + 1),
799
+ colorHex: sliceColor(series, i, chart.seriesColorCycle)
585
800
  })), chart.hasLegend, chart.legendPos ?? "r", wPt, hPt, measure);
586
801
  const availW = Math.max(1, wPt - 8 - legend.rightWidth);
587
802
  const availH = Math.max(1, hPt - top - 4 - legend.bottomHeight);
@@ -601,17 +816,18 @@ function buildPieScene(chart, wPt, hPt, measure) {
601
816
  r,
602
817
  startRad: ang,
603
818
  sweepRad: sweep,
604
- fillHex: sliceColor(series, i),
819
+ fillHex: sliceColor(series, i, chart.seriesColorCycle),
605
820
  strokeHex: "FFFFFF"
606
821
  });
607
822
  const mid = ang + sweep / 2;
608
823
  const pct = Math.round(v / total * 100);
609
- if (pct >= 5) labels.push({
610
- text: `${pct}%`,
824
+ const custom = series.pointLabels?.find((l) => l.idx === i)?.text;
825
+ if (custom !== void 0 || pct >= 5) labels.push({
826
+ text: custom ?? `${pct}%`,
611
827
  x: cx + Math.cos(mid) * labelR,
612
828
  y: cy + Math.sin(mid) * labelR - 9 / 3,
613
829
  sizePt: 9,
614
- colorHex: chart.doughnut ? LABEL_COLOR : "FFFFFF",
830
+ colorHex: custom !== void 0 || chart.doughnut ? LABEL_COLOR : "FFFFFF",
615
831
  align: "center"
616
832
  });
617
833
  ang += sweep;
@@ -643,7 +859,7 @@ function buildPieScene(chart, wPt, hPt, measure) {
643
859
  function pointColor(series, idx) {
644
860
  return series.pointColors?.find((p) => p.idx === idx)?.colorHex;
645
861
  }
646
- function layoutLegend(entries, hasLegend, pos, wPt, hPt, measure) {
862
+ function layoutLegend(entries, hasLegend, pos, wPt, hPt, measure, marker = "box") {
647
863
  if (!hasLegend || entries.length === 0) return {
648
864
  rightWidth: 0,
649
865
  bottomHeight: 0,
@@ -651,7 +867,22 @@ function layoutLegend(entries, hasLegend, pos, wPt, hPt, measure) {
651
867
  };
652
868
  const sw = 9;
653
869
  const gap = 4;
654
- const entryW = (e) => sw + 3 + measure(e.name, 9) + gap * 2;
870
+ const keyW = sw * 2.2;
871
+ const keyH = sw * .55;
872
+ const key = (x, y, e) => (e.marker ?? marker) === "line" ? {
873
+ x,
874
+ y: y + sw / 2 - .75,
875
+ w: keyW,
876
+ h: 1.5,
877
+ fillHex: e.colorHex
878
+ } : {
879
+ x,
880
+ y: y + (sw - keyH) / 2,
881
+ w: keyW,
882
+ h: keyH,
883
+ fillHex: e.colorHex
884
+ };
885
+ const entryW = (e) => keyW + 3 + measure(e.name, 9) + gap * 2;
655
886
  if (pos === "r" || pos === "l") {
656
887
  const colW = Math.max(...entries.map(entryW));
657
888
  return {
@@ -661,16 +892,10 @@ function layoutLegend(entries, hasLegend, pos, wPt, hPt, measure) {
661
892
  const lx = pos === "r" ? wPt - colW + gap : gap;
662
893
  let ly = hPt / 2 + entries.length * (sw + 4) / 2 - sw;
663
894
  for (const e of entries) {
664
- rects.push({
665
- x: lx,
666
- y: ly,
667
- w: sw,
668
- h: sw,
669
- fillHex: e.colorHex
670
- });
895
+ rects.push(key(lx, ly, e));
671
896
  labels.push({
672
897
  text: e.name,
673
- x: lx + sw + 3,
898
+ x: lx + keyW + 3,
674
899
  y: ly + 1,
675
900
  sizePt: 9,
676
901
  colorHex: LABEL_COLOR,
@@ -689,16 +914,10 @@ function layoutLegend(entries, hasLegend, pos, wPt, hPt, measure) {
689
914
  let lx = (wPt - totalW) / 2 + gap;
690
915
  const ly = 2;
691
916
  for (const e of entries) {
692
- rects.push({
693
- x: lx,
694
- y: ly,
695
- w: sw,
696
- h: sw,
697
- fillHex: e.colorHex
698
- });
917
+ rects.push(key(lx, ly, e));
699
918
  labels.push({
700
919
  text: e.name,
701
- x: lx + sw + 3,
920
+ x: lx + keyW + 3,
702
921
  y: ly + 1,
703
922
  sizePt: 9,
704
923
  colorHex: LABEL_COLOR,
@@ -721,6 +940,41 @@ function layoutLegend(entries, hasLegend, pos, wPt, hPt, measure) {
721
940
  * renderer then reserves the box with a light border).
722
941
  */
723
942
  function buildChartScene(chart, wPt, hPt, measure) {
943
+ const scene = buildTypedScene(chart.catAxisReversed ? withReversedCategories(chart) : chart, wPt, hPt, measure);
944
+ return scene && withFrame(scene, chart, wPt, hPt);
945
+ }
946
+ /**
947
+ * §21.2.2.134 `maxMin` on the category axis — the categories run the other way,
948
+ * which for a horizontal bar chart puts the FIRST one at the top (how every
949
+ * ranked list reads). Reversing the plotted order says exactly that and leaves
950
+ * every builder's geometry alone; the per-point overrides move with their
951
+ * points.
952
+ *
953
+ * @param chart The chart as the file declares it.
954
+ * @returns The same chart with its categories, values and per-point overrides
955
+ * in reverse.
956
+ */
957
+ function withReversedCategories(chart) {
958
+ const n = Math.max(chart.categories.length, ...chart.series.map((s) => s.values.length), 1);
959
+ const flip = (i) => n - 1 - i;
960
+ return {
961
+ ...chart,
962
+ categories: chart.categories.length > 0 ? Array.from({ length: n }, (_, i) => chart.categories[flip(i)] ?? "") : chart.categories,
963
+ series: chart.series.map((s) => ({
964
+ ...s,
965
+ values: Array.from({ length: n }, (_, i) => s.values[flip(i)] ?? 0),
966
+ ...s.pointColors ? { pointColors: s.pointColors.map((p) => ({
967
+ ...p,
968
+ idx: flip(p.idx)
969
+ })) } : {},
970
+ ...s.pointLabels ? { pointLabels: s.pointLabels.map((p) => ({
971
+ ...p,
972
+ idx: flip(p.idx)
973
+ })) } : {}
974
+ }))
975
+ };
976
+ }
977
+ function buildTypedScene(chart, wPt, hPt, measure) {
724
978
  if (chart.type === "bar") return buildBarScene(chart, wPt, hPt, measure);
725
979
  if (chart.type === "line") return buildLineScene(chart, wPt, hPt, measure);
726
980
  if (chart.type === "pie") return buildPieScene(chart, wPt, hPt, measure);
@@ -728,5 +982,34 @@ function buildChartScene(chart, wPt, hPt, measure) {
728
982
  if (chart.type === "scatter") return buildScatterScene(chart, wPt, hPt, measure);
729
983
  return null;
730
984
  }
985
+ /**
986
+ * §21.2.2.198 — the chart-space frame, first in the scene so everything else
987
+ * draws over it. A chart that declares neither fill nor outline is returned
988
+ * untouched, so a scene without one is unchanged rect for rect.
989
+ *
990
+ * @param scene The typed scene.
991
+ * @param chart The chart (for its frame fill/outline).
992
+ * @param wPt The chart's width in points.
993
+ * @param hPt Its height.
994
+ * @returns The scene with the frame behind it.
995
+ */
996
+ function withFrame(scene, chart, wPt, hPt) {
997
+ if (!chart.frameFillHex && !chart.frameLineHex) return scene;
998
+ const frame = {
999
+ x: 0,
1000
+ y: 0,
1001
+ w: wPt,
1002
+ h: hPt,
1003
+ ...chart.frameFillHex ? { fillHex: chart.frameFillHex } : {},
1004
+ ...chart.frameLineHex ? {
1005
+ strokeHex: chart.frameLineHex,
1006
+ strokeWidthPt: .75
1007
+ } : {}
1008
+ };
1009
+ return {
1010
+ ...scene,
1011
+ background: frame
1012
+ };
1013
+ }
731
1014
  //#endregion
732
- export { buildChartScene };
1015
+ export { buildChartScene, legendSeriesName };