reamkit 1.32.1 → 1.33.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 (153) hide show
  1. package/README.md +5 -4
  2. package/dist/esm/core/bidi/algorithm.js +2 -2
  3. package/dist/esm/core/bidi/char-types.js +1 -1
  4. package/dist/esm/core/bidi/index.js +3 -3
  5. package/dist/esm/core/bytes.js +1 -1
  6. package/dist/esm/core/converter/facade.js +3 -2
  7. package/dist/esm/core/converter/ream.js +12 -5
  8. package/dist/esm/core/crypto/asn1.js +2 -2
  9. package/dist/esm/core/crypto/cms.js +5 -3
  10. package/dist/esm/core/crypto/index.js +2 -1
  11. package/dist/esm/core/crypto/offcrypto.js +8 -7
  12. package/dist/esm/core/crypto/primitives.js +16 -16
  13. package/dist/esm/core/document-model/index.d.ts +1 -1
  14. package/dist/esm/core/document-model/types.d.ts +174 -2
  15. package/dist/esm/core/drawingml/chart-geometry.d.ts +24 -6
  16. package/dist/esm/core/drawingml/chart-geometry.js +905 -208
  17. package/dist/esm/core/drawingml/chart-parser.d.ts +5 -1
  18. package/dist/esm/core/drawingml/chart-parser.js +275 -19
  19. package/dist/esm/core/drawingml/chart-text.d.ts +41 -0
  20. package/dist/esm/core/drawingml/chart-text.js +93 -0
  21. package/dist/esm/core/drawingml/colors.js +5 -5
  22. package/dist/esm/core/drawingml/diagram/layout-engine.js +1 -4
  23. package/dist/esm/core/drawingml/diagram/run.js +2 -1
  24. package/dist/esm/core/drawingml/preset-geometry.js +4 -6
  25. package/dist/esm/core/drawingml/shape-render.js +1 -1
  26. package/dist/esm/core/drawingml/text-warp.js +4 -16
  27. package/dist/esm/core/drawingml/theme-parser.js +14 -8
  28. package/dist/esm/core/font/arabic-joining.js +2 -2
  29. package/dist/esm/core/font/binary-reader.js +2 -0
  30. package/dist/esm/core/font/font-registry.js +1 -0
  31. package/dist/esm/core/font/ligatures.js +1 -1
  32. package/dist/esm/core/font/opentype-layout.js +11 -5
  33. package/dist/esm/core/font/ttf-build.js +6 -10
  34. package/dist/esm/core/font/ttf-parser.js +2 -1
  35. package/dist/esm/core/font/ttf-subset.js +8 -8
  36. package/dist/esm/core/fonts/families.js +1 -1
  37. package/dist/esm/core/fonts/remote-fonts.js +4 -4
  38. package/dist/esm/core/hyphenation/index.js +1 -1
  39. package/dist/esm/core/images.js +8 -8
  40. package/dist/esm/core/ir/loss.js +1 -0
  41. package/dist/esm/core/line-breaker/cjk.js +1 -1
  42. package/dist/esm/core/line-breaker/index.js +4 -3
  43. package/dist/esm/core/links.js +1 -1
  44. package/dist/esm/core/lzw.js +1 -1
  45. package/dist/esm/core/metafile/blit.js +1 -1
  46. package/dist/esm/core/metafile/emf.js +5 -7
  47. package/dist/esm/core/metafile/picture.js +1 -1
  48. package/dist/esm/core/metafile/symbol-fonts.js +7 -7
  49. package/dist/esm/core/metafile/wmf.js +5 -7
  50. package/dist/esm/core/number-format.js +4 -6
  51. package/dist/esm/core/numbering/index.js +3 -2
  52. package/dist/esm/core/ole/cfb.js +4 -4
  53. package/dist/esm/core/ole/escher-blip.js +1 -1
  54. package/dist/esm/core/opc/index.js +5 -4
  55. package/dist/esm/core/opc/package.js +3 -1
  56. package/dist/esm/core/opc/xml-entities.js +1 -1
  57. package/dist/esm/core/outline.js +1 -1
  58. package/dist/esm/core/png-encode.js +3 -3
  59. package/dist/esm/core/po-helpers.js +1 -1
  60. package/dist/esm/core/spreadsheet-model/types.d.ts +6 -0
  61. package/dist/esm/core/style-cascade/index.js +4 -3
  62. package/dist/esm/core/tiff.js +3 -1
  63. package/dist/esm/excel/activex-parser.js +1 -1
  64. package/dist/esm/excel/conditional-format.js +7 -7
  65. package/dist/esm/excel/formula/functions.js +8 -4
  66. package/dist/esm/excel/formula/index.js +3 -3
  67. package/dist/esm/excel/formula/lexer.js +2 -2
  68. package/dist/esm/excel/formula/parser.js +4 -3
  69. package/dist/esm/excel/formula/value.js +1 -1
  70. package/dist/esm/excel/index.js +7 -6
  71. package/dist/esm/excel/print-model.js +4 -4
  72. package/dist/esm/excel/printer-settings.js +6 -7
  73. package/dist/esm/excel/rich-value.js +1 -1
  74. package/dist/esm/excel/sheet-shape-parser.js +4 -2
  75. package/dist/esm/excel/sheet-to-flow.js +13 -11
  76. package/dist/esm/excel/styles-parser.js +5 -3
  77. package/dist/esm/excel/tint.js +5 -5
  78. package/dist/esm/excel/vml-drawing.js +2 -2
  79. package/dist/esm/excel/worksheet-parser.js +5 -5
  80. package/dist/esm/excel/xls/biff-chart.js +1 -3
  81. package/dist/esm/excel/xls/biff-reader.js +10 -11
  82. package/dist/esm/excel/xls/xls-reader.js +1 -1
  83. package/dist/esm/excel/xlsx-reader.js +17 -4
  84. package/dist/esm/excel/xlsx-to-pdf.js +1 -1
  85. package/dist/esm/excel/xlsx-writer.js +2 -2
  86. package/dist/esm/html/html-writer.js +18 -10
  87. package/dist/esm/layout/line-paint.js +1 -1
  88. package/dist/esm/layout/math-layout.js +5 -8
  89. package/dist/esm/layout/styled-layout.js +103 -31
  90. package/dist/esm/layout/turned-section.js +2 -2
  91. package/dist/esm/markdown/markdown-writer.js +1 -1
  92. package/dist/esm/pdf/encryption.js +9 -9
  93. package/dist/esm/pdf/icc-profile.js +2 -2
  94. package/dist/esm/pdf/index.js +3 -2
  95. package/dist/esm/pdf/objects.js +7 -0
  96. package/dist/esm/pdf/serialize.js +1 -1
  97. package/dist/esm/pdf/struct-tree.js +3 -1
  98. package/dist/esm/pdf/styled-page-emitter.js +23 -15
  99. package/dist/esm/pdf/styled-page-renderer.js +9 -5
  100. package/dist/esm/pdf-reader/annot-draw.js +3 -3
  101. package/dist/esm/pdf-reader/ccitt.js +2 -1
  102. package/dist/esm/pdf-reader/cff-outline.js +1 -4
  103. package/dist/esm/pdf-reader/content.js +13 -15
  104. package/dist/esm/pdf-reader/decrypt.js +5 -4
  105. package/dist/esm/pdf-reader/document.js +6 -2
  106. package/dist/esm/pdf-reader/encodings.js +1 -1
  107. package/dist/esm/pdf-reader/flow-build.js +4 -6
  108. package/dist/esm/pdf-reader/font.js +3 -3
  109. package/dist/esm/pdf-reader/function.js +8 -7
  110. package/dist/esm/pdf-reader/icc.js +1 -1
  111. package/dist/esm/pdf-reader/images.js +1 -1
  112. package/dist/esm/pdf-reader/jbig2.js +36 -38
  113. package/dist/esm/pdf-reader/jpeg.js +14 -13
  114. package/dist/esm/pdf-reader/layout.js +12 -9
  115. package/dist/esm/pdf-reader/lexer.js +2 -1
  116. package/dist/esm/pdf-reader/math-rows.js +2 -2
  117. package/dist/esm/pdf-reader/optional-content.js +1 -1
  118. package/dist/esm/pdf-reader/page-numbers.js +1 -1
  119. package/dist/esm/pdf-reader/pattern-tint.js +1 -2
  120. package/dist/esm/pdf-reader/reader.js +2 -2
  121. package/dist/esm/pdf-reader/struct-tree.js +1 -1
  122. package/dist/esm/pdf-reader/tagged.js +3 -2
  123. package/dist/esm/pdf-reader/text.js +2 -2
  124. package/dist/esm/pdf-reader/type1-outline.js +1 -3
  125. package/dist/esm/pptx/embedded-fonts.js +3 -5
  126. package/dist/esm/pptx/placeholder-cascade.js +1 -1
  127. package/dist/esm/pptx/ppt/ppt-reader.js +5 -4
  128. package/dist/esm/pptx/ppt/ppt-text.js +9 -10
  129. package/dist/esm/pptx/pptx-reader.js +6 -5
  130. package/dist/esm/pptx/preset-table-styles.js +1 -1
  131. package/dist/esm/pptx/slide-parser.js +6 -4
  132. package/dist/esm/pptx/sp-helpers.js +1 -1
  133. package/dist/esm/pptx/table-style.js +2 -1
  134. package/dist/esm/svg/svg-writer.js +1 -1
  135. package/dist/esm/word/doc/doc-reader.js +2 -2
  136. package/dist/esm/word/doc/doc-text.js +20 -22
  137. package/dist/esm/word/document-parser.js +26 -21
  138. package/dist/esm/word/docx-reader.js +2 -2
  139. package/dist/esm/word/docx-to-pdf.js +3 -3
  140. package/dist/esm/word/docx-writer.js +10 -6
  141. package/dist/esm/word/drawing-parser.js +18 -18
  142. package/dist/esm/word/font-table.js +1 -1
  143. package/dist/esm/word/index.js +6 -5
  144. package/dist/esm/word/numbering-parser.js +4 -3
  145. package/dist/esm/word/omml-parser.js +1 -1
  146. package/dist/esm/word/paragraph-properties.js +11 -10
  147. package/dist/esm/word/run-properties.js +4 -4
  148. package/dist/esm/word/settings-parser.js +2 -1
  149. package/dist/esm/word/shading.js +1 -1
  150. package/dist/esm/word/styles-parser.js +5 -4
  151. package/dist/esm/word/table-parser.js +4 -4
  152. package/dist/esm/word/theme-fonts.js +1 -1
  153. package/package.json +8 -2
@@ -3,6 +3,24 @@ var AXIS_COLOR = "595959";
3
3
  var GRID_COLOR = "D9D9D9";
4
4
  var LABEL_COLOR = "595959";
5
5
  var TITLE_COLOR = "404040";
6
+ /**
7
+ * The face one role of the chart's text is set in: the chart's own
8
+ * ({@link Chart.text}) where the reader resolved it, and otherwise the sizes
9
+ * and greys this module has always drawn in, in the document's face.
10
+ */
11
+ function faceOf(chart, role) {
12
+ const own = chart.text?.[role];
13
+ const title = role === "title";
14
+ return {
15
+ sizePt: own?.sizePt ?? (title ? 13 : 9),
16
+ colorHex: own?.colorHex ?? (title ? TITLE_COLOR : LABEL_COLOR),
17
+ ...own?.family ? { family: own.family } : {},
18
+ ...own?.bold ? { bold: true } : {},
19
+ ...own?.italic ? { italic: true } : {}
20
+ };
21
+ }
22
+ /** The width of `text` set in `face`. */
23
+ var widthIn = (measure, text, face) => measure(text, face.sizePt, face);
6
24
  /** The Office accent cycle (RRGGBB) for series without an explicit colour. */
7
25
  var SERIES_COLORS = [
8
26
  "4472C4",
@@ -61,33 +79,38 @@ function niceScale(dataMin, dataMax, maxIntervals = 10) {
61
79
  }
62
80
  /**
63
81
  * The ends {@link niceScale} rounds out to its step: the data's, padded as
64
- * Excel pads them. Never past the largest double, which the padding of values
82
+ * Excel pads them — by `pad` of their reach, 5%, which a 3-D chart's axis
83
+ * does without: Excel's PDF runs 3-D columns of 70…96 on 0…100 by 10 and of
84
+ * −5…15 on −6…16 by 2, where flat ones run 0…120 by 20 and −10…20 by 5
85
+ * (2026-10-02). Never past the largest double, which the padding of values
65
86
  * out at ±1.7E+308 would run them to.
66
87
  */
67
- function paddedEnds(dataMin, dataMax) {
88
+ function paddedEnds(dataMin, dataMax, pad = .05) {
68
89
  let lo = Math.min(dataMin, dataMax);
69
90
  let hi = Math.max(dataMin, dataMax);
70
- if (lo === hi) if (hi > 0) lo = 0;
71
- else if (lo < 0) hi = 0;
72
- else hi = 1;
91
+ if (lo === hi) {
92
+ if (hi > 0) lo = 0;
93
+ else if (lo < 0) hi = 0;
94
+ else hi = 1;
95
+ }
73
96
  const spread = hi - lo;
74
97
  if (lo >= 0) {
75
98
  const from = spread < hi / 6 ? lo - spread / 2 : 0;
76
99
  return {
77
100
  lo: from,
78
- hi: finite(hi + (hi - from) * .05)
101
+ hi: finite(hi + (hi - from) * pad)
79
102
  };
80
103
  }
81
104
  if (hi <= 0) {
82
105
  const to = spread < -lo / 6 ? hi + spread / 2 : 0;
83
106
  return {
84
- lo: finite(lo - (to - lo) * .05),
107
+ lo: finite(lo - (to - lo) * pad),
85
108
  hi: to
86
109
  };
87
110
  }
88
111
  return {
89
- lo: finite(lo - spread * .05),
90
- hi: finite(hi + spread * .05)
112
+ lo: finite(lo - spread * pad),
113
+ hi: finite(hi + spread * pad)
91
114
  };
92
115
  }
93
116
  /** `v`, or the largest double of its sign where it ran past it. */
@@ -113,7 +136,7 @@ function stepFor(lo, hi, maxIntervals) {
113
136
  1,
114
137
  2,
115
138
  5
116
- ]) if (share <= m * base * 1.000000001) return Math.min(m * base, Number.MAX_VALUE);
139
+ ]) if (share <= m * base * (1 + 1e-9)) return Math.min(m * base, Number.MAX_VALUE);
117
140
  base *= 10;
118
141
  }
119
142
  }
@@ -166,7 +189,7 @@ function formatTick(v) {
166
189
  * the ends out to a widened step can add that one.
167
190
  */
168
191
  var ticks = (s) => {
169
- const count = Math.min(Math.floor(intervalsOf(s) + 1e-9), MOST_INTERVALS + 1);
192
+ const count = Math.min(Math.floor(intervalsOf(s) + 1e-9), 501);
170
193
  const out = [];
171
194
  for (let i = 0; i <= count; i++) {
172
195
  const sum = s.min + i * s.step;
@@ -181,7 +204,7 @@ var ticks = (s) => {
181
204
  * {@link niceScale} pads them, where they did not. A fixed end is exact —
182
205
  * nice-rounding it would move a number the author chose.
183
206
  *
184
- * The step is the author's (§21.2.2.98 `c:majorUnit`), else the 1-2-5 step
207
+ * The step is the author's (§21.2.2.103 `c:majorUnit`), else the 1-2-5 step
185
208
  * of the span between the ends — with both fixed, of the span they fix:
186
209
  * −1E+300…1E+300 runs by 2E+299 in Excel, where padding the ends first
187
210
  * stepped it by 5E+299. The automatic ends round out to an author's step
@@ -198,12 +221,14 @@ var ticks = (s) => {
198
221
  * @param extentPt The axis's length — how many ticks fit is a question
199
222
  * about the plot, not about the numbers.
200
223
  * @param horizontal Whether the axis lies along the foot.
224
+ * @param pad How far past the data the automatic ends reach, as a
225
+ * share of its reach ({@link paddedEnds}).
201
226
  * @returns The axis min/max and tick step.
202
227
  */
203
- function axisScale(chart, dataMin, dataMax, extentPt, horizontal = false) {
228
+ function axisScale(chart, dataMin, dataMax, extentPt, horizontal = false, pad = .05) {
204
229
  const fixedMin = chart.valAxisMin;
205
230
  const fixedMax = chart.valAxisMax;
206
- const padded = paddedEnds(fixedMin ?? dataMin, fixedMax ?? dataMax);
231
+ const padded = paddedEnds(fixedMin ?? dataMin, fixedMax ?? dataMax, pad);
207
232
  const lo = fixedMin ?? padded.lo;
208
233
  const hi = fixedMax ?? padded.hi;
209
234
  const rounded = (step) => ({
@@ -217,6 +242,43 @@ function axisScale(chart, dataMin, dataMax, extentPt, horizontal = false) {
217
242
  return rounded(Number.isFinite(span) ? span / MOST_INTERVALS : hi / MOST_INTERVALS - lo / MOST_INTERVALS);
218
243
  }
219
244
  /**
245
+ * §21.2.2.104 — the plot's own rectangle where its author sized it (an
246
+ * `inner` manual layout), in the scene's y-up frame: the axes' labels and
247
+ * titles stand outside it, where the chart sets them.
248
+ */
249
+ function innerPlot(chart, wPt, hPt) {
250
+ const box = chart.plotBox;
251
+ if (!box?.inner) return void 0;
252
+ return {
253
+ x0: box.x * wPt,
254
+ y0: hPt - (box.y + box.h) * hPt,
255
+ plotW: Math.max(1, box.w * wPt),
256
+ plotH: Math.max(1, box.h * hPt)
257
+ };
258
+ }
259
+ /**
260
+ * A scatter axis's {@link Scale}: {@link niceScale}'s where its author fixed
261
+ * nothing, and where they did, their ends exactly and their step, the ends
262
+ * they left to the data rounded out to it — never cut into more than
263
+ * {@link MOST_INTERVALS}, as a frame chart's value axis is not.
264
+ */
265
+ function scatterScale(lo, hi, maxIntervals, fix) {
266
+ if (fix.min === void 0 && fix.max === void 0 && fix.unit === void 0) return niceScale(lo, hi, maxIntervals);
267
+ const auto = niceScale(fix.min ?? lo, fix.max ?? hi, maxIntervals);
268
+ const step = fix.unit ?? (fix.min !== void 0 && fix.max !== void 0 ? stepFor(fix.min, fix.max, maxIntervals) : auto.step);
269
+ const scale = {
270
+ min: fix.min ?? below(auto.min, step),
271
+ max: fix.max ?? above(auto.max, step),
272
+ step
273
+ };
274
+ if (intervalsOf(scale) <= MOST_INTERVALS) return scale;
275
+ const span = scale.max - scale.min;
276
+ return {
277
+ ...scale,
278
+ step: Number.isFinite(span) ? span / MOST_INTERVALS : scale.max / MOST_INTERVALS - scale.min / MOST_INTERVALS
279
+ };
280
+ }
281
+ /**
220
282
  * The chart title's lines. A title longer than the chart is wide wraps at its
221
283
  * spaces, as Excel's and Calc's do, rather than running out past both sides
222
284
  * of the frame: dataValidationTableRange.xlsx's "Ranking of Washington
@@ -230,12 +292,13 @@ function axisScale(chart, dataMin, dataMax, extentPt, horizontal = false) {
230
292
  */
231
293
  function titleLines(chart, wPt, measure) {
232
294
  if (!chart.title) return [];
295
+ const face = faceOf(chart, "title");
233
296
  const room = wPt * .8;
234
297
  const lines = [];
235
298
  let line = "";
236
299
  for (const word of chart.title.split(/\s+/).filter((w) => w.length > 0)) {
237
300
  const longer = line ? `${line} ${word}` : word;
238
- if (line && measure(longer, 13) > room) {
301
+ if (line && widthIn(measure, longer, face) > room) {
239
302
  lines.push(line);
240
303
  line = word;
241
304
  } else line = longer;
@@ -244,15 +307,15 @@ function titleLines(chart, wPt, measure) {
244
307
  return lines;
245
308
  }
246
309
  /** The band a title of `lines` takes at the top of the chart. */
247
- var titleHeight = (lines) => lines.length === 0 ? 0 : 13 * (1.6 + 1.2 * (lines.length - 1));
248
- function pushChartTitle(labels, lines, wPt, hPt) {
310
+ var titleHeight = (lines, chart) => lines.length === 0 ? 0 : faceOf(chart, "title").sizePt * (1.6 + 1.2 * (lines.length - 1));
311
+ function pushChartTitle(labels, lines, wPt, hPt, chart) {
312
+ const face = faceOf(chart, "title");
249
313
  lines.forEach((text, i) => {
250
314
  labels.push({
251
315
  text,
252
316
  x: wPt / 2,
253
- y: hPt - 4 - 13 * (1 + 1.2 * i),
254
- sizePt: 13,
255
- colorHex: TITLE_COLOR,
317
+ y: hPt - 4 - face.sizePt * (1 + 1.2 * i),
318
+ ...face,
256
319
  align: "center"
257
320
  });
258
321
  });
@@ -264,37 +327,37 @@ function buildLegendBlock(chart, wPt, hPt, measure) {
264
327
  ...s.line?.none ? { marker: "box" } : isLineLike(s) ? { marker: "line" } : {}
265
328
  }));
266
329
  const marker = chart.type === "line" || chart.type === "scatter" ? "line" : "box";
267
- return layoutLegend(legendEntries, chart.hasLegend, chart.legendPos ?? "b", wPt, hPt, measure, marker);
330
+ return layoutLegend(legendEntries, chart.hasLegend, chart.legendPos ?? "b", wPt, hPt, measure, faceOf(chart, "legend"), marker);
268
331
  }
269
- function pushGridTicks(gridlines, labels, tickVals, fmt, axis, at, x0, y0, plotW, plotH, grid, atEnd = false) {
332
+ function pushGridTicks(gridlines, labels, tickVals, fmt, axis, at, x0, y0, plotW, plotH, grid, face, atEnd = false, labelled = true) {
333
+ const rule = grid?.none ? void 0 : {
334
+ strokeHex: grid?.colorHex ?? GRID_COLOR,
335
+ widthPt: grid?.widthPt ?? .75
336
+ };
270
337
  for (const v of tickVals) if (axis === "x") {
271
338
  const gx = at(v);
272
- gridlines.push({
339
+ if (rule) gridlines.push({
273
340
  points: [[gx, y0], [gx, y0 + plotH]],
274
- strokeHex: grid?.colorHex ?? GRID_COLOR,
275
- widthPt: .75
341
+ ...rule
276
342
  });
277
- labels.push({
343
+ if (labelled) labels.push({
278
344
  text: fmt(v),
279
345
  x: gx,
280
- y: atEnd ? y0 + plotH + 9 * .4 : y0 - 9,
281
- sizePt: 9,
282
- colorHex: LABEL_COLOR,
346
+ y: atEnd ? y0 + plotH + face.sizePt * .4 : y0 - face.sizePt,
347
+ ...face,
283
348
  align: "center"
284
349
  });
285
350
  } else {
286
351
  const gy = at(v);
287
- gridlines.push({
352
+ if (rule) gridlines.push({
288
353
  points: [[x0, gy], [x0 + plotW, gy]],
289
- strokeHex: grid?.colorHex ?? GRID_COLOR,
290
- widthPt: .75
354
+ ...rule
291
355
  });
292
- labels.push({
356
+ if (labelled) labels.push({
293
357
  text: fmt(v),
294
358
  x: atEnd ? x0 + plotW + 3 : x0 - 3,
295
- y: gy - 9 / 3,
296
- sizePt: 9,
297
- colorHex: LABEL_COLOR,
359
+ y: gy - face.sizePt / 3,
360
+ ...face,
298
361
  align: atEnd ? "left" : "right"
299
362
  });
300
363
  }
@@ -318,12 +381,16 @@ function axisStroke(style) {
318
381
  * the value axis — `cross.y` up a column chart, `cross.x` along a bar chart —
319
382
  * and the other stands at the plot's edge.
320
383
  */
321
- function pushAxisLines(polylines, x0, y0, plotW, plotH, chart, cross = {}) {
384
+ function pushAxisLines(polylines, x0, y0, plotW, plotH, chart, cross = {}, shift = {}) {
385
+ const up = shift.upright ?? {
386
+ dx: 0,
387
+ dy: 0
388
+ };
322
389
  const val = axisStroke(chart.valAxisLine);
323
- const x = x0 + (cross.x ?? 0);
390
+ const x = x0 + (cross.x ?? 0) + up.dx;
324
391
  const y = y0 + (cross.y ?? 0);
325
392
  if (val) polylines.push({
326
- points: [[x, y0], [x, y0 + plotH]],
393
+ points: [[x, y0 + up.dy], [x, y0 + plotH + up.dy]],
327
394
  strokeHex: val.hex,
328
395
  widthPt: val.widthPt
329
396
  });
@@ -334,6 +401,126 @@ function pushAxisLines(polylines, x0, y0, plotW, plotH, chart, cross = {}) {
334
401
  widthPt: cat.widthPt
335
402
  });
336
403
  }
404
+ /**
405
+ * Where a 3-D box's back stands from its front. Each point of depth moves
406
+ * `sin rotY` across and `sin rotX` up — Excel's PDF of its own 3-D columns,
407
+ * at four views and three depths (2026-10-02) — and the box is as deep as a
408
+ * share of one category slot, the slot a share of the front plane the depth
409
+ * leaves: solved for, so box and front fill the plot between them. Never so
410
+ * deep that the front plane is left with no height (or width) across.
411
+ *
412
+ * @returns The offset from the front plane's corner to the back wall's.
413
+ */
414
+ function boxDepth(d, boxW, boxH, nCats, horizontal) {
415
+ const k = d.perSlot / Math.max(1, nCats);
416
+ const along = horizontal ? boxH / (1 + k * Math.abs(d.sinX)) : boxW / (1 + k * Math.abs(d.sinY));
417
+ const across = Math.abs(horizontal ? d.sinY : d.sinX);
418
+ const room = (horizontal ? boxW : boxH) * .9;
419
+ const t = across > 0 ? Math.min(k * along, room / across) : k * along;
420
+ return {
421
+ dx: t * d.sinY,
422
+ dy: t * d.sinX
423
+ };
424
+ }
425
+ /** The light grey Excel rules a 3-D chart's floor in where it states none. */
426
+ var FLOOR_RULE = {
427
+ colorHex: GRID_COLOR,
428
+ widthPt: .75
429
+ };
430
+ /**
431
+ * §21.2.2.69/§21.2.2.11/§21.2.2.191 — a 3-D box's floor, back wall and side
432
+ * wall, as the chart fills and rules them; the floor ruled in light grey
433
+ * unless the chart says otherwise, the walls drawn only as stated. Which wall
434
+ * is which is as Excel's PDF draws them (2026-10-02): columns stand on the
435
+ * bottom, which a view from below does not show, and their side wall is the
436
+ * one the view looks into — the left, turned right; bars stand on the side
437
+ * the view looks into — the left, turned right — and their side wall is the
438
+ * bottom, whatever the view.
439
+ */
440
+ function pushBoxWalls(walls, chart, f, d, horizontal) {
441
+ const back = sideOf(f, d, "back");
442
+ const turnedRight = d.dx >= 0;
443
+ const floor = horizontal ? sideOf(f, d, turnedRight ? "left" : "right") : d.dy >= 0 ? sideOf(f, d, "bottom") : void 0;
444
+ const side = horizontal ? sideOf(f, d, "bottom") : sideOf(f, d, turnedRight ? "left" : "right");
445
+ const surface = (points, fillHex, rule) => {
446
+ const stroke = rule && !rule.none && rule.colorHex ? rule : void 0;
447
+ if (!points || !fillHex && !stroke) return;
448
+ walls.push({
449
+ points,
450
+ ...fillHex ? { fillHex } : {},
451
+ ...stroke ? {
452
+ strokeHex: stroke.colorHex,
453
+ widthPt: stroke.widthPt ?? .75
454
+ } : {}
455
+ });
456
+ };
457
+ surface(back, chart.backWall?.fillHex, chart.backWall?.line);
458
+ surface(side, chart.sideWall?.fillHex, chart.sideWall?.line);
459
+ surface(floor, chart.floor?.fillHex, chart.floor ? chart.floor.line : FLOOR_RULE);
460
+ }
461
+ /** One face of a 3-D box: its back wall, or the wall along one side of its front. */
462
+ function sideOf(f, d, which) {
463
+ const { x0, y0, plotW, plotH } = f;
464
+ if (which === "back") return [
465
+ [x0 + d.dx, y0 + d.dy],
466
+ [x0 + plotW + d.dx, y0 + d.dy],
467
+ [x0 + plotW + d.dx, y0 + plotH + d.dy],
468
+ [x0 + d.dx, y0 + plotH + d.dy]
469
+ ];
470
+ if (which === "bottom") return [
471
+ [x0, y0],
472
+ [x0 + plotW, y0],
473
+ [x0 + plotW + d.dx, y0 + d.dy],
474
+ [x0 + d.dx, y0 + d.dy]
475
+ ];
476
+ const x = which === "left" ? x0 : x0 + plotW;
477
+ return [
478
+ [x, y0],
479
+ [x + d.dx, y0 + d.dy],
480
+ [x + d.dx, y0 + plotH + d.dy],
481
+ [x, y0 + plotH]
482
+ ];
483
+ }
484
+ /**
485
+ * A 3-D box's major gridlines: each value's line across the side wall from
486
+ * the front plane and on along the back wall, as Excel rules them — a bar
487
+ * chart's across its bottom whatever the view.
488
+ *
489
+ * @param offsets Each tick's distance along the value axis from its start.
490
+ */
491
+ function pushBoxGridlines(gridlines, grid, offsets, f, d, horizontal) {
492
+ if (grid?.none) return;
493
+ const rule = {
494
+ strokeHex: grid?.colorHex ?? GRID_COLOR,
495
+ widthPt: grid?.widthPt ?? .75
496
+ };
497
+ const { x0, y0, plotW, plotH } = f;
498
+ for (const off of offsets) if (horizontal) {
499
+ const gx = x0 + off;
500
+ gridlines.push({
501
+ points: [
502
+ [gx, y0],
503
+ [gx + d.dx, y0 + d.dy],
504
+ [gx + d.dx, y0 + plotH + d.dy]
505
+ ],
506
+ ...rule
507
+ });
508
+ } else {
509
+ const gy = y0 + off;
510
+ gridlines.push({
511
+ points: d.dx >= 0 ? [
512
+ [x0, gy],
513
+ [x0 + d.dx, gy + d.dy],
514
+ [x0 + plotW + d.dx, gy + d.dy]
515
+ ] : [
516
+ [x0 + d.dx, gy + d.dy],
517
+ [x0 + plotW + d.dx, gy + d.dy],
518
+ [x0 + plotW, gy]
519
+ ],
520
+ ...rule
521
+ });
522
+ }
523
+ }
337
524
  function buildFrame(chart, wPt, hPt, measure, horizontal, opts = {}) {
338
525
  const rects = [];
339
526
  const polylines = [];
@@ -347,7 +534,7 @@ function buildFrame(chart, wPt, hPt, measure, horizontal, opts = {}) {
347
534
  min: dataMin,
348
535
  max: dataMax,
349
536
  step: niceScale(dataMin, dataMax, intervalsThatFit(horizontal ? wPt : hPt, horizontal)).step
350
- } : axisScale(chart, dataMin, dataMax, horizontal ? wPt : hPt, horizontal);
537
+ } : axisScale(chart, dataMin, dataMax, horizontal ? wPt : hPt, horizontal, opts.unpadded ? 0 : .05);
351
538
  const fmtVal = opts.formatValue ?? formatTick;
352
539
  const tickVals = ticks(scale);
353
540
  const range2 = extentOf(onSecondary.flatMap((s) => s.values.slice(0, nCats)));
@@ -355,36 +542,56 @@ function buildFrame(chart, wPt, hPt, measure, horizontal, opts = {}) {
355
542
  const tickVals2 = scale2 ? ticks(scale2) : [];
356
543
  const crosses = chart.catAxisCrosses;
357
544
  const crossValue = Math.min(Math.max(crosses === "min" ? scale.min : crosses === "max" ? scale.max : crosses ?? 0, scale.min), scale.max);
358
- const labelsAt = chart.catTickLabelPos === "none" ? "none" : chart.catTickLabelPos === "low" ? "start" : chart.catTickLabelPos === "high" ? "end" : crossValue <= scale.min ? "start" : crossValue >= scale.max ? "end" : "cross";
545
+ const fromBelow = (opts.depth?.sinX ?? 0) < 0;
546
+ const labelsAt = chart.catAxisDeleted === true || chart.catTickLabelPos === "none" || fromBelow && !horizontal ? "none" : chart.catTickLabelPos === "low" ? "start" : chart.catTickLabelPos === "high" ? "end" : crossValue <= scale.min ? "start" : crossValue >= scale.max ? "end" : "cross";
359
547
  const catText = (c) => chart.categories[c] ?? String(c + 1);
548
+ const catFace = faceOf(chart, "catAxis");
549
+ const valFace = faceOf(chart, "valAxis");
550
+ const val2Face = faceOf(chart, "secondaryValAxis");
360
551
  let widestCat = 0;
361
- for (let c = 0; c < nCats; c++) widestCat = Math.max(widestCat, measure(catText(c), 9));
362
- const catBand = 9 * 1.6;
552
+ for (let c = 0; c < nCats; c++) widestCat = Math.max(widestCat, widthIn(measure, catText(c), catFace));
553
+ const catBand = catFace.sizePt * 1.6;
363
554
  const groupLevels = horizontal ? [] : chart.categoryGroups ?? [];
364
555
  const valAtEnd = chart.catAxisReversed === true ? chart.valAxisCrosses !== "max" : chart.valAxisCrosses === "max";
556
+ const valLabelled = chart.valAxisDeleted !== true && !(fromBelow && horizontal);
365
557
  const title = titleLines(chart, wPt, measure);
366
- const top = 4 + titleHeight(title) + (!horizontal && labelsAt === "end" ? catBand : 0) + (horizontal && valAtEnd ? catBand : 0);
558
+ const top = 4 + titleHeight(title, chart) + (!horizontal && labelsAt === "end" ? catBand : 0) + (horizontal && valAtEnd && valLabelled ? catBand : 0);
367
559
  const legend = buildLegendBlock(chart, wPt, hPt, measure);
368
- const tick2W = scale2 && !horizontal ? Math.max(0, ...tickVals2.map((v) => measure(formatTick(v), 9))) + 4 + (chart.secondaryValAxisTitle ? 9 * 1.5 : 0) : 0;
369
- const tickHalf = (v) => horizontal && v !== void 0 ? measure(fmtVal(v), 9) / 2 : 0;
560
+ const tick2W = scale2 && !horizontal ? Math.max(0, ...tickVals2.map((v) => widthIn(measure, formatTick(v), val2Face))) + 4 + (chart.secondaryValAxisTitle ? faceOf(chart, "secondaryValAxisTitle").sizePt * 1.5 : 0) : 0;
561
+ const tickHalf = (v) => horizontal && valLabelled && v !== void 0 ? widthIn(measure, fmtVal(v), valFace) / 2 : 0;
370
562
  const lastTickHalf = tickHalf(tickVals[tickVals.length - 1]);
371
563
  const catLabelW = Math.min(wPt * .4, widestCat + 6);
372
- const tickLabelW = Math.max(0, ...tickVals.map((v) => measure(fmtVal(v), 9))) + 4;
564
+ const tickLabelW = valLabelled ? Math.max(0, ...tickVals.map((v) => widthIn(measure, fmtVal(v), valFace))) + 4 : 0;
373
565
  const plotRight = wPt - 4 - legend.rightWidth - tick2W - (horizontal && labelsAt === "end" ? catLabelW : lastTickHalf) - (!horizontal && valAtEnd ? tickLabelW : 0);
374
- const axisTitleBand = 9 * 1.5;
375
566
  const leftTitle = horizontal ? chart.catAxisTitle : chart.valAxisTitle;
376
567
  const footTitle = horizontal ? chart.valAxisTitle : chart.catAxisTitle;
377
- const x0 = 4 + (leftTitle ? axisTitleBand : 0) + (horizontal ? labelsAt === "start" ? catLabelW : tickHalf(tickVals[0]) : valAtEnd ? 0 : tickLabelW);
378
- const y0 = 4 + legend.bottomHeight + (footTitle ? axisTitleBand : 0) + ((horizontal ? !valAtEnd : labelsAt === "start") ? catBand : 0) + (!horizontal && labelsAt === "start" ? groupLevels.length * catBand : 0);
379
- const plotW = Math.max(1, plotRight - x0);
380
- const plotH = Math.max(1, hPt - top - y0);
568
+ const leftTitleFace = faceOf(chart, horizontal ? "catAxisTitle" : "valAxisTitle");
569
+ const footTitleFace = faceOf(chart, horizontal ? "valAxisTitle" : "catAxisTitle");
570
+ const own = innerPlot(chart, wPt, hPt);
571
+ const boxX0 = own?.x0 ?? 4 + (leftTitle ? leftTitleFace.sizePt * 1.5 : 0) + (horizontal ? labelsAt === "start" ? catLabelW : tickHalf(tickVals[0]) : valAtEnd ? 0 : tickLabelW);
572
+ const boxY0 = own?.y0 ?? 4 + legend.bottomHeight + (footTitle ? footTitleFace.sizePt * 1.5 : 0) + ((horizontal ? !valAtEnd && valLabelled : labelsAt === "start") ? catBand : 0) + (!horizontal && labelsAt === "start" ? groupLevels.length * catBand : 0);
573
+ const boxW = own?.plotW ?? Math.max(1, plotRight - boxX0);
574
+ const boxH = own?.plotH ?? Math.max(1, hPt - top - boxY0);
575
+ const depth = opts.depth ? boxDepth(opts.depth, boxW, boxH, nCats, horizontal) : void 0;
576
+ const x0 = boxX0 + Math.max(0, -(depth?.dx ?? 0));
577
+ const y0 = boxY0 + Math.max(0, -(depth?.dy ?? 0));
578
+ const plotW = Math.max(1, boxW - Math.abs(depth?.dx ?? 0));
579
+ const plotH = Math.max(1, boxH - Math.abs(depth?.dy ?? 0));
580
+ const upright = depth && depth.dx < 0 ? depth : {
581
+ dx: 0,
582
+ dy: 0
583
+ };
584
+ const edge = horizontal ? {
585
+ dx: 0,
586
+ dy: 0
587
+ } : upright;
381
588
  const valueOffset = (v) => fractionOf(v, scale) * (horizontal ? plotW : plotH);
382
589
  const zeroOffset = valueOffset(crossValue);
383
590
  const plotBackground = chart.plotFillHex ?? chart.plotLine?.colorHex ? {
384
- x: x0,
385
- y: y0,
386
- w: plotW,
387
- h: plotH,
591
+ x: boxX0,
592
+ y: boxY0,
593
+ w: boxW,
594
+ h: boxH,
388
595
  ...chart.plotFillHex ? { fillHex: chart.plotFillHex } : {},
389
596
  ...chart.plotLine?.colorHex ? {
390
597
  strokeHex: chart.plotLine.colorHex,
@@ -392,13 +599,12 @@ function buildFrame(chart, wPt, hPt, measure, horizontal, opts = {}) {
392
599
  ...chart.plotLine.dash ? { strokeDash: chart.plotLine.dash } : {}
393
600
  } : {}
394
601
  } : void 0;
395
- pushChartTitle(labels, title, wPt, hPt);
602
+ pushChartTitle(labels, title, wPt, hPt, chart);
396
603
  if (leftTitle) labels.push({
397
604
  text: leftTitle,
398
- x: 12.1,
605
+ x: 4 + leftTitleFace.sizePt * .9,
399
606
  y: y0 + plotH / 2,
400
- sizePt: 9,
401
- colorHex: LABEL_COLOR,
607
+ ...leftTitleFace,
402
608
  align: "center",
403
609
  rotationDeg: 90
404
610
  });
@@ -406,20 +612,30 @@ function buildFrame(chart, wPt, hPt, measure, horizontal, opts = {}) {
406
612
  text: footTitle,
407
613
  x: x0 + plotW / 2,
408
614
  y: legend.bottomHeight + 2,
409
- sizePt: 9,
410
- colorHex: LABEL_COLOR,
615
+ ...footTitleFace,
411
616
  align: "center"
412
617
  });
413
- if (horizontal) pushGridTicks(gridlines, labels, tickVals, fmtVal, "x", (v) => x0 + valueOffset(v), x0, y0, plotW, plotH, chart.gridLine, valAtEnd);
414
- else pushGridTicks(gridlines, labels, tickVals, fmtVal, "y", (v) => y0 + valueOffset(v), x0, y0, plotW, plotH, chart.gridLine, valAtEnd);
618
+ const flatGrid = depth ? { none: true } : chart.gridLine;
619
+ if (horizontal) pushGridTicks(gridlines, labels, tickVals, fmtVal, "x", (v) => x0 + edge.dx + valueOffset(v), x0 + edge.dx, y0 + edge.dy, plotW, plotH, flatGrid, valFace, valAtEnd, valLabelled);
620
+ else pushGridTicks(gridlines, labels, tickVals, fmtVal, "y", (v) => y0 + edge.dy + valueOffset(v), x0 + edge.dx, y0 + edge.dy, plotW, plotH, flatGrid, valFace, valAtEnd, valLabelled);
621
+ const walls = [];
622
+ if (depth) {
623
+ const front = {
624
+ x0,
625
+ y0,
626
+ plotW,
627
+ plotH
628
+ };
629
+ pushBoxWalls(walls, chart, front, depth, horizontal);
630
+ pushBoxGridlines(gridlines, chart.gridLine, tickVals.map((v) => valueOffset(v)), front, depth, horizontal);
631
+ }
415
632
  const valueOffset2 = scale2 && !horizontal ? (v) => fractionOf(v, scale2) * plotH : void 0;
416
633
  if (scale2 && valueOffset2) {
417
634
  for (const v of tickVals2) labels.push({
418
635
  text: formatTick(v),
419
636
  x: x0 + plotW + 3,
420
- y: y0 + valueOffset2(v) - 9 / 3,
421
- sizePt: 9,
422
- colorHex: LABEL_COLOR,
637
+ y: y0 + valueOffset2(v) - val2Face.sizePt / 3,
638
+ ...val2Face,
423
639
  align: "left"
424
640
  });
425
641
  const sec = axisStroke(chart.secondaryValAxisLine);
@@ -432,42 +648,41 @@ function buildFrame(chart, wPt, hPt, measure, horizontal, opts = {}) {
432
648
  text: chart.secondaryValAxisTitle,
433
649
  x: wPt - 4,
434
650
  y: y0 + plotH / 2,
435
- sizePt: 9,
436
- colorHex: LABEL_COLOR,
651
+ ...faceOf(chart, "secondaryValAxisTitle"),
437
652
  align: "center",
438
653
  rotationDeg: 90
439
654
  });
440
655
  }
441
656
  const slot = (horizontal ? plotH : plotW) / nCats;
442
- const need = horizontal ? 9 * 1.4 : widestCat + 4;
657
+ const need = horizontal ? catFace.sizePt * 1.4 : widestCat + 4;
443
658
  const step = Math.max(1, Math.ceil(need / Math.max(slot, .01)));
444
659
  const across = labelsAt === "start" ? 0 : labelsAt === "end" ? horizontal ? plotW : plotH : zeroOffset;
445
660
  const labelled = [];
446
- if (labelsAt !== "none") if (chart.catAxisReversed === true) for (let c = nCats - 1; c >= 0; c -= step) labelled.push(c);
447
- else for (let c = 0; c < nCats; c += step) labelled.push(c);
661
+ if (labelsAt !== "none") {
662
+ if (chart.catAxisReversed === true) for (let c = nCats - 1; c >= 0; c -= step) labelled.push(c);
663
+ else for (let c = 0; c < nCats; c += step) labelled.push(c);
664
+ }
448
665
  for (const c of labelled) {
449
666
  const cat = catText(c);
450
667
  if (!cat) continue;
451
668
  const center = (horizontal ? y0 : x0) + c * slot + slot / 2;
452
669
  if (horizontal) labels.push({
453
670
  text: cat,
454
- x: labelsAt === "end" ? x0 + across + 3 : x0 + across - 3,
455
- y: center - 9 / 3,
456
- sizePt: 9,
457
- colorHex: LABEL_COLOR,
671
+ x: (labelsAt === "end" ? x0 + across + 3 : x0 + across - 3) + upright.dx,
672
+ y: center - catFace.sizePt / 3 + upright.dy,
673
+ ...catFace,
458
674
  align: labelsAt === "end" ? "left" : "right"
459
675
  });
460
676
  else labels.push({
461
677
  text: cat,
462
678
  x: center,
463
- y: labelsAt === "end" ? y0 + across + 9 * .4 : y0 + across - 9,
464
- sizePt: 9,
465
- colorHex: LABEL_COLOR,
679
+ y: labelsAt === "end" ? y0 + across + catFace.sizePt * .4 : y0 + across - catFace.sizePt,
680
+ ...catFace,
466
681
  align: "center"
467
682
  });
468
683
  }
469
684
  if (labelsAt !== "none" && labelsAt !== "end") groupLevels.forEach((groups, level) => {
470
- const rowY = y0 + across - 9 - (level + 1) * catBand;
685
+ const rowY = y0 + across - catFace.sizePt - (level + 1) * catBand;
471
686
  groups.forEach((group, i) => {
472
687
  const end = Math.min(nCats, groups[i + 1]?.start ?? nCats);
473
688
  if (end <= group.start) return;
@@ -475,24 +690,26 @@ function buildFrame(chart, wPt, hPt, measure, horizontal, opts = {}) {
475
690
  text: group.label,
476
691
  x: x0 + (group.start + end) / 2 * slot,
477
692
  y: rowY,
478
- sizePt: 9,
479
- colorHex: LABEL_COLOR,
693
+ ...catFace,
480
694
  align: "center"
481
695
  });
482
696
  if (i > 0) polylines.push({
483
- points: [[x0 + group.start * slot, y0 + across], [x0 + group.start * slot, rowY - 9 * .4]],
697
+ points: [[x0 + group.start * slot, y0 + across], [x0 + group.start * slot, rowY - catFace.sizePt * .4]],
484
698
  strokeHex: GRID_COLOR,
485
699
  widthPt: .75
486
700
  });
487
701
  });
488
702
  });
489
- pushAxisLines(polylines, x0, y0, plotW, plotH, chart, horizontal ? {
703
+ pushAxisLines(polylines, x0, y0, plotW, plotH, fromBelow ? {
704
+ ...chart,
705
+ catAxisLine: { none: true }
706
+ } : chart, horizontal ? {
490
707
  x: zeroOffset,
491
708
  ...valAtEnd ? { y: plotH } : {}
492
709
  } : {
493
710
  y: zeroOffset,
494
711
  ...valAtEnd ? { x: plotW } : {}
495
- });
712
+ }, { upright });
496
713
  legend.emit(rects, labels);
497
714
  return {
498
715
  x0,
@@ -509,7 +726,11 @@ function buildFrame(chart, wPt, hPt, measure, horizontal, opts = {}) {
509
726
  polylines,
510
727
  gridlines,
511
728
  ...plotBackground ? { plotBackground } : {},
512
- labels
729
+ labels,
730
+ ...depth ? {
731
+ depth,
732
+ walls
733
+ } : {}
513
734
  };
514
735
  }
515
736
  var pctLabel = (v) => `${Math.round(v * 100)}%`;
@@ -576,15 +797,72 @@ function groupingFrameOpts(chart, nCats) {
576
797
  }
577
798
  var CHART_FORMAT_ID = 1e6;
578
799
  var formatterCache = /* @__PURE__ */ new WeakMap();
800
+ /**
801
+ * A tick formatter for a number format code — made per scene and not kept: a
802
+ * cache by code would grow with every code every document ever named.
803
+ */
804
+ function formatterOf(code) {
805
+ if (code === void 0) return void 0;
806
+ const formats = /* @__PURE__ */ new Map([[CHART_FORMAT_ID, code]]);
807
+ return (v) => applyNumberFormat(String(v), CHART_FORMAT_ID, formats);
808
+ }
579
809
  function chartValueFormatter(chart) {
580
810
  const hit = formatterCache.get(chart);
581
811
  if (hit !== void 0) return hit ?? void 0;
582
812
  const code = chart.numberFormat;
583
- const made = code === void 0 ? null : ((formats) => (v) => applyNumberFormat(String(v), CHART_FORMAT_ID, formats))(new Map([[CHART_FORMAT_ID, code]]));
813
+ const made = code === void 0 ? null : ((formats) => (v) => applyNumberFormat(String(v), CHART_FORMAT_ID, formats))(/* @__PURE__ */ new Map([[CHART_FORMAT_ID, code]]));
584
814
  formatterCache.set(chart, made);
585
815
  return made ?? void 0;
586
816
  }
587
817
  /**
818
+ * Whole percentages that add up to a hundred, as Excel labels a pie's slices:
819
+ * each share's floor, and the points still missing given to the slices with
820
+ * the largest remainders — 10, 20 and 15 of 45 read 22%, 45% and 33% in
821
+ * Excel's PDF, where rounding each one reads 22, 44 and 33.
822
+ *
823
+ * @param values The slices' values, a slice that is not positive drawing none.
824
+ * @param total Their sum.
825
+ * @returns Each slice's whole percentage.
826
+ */
827
+ function wholePercents(values, total) {
828
+ const exact = values.map((v) => v > 0 && total > 0 ? v / total * 100 : 0);
829
+ const out = exact.map((e) => Math.floor(e));
830
+ let missing = 100 - out.reduce((a, b) => a + b, 0);
831
+ const byRemainder = exact.map((e, i) => ({
832
+ i,
833
+ rest: e - (out[i] ?? 0)
834
+ })).filter(({ i }) => (values[i] ?? 0) > 0).sort((a, b) => b.rest - a.rest || a.i - b.i);
835
+ for (const { i } of byRemainder) {
836
+ if (missing <= 0) break;
837
+ out[i] = (out[i] ?? 0) + 1;
838
+ missing--;
839
+ }
840
+ return out;
841
+ }
842
+ /**
843
+ * §21.2.2.49 — the text a slice's data label shows: the parts the chart's
844
+ * switches turn on, in Excel's order — series name, category name, value,
845
+ * percentage — joined by its separator, ", " where it states none. The
846
+ * labels' own number format is the value's where the value is shown, and the
847
+ * percentage's where only that is; a percentage is otherwise whole.
848
+ * LIBRE_OFFICE-100610-0.pptx's five pies show the 4, 6, 3 and 6 they ask for,
849
+ * 45540_classic_Footer.xlsx its functions' names over their shares, and a pie
850
+ * whose labels show nothing — or that has none — shows nothing, as Excel's.
851
+ *
852
+ * @returns The label, or undefined when the chart labels nothing.
853
+ */
854
+ function sliceLabel(chart, series, i, v, total, percent) {
855
+ const dl = chart.dataLabels;
856
+ if (!dl) return void 0;
857
+ const own = formatterOf(dl.numberFormat);
858
+ const parts = [];
859
+ if (dl.showSerName) parts.push(series.name ?? legendSeriesName(series, 0));
860
+ if (dl.showCatName) parts.push(chart.categories[i] ?? String(i + 1));
861
+ if (dl.showVal) parts.push(own ? own(v) : fmtDataLabel(chart, v));
862
+ if (dl.showPercent) parts.push(own && !dl.showVal ? own(v / total) : `${String(percent)}%`);
863
+ return parts.length > 0 ? parts.join(dl.separator ?? ", ") : void 0;
864
+ }
865
+ /**
588
866
  * Lay out a bar/column {@link Chart} into a {@link ChartScene}. Honours
589
867
  * `chart.grouping` (clustered / stacked / percentStacked) and `chart.barDir`
590
868
  * (column vs horizontal bar), over the shared cartesian frame.
@@ -618,13 +896,39 @@ function buildBarScene(chart, wPt, hPt, measure) {
618
896
  const stacked = g === "stacked" || g === "percentStacked";
619
897
  const percent = g === "percentStacked";
620
898
  const horizontal = chart.barDir === "bar";
621
- const f = buildFrame(chart, wPt, hPt, measure, horizontal, groupingFrameOpts(chart, catCount(chart)));
899
+ const nCats = catCount(chart);
622
900
  const barIdx = [];
623
901
  const lineIdx = [];
624
902
  chart.series.forEach((s, i) => (isLineLike(s) ? lineIdx : barIdx).push(i));
903
+ const nSer = Math.max(1, barIdx.length);
904
+ const gap = (chart.gapPercent ?? 150) / 100;
905
+ const overlap = (chart.overlapPercent ?? 0) / 100;
906
+ const barShare = 1 / (stacked ? 1 + gap : nSer - (nSer - 1) * overlap + gap);
907
+ const view = chart.bar3D;
908
+ const sinX = view ? Math.sin(view.rotX * Math.PI / 180) : 0;
909
+ const sinY = view ? Math.sin(view.rotY * Math.PI / 180) : 0;
910
+ const seen = view !== void 0 && (Math.abs(sinX) > 1e-9 || Math.abs(sinY) > 1e-9);
911
+ const f = buildFrame(chart, wPt, hPt, measure, horizontal, {
912
+ ...groupingFrameOpts(chart, nCats),
913
+ ...view ? { unpadded: true } : {},
914
+ ...view && seen ? { depth: {
915
+ perSlot: barShare * (view.depthPercent / 100) * (1 + view.gapDepth / 100),
916
+ sinX,
917
+ sinY
918
+ } } : {}
919
+ });
920
+ const barW = f.slot * barShare;
921
+ const boxes = [];
922
+ const put = (rect, lift = [0, 0]) => {
923
+ if (f.depth) boxes.push({
924
+ rect,
925
+ lift
926
+ });
927
+ else f.rects.push(rect);
928
+ };
929
+ const values = [];
625
930
  if (stacked) {
626
- const groupPad = f.slot * .15;
627
- const barW = f.slot - 2 * groupPad;
931
+ const groupPad = (f.slot - barW) / 2;
628
932
  for (let c = 0; c < f.nCats; c++) {
629
933
  const along = (horizontal ? f.y0 : f.x0) + c * f.slot + groupPad;
630
934
  const denom = percent ? barIdx.reduce((a, i) => a + Math.abs(chart.series[i].values[c] ?? 0), 0) || 1 : 1;
@@ -641,7 +945,7 @@ function buildBarScene(chart, wPt, hPt, measure) {
641
945
  const span = Math.abs(o1 - o0);
642
946
  const color = pointColor(series, c) ?? seriesColor(series, s, chart.seriesColorCycle);
643
947
  const outline = barOutline(series);
644
- if (horizontal) f.rects.push({
948
+ if (horizontal) put({
645
949
  x: f.x0 + lo,
646
950
  y: along,
647
951
  w: span,
@@ -649,7 +953,7 @@ function buildBarScene(chart, wPt, hPt, measure) {
649
953
  fillHex: color,
650
954
  ...outline
651
955
  });
652
- else f.rects.push({
956
+ else put({
653
957
  x: along,
654
958
  y: f.y0 + lo,
655
959
  w: barW,
@@ -657,90 +961,170 @@ function buildBarScene(chart, wPt, hPt, measure) {
657
961
  fillHex: color,
658
962
  ...outline
659
963
  });
660
- if (chart.showValues && span > 9) {
964
+ if (chart.showValues && span > faceOf(chart, "dataLabels").sizePt) {
661
965
  const raw = series.values[c] ?? 0;
662
- if (horizontal) f.labels.push(centeredLabel(fmtDataLabel(chart, raw), f.x0 + lo + span / 2, along + barW * .3));
663
- else f.labels.push(centeredLabel(fmtDataLabel(chart, raw), along + barW / 2, f.y0 + lo + span / 2 - 3));
966
+ const face = faceOf(chart, "dataLabels");
967
+ values.push({
968
+ label: horizontal ? centeredLabel(fmtDataLabel(chart, raw), f.x0 + lo + span / 2, f.depth ? along + barW / 2 - face.sizePt * .35 : along + barW * .3, face) : centeredLabel(fmtDataLabel(chart, raw), along + barW / 2, f.y0 + lo + span / 2 - 3, face),
969
+ lift: [0, 0]
970
+ });
664
971
  }
665
972
  if (v >= 0) cumPos = top;
666
973
  else cumNeg = top;
667
974
  }
668
975
  }
669
- pushComboLines(chart, f, lineIdx);
670
- return {
671
- rects: f.rects,
672
- polylines: f.polylines,
673
- gridlines: f.gridlines,
674
- ...f.plotBackground ? { plotBackground: f.plotBackground } : {},
675
- ...f.plotBackground ? { plotBackground: f.plotBackground } : {},
676
- wedges: [],
677
- labels: f.labels
678
- };
679
- }
680
- const nSer = Math.max(1, barIdx.length);
681
- const gap = (chart.gapPercent ?? 150) / 100;
682
- const barW = f.slot / (nSer + gap);
683
- const groupPad = (f.slot - barW * nSer) / 2;
684
- for (let c = 0; c < f.nCats; c++) {
685
- const slotStart = (horizontal ? f.y0 : f.x0) + c * f.slot + groupPad;
686
- for (let b = 0; b < barIdx.length; b++) {
687
- const s = barIdx[b];
688
- const series = chart.series[s];
689
- const len = f.valueOffset(series.values[c] ?? 0) - f.zeroOffset;
690
- const color = pointColor(series, c) ?? seriesColor(series, s, chart.seriesColorCycle);
691
- const along = slotStart + b * barW;
692
- const outline = barOutline(series);
693
- if (horizontal) {
694
- const bx = f.x0 + f.zeroOffset + Math.min(0, len);
695
- f.rects.push({
696
- x: bx,
976
+ } else {
977
+ const pitch = barW * (1 - overlap);
978
+ const groupPad = (f.slot - barW - (nSer - 1) * pitch) / 2;
979
+ for (let c = 0; c < f.nCats; c++) {
980
+ const slotStart = (horizontal ? f.y0 : f.x0) + c * f.slot + groupPad;
981
+ for (let b = 0; b < barIdx.length; b++) {
982
+ const s = barIdx[b];
983
+ const series = chart.series[s];
984
+ const len = f.valueOffset(series.values[c] ?? 0) - f.zeroOffset;
985
+ const color = pointColor(series, c) ?? seriesColor(series, s, chart.seriesColorCycle);
986
+ const along = slotStart + b * pitch;
987
+ const outline = barOutline(series);
988
+ if (horizontal) put({
989
+ x: f.x0 + f.zeroOffset + Math.min(0, len),
697
990
  y: along,
698
991
  w: Math.abs(len),
699
992
  h: barW,
700
993
  fillHex: color,
701
994
  ...outline
702
995
  });
703
- } else {
704
- const by = f.y0 + f.zeroOffset + Math.min(0, len);
705
- f.rects.push({
996
+ else put({
706
997
  x: along,
707
- y: by,
998
+ y: f.y0 + f.zeroOffset + Math.min(0, len),
708
999
  w: barW,
709
1000
  h: Math.abs(len),
710
1001
  fillHex: color,
711
1002
  ...outline
712
1003
  });
713
- }
714
- if (chart.showValues) {
715
- const txt = fmtDataLabel(chart, series.values[c] ?? 0);
716
- if (horizontal) {
717
- const end = f.x0 + f.zeroOffset + len;
718
- f.labels.push({
719
- text: txt,
720
- x: end + (len >= 0 ? 3 : -3),
721
- y: along + barW * .25,
722
- sizePt: 9,
723
- colorHex: LABEL_COLOR,
724
- align: len >= 0 ? "left" : "right"
725
- });
726
- } else {
727
- const end = f.y0 + f.zeroOffset + len;
728
- f.labels.push(centeredLabel(txt, along + barW * .45, len >= 0 ? end + 2 : end - 9));
1004
+ if (chart.showValues) {
1005
+ const txt = fmtDataLabel(chart, series.values[c] ?? 0);
1006
+ const face = faceOf(chart, "dataLabels");
1007
+ if (horizontal) {
1008
+ const end = f.x0 + f.zeroOffset + len;
1009
+ values.push({
1010
+ label: {
1011
+ text: txt,
1012
+ x: end + (len >= 0 ? 3 : -3),
1013
+ y: f.depth ? along + barW / 2 - face.sizePt * .35 : along + barW * .25,
1014
+ ...face,
1015
+ align: len >= 0 ? "left" : "right"
1016
+ },
1017
+ lift: [len >= 0 ? .5 : 0, 0]
1018
+ });
1019
+ } else {
1020
+ const end = f.y0 + f.zeroOffset + len;
1021
+ const above = f.depth ? end - face.sizePt * .35 : end + 2;
1022
+ values.push({
1023
+ label: centeredLabel(txt, along + barW * .45, len >= 0 ? above : end - face.sizePt, face),
1024
+ lift: [0, len >= 0 ? 1 : 0]
1025
+ });
1026
+ }
729
1027
  }
730
1028
  }
731
1029
  }
732
1030
  }
1031
+ const polygons = [];
1032
+ let shift = {
1033
+ dx: 0,
1034
+ dy: 0
1035
+ };
1036
+ let own = {
1037
+ dx: 0,
1038
+ dy: 0
1039
+ };
1040
+ if (f.depth && view) {
1041
+ const share = 1 / (1 + view.gapDepth / 100);
1042
+ own = {
1043
+ dx: f.depth.dx * share,
1044
+ dy: f.depth.dy * share
1045
+ };
1046
+ const back = view.gapDepth / 200;
1047
+ shift = {
1048
+ dx: own.dx * back,
1049
+ dy: own.dy * back
1050
+ };
1051
+ const across = (r) => horizontal ? r.y + r.h / 2 : r.x + r.w / 2;
1052
+ const up = (r) => horizontal ? r.x + r.w / 2 : r.y + r.h / 2;
1053
+ const first = Math.sign(horizontal ? sinX : sinY) || 1;
1054
+ const then = Math.sign(horizontal ? sinY : sinX) || 1;
1055
+ boxes.sort((a, b) => first * (across(a.rect) - across(b.rect)) || then * (up(a.rect) - up(b.rect)));
1056
+ for (const { rect } of boxes) {
1057
+ const front = {
1058
+ ...rect,
1059
+ x: rect.x + shift.dx,
1060
+ y: rect.y + shift.dy
1061
+ };
1062
+ pushBarFaces(polygons, front, own);
1063
+ f.rects.push(front);
1064
+ }
1065
+ }
1066
+ for (const { label, lift } of values) f.labels.push(f.depth ? {
1067
+ ...label,
1068
+ x: label.x + shift.dx + lift[0] * own.dx,
1069
+ y: label.y + shift.dy + lift[1] * own.dy
1070
+ } : label);
733
1071
  pushComboLines(chart, f, lineIdx);
734
1072
  return {
735
1073
  rects: f.rects,
736
1074
  polylines: f.polylines,
737
1075
  gridlines: f.gridlines,
738
1076
  ...f.plotBackground ? { plotBackground: f.plotBackground } : {},
1077
+ ...f.walls && f.walls.length > 0 ? { walls: f.walls } : {},
1078
+ ...polygons.length > 0 ? { polygons } : {},
739
1079
  wedges: [],
740
1080
  labels: f.labels
741
1081
  };
742
1082
  }
743
1083
  /**
1084
+ * The faces of one 3-D bar a viewer sees beside its front: its top when the
1085
+ * view looks down on it (its bottom when up at it), its right side when the
1086
+ * view is turned to the right (its left when to the left) — each shaded as
1087
+ * Excel's own PDF shades them: a top 0.76 of the bar's colour, a right side
1088
+ * 0.635, a bottom or a left side 0.4 (2026-10-02).
1089
+ *
1090
+ * @param r The bar's front.
1091
+ * @param d Its depth, as an offset from the front.
1092
+ */
1093
+ function pushBarFaces(polygons, r, d) {
1094
+ const fill = r.fillHex ?? "000000";
1095
+ const stroke = r.strokeHex ? {
1096
+ strokeHex: r.strokeHex,
1097
+ widthPt: r.strokeWidthPt ?? .75
1098
+ } : {};
1099
+ const { x, y, w, h } = r;
1100
+ if (d.dy !== 0) {
1101
+ const at = d.dy > 0 ? y + h : y;
1102
+ polygons.push({
1103
+ points: [
1104
+ [x, at],
1105
+ [x + w, at],
1106
+ [x + w + d.dx, at + d.dy],
1107
+ [x + d.dx, at + d.dy]
1108
+ ],
1109
+ fillHex: shadeHex(fill, d.dy > 0 ? .76 : .4),
1110
+ ...stroke
1111
+ });
1112
+ }
1113
+ if (d.dx !== 0) {
1114
+ const at = d.dx > 0 ? x + w : x;
1115
+ polygons.push({
1116
+ points: [
1117
+ [at, y],
1118
+ [at + d.dx, y + d.dy],
1119
+ [at + d.dx, y + h + d.dy],
1120
+ [at, y + h]
1121
+ ],
1122
+ fillHex: shadeHex(fill, d.dx > 0 ? .635 : .4),
1123
+ ...stroke
1124
+ });
1125
+ }
1126
+ }
1127
+ /**
744
1128
  * The name a series shows in the legend: its own `c:tx`, or Excel's positional
745
1129
  * `SeriesN` when it has none. Exported because the SUBSET has to know it too —
746
1130
  * a name invented at draw time is a name no glyph collector ever saw, and
@@ -786,13 +1170,12 @@ function pushComboLines(chart, f, lineIdx) {
786
1170
  }
787
1171
  }
788
1172
  }
789
- function centeredLabel(text, x, y) {
1173
+ function centeredLabel(text, x, y, face) {
790
1174
  return {
791
1175
  text,
792
1176
  x,
793
1177
  y,
794
- sizePt: 9,
795
- colorHex: LABEL_COLOR,
1178
+ ...face,
796
1179
  align: "center"
797
1180
  };
798
1181
  }
@@ -887,22 +1270,53 @@ function buildScatterScene(chart, wPt, hPt, measure) {
887
1270
  };
888
1271
  const [xLo, xHi] = extentOf(xs) ?? [0, 1];
889
1272
  const [yLo, yHi] = extentOf(ys) ?? [0, 1];
890
- const xScale = niceScale(xLo, xHi, intervalsThatFit(wPt, true));
891
- const yScale = niceScale(yLo, yHi, intervalsThatFit(hPt, false));
1273
+ const xScale = scatterScale(xLo, xHi, intervalsThatFit(wPt, true), {
1274
+ min: chart.xAxisMin,
1275
+ max: chart.xAxisMax,
1276
+ unit: chart.xAxisMajorUnit
1277
+ });
1278
+ const yScale = scatterScale(yLo, yHi, intervalsThatFit(hPt, false), {
1279
+ min: chart.valAxisMin,
1280
+ max: chart.valAxisMax,
1281
+ unit: chart.valAxisMajorUnit
1282
+ });
892
1283
  const xTicks = ticks(xScale);
893
1284
  const yTicks = ticks(yScale);
1285
+ const fmtX = formatterOf(chart.xNumberFormat) ?? formatTick;
1286
+ const fmtY = formatterOf(chart.numberFormat) ?? formatTick;
894
1287
  const title = titleLines(chart, wPt, measure);
895
- const top = 4 + titleHeight(title);
1288
+ const top = 4 + titleHeight(title, chart);
896
1289
  const legend = buildLegendBlock(chart, wPt, hPt, measure);
897
- const x0 = 4 + (Math.max(0, ...yTicks.map((v) => measure(formatTick(v), 9))) + 4);
898
- const y0 = 4 + legend.bottomHeight + 9 * 1.6;
899
- const plotW = Math.max(1, wPt - 4 - legend.rightWidth - x0);
900
- const plotH = Math.max(1, hPt - top - y0);
1290
+ const xFace = faceOf(chart, "catAxis");
1291
+ const yFace = faceOf(chart, "valAxis");
1292
+ const yTitleFace = faceOf(chart, "valAxisTitle");
1293
+ const xTitleFace = faceOf(chart, "catAxisTitle");
1294
+ const tickLabelW = chart.valAxisDeleted === true ? 0 : Math.max(0, ...yTicks.map((v) => widthIn(measure, fmtY(v), yFace))) + 4;
1295
+ const own = innerPlot(chart, wPt, hPt);
1296
+ const x0 = own?.x0 ?? 4 + (chart.valAxisTitle ? yTitleFace.sizePt * 1.5 : 0) + tickLabelW;
1297
+ const y0 = own?.y0 ?? 4 + legend.bottomHeight + (chart.catAxisTitle ? xTitleFace.sizePt * 1.5 : 0) + (chart.catAxisDeleted === true ? 0 : xFace.sizePt * 1.6);
1298
+ const plotW = own?.plotW ?? Math.max(1, wPt - 4 - legend.rightWidth - x0);
1299
+ const plotH = own?.plotH ?? Math.max(1, hPt - top - y0);
901
1300
  const xAt = (v) => x0 + fractionOf(v, xScale) * plotW;
902
1301
  const yAt = (v) => y0 + fractionOf(v, yScale) * plotH;
903
- pushChartTitle(labels, title, wPt, hPt);
904
- pushGridTicks(gridlines, labels, yTicks, formatTick, "y", yAt, x0, y0, plotW, plotH, chart.gridLine);
905
- pushGridTicks(gridlines, labels, xTicks, formatTick, "x", xAt, x0, y0, plotW, plotH, chart.gridLine);
1302
+ pushChartTitle(labels, title, wPt, hPt, chart);
1303
+ if (chart.valAxisTitle) labels.push({
1304
+ text: chart.valAxisTitle,
1305
+ x: 4 + yTitleFace.sizePt * .9,
1306
+ y: y0 + plotH / 2,
1307
+ ...yTitleFace,
1308
+ align: "center",
1309
+ rotationDeg: 90
1310
+ });
1311
+ if (chart.catAxisTitle) labels.push({
1312
+ text: chart.catAxisTitle,
1313
+ x: x0 + plotW / 2,
1314
+ y: legend.bottomHeight + 2,
1315
+ ...xTitleFace,
1316
+ align: "center"
1317
+ });
1318
+ pushGridTicks(gridlines, labels, yTicks, fmtY, "y", yAt, x0, y0, plotW, plotH, chart.gridLine, yFace, false, chart.valAxisDeleted !== true);
1319
+ pushGridTicks(gridlines, labels, xTicks, fmtX, "x", xAt, x0, y0, plotW, plotH, chart.xGridLine, xFace, false, chart.catAxisDeleted !== true);
906
1320
  pushAxisLines(polylines, x0, y0, plotW, plotH, chart);
907
1321
  const style = chart.scatterStyle ?? "marker";
908
1322
  const joins = style === "line" || style === "lineMarker" || style.startsWith("smooth");
@@ -1006,7 +1420,7 @@ function buildLineScene(chart, wPt, hPt, measure) {
1006
1420
  const y = f.y0 + f.valueOffset(series.values[c] ?? 0);
1007
1421
  pts.push([x, y]);
1008
1422
  if (chart.lineMarkers) pushMarker(f.rects, wedges, series.marker, x, y, color);
1009
- if (chart.showValues) f.labels.push(centeredLabel(fmtDataLabel(chart, series.values[c] ?? 0), x, y + 3));
1423
+ if (chart.showValues) f.labels.push(centeredLabel(fmtDataLabel(chart, series.values[c] ?? 0), x, y + 3, faceOf(chart, "dataLabels")));
1010
1424
  }
1011
1425
  if (pts.length >= 2) f.polylines.push({
1012
1426
  points: pts,
@@ -1062,63 +1476,344 @@ function buildPieScene(chart, wPt, hPt, measure) {
1062
1476
  labels
1063
1477
  };
1064
1478
  const title = titleLines(chart, wPt, measure);
1065
- const top = 4 + titleHeight(title);
1479
+ const top = 4 + titleHeight(title, chart);
1066
1480
  const legend = layoutLegend(values.map((_, i) => ({
1067
1481
  name: chart.categories[i] ?? String(i + 1),
1068
1482
  colorHex: sliceColor(series, i, chart.seriesColorCycle)
1069
- })), chart.hasLegend, chart.legendPos ?? "r", wPt, hPt, measure);
1070
- const availW = Math.max(1, wPt - 8 - legend.rightWidth);
1071
- const availH = Math.max(1, hPt - top - 4 - legend.bottomHeight);
1072
- const r = Math.max(1, Math.min(availW, availH) / 2 * .95);
1073
- const cx = 4 + availW / 2;
1074
- const cy = 4 + legend.bottomHeight + availH / 2;
1483
+ })), chart.hasLegend, chart.legendPos ?? "r", wPt, hPt, measure, faceOf(chart, "legend"));
1484
+ const plotBox = chart.plotBox;
1485
+ const availW = plotBox ? plotBox.w * wPt : Math.max(1, wPt - 8 - legend.rightWidth);
1486
+ const availH = plotBox ? plotBox.h * hPt : Math.max(1, hPt - top - 4 - legend.bottomHeight);
1487
+ const cx = plotBox ? (plotBox.x + plotBox.w / 2) * wPt : 4 + availW / 2;
1488
+ const cy = plotBox ? hPt - (plotBox.y + plotBox.h / 2) * hPt : 4 + legend.bottomHeight + availH / 2;
1489
+ const disc = chart.pie3D;
1490
+ const elevation = disc ? Math.min(90, Math.max(0, disc.rotX)) * Math.PI / 180 : Math.PI / 2;
1491
+ const tilt = Math.max(Math.sin(elevation), .02);
1492
+ const thickF = disc ? .25 * Math.cos(elevation) ** 2 : 0;
1493
+ const explosionOfSlice = (i) => (atPoint(series.pointExplosions, i)?.percent ?? series.explosion ?? 0) / 100;
1494
+ let maxOut = 0;
1495
+ for (let i = 0; i < values.length; i++) if ((values[i] ?? 0) > 0) maxOut = Math.max(maxOut, explosionOfSlice(i));
1496
+ const fullR = Math.max(1, Math.min(availW / 2 / (1 + maxOut), availH / (2 * tilt * (1 + maxOut) + thickF)) * (plotBox ? 1 : .95));
1497
+ const turn = (disc ? disc.rotY : chart.firstSliceAngle ?? 0) * Math.PI / 180;
1498
+ const sweeps = values.map((v) => v > 0 ? -(v / total) * 2 * Math.PI : 0);
1499
+ const starts = [];
1500
+ let ang = Math.PI / 2 - turn;
1501
+ for (const sweep of sweeps) {
1502
+ starts.push(ang);
1503
+ ang += sweep;
1504
+ }
1505
+ const midOf = (i) => (starts[i] ?? 0) + (sweeps[i] ?? 0) / 2;
1506
+ const boxes = pieLabelBoxes(chart, series, values, total, sweeps, starts, fullR, wPt, measure);
1507
+ let room = fullR;
1508
+ for (const b of plotBox ? [] : boxes) {
1509
+ if (b.where !== "out" || b.placement) continue;
1510
+ const c = Math.abs(Math.cos(b.mid));
1511
+ const sn = Math.abs(Math.sin(b.mid)) * tilt;
1512
+ if (c > .05) room = Math.min(room, (availW / 2 - PIE_LABEL_GAP - b.width) / c);
1513
+ if (sn > .05) room = Math.min(room, (availH / 2 - PIE_LABEL_GAP - b.height) / sn);
1514
+ }
1515
+ const discOf = (rr) => {
1516
+ const thickness = thickF * rr;
1517
+ const cyTop = cy + thickness / 2;
1518
+ const centre = (i) => {
1519
+ const out = explosionOfSlice(i) * rr;
1520
+ const mid = midOf(i);
1521
+ return [cx + Math.cos(mid) * out, cyTop + Math.sin(mid) * out * tilt];
1522
+ };
1523
+ const point = (i, t, along) => {
1524
+ const [x0, y0] = centre(i);
1525
+ return [x0 + Math.cos(t) * along, y0 + Math.sin(t) * along * tilt];
1526
+ };
1527
+ return {
1528
+ thickness,
1529
+ cyTop,
1530
+ centre,
1531
+ point
1532
+ };
1533
+ };
1534
+ const positionOf = (box, pieR) => {
1535
+ const rr = box.placement ? fullR : pieR;
1536
+ const d = discOf(rr);
1537
+ const i = box.index;
1538
+ const cos = Math.cos(box.mid);
1539
+ const sin = Math.sin(box.mid);
1540
+ let bx;
1541
+ let by;
1542
+ if (box.where === "out") {
1543
+ const [px, py0] = d.point(i, box.mid, rr + PIE_LABEL_GAP / Math.max(tilt, .2));
1544
+ const py = sin < 0 ? py0 - d.thickness : py0;
1545
+ bx = px + (cos >= 0 ? box.width / 2 : -box.width / 2);
1546
+ by = py + sin * box.height / 2;
1547
+ } else {
1548
+ const depth = chart.doughnut ? (rr * .5 + rr) / 2 : rr * (box.where === "ctr" ? .5 : box.where === "end" ? .75 : .6);
1549
+ [bx, by] = d.point(i, box.mid, depth);
1550
+ }
1551
+ const moved = box.placement;
1552
+ if (moved?.edge) {
1553
+ if (moved.x !== void 0) bx = moved.x * wPt + box.width / 2;
1554
+ if (moved.y !== void 0) by = hPt - moved.y * hPt - box.height / 2;
1555
+ } else if (moved) {
1556
+ bx += (moved.x ?? 0) * wPt;
1557
+ by -= (moved.y ?? 0) * hPt;
1558
+ }
1559
+ return {
1560
+ bx: Math.min(Math.max(bx, box.width / 2 + 2), wPt - box.width / 2 - 2),
1561
+ by: Math.min(Math.max(by, box.height / 2 + 2), hPt - box.height / 2 - 2)
1562
+ };
1563
+ };
1564
+ const across = (x, y, cyTop) => Math.hypot(x - cx, (y - cyTop) / tilt);
1565
+ let r = Math.max(fullR * .4, room);
1566
+ for (const box of plotBox ? [] : boxes) {
1567
+ if (box.where !== "out" || !box.placement) continue;
1568
+ const { bx, by } = positionOf(box, r);
1569
+ const cyTop = discOf(fullR).cyTop;
1570
+ if (across(bx, by, cyTop) < fullR * .9) continue;
1571
+ const nx = Math.min(Math.max(cx, bx - box.width / 2), bx + box.width / 2);
1572
+ const ny = Math.min(Math.max(cyTop, by - box.height / 2), by + box.height / 2);
1573
+ r = Math.max(fullR * .4, Math.min(r, across(nx, ny, cyTop) - PIE_LABEL_GAP / 2));
1574
+ }
1575
+ const d = discOf(r);
1075
1576
  const holeR = chart.doughnut ? r * .5 : 0;
1076
- const labelR = chart.doughnut ? (holeR + r) / 2 : r * .6;
1077
- let ang = Math.PI / 2;
1078
- for (let i = 0; i < values.length; i++) {
1079
- const v = values[i];
1080
- if (v <= 0) continue;
1081
- const sweep = -(v / total) * 2 * Math.PI;
1082
- wedges.push({
1577
+ const polygons = [];
1578
+ if (disc) {
1579
+ const sides = [];
1580
+ for (let i = 0; i < values.length; i++) {
1581
+ if ((values[i] ?? 0) <= 0) continue;
1582
+ const fill = shadeHex(sliceColor(series, i, chart.seriesColorCycle), .7);
1583
+ const a0 = starts[i];
1584
+ const a1 = a0 + sweeps[i];
1585
+ for (const [from, to] of frontArcs(a1, a0)) {
1586
+ const topArc = arcPoints(from, to, (t) => d.point(i, t, r));
1587
+ const bottom = topArc.map(([x, y]) => [x, y - d.thickness]).reverse();
1588
+ sides.push({
1589
+ points: [...topArc, ...bottom],
1590
+ fillHex: fill,
1591
+ strokeHex: fill,
1592
+ widthPt: .5,
1593
+ depth: Math.min(...topArc.map(([, y]) => y))
1594
+ });
1595
+ }
1596
+ if (explosionOfSlice(i) > 0) {
1597
+ const [x0, y0] = d.centre(i);
1598
+ for (const t of [a0, a1]) {
1599
+ const [ex, ey] = d.point(i, t, r);
1600
+ sides.push({
1601
+ points: [
1602
+ [x0, y0],
1603
+ [ex, ey],
1604
+ [ex, ey - d.thickness],
1605
+ [x0, y0 - d.thickness]
1606
+ ],
1607
+ fillHex: fill,
1608
+ strokeHex: fill,
1609
+ widthPt: .5,
1610
+ depth: Math.min(y0, ey)
1611
+ });
1612
+ }
1613
+ }
1614
+ }
1615
+ sides.sort((a, b) => b.depth - a.depth);
1616
+ for (const { depth: _depth, ...side } of sides) polygons.push(side);
1617
+ for (let i = 0; i < values.length; i++) {
1618
+ if ((values[i] ?? 0) <= 0) continue;
1619
+ const a0 = starts[i];
1620
+ polygons.push({
1621
+ points: [d.centre(i), ...arcPoints(a0, a0 + sweeps[i], (t) => d.point(i, t, r))],
1622
+ fillHex: sliceColor(series, i, chart.seriesColorCycle),
1623
+ strokeHex: "FFFFFF",
1624
+ widthPt: .75
1625
+ });
1626
+ }
1627
+ } else {
1628
+ for (let i = 0; i < values.length; i++) {
1629
+ if ((values[i] ?? 0) <= 0) continue;
1630
+ const [x0, y0] = d.centre(i);
1631
+ wedges.push({
1632
+ cx: x0,
1633
+ cy: y0,
1634
+ r,
1635
+ startRad: starts[i],
1636
+ sweepRad: sweeps[i],
1637
+ fillHex: sliceColor(series, i, chart.seriesColorCycle),
1638
+ strokeHex: "FFFFFF"
1639
+ });
1640
+ }
1641
+ if (holeR > 0) wedges.push({
1083
1642
  cx,
1084
1643
  cy,
1085
- r,
1086
- startRad: ang,
1087
- sweepRad: sweep,
1088
- fillHex: sliceColor(series, i, chart.seriesColorCycle),
1089
- strokeHex: "FFFFFF"
1644
+ r: holeR,
1645
+ startRad: 0,
1646
+ sweepRad: -2 * Math.PI,
1647
+ fillHex: "FFFFFF"
1090
1648
  });
1091
- const mid = ang + sweep / 2;
1092
- const pct = Math.round(v / total * 100);
1093
- const own = chart.showValues ? fmtDataLabel(chart, v) : `${pct}%`;
1094
- const custom = atPoint(series.pointLabels, i)?.text;
1095
- if (custom !== void 0 || chart.showValues || pct >= 5) labels.push({
1096
- text: custom ?? own,
1097
- x: cx + Math.cos(mid) * labelR,
1098
- y: cy + Math.sin(mid) * labelR - 9 / 3,
1099
- sizePt: 9,
1100
- colorHex: custom !== void 0 || chart.doughnut ? LABEL_COLOR : "FFFFFF",
1101
- align: "center"
1649
+ }
1650
+ const polylines = [];
1651
+ const face = faceOf(chart, "dataLabels");
1652
+ const lineH = face.sizePt * 1.2;
1653
+ const placed = boxes.map((box) => ({
1654
+ box,
1655
+ ...positionOf(box, r)
1656
+ }));
1657
+ const outside = placed.filter((p) => p.box.where === "out" || across(p.bx, p.by, d.cyTop) >= r).sort((a, b) => b.by - a.by);
1658
+ const overlaps = (a, b) => Math.abs(a.bx - b.bx) < (a.box.width + b.box.width) / 2 && Math.abs(a.by - b.by) < (a.box.height + b.box.height) / 2 + 2;
1659
+ const fixed = outside.filter((p) => p.box.placement);
1660
+ const chartSet = outside.filter((p) => !p.box.placement);
1661
+ if (fixed.length + chartSet.length <= MOST_SPREAD_LABELS) {
1662
+ chartSet.forEach((here, k) => {
1663
+ const blocks = (other) => overlaps(other, here);
1664
+ for (let pass = 0; pass <= fixed.length + k; pass++) {
1665
+ const hit = fixed.find(blocks) ?? chartSet.slice(0, k).find(blocks);
1666
+ if (!hit) break;
1667
+ here.by = hit.by - (hit.box.height + here.box.height) / 2 - 2;
1668
+ }
1102
1669
  });
1103
- ang += sweep;
1670
+ const lowest = chartSet.reduce((low, p) => low === void 0 || p.by < low.by ? p : low, void 0);
1671
+ const under = lowest ? 2 - (lowest.by - lowest.box.height / 2) : 0;
1672
+ if (under > 0) for (const p of chartSet) p.by += under;
1104
1673
  }
1105
- if (holeR > 0) wedges.push({
1106
- cx,
1107
- cy,
1108
- r: holeR,
1109
- startRad: 0,
1110
- sweepRad: -2 * Math.PI,
1111
- fillHex: "FFFFFF"
1112
- });
1113
- pushChartTitle(labels, title, wPt, hPt);
1674
+ for (const { box, bx, by } of placed) {
1675
+ const moved = box.placement;
1676
+ if ((box.where === "out" || moved !== void 0) && across(bx, by, d.cyTop) >= r && chart.dataLabels?.showLeaderLines) {
1677
+ const [ex, ey] = d.point(box.index, box.mid, r);
1678
+ const tx = Math.min(Math.max(ex, bx - box.width / 2), bx + box.width / 2);
1679
+ const ty = Math.min(Math.max(ey, by - box.height / 2), by + box.height / 2);
1680
+ if (Math.hypot(tx - ex, ty - ey) > 3) polylines.push({
1681
+ points: [[ex, ey], [tx, ty]],
1682
+ strokeHex: LABEL_COLOR,
1683
+ widthPt: .75
1684
+ });
1685
+ }
1686
+ const colorHex = box.where === "out" || moved !== void 0 || box.custom || chart.doughnut || chart.text?.dataLabels?.colorHex ? face.colorHex : "FFFFFF";
1687
+ box.lines.forEach((line, k) => {
1688
+ labels.push({
1689
+ text: line,
1690
+ x: bx,
1691
+ y: by - face.sizePt / 3 + ((box.lines.length - 1) / 2 - k) * lineH,
1692
+ ...face,
1693
+ colorHex,
1694
+ align: "center"
1695
+ });
1696
+ });
1697
+ }
1698
+ pushChartTitle(labels, title, wPt, hPt, chart);
1114
1699
  legend.emit(rects, labels);
1115
1700
  return {
1116
1701
  rects,
1117
- polylines: [],
1702
+ polylines,
1118
1703
  wedges,
1119
- labels
1704
+ labels,
1705
+ ...polygons.length > 0 ? { polygons } : {}
1120
1706
  };
1121
1707
  }
1708
+ /**
1709
+ * The parts of an arc from `from` to `to` (radians, `from` < `to`) that lie
1710
+ * on the disc's front half — below its centre in the y-up frame, where sin
1711
+ * is negative — each as a [from, to] pair.
1712
+ */
1713
+ function frontArcs(from, to) {
1714
+ const out = [];
1715
+ const TAU = 2 * Math.PI;
1716
+ const k0 = Math.floor((from - Math.PI) / TAU) - 1;
1717
+ const k1 = Math.ceil((to - Math.PI) / TAU) + 1;
1718
+ for (let k = k0; k <= k1; k++) {
1719
+ const lo = Math.max(from, Math.PI + k * TAU);
1720
+ const hi = Math.min(to, TAU + k * TAU);
1721
+ if (hi - lo > 1e-6) out.push([lo, hi]);
1722
+ }
1723
+ return out;
1724
+ }
1725
+ /** Points along an arc from `a0` by its sweep to `a1`, one every 3.75°. */
1726
+ function arcPoints(a0, a1, at) {
1727
+ const n = Math.max(2, Math.ceil(Math.abs(a1 - a0) / (Math.PI / 48)));
1728
+ const out = [];
1729
+ for (let k = 0; k <= n; k++) out.push(at(a0 + (a1 - a0) * k / n));
1730
+ return out;
1731
+ }
1732
+ /** `hex` with each channel scaled by `f` — a darker shade of it below 1. */
1733
+ function shadeHex(hex, f) {
1734
+ const n = parseInt(hex, 16);
1735
+ const ch = (shift) => Math.max(0, Math.min(255, Math.round((n >> shift & 255) * f))).toString(16).padStart(2, "0");
1736
+ return `${ch(16)}${ch(8)}${ch(0)}`.toUpperCase();
1737
+ }
1738
+ /** The room kept between a pie and a label set outside it. */
1739
+ var PIE_LABEL_GAP = 6;
1740
+ /** The most labels round a pie that are spread apart (see buildPieScene). */
1741
+ var MOST_SPREAD_LABELS = 64;
1742
+ /**
1743
+ * §21.2.2.48 — each slice's label, measured and placed: inside where the
1744
+ * position asks for it, outside for `outEnd`, and for `bestFit`, Excel's
1745
+ * default, inside where it fits across the slice and outside where it does
1746
+ * not — wrapped at its spaces to under a third of the chart's width, as
1747
+ * 45544.xlsx's function names stand beside their slices.
1748
+ */
1749
+ function pieLabelBoxes(chart, series, values, total, sweeps, starts, fullR, wPt, measure) {
1750
+ const face = faceOf(chart, "dataLabels");
1751
+ const lineH = face.sizePt * 1.2;
1752
+ const percents = wholePercents(values, total);
1753
+ const widest = (lines) => lines.reduce((m, l) => Math.max(m, widthIn(measure, l, face)), 0);
1754
+ const out = [];
1755
+ for (let i = 0; i < values.length; i++) {
1756
+ const v = values[i] ?? 0;
1757
+ if (v <= 0) continue;
1758
+ const custom = atPoint(series.pointLabels, i)?.text;
1759
+ const text = custom ?? sliceLabel(chart, series, i, v, total, percents[i] ?? 0);
1760
+ if (text === void 0 || text.length === 0) continue;
1761
+ const placement = atPoint(series.pointLabelPlacements, i);
1762
+ const position = placement?.position ?? chart.dataLabels?.position ?? "bestFit";
1763
+ const sweep = Math.abs(sweeps[i] ?? 0);
1764
+ const mid = (starts[i] ?? 0) - sweep / 2;
1765
+ let lines = text.split("\n");
1766
+ let width = widest(lines);
1767
+ const across = sweep >= Math.PI ? fullR * 1.6 : 2 * fullR * .6 * Math.sin(Math.min(sweep, Math.PI) / 2);
1768
+ let fits = width + 4 <= across && lines.length * lineH <= fullR * .7;
1769
+ const dragged = placement !== void 0 && !placement.edge && (placement.x !== void 0 || placement.y !== void 0);
1770
+ if (!fits && position === "bestFit" && !dragged) {
1771
+ const wrapped = lines.flatMap((line) => wrapAtSpaces(line, Math.min(across * .9, wPt * .3), (t) => widthIn(measure, t, face)));
1772
+ const wrappedWidth = widest(wrapped);
1773
+ if (wrappedWidth + 4 <= across && wrapped.length * lineH <= fullR * .7) {
1774
+ lines = wrapped;
1775
+ width = wrappedWidth;
1776
+ fits = true;
1777
+ }
1778
+ }
1779
+ const where = chart.doughnut ? "in" : position === "outEnd" || position === "bestFit" && !fits ? "out" : position === "ctr" || position === "inBase" ? "ctr" : position === "inEnd" ? "end" : "in";
1780
+ if (where === "out") {
1781
+ const room = Math.max(40, wPt * .3);
1782
+ lines = lines.flatMap((line) => wrapAtSpaces(line, room, (t) => widthIn(measure, t, face)));
1783
+ width = widest(lines);
1784
+ }
1785
+ out.push({
1786
+ index: i,
1787
+ mid,
1788
+ lines,
1789
+ width,
1790
+ height: lines.length * lineH,
1791
+ where,
1792
+ ...placement && (placement.x !== void 0 || placement.y !== void 0) ? { placement: {
1793
+ ...placement.x !== void 0 ? { x: placement.x } : {},
1794
+ ...placement.y !== void 0 ? { y: placement.y } : {},
1795
+ ...placement.edge ? { edge: true } : {}
1796
+ } } : {},
1797
+ custom: custom !== void 0
1798
+ });
1799
+ }
1800
+ return out;
1801
+ }
1802
+ /** A line broken at its spaces into lines no wider than `room` where a break allows. */
1803
+ function wrapAtSpaces(line, room, width) {
1804
+ const words = line.split(" ");
1805
+ const out = [];
1806
+ let cur = "";
1807
+ for (const word of words) {
1808
+ const longer = cur ? `${cur} ${word}` : word;
1809
+ if (cur && width(longer) > room) {
1810
+ out.push(cur);
1811
+ cur = word;
1812
+ } else cur = longer;
1813
+ }
1814
+ out.push(cur);
1815
+ return out;
1816
+ }
1122
1817
  function pointColor(series, idx) {
1123
1818
  return atPoint(series.pointColors, idx)?.colorHex;
1124
1819
  }
@@ -1140,13 +1835,13 @@ function atPoint(overrides, idx) {
1140
1835
  }
1141
1836
  return byPoint.get(idx);
1142
1837
  }
1143
- function layoutLegend(entries, hasLegend, pos, wPt, hPt, measure, marker = "box") {
1838
+ function layoutLegend(entries, hasLegend, pos, wPt, hPt, measure, face, marker = "box") {
1144
1839
  if (!hasLegend || entries.length === 0) return {
1145
1840
  rightWidth: 0,
1146
1841
  bottomHeight: 0,
1147
1842
  emit: () => {}
1148
1843
  };
1149
- const sw = 9;
1844
+ const sw = face.sizePt;
1150
1845
  const gap = 4;
1151
1846
  const keyW = sw * 2.2;
1152
1847
  const keyH = sw * .55;
@@ -1163,7 +1858,7 @@ function layoutLegend(entries, hasLegend, pos, wPt, hPt, measure, marker = "box"
1163
1858
  h: keyH,
1164
1859
  fillHex: e.colorHex
1165
1860
  };
1166
- const entryW = (e) => keyW + 3 + measure(e.name, 9) + gap * 2;
1861
+ const entryW = (e) => keyW + 3 + widthIn(measure, e.name, face) + 8;
1167
1862
  if (pos === "r" || pos === "l") {
1168
1863
  const colW = entries.reduce((w, e) => Math.max(w, entryW(e)), 0);
1169
1864
  return {
@@ -1178,8 +1873,7 @@ function layoutLegend(entries, hasLegend, pos, wPt, hPt, measure, marker = "box"
1178
1873
  text: e.name,
1179
1874
  x: lx + keyW + 3,
1180
1875
  y: ly + 1,
1181
- sizePt: 9,
1182
- colorHex: LABEL_COLOR,
1876
+ ...face,
1183
1877
  align: "left"
1184
1878
  });
1185
1879
  ly -= sw + 4;
@@ -1199,9 +1893,8 @@ function layoutLegend(entries, hasLegend, pos, wPt, hPt, measure, marker = "box"
1199
1893
  labels.push({
1200
1894
  text: e.name,
1201
1895
  x: lx + keyW + 3,
1202
- y: ly + 1,
1203
- sizePt: 9,
1204
- colorHex: LABEL_COLOR,
1896
+ y: 3,
1897
+ ...face,
1205
1898
  align: "left"
1206
1899
  });
1207
1900
  lx += entryW(e);
@@ -1251,6 +1944,10 @@ function withReversedCategories(chart) {
1251
1944
  ...s.pointLabels ? { pointLabels: s.pointLabels.map((p) => ({
1252
1945
  ...p,
1253
1946
  idx: flip(p.idx)
1947
+ })) } : {},
1948
+ ...s.pointLabelPlacements ? { pointLabelPlacements: s.pointLabelPlacements.map((p) => ({
1949
+ ...p,
1950
+ idx: flip(p.idx)
1254
1951
  })) } : {}
1255
1952
  }))
1256
1953
  };
@@ -1294,4 +1991,4 @@ function withFrame(scene, chart, wPt, hPt) {
1294
1991
  };
1295
1992
  }
1296
1993
  //#endregion
1297
- export { buildChartScene, legendSeriesName };
1994
+ export { SERIES_COLORS, buildAreaScene, buildBarScene, buildChartScene, buildLineScene, buildPieScene, buildScatterScene, formatTick, legendSeriesName, niceScale, seriesColor };