reamkit 1.15.4 → 1.17.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 (127) hide show
  1. package/dist/esm/core/bidi/segments.js +1 -2
  2. package/dist/esm/core/converter/facade.d.ts +5 -0
  3. package/dist/esm/core/converter/project.js +5 -1
  4. package/dist/esm/core/converter/ream.d.ts +6 -0
  5. package/dist/esm/core/converter/ream.js +34 -8
  6. package/dist/esm/core/document-model/index.d.ts +1 -1
  7. package/dist/esm/core/document-model/types.d.ts +659 -7
  8. package/dist/esm/core/drawingml/chart-geometry.d.ts +34 -10
  9. package/dist/esm/core/drawingml/chart-geometry.js +424 -92
  10. package/dist/esm/core/drawingml/chart-parser.js +301 -11
  11. package/dist/esm/core/drawingml/colors.d.ts +7 -7
  12. package/dist/esm/core/drawingml/colors.js +84 -18
  13. package/dist/esm/core/drawingml/preset-geometry.js +240 -6
  14. package/dist/esm/core/drawingml/shape-render.d.ts +12 -0
  15. package/dist/esm/core/drawingml/shape-render.js +116 -3
  16. package/dist/esm/core/drawingml/sparkline-geometry.js +6 -4
  17. package/dist/esm/core/drawingml/theme-parser.d.ts +51 -0
  18. package/dist/esm/core/drawingml/theme-parser.js +92 -1
  19. package/dist/esm/core/font/measure.d.ts +5 -0
  20. package/dist/esm/core/font/measure.js +21 -1
  21. package/dist/esm/core/font/ttf-subset.js +26 -1
  22. package/dist/esm/core/images.d.ts +2 -2
  23. package/dist/esm/core/images.js +353 -43
  24. package/dist/esm/core/indexed-colors.d.ts +11 -0
  25. package/dist/esm/core/indexed-colors.js +81 -0
  26. package/dist/esm/core/ir/flow.d.ts +23 -1
  27. package/dist/esm/core/ir/sheet.d.ts +61 -0
  28. package/dist/esm/{excel → core}/number-format.d.ts +37 -0
  29. package/dist/esm/core/number-format.js +800 -0
  30. package/dist/esm/core/numbering/apply.d.ts +5 -3
  31. package/dist/esm/core/numbering/apply.js +42 -11
  32. package/dist/esm/core/numbering/index.js +2 -0
  33. package/dist/esm/core/numbering/state.d.ts +13 -1
  34. package/dist/esm/core/numbering/state.js +256 -15
  35. package/dist/esm/core/opc/alternate-content.d.ts +13 -0
  36. package/dist/esm/core/opc/alternate-content.js +72 -0
  37. package/dist/esm/core/opc/core-properties.js +1 -0
  38. package/dist/esm/core/opc/package.d.ts +14 -1
  39. package/dist/esm/core/opc/package.js +43 -4
  40. package/dist/esm/core/opc/relationships.js +1 -0
  41. package/dist/esm/core/opc/xml-entities.d.ts +12 -0
  42. package/dist/esm/core/opc/xml-entities.js +91 -0
  43. package/dist/esm/core/po-helpers.js +16 -1
  44. package/dist/esm/core/spreadsheet-model/index.d.ts +1 -1
  45. package/dist/esm/core/spreadsheet-model/types.d.ts +137 -0
  46. package/dist/esm/core/style-cascade/resolver.js +66 -11
  47. package/dist/esm/core/style-cascade/table.js +35 -11
  48. package/dist/esm/core/style-cascade/types.d.ts +42 -1
  49. package/dist/esm/core/style-cascade/types.js +4 -0
  50. package/dist/esm/core/vector.d.ts +30 -0
  51. package/dist/esm/excel/activex-parser.js +1 -0
  52. package/dist/esm/excel/column-bands.d.ts +26 -1
  53. package/dist/esm/excel/column-bands.js +156 -20
  54. package/dist/esm/excel/comments-parser.js +5 -3
  55. package/dist/esm/excel/conditional-format.d.ts +25 -2
  56. package/dist/esm/excel/conditional-format.js +63 -19
  57. package/dist/esm/excel/escaped-text.d.ts +9 -0
  58. package/dist/esm/excel/escaped-text.js +20 -0
  59. package/dist/esm/excel/form-control-parser.js +1 -0
  60. package/dist/esm/excel/formula/dates.js +1 -1
  61. package/dist/esm/excel/header-footer.d.ts +9 -1
  62. package/dist/esm/excel/header-footer.js +162 -14
  63. package/dist/esm/excel/index.d.ts +1 -1
  64. package/dist/esm/excel/index.js +2 -2
  65. package/dist/esm/excel/pivot-table-parser.js +1 -0
  66. package/dist/esm/excel/print-model.d.ts +75 -0
  67. package/dist/esm/excel/print-model.js +899 -77
  68. package/dist/esm/excel/printer-settings.d.ts +16 -0
  69. package/dist/esm/excel/printer-settings.js +36 -0
  70. package/dist/esm/excel/rich-value.d.ts +13 -0
  71. package/dist/esm/excel/rich-value.js +120 -0
  72. package/dist/esm/excel/shared-strings-parser.js +11 -7
  73. package/dist/esm/excel/sheet-drawing.d.ts +11 -0
  74. package/dist/esm/excel/sheet-drawing.js +26 -3
  75. package/dist/esm/excel/sheet-shape-parser.d.ts +53 -1
  76. package/dist/esm/excel/sheet-shape-parser.js +461 -32
  77. package/dist/esm/excel/sheet-to-flow.d.ts +23 -2
  78. package/dist/esm/excel/sheet-to-flow.js +553 -36
  79. package/dist/esm/excel/slicer-parser.js +1 -0
  80. package/dist/esm/excel/styles-parser.d.ts +30 -2
  81. package/dist/esm/excel/styles-parser.js +177 -40
  82. package/dist/esm/excel/table-parser.js +1 -0
  83. package/dist/esm/excel/vml-drawing.d.ts +68 -0
  84. package/dist/esm/excel/vml-drawing.js +158 -0
  85. package/dist/esm/excel/workbook-parser.d.ts +5 -0
  86. package/dist/esm/excel/workbook-parser.js +5 -1
  87. package/dist/esm/excel/worksheet-parser.d.ts +2 -1
  88. package/dist/esm/excel/worksheet-parser.js +150 -61
  89. package/dist/esm/excel/xlsx-reader.js +242 -11
  90. package/dist/esm/excel/xlsx-to-pdf.js +12 -1
  91. package/dist/esm/html/html-writer.js +29 -6
  92. package/dist/esm/layout/page-doc.d.ts +87 -1
  93. package/dist/esm/layout/styled-layout.d.ts +46 -2
  94. package/dist/esm/layout/styled-layout.js +2345 -274
  95. package/dist/esm/pdf/cid-font.js +20 -9
  96. package/dist/esm/pdf/objects.d.ts +13 -0
  97. package/dist/esm/pdf/objects.js +18 -1
  98. package/dist/esm/pdf/shading.js +76 -6
  99. package/dist/esm/pdf/styled-page-emitter.js +355 -49
  100. package/dist/esm/pdf/text-page-renderer.js +1 -0
  101. package/dist/esm/pdf/vector-graphics.d.ts +31 -2
  102. package/dist/esm/pdf/vector-graphics.js +103 -3
  103. package/dist/esm/pptx/pptx-reader.js +2 -0
  104. package/dist/esm/pptx/slide-parser.d.ts +21 -4
  105. package/dist/esm/pptx/slide-parser.js +98 -24
  106. package/dist/esm/svg/svg-writer.js +23 -4
  107. package/dist/esm/word/document-parser.d.ts +96 -7
  108. package/dist/esm/word/document-parser.js +538 -116
  109. package/dist/esm/word/docx-reader.js +135 -43
  110. package/dist/esm/word/docx-writer.js +6 -1
  111. package/dist/esm/word/drawing-parser.d.ts +86 -5
  112. package/dist/esm/word/drawing-parser.js +1590 -54
  113. package/dist/esm/word/index.d.ts +4 -4
  114. package/dist/esm/word/numbering-parser.d.ts +2 -1
  115. package/dist/esm/word/numbering-parser.js +108 -21
  116. package/dist/esm/word/paragraph-properties.d.ts +11 -1
  117. package/dist/esm/word/paragraph-properties.js +219 -10
  118. package/dist/esm/word/run-properties.js +45 -1
  119. package/dist/esm/word/settings-parser.d.ts +24 -0
  120. package/dist/esm/word/settings-parser.js +32 -5
  121. package/dist/esm/word/shading.d.ts +11 -0
  122. package/dist/esm/word/shading.js +64 -0
  123. package/dist/esm/word/styles-parser.js +9 -2
  124. package/dist/esm/word/table-parser.d.ts +9 -1
  125. package/dist/esm/word/table-parser.js +182 -30
  126. package/package.json +7 -3
  127. 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,50 +146,83 @@ 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), tickBudget(hPt)) : 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);
157
203
  const valueOffset = (v) => (v - scale.min) / (scale.max - scale.min) * (horizontal ? plotW : plotH);
158
204
  const zeroOffset = valueOffset(0);
205
+ const plotBackground = chart.plotFillHex ?? chart.plotLine?.colorHex ? {
206
+ x: x0,
207
+ y: y0,
208
+ w: plotW,
209
+ h: plotH,
210
+ ...chart.plotFillHex ? { fillHex: chart.plotFillHex } : {},
211
+ ...chart.plotLine?.colorHex ? {
212
+ strokeHex: chart.plotLine.colorHex,
213
+ strokeWidthPt: chart.plotLine.widthPt ?? .75,
214
+ ...chart.plotLine.dash ? { strokeDash: chart.plotLine.dash } : {}
215
+ } : {}
216
+ } : void 0;
159
217
  pushChartTitle(labels, chart, wPt, hPt);
160
218
  if (chart.valAxisTitle) labels.push({
161
219
  text: chart.valAxisTitle,
162
- x: x0,
163
- y: hPt - top + 3,
220
+ x: 12.1,
221
+ y: y0 + plotH / 2,
164
222
  sizePt: 9,
165
223
  colorHex: LABEL_COLOR,
166
- align: "left"
224
+ align: "center",
225
+ rotationDeg: 90
167
226
  });
168
227
  if (chart.catAxisTitle) labels.push({
169
228
  text: chart.catAxisTitle,
@@ -173,11 +232,39 @@ function buildFrame(chart, wPt, hPt, measure, horizontal, opts = {}) {
173
232
  colorHex: LABEL_COLOR,
174
233
  align: "center"
175
234
  });
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);
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;
238
+ if (scale2 && valueOffset2) {
239
+ for (const v of tickVals2) labels.push({
240
+ text: formatTick(v, scale2.step),
241
+ x: x0 + plotW + 3,
242
+ y: y0 + valueOffset2(v) - 9 / 3,
243
+ sizePt: 9,
244
+ colorHex: LABEL_COLOR,
245
+ align: "left"
246
+ });
247
+ const sec = axisStroke(chart.secondaryValAxisLine);
248
+ if (sec) polylines.push({
249
+ points: [[x0 + plotW, y0], [x0 + plotW, y0 + plotH]],
250
+ strokeHex: sec.hex,
251
+ widthPt: sec.widthPt
252
+ });
253
+ if (chart.secondaryValAxisTitle) labels.push({
254
+ text: chart.secondaryValAxisTitle,
255
+ x: wPt - 4,
256
+ y: y0 + plotH / 2,
257
+ sizePt: 9,
258
+ colorHex: LABEL_COLOR,
259
+ align: "center",
260
+ rotationDeg: 90
261
+ });
262
+ }
178
263
  const slot = (horizontal ? plotH : plotW) / nCats;
179
- for (let c = 0; c < nCats; c++) {
180
- const cat = chart.categories[c] ?? "";
264
+ const need = horizontal ? 9 * 1.4 : Math.max(0, ...chart.categories.map((t) => measure(t, 9))) + 4;
265
+ 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);
181
268
  if (!cat) continue;
182
269
  const center = (horizontal ? y0 : x0) + c * slot + slot / 2;
183
270
  if (horizontal) labels.push({
@@ -197,7 +284,7 @@ function buildFrame(chart, wPt, hPt, measure, horizontal, opts = {}) {
197
284
  align: "center"
198
285
  });
199
286
  }
200
- pushAxisLines(polylines, x0, y0, plotW, plotH);
287
+ pushAxisLines(polylines, x0, y0, plotW, plotH, chart);
201
288
  legend.emit(rects, labels);
202
289
  return {
203
290
  x0,
@@ -209,13 +296,16 @@ function buildFrame(chart, wPt, hPt, measure, horizontal, opts = {}) {
209
296
  horizontal,
210
297
  zeroOffset,
211
298
  valueOffset,
299
+ ...valueOffset2 ? { valueOffset2 } : {},
212
300
  rects,
213
301
  polylines,
302
+ gridlines,
303
+ ...plotBackground ? { plotBackground } : {},
214
304
  labels
215
305
  };
216
306
  }
217
307
  var pctLabel = (v) => `${Math.round(v * 100)}%`;
218
- var fmtDataLabel = (v) => Number.isInteger(v) ? String(v) : String(Math.round(v * 100) / 100);
308
+ var fmtDataLabel = (chart, v) => chartValueFormatter(chart)?.(v) ?? (Number.isInteger(v) ? String(v) : String(Math.round(v * 100) / 100));
219
309
  function catCount(chart) {
220
310
  return Math.max(chart.categories.length, ...chart.series.map((s) => s.values.length), 1);
221
311
  }
@@ -244,11 +334,25 @@ function groupingFrameOpts(chart, nCats) {
244
334
  dataRange: [0, 1],
245
335
  formatValue: pctLabel
246
336
  };
337
+ const format = chartValueFormatter(chart);
247
338
  if (g === "stacked") {
248
339
  const t = stackedTotals(chart, nCats);
249
- return { dataRange: [Math.min(0, t.min), Math.max(0, t.max)] };
340
+ return {
341
+ dataRange: [Math.min(0, t.min), Math.max(0, t.max)],
342
+ ...format ? { formatValue: format } : {}
343
+ };
250
344
  }
251
- return {};
345
+ return format ? { formatValue: format } : {};
346
+ }
347
+ var CHART_FORMAT_ID = 1e6;
348
+ var formatterCache = /* @__PURE__ */ new WeakMap();
349
+ function chartValueFormatter(chart) {
350
+ const hit = formatterCache.get(chart);
351
+ if (hit !== void 0) return hit ?? void 0;
352
+ const code = chart.numberFormat;
353
+ const made = code === void 0 ? null : ((formats) => (v) => applyNumberFormat(String(v), CHART_FORMAT_ID, formats))(new Map([[CHART_FORMAT_ID, code]]));
354
+ formatterCache.set(chart, made);
355
+ return made ?? void 0;
252
356
  }
253
357
  /**
254
358
  * Lay out a bar/column {@link Chart} into a {@link ChartScene}. Honours
@@ -261,21 +365,42 @@ function groupingFrameOpts(chart, nCats) {
261
365
  * @param measure Text measurer used to size labels and reserve axis gutters.
262
366
  * @returns The positioned scene primitives.
263
367
  */
368
+ /**
369
+ * §21.2.2.198 `c:ser/c:spPr/a:ln` — a bar's own outline, as the stroke fields a
370
+ * {@link ChartRect} carries. Chart_BorderLine_Style.docx outlines each of its
371
+ * series in a colour and dash of its own, and drawing bars unstroked lost all
372
+ * of it.
373
+ *
374
+ * @param series The series the bar belongs to.
375
+ * @returns The stroke fields, or an empty object when the series has no rule.
376
+ */
377
+ function barOutline(series) {
378
+ const ln = series.line;
379
+ if (!ln || ln.none === true || !ln.colorHex) return {};
380
+ return {
381
+ strokeHex: ln.colorHex,
382
+ strokeWidthPt: ln.widthPt ?? .75,
383
+ ...ln.dash && ln.dash !== "solid" ? { strokeDash: ln.dash } : {}
384
+ };
385
+ }
264
386
  function buildBarScene(chart, wPt, hPt, measure) {
265
387
  const g = chart.grouping ?? "clustered";
266
388
  const stacked = g === "stacked" || g === "percentStacked";
267
389
  const percent = g === "percentStacked";
268
390
  const horizontal = chart.barDir === "bar";
269
391
  const f = buildFrame(chart, wPt, hPt, measure, horizontal, groupingFrameOpts(chart, catCount(chart)));
392
+ const barIdx = [];
393
+ const lineIdx = [];
394
+ chart.series.forEach((s, i) => (isLineLike(s) ? lineIdx : barIdx).push(i));
270
395
  if (stacked) {
271
396
  const groupPad = f.slot * .15;
272
397
  const barW = f.slot - 2 * groupPad;
273
398
  for (let c = 0; c < f.nCats; c++) {
274
399
  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;
400
+ const denom = percent ? barIdx.reduce((a, i) => a + Math.abs(chart.series[i].values[c] ?? 0), 0) || 1 : 1;
276
401
  let cumPos = 0;
277
402
  let cumNeg = 0;
278
- for (let s = 0; s < chart.series.length; s++) {
403
+ for (const s of barIdx) {
279
404
  const series = chart.series[s];
280
405
  const v = (series.values[c] ?? 0) / denom;
281
406
  const base = v >= 0 ? cumPos : cumNeg;
@@ -285,67 +410,79 @@ function buildBarScene(chart, wPt, hPt, measure) {
285
410
  const lo = Math.min(o0, o1);
286
411
  const span = Math.abs(o1 - o0);
287
412
  const color = pointColor(series, c) ?? seriesColor(series, s, chart.seriesColorCycle);
413
+ const outline = barOutline(series);
288
414
  if (horizontal) f.rects.push({
289
415
  x: f.x0 + lo,
290
416
  y: along,
291
417
  w: span,
292
418
  h: barW,
293
- fillHex: color
419
+ fillHex: color,
420
+ ...outline
294
421
  });
295
422
  else f.rects.push({
296
423
  x: along,
297
424
  y: f.y0 + lo,
298
425
  w: barW,
299
426
  h: span,
300
- fillHex: color
427
+ fillHex: color,
428
+ ...outline
301
429
  });
302
430
  if (chart.showValues && span > 9) {
303
431
  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));
432
+ if (horizontal) f.labels.push(centeredLabel(fmtDataLabel(chart, raw), f.x0 + lo + span / 2, along + barW * .3));
433
+ else f.labels.push(centeredLabel(fmtDataLabel(chart, raw), along + barW / 2, f.y0 + lo + span / 2 - 3));
306
434
  }
307
435
  if (v >= 0) cumPos = top;
308
436
  else cumNeg = top;
309
437
  }
310
438
  }
439
+ pushComboLines(chart, f, lineIdx);
311
440
  return {
312
441
  rects: f.rects,
313
442
  polylines: f.polylines,
443
+ gridlines: f.gridlines,
444
+ ...f.plotBackground ? { plotBackground: f.plotBackground } : {},
445
+ ...f.plotBackground ? { plotBackground: f.plotBackground } : {},
314
446
  wedges: [],
315
447
  labels: f.labels
316
448
  };
317
449
  }
318
- const nSer = Math.max(1, chart.series.length);
319
- const groupPad = f.slot * .15;
320
- const barW = (f.slot - 2 * groupPad) / nSer;
450
+ const nSer = Math.max(1, barIdx.length);
451
+ const gap = (chart.gapPercent ?? 150) / 100;
452
+ const barW = f.slot / (nSer + gap);
453
+ const groupPad = (f.slot - barW * nSer) / 2;
321
454
  for (let c = 0; c < f.nCats; c++) {
322
455
  const slotStart = (horizontal ? f.y0 : f.x0) + c * f.slot + groupPad;
323
- for (let s = 0; s < chart.series.length; s++) {
456
+ for (let b = 0; b < barIdx.length; b++) {
457
+ const s = barIdx[b];
324
458
  const series = chart.series[s];
325
459
  const len = f.valueOffset(series.values[c] ?? 0) - f.zeroOffset;
326
460
  const color = pointColor(series, c) ?? seriesColor(series, s, chart.seriesColorCycle);
327
- const along = slotStart + s * barW;
461
+ const along = slotStart + b * barW;
462
+ const outline = barOutline(series);
328
463
  if (horizontal) {
329
464
  const bx = f.x0 + f.zeroOffset + Math.min(0, len);
330
465
  f.rects.push({
331
466
  x: bx,
332
467
  y: along,
333
468
  w: Math.abs(len),
334
- h: barW * .9,
335
- fillHex: color
469
+ h: barW,
470
+ fillHex: color,
471
+ ...outline
336
472
  });
337
473
  } else {
338
474
  const by = f.y0 + f.zeroOffset + Math.min(0, len);
339
475
  f.rects.push({
340
476
  x: along,
341
477
  y: by,
342
- w: barW * .9,
478
+ w: barW,
343
479
  h: Math.abs(len),
344
- fillHex: color
480
+ fillHex: color,
481
+ ...outline
345
482
  });
346
483
  }
347
484
  if (chart.showValues) {
348
- const txt = fmtDataLabel(series.values[c] ?? 0);
485
+ const txt = fmtDataLabel(chart, series.values[c] ?? 0);
349
486
  if (horizontal) {
350
487
  const end = f.x0 + f.zeroOffset + len;
351
488
  f.labels.push({
@@ -363,13 +500,62 @@ function buildBarScene(chart, wPt, hPt, measure) {
363
500
  }
364
501
  }
365
502
  }
503
+ pushComboLines(chart, f, lineIdx);
366
504
  return {
367
505
  rects: f.rects,
368
506
  polylines: f.polylines,
507
+ gridlines: f.gridlines,
508
+ ...f.plotBackground ? { plotBackground: f.plotBackground } : {},
369
509
  wedges: [],
370
510
  labels: f.labels
371
511
  };
372
512
  }
513
+ /**
514
+ * The name a series shows in the legend: its own `c:tx`, or Excel's positional
515
+ * `SeriesN` when it has none. Exported because the SUBSET has to know it too —
516
+ * a name invented at draw time is a name no glyph collector ever saw, and
517
+ * 57362.xlsx drew its unnamed series as "eries1", the capital S appearing
518
+ * nowhere else on the page and so nowhere in the font.
519
+ *
520
+ * @param series The series.
521
+ * @param index Its zero-based index in the chart.
522
+ * @returns The legend text.
523
+ */
524
+ function legendSeriesName(series, index) {
525
+ return series.name && series.name.length > 0 ? series.name : `Series${index + 1}`;
526
+ }
527
+ /** Whether a series plots as a line rather than as a bar of its own cluster. */
528
+ function isLineLike(series) {
529
+ return series.type === "line" || series.type === "scatter";
530
+ }
531
+ /**
532
+ * Draw a combo's line-group series over an already-built cartesian frame, at the
533
+ * category slot centres the bars use (§21.2.2.145).
534
+ */
535
+ function pushComboLines(chart, f, lineIdx) {
536
+ for (const s of lineIdx) {
537
+ const series = chart.series[s];
538
+ const color = seriesColor(series, s, chart.seriesColorCycle);
539
+ const at = (series.secondaryAxis ? f.valueOffset2 : void 0) ?? f.valueOffset;
540
+ const pts = [];
541
+ 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)]);
542
+ if (pts.length >= 2) f.polylines.push({
543
+ points: pts,
544
+ strokeHex: color,
545
+ widthPt: 1.5
546
+ });
547
+ else if (pts.length === 1) {
548
+ const [px, py] = pts[0];
549
+ f.rects.push({
550
+ x: px - 1.5,
551
+ y: py - 1.5,
552
+ w: 3,
553
+ h: 3,
554
+ fillHex: color
555
+ });
556
+ }
557
+ }
558
+ }
373
559
  function centeredLabel(text, x, y) {
374
560
  return {
375
561
  text,
@@ -433,6 +619,8 @@ function buildAreaScene(chart, wPt, hPt, measure) {
433
619
  return {
434
620
  rects: f.rects,
435
621
  polylines: f.polylines,
622
+ gridlines: f.gridlines,
623
+ ...f.plotBackground ? { plotBackground: f.plotBackground } : {},
436
624
  wedges: [],
437
625
  labels: f.labels,
438
626
  polygons
@@ -452,6 +640,7 @@ function buildAreaScene(chart, wPt, hPt, measure) {
452
640
  function buildScatterScene(chart, wPt, hPt, measure) {
453
641
  const rects = [];
454
642
  const polylines = [];
643
+ const gridlines = [];
455
644
  const labels = [];
456
645
  const xs = [];
457
646
  const ys = [];
@@ -462,11 +651,12 @@ function buildScatterScene(chart, wPt, hPt, measure) {
462
651
  if (xs.length === 0) return {
463
652
  rects,
464
653
  polylines,
654
+ gridlines,
465
655
  wedges: [],
466
656
  labels
467
657
  };
468
- const xScale = niceScale(Math.min(...xs), Math.max(...xs));
469
- const yScale = niceScale(Math.min(...ys), Math.max(...ys));
658
+ const xScale = niceScale(...autoRange(xs), tickBudget(wPt / 2));
659
+ const yScale = niceScale(...autoRange(ys), tickBudget(hPt));
470
660
  const xTicks = ticks(xScale);
471
661
  const yTicks = ticks(yScale);
472
662
  let top = 4;
@@ -479,33 +669,92 @@ function buildScatterScene(chart, wPt, hPt, measure) {
479
669
  const xAt = (v) => x0 + (v - xScale.min) / (xScale.max - xScale.min) * plotW;
480
670
  const yAt = (v) => y0 + (v - yScale.min) / (yScale.max - yScale.min) * plotH;
481
671
  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);
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);
674
+ pushAxisLines(polylines, x0, y0, plotW, plotH, chart);
675
+ const style = chart.scatterStyle ?? "marker";
676
+ const joins = style === "line" || style === "lineMarker" || style.startsWith("smooth");
677
+ const marks = style === "marker" || style === "lineMarker" || style === "smoothMarker";
678
+ const wedges = [];
485
679
  for (let s = 0; s < chart.series.length; s++) {
486
680
  const series = chart.series[s];
487
681
  const color = seriesColor(series, s, chart.seriesColorCycle);
682
+ const pts = [];
488
683
  for (let i = 0; i < series.values.length; i++) {
489
684
  const px = xAt(series.xValues?.[i] ?? i);
490
685
  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
- });
686
+ pts.push([px, py]);
687
+ if (marks) pushMarker(rects, wedges, series.marker, px, py, color);
498
688
  }
689
+ if (joins && !series.line?.none && pts.length >= 2) polylines.push({
690
+ points: pts,
691
+ strokeHex: color,
692
+ widthPt: series.line?.widthPt ?? 1.5
693
+ });
499
694
  }
500
695
  legend.emit(rects, labels);
501
696
  return {
502
697
  rects,
503
698
  polylines,
504
- wedges: [],
699
+ gridlines,
700
+ wedges,
505
701
  labels
506
702
  };
507
703
  }
508
704
  /**
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.
708
+ */
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
+ * Ticks that fit along an axis `extentPt` long (mirrors {@link axisScale}).
716
+ * A tick every 32pt is what both references draw: at 24 a 250pt plot carried
717
+ * eleven of them (0, 0.5, 1 … 5.5) where both label 0…6 in whole numbers.
718
+ */
719
+ var tickBudget = (extentPt) => Math.min(10, Math.max(4, Math.round(extentPt / 32)));
720
+ /** Side (points) of the square stamped for a series that names no symbol. */
721
+ var DEFAULT_MARKER_PT = 4;
722
+ /**
723
+ * Stamp one data point's marker. §21.2.2.107: `none` draws nothing at all even
724
+ * when the scatter style marks its points, a round symbol is a disc (a full
725
+ * wedge — drawn last, so it sits over the line joining the points), and every
726
+ * other symbol keeps the square we have always drawn.
727
+ *
728
+ * @param rects Collects a square marker.
729
+ * @param wedges Collects a round marker.
730
+ * @param marker The series' `c:marker`, if it declared one.
731
+ * @param x Point centre, scene x.
732
+ * @param y Point centre, scene y.
733
+ * @param color The series colour.
734
+ */
735
+ function pushMarker(rects, wedges, marker, x, y, color) {
736
+ if (marker?.symbol === "none") return;
737
+ const size = marker?.sizePt ?? DEFAULT_MARKER_PT;
738
+ if (marker?.symbol === "circle" || marker?.symbol === "dot") {
739
+ wedges.push({
740
+ cx: x,
741
+ cy: y,
742
+ r: size / 2,
743
+ startRad: 0,
744
+ sweepRad: -2 * Math.PI,
745
+ fillHex: color
746
+ });
747
+ return;
748
+ }
749
+ rects.push({
750
+ x: x - size / 2,
751
+ y: y - size / 2,
752
+ w: size,
753
+ h: size,
754
+ fillHex: color
755
+ });
756
+ }
757
+ /**
509
758
  * Lay out a line {@link Chart} into a {@link ChartScene}: each series becomes a
510
759
  * stroked polyline across the category slots, over the shared cartesian frame.
511
760
  * Unlike bars/areas the value axis auto-mins (it need not include 0).
@@ -518,7 +767,15 @@ function buildScatterScene(chart, wPt, hPt, measure) {
518
767
  */
519
768
  function buildLineScene(chart, wPt, hPt, measure) {
520
769
  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] });
770
+ const lo = allVals.length > 0 ? Math.min(...allVals) : 0;
771
+ const hi = allVals.length > 0 ? Math.max(...allVals) : 1;
772
+ const range = lo > 0 && lo > 5 / 6 * hi ? [lo, hi] : [Math.min(0, lo), Math.max(0, hi)];
773
+ const lineFormat = chartValueFormatter(chart);
774
+ const f = buildFrame(chart, wPt, hPt, measure, false, {
775
+ dataRange: range,
776
+ ...lineFormat ? { formatValue: lineFormat } : {}
777
+ });
778
+ const wedges = [];
522
779
  for (let s = 0; s < chart.series.length; s++) {
523
780
  const series = chart.series[s];
524
781
  const color = seriesColor(series, s, chart.seriesColorCycle);
@@ -527,7 +784,8 @@ function buildLineScene(chart, wPt, hPt, measure) {
527
784
  const x = f.x0 + c * f.slot + f.slot / 2;
528
785
  const y = f.y0 + f.valueOffset(series.values[c] ?? 0);
529
786
  pts.push([x, y]);
530
- if (chart.showValues) f.labels.push(centeredLabel(fmtDataLabel(series.values[c] ?? 0), x, y + 3));
787
+ if (chart.lineMarkers) pushMarker(f.rects, wedges, series.marker, x, y, color);
788
+ if (chart.showValues) f.labels.push(centeredLabel(fmtDataLabel(chart, series.values[c] ?? 0), x, y + 3));
531
789
  }
532
790
  if (pts.length >= 2) f.polylines.push({
533
791
  points: pts,
@@ -548,11 +806,16 @@ function buildLineScene(chart, wPt, hPt, measure) {
548
806
  return {
549
807
  rects: f.rects,
550
808
  polylines: f.polylines,
551
- wedges: [],
809
+ gridlines: f.gridlines,
810
+ ...f.plotBackground ? { plotBackground: f.plotBackground } : {},
811
+ wedges,
552
812
  labels: f.labels
553
813
  };
554
814
  }
555
- var sliceColor = (series, i) => pointColor(series, i) ?? SERIES_COLORS[i % SERIES_COLORS.length];
815
+ var sliceColor = (series, i, cycle) => {
816
+ const palette = cycle && cycle.length > 0 ? cycle : SERIES_COLORS;
817
+ return pointColor(series, i) ?? palette[i % palette.length];
818
+ };
556
819
  /**
557
820
  * Lay out a pie/doughnut {@link Chart} into a {@link ChartScene}: the first
558
821
  * series' values become proportional wedges (a centre hole for doughnut),
@@ -579,9 +842,9 @@ function buildPieScene(chart, wPt, hPt, measure) {
579
842
  };
580
843
  let top = 4;
581
844
  if (chart.title) top += 13 * 1.6;
582
- const legend = layoutLegend(chart.categories.map((c, i) => ({
583
- name: c,
584
- colorHex: sliceColor(series, i)
845
+ const legend = layoutLegend(values.map((_, i) => ({
846
+ name: chart.categories[i] ?? String(i + 1),
847
+ colorHex: sliceColor(series, i, chart.seriesColorCycle)
585
848
  })), chart.hasLegend, chart.legendPos ?? "r", wPt, hPt, measure);
586
849
  const availW = Math.max(1, wPt - 8 - legend.rightWidth);
587
850
  const availH = Math.max(1, hPt - top - 4 - legend.bottomHeight);
@@ -601,17 +864,18 @@ function buildPieScene(chart, wPt, hPt, measure) {
601
864
  r,
602
865
  startRad: ang,
603
866
  sweepRad: sweep,
604
- fillHex: sliceColor(series, i),
867
+ fillHex: sliceColor(series, i, chart.seriesColorCycle),
605
868
  strokeHex: "FFFFFF"
606
869
  });
607
870
  const mid = ang + sweep / 2;
608
871
  const pct = Math.round(v / total * 100);
609
- if (pct >= 5) labels.push({
610
- text: `${pct}%`,
872
+ const custom = series.pointLabels?.find((l) => l.idx === i)?.text;
873
+ if (custom !== void 0 || pct >= 5) labels.push({
874
+ text: custom ?? `${pct}%`,
611
875
  x: cx + Math.cos(mid) * labelR,
612
876
  y: cy + Math.sin(mid) * labelR - 9 / 3,
613
877
  sizePt: 9,
614
- colorHex: chart.doughnut ? LABEL_COLOR : "FFFFFF",
878
+ colorHex: custom !== void 0 || chart.doughnut ? LABEL_COLOR : "FFFFFF",
615
879
  align: "center"
616
880
  });
617
881
  ang += sweep;
@@ -643,7 +907,7 @@ function buildPieScene(chart, wPt, hPt, measure) {
643
907
  function pointColor(series, idx) {
644
908
  return series.pointColors?.find((p) => p.idx === idx)?.colorHex;
645
909
  }
646
- function layoutLegend(entries, hasLegend, pos, wPt, hPt, measure) {
910
+ function layoutLegend(entries, hasLegend, pos, wPt, hPt, measure, marker = "box") {
647
911
  if (!hasLegend || entries.length === 0) return {
648
912
  rightWidth: 0,
649
913
  bottomHeight: 0,
@@ -651,7 +915,22 @@ function layoutLegend(entries, hasLegend, pos, wPt, hPt, measure) {
651
915
  };
652
916
  const sw = 9;
653
917
  const gap = 4;
654
- const entryW = (e) => sw + 3 + measure(e.name, 9) + gap * 2;
918
+ const keyW = sw * 2.2;
919
+ const keyH = sw * .55;
920
+ const key = (x, y, e) => (e.marker ?? marker) === "line" ? {
921
+ x,
922
+ y: y + sw / 2 - .75,
923
+ w: keyW,
924
+ h: 1.5,
925
+ fillHex: e.colorHex
926
+ } : {
927
+ x,
928
+ y: y + (sw - keyH) / 2,
929
+ w: keyW,
930
+ h: keyH,
931
+ fillHex: e.colorHex
932
+ };
933
+ const entryW = (e) => keyW + 3 + measure(e.name, 9) + gap * 2;
655
934
  if (pos === "r" || pos === "l") {
656
935
  const colW = Math.max(...entries.map(entryW));
657
936
  return {
@@ -661,16 +940,10 @@ function layoutLegend(entries, hasLegend, pos, wPt, hPt, measure) {
661
940
  const lx = pos === "r" ? wPt - colW + gap : gap;
662
941
  let ly = hPt / 2 + entries.length * (sw + 4) / 2 - sw;
663
942
  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
- });
943
+ rects.push(key(lx, ly, e));
671
944
  labels.push({
672
945
  text: e.name,
673
- x: lx + sw + 3,
946
+ x: lx + keyW + 3,
674
947
  y: ly + 1,
675
948
  sizePt: 9,
676
949
  colorHex: LABEL_COLOR,
@@ -689,16 +962,10 @@ function layoutLegend(entries, hasLegend, pos, wPt, hPt, measure) {
689
962
  let lx = (wPt - totalW) / 2 + gap;
690
963
  const ly = 2;
691
964
  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
- });
965
+ rects.push(key(lx, ly, e));
699
966
  labels.push({
700
967
  text: e.name,
701
- x: lx + sw + 3,
968
+ x: lx + keyW + 3,
702
969
  y: ly + 1,
703
970
  sizePt: 9,
704
971
  colorHex: LABEL_COLOR,
@@ -721,6 +988,41 @@ function layoutLegend(entries, hasLegend, pos, wPt, hPt, measure) {
721
988
  * renderer then reserves the box with a light border).
722
989
  */
723
990
  function buildChartScene(chart, wPt, hPt, measure) {
991
+ const scene = buildTypedScene(chart.catAxisReversed ? withReversedCategories(chart) : chart, wPt, hPt, measure);
992
+ return scene && withFrame(scene, chart, wPt, hPt);
993
+ }
994
+ /**
995
+ * §21.2.2.134 `maxMin` on the category axis — the categories run the other way,
996
+ * which for a horizontal bar chart puts the FIRST one at the top (how every
997
+ * ranked list reads). Reversing the plotted order says exactly that and leaves
998
+ * every builder's geometry alone; the per-point overrides move with their
999
+ * points.
1000
+ *
1001
+ * @param chart The chart as the file declares it.
1002
+ * @returns The same chart with its categories, values and per-point overrides
1003
+ * in reverse.
1004
+ */
1005
+ function withReversedCategories(chart) {
1006
+ const n = Math.max(chart.categories.length, ...chart.series.map((s) => s.values.length), 1);
1007
+ const flip = (i) => n - 1 - i;
1008
+ return {
1009
+ ...chart,
1010
+ categories: chart.categories.length > 0 ? Array.from({ length: n }, (_, i) => chart.categories[flip(i)] ?? "") : chart.categories,
1011
+ series: chart.series.map((s) => ({
1012
+ ...s,
1013
+ values: Array.from({ length: n }, (_, i) => s.values[flip(i)] ?? 0),
1014
+ ...s.pointColors ? { pointColors: s.pointColors.map((p) => ({
1015
+ ...p,
1016
+ idx: flip(p.idx)
1017
+ })) } : {},
1018
+ ...s.pointLabels ? { pointLabels: s.pointLabels.map((p) => ({
1019
+ ...p,
1020
+ idx: flip(p.idx)
1021
+ })) } : {}
1022
+ }))
1023
+ };
1024
+ }
1025
+ function buildTypedScene(chart, wPt, hPt, measure) {
724
1026
  if (chart.type === "bar") return buildBarScene(chart, wPt, hPt, measure);
725
1027
  if (chart.type === "line") return buildLineScene(chart, wPt, hPt, measure);
726
1028
  if (chart.type === "pie") return buildPieScene(chart, wPt, hPt, measure);
@@ -728,5 +1030,35 @@ function buildChartScene(chart, wPt, hPt, measure) {
728
1030
  if (chart.type === "scatter") return buildScatterScene(chart, wPt, hPt, measure);
729
1031
  return null;
730
1032
  }
1033
+ /**
1034
+ * §21.2.2.198 — the chart-space frame, first in the scene so everything else
1035
+ * draws over it. A chart that declares neither fill nor outline is returned
1036
+ * untouched, so a scene without one is unchanged rect for rect.
1037
+ *
1038
+ * @param scene The typed scene.
1039
+ * @param chart The chart (for its frame fill/outline).
1040
+ * @param wPt The chart's width in points.
1041
+ * @param hPt Its height.
1042
+ * @returns The scene with the frame behind it.
1043
+ */
1044
+ function withFrame(scene, chart, wPt, hPt) {
1045
+ if (!chart.frameFillHex && !chart.frameLineHex) return scene;
1046
+ const frame = {
1047
+ x: 0,
1048
+ y: 0,
1049
+ w: wPt,
1050
+ h: hPt,
1051
+ ...chart.frameFillHex ? { fillHex: chart.frameFillHex } : {},
1052
+ ...chart.frameLineHex ? {
1053
+ strokeHex: chart.frameLineHex,
1054
+ strokeWidthPt: chart.frameLineWidthPt ?? .75,
1055
+ ...chart.frameLineDash ? { strokeDash: chart.frameLineDash } : {}
1056
+ } : {}
1057
+ };
1058
+ return {
1059
+ ...scene,
1060
+ background: frame
1061
+ };
1062
+ }
731
1063
  //#endregion
732
- export { buildChartScene };
1064
+ export { buildChartScene, legendSeriesName };