reamkit 1.32.0 → 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 (157) 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 +64 -19
  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 +45 -17
  16. package/dist/esm/core/drawingml/chart-geometry.js +1090 -257
  17. package/dist/esm/core/drawingml/chart-parser.d.ts +24 -1
  18. package/dist/esm/core/drawingml/chart-parser.js +334 -41
  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.d.ts +40 -15
  51. package/dist/esm/core/number-format.js +284 -79
  52. package/dist/esm/core/numbering/index.js +3 -2
  53. package/dist/esm/core/ole/cfb.js +4 -4
  54. package/dist/esm/core/ole/escher-blip.js +1 -1
  55. package/dist/esm/core/opc/index.js +5 -4
  56. package/dist/esm/core/opc/package.js +3 -1
  57. package/dist/esm/core/opc/tag-scan.d.ts +15 -0
  58. package/dist/esm/core/opc/tag-scan.js +41 -0
  59. package/dist/esm/core/opc/xml-entities.js +40 -3
  60. package/dist/esm/core/outline.js +1 -1
  61. package/dist/esm/core/png-encode.js +3 -3
  62. package/dist/esm/core/po-helpers.js +1 -1
  63. package/dist/esm/core/spreadsheet-model/types.d.ts +6 -0
  64. package/dist/esm/core/style-cascade/index.js +4 -3
  65. package/dist/esm/core/tiff.js +3 -1
  66. package/dist/esm/excel/activex-parser.js +1 -1
  67. package/dist/esm/excel/conditional-format.js +7 -7
  68. package/dist/esm/excel/formula/functions.js +45 -18
  69. package/dist/esm/excel/formula/index.js +3 -3
  70. package/dist/esm/excel/formula/lexer.js +2 -2
  71. package/dist/esm/excel/formula/parser.js +4 -3
  72. package/dist/esm/excel/formula/value.js +1 -1
  73. package/dist/esm/excel/index.js +7 -6
  74. package/dist/esm/excel/print-model.js +18 -7
  75. package/dist/esm/excel/printer-settings.js +6 -7
  76. package/dist/esm/excel/rich-value.js +1 -1
  77. package/dist/esm/excel/sheet-shape-parser.js +4 -2
  78. package/dist/esm/excel/sheet-to-flow.js +13 -11
  79. package/dist/esm/excel/styles-parser.js +5 -3
  80. package/dist/esm/excel/tint.js +5 -5
  81. package/dist/esm/excel/vml-drawing.js +3 -3
  82. package/dist/esm/excel/worksheet-parser.js +5 -5
  83. package/dist/esm/excel/xls/biff-chart.js +1 -3
  84. package/dist/esm/excel/xls/biff-reader.js +10 -11
  85. package/dist/esm/excel/xls/xls-reader.js +1 -1
  86. package/dist/esm/excel/xlsx-reader.js +56 -9
  87. package/dist/esm/excel/xlsx-to-pdf.js +1 -1
  88. package/dist/esm/excel/xlsx-writer.js +2 -2
  89. package/dist/esm/html/html-writer.js +18 -10
  90. package/dist/esm/layout/line-paint.js +1 -1
  91. package/dist/esm/layout/math-layout.js +5 -8
  92. package/dist/esm/layout/styled-layout.js +104 -32
  93. package/dist/esm/layout/turned-section.js +2 -2
  94. package/dist/esm/markdown/markdown-writer.js +59 -8
  95. package/dist/esm/pdf/encryption.js +9 -9
  96. package/dist/esm/pdf/icc-profile.js +2 -2
  97. package/dist/esm/pdf/index.js +3 -2
  98. package/dist/esm/pdf/objects.js +7 -0
  99. package/dist/esm/pdf/serialize.js +1 -1
  100. package/dist/esm/pdf/struct-tree.js +3 -1
  101. package/dist/esm/pdf/styled-page-emitter.js +23 -15
  102. package/dist/esm/pdf/styled-page-renderer.js +9 -5
  103. package/dist/esm/pdf-reader/annot-draw.js +5 -5
  104. package/dist/esm/pdf-reader/annots.js +3 -1
  105. package/dist/esm/pdf-reader/ccitt.js +2 -1
  106. package/dist/esm/pdf-reader/cff-outline.js +1 -4
  107. package/dist/esm/pdf-reader/content.js +13 -15
  108. package/dist/esm/pdf-reader/decrypt.js +5 -4
  109. package/dist/esm/pdf-reader/document.js +6 -2
  110. package/dist/esm/pdf-reader/encodings.js +1 -1
  111. package/dist/esm/pdf-reader/flow-build.js +4 -6
  112. package/dist/esm/pdf-reader/font.js +4 -4
  113. package/dist/esm/pdf-reader/function.js +8 -7
  114. package/dist/esm/pdf-reader/icc.js +1 -1
  115. package/dist/esm/pdf-reader/images.js +1 -1
  116. package/dist/esm/pdf-reader/jbig2.js +36 -38
  117. package/dist/esm/pdf-reader/jpeg.js +14 -13
  118. package/dist/esm/pdf-reader/layout.js +13 -10
  119. package/dist/esm/pdf-reader/lexer.js +2 -1
  120. package/dist/esm/pdf-reader/math-rows.js +2 -2
  121. package/dist/esm/pdf-reader/optional-content.js +1 -1
  122. package/dist/esm/pdf-reader/page-numbers.js +1 -1
  123. package/dist/esm/pdf-reader/pattern-tint.js +1 -2
  124. package/dist/esm/pdf-reader/reader.js +2 -2
  125. package/dist/esm/pdf-reader/struct-tree.js +1 -1
  126. package/dist/esm/pdf-reader/tagged.js +3 -2
  127. package/dist/esm/pdf-reader/text.js +2 -2
  128. package/dist/esm/pdf-reader/type1-outline.js +1 -3
  129. package/dist/esm/pptx/embedded-fonts.js +3 -5
  130. package/dist/esm/pptx/placeholder-cascade.js +1 -1
  131. package/dist/esm/pptx/ppt/ppt-reader.js +5 -4
  132. package/dist/esm/pptx/ppt/ppt-text.js +9 -10
  133. package/dist/esm/pptx/pptx-reader.js +6 -5
  134. package/dist/esm/pptx/preset-table-styles.js +1 -1
  135. package/dist/esm/pptx/slide-parser.js +6 -4
  136. package/dist/esm/pptx/sp-helpers.js +1 -1
  137. package/dist/esm/pptx/table-style.js +2 -1
  138. package/dist/esm/svg/svg-writer.js +1 -1
  139. package/dist/esm/word/doc/doc-reader.js +2 -2
  140. package/dist/esm/word/doc/doc-text.js +20 -22
  141. package/dist/esm/word/document-parser.js +27 -22
  142. package/dist/esm/word/docx-reader.js +15 -10
  143. package/dist/esm/word/docx-to-pdf.js +6 -6
  144. package/dist/esm/word/docx-writer.js +10 -6
  145. package/dist/esm/word/drawing-parser.js +18 -18
  146. package/dist/esm/word/font-table.js +90 -11
  147. package/dist/esm/word/index.js +6 -5
  148. package/dist/esm/word/numbering-parser.js +5 -4
  149. package/dist/esm/word/omml-parser.js +1 -1
  150. package/dist/esm/word/paragraph-properties.js +11 -10
  151. package/dist/esm/word/run-properties.js +4 -4
  152. package/dist/esm/word/settings-parser.js +2 -1
  153. package/dist/esm/word/shading.js +1 -1
  154. package/dist/esm/word/styles-parser.js +5 -4
  155. package/dist/esm/word/table-parser.js +4 -4
  156. package/dist/esm/word/theme-fonts.js +1 -1
  157. package/package.json +10 -4
@@ -1,8 +1,26 @@
1
- import { applyNumberFormat } from "../number-format.js";
1
+ import { applyNumberFormat, generalRenderings } from "../number-format.js";
2
2
  var AXIS_COLOR = "595959";
3
3
  var GRID_COLOR = "D9D9D9";
4
4
  var LABEL_COLOR = "595959";
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",
@@ -25,22 +43,25 @@ var SERIES_COLORS = [
25
43
  var seriesColor = (s, i, cycle) => s.colorHex ?? (cycle && cycle.length > 0 ? cycle[i % cycle.length] : SERIES_COLORS[i % SERIES_COLORS.length]);
26
44
  /**
27
45
  * The scale Excel gives a value axis it is left to choose, measured against
28
- * Excel's own PDF of eight column charts (2026-10-01):
46
+ * Excel's own PDF of its column charts (2026-10-01 and -02):
29
47
  *
30
48
  * - the ends: values all of one sign put the axis's near end at zero —
31
49
  * unless their spread is under a sixth of the largest of them, when it
32
50
  * stops half a spread short of the nearest (93…97 runs from 91) — and the
33
- * far end, like both ends of a range across zero, takes 5% of the spread
34
- * beyond the data before it is rounded out to the step;
51
+ * far end takes 5% of its reach from that near end before it is rounded
52
+ * out to the step. 70…96 reaches 100.8 and runs 0…120 by 20, where 5% of
53
+ * the data's own spread stopped at 97.3 and ran it 0…100 by 10. Both ends
54
+ * of a range across zero take 5% of the spread;
35
55
  * - the step: the smallest 1, 2 or 5 × 10ⁿ that leaves at most ten intervals
36
56
  * (`maxIntervals`), whatever the chart's height — 3 750 runs 0…4 000 by 500
37
57
  * on a 150pt chart as on a 400pt one. Only an axis too short to hold the
38
58
  * labels asks for fewer, and the caller says so.
39
59
  *
40
- * Every case of the probe lands where Excel puts it: −1…1.2 on −1.5…1.5 by
60
+ * Every case of the probes lands where Excel puts it: −1…1.2 on −1.5…1.5 by
41
61
  * 0.5, 0.3…4.7 on 0…5 by 0.5, 120…950 on 0…1 000 by 100, 0.012…0.047 on
42
- * 0…0.05 by 0.005. Heckbert's nice numbers with a budget of ticks by height
43
- * stepped the budget chart by 1 000 and started 93…97 at zero.
62
+ * 0…0.05 by 0.005, 1E-150…3E-150 on 0…3.5E-150. Heckbert's nice numbers with
63
+ * a budget of ticks by height stepped the budget chart by 1 000 and started
64
+ * 93…97 at zero.
44
65
  *
45
66
  * @param dataMin The smallest value to cover.
46
67
  * @param dataMax The largest value to cover.
@@ -48,78 +69,214 @@ var seriesColor = (s, i, cycle) => s.colorHex ?? (cycle && cycle.length > 0 ? cy
48
69
  * @returns The rounded min/max and tick step.
49
70
  */
50
71
  function niceScale(dataMin, dataMax, maxIntervals = 10) {
72
+ const end = paddedEnds(dataMin, dataMax);
73
+ const step = stepFor(end.lo, end.hi, maxIntervals);
74
+ return {
75
+ min: below(end.lo, step),
76
+ max: above(end.hi, step),
77
+ step
78
+ };
79
+ }
80
+ /**
81
+ * The ends {@link niceScale} rounds out to its step: the data's, padded as
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
86
+ * out at ±1.7E+308 would run them to.
87
+ */
88
+ function paddedEnds(dataMin, dataMax, pad = .05) {
51
89
  let lo = Math.min(dataMin, dataMax);
52
90
  let hi = Math.max(dataMin, dataMax);
53
- if (lo === hi) if (hi > 0) lo = 0;
54
- else if (lo < 0) hi = 0;
55
- 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
+ }
56
96
  const spread = hi - lo;
57
- const end = {
58
- lo: lo - spread * .05,
59
- hi: hi + spread * .05
60
- };
61
- if (lo >= 0) end.lo = spread < hi / 6 ? lo - spread / 2 : 0;
62
- else if (hi <= 0) end.hi = spread < -lo / 6 ? hi + spread / 2 : 0;
63
- const step = stepFor(end.hi - end.lo, maxIntervals);
97
+ if (lo >= 0) {
98
+ const from = spread < hi / 6 ? lo - spread / 2 : 0;
99
+ return {
100
+ lo: from,
101
+ hi: finite(hi + (hi - from) * pad)
102
+ };
103
+ }
104
+ if (hi <= 0) {
105
+ const to = spread < -lo / 6 ? hi + spread / 2 : 0;
106
+ return {
107
+ lo: finite(lo - (to - lo) * pad),
108
+ hi: to
109
+ };
110
+ }
64
111
  return {
65
- min: Math.floor(end.lo / step + 1e-9) * step,
66
- max: Math.ceil(end.hi / step - 1e-9) * step,
67
- step
112
+ lo: finite(lo - spread * pad),
113
+ hi: finite(hi + spread * pad)
68
114
  };
69
115
  }
70
- /** The smallest 1, 2 or 5 × 10ⁿ that cuts `span` into at most `maxIntervals` steps. */
71
- function stepFor(span, maxIntervals) {
116
+ /** `v`, or the largest double of its sign where it ran past it. */
117
+ var finite = (v) => Math.min(Number.MAX_VALUE, Math.max(-Number.MAX_VALUE, v));
118
+ /** `v` rounded down to a multiple of `step`, short of the largest double. */
119
+ var below = (v, step) => finite(Math.floor(v / step + 1e-9) * step);
120
+ /** `v` rounded up to a multiple of `step`, short of the largest double. */
121
+ var above = (v, step) => finite(Math.ceil(v / step - 1e-9) * step);
122
+ /**
123
+ * The smallest 1, 2 or 5 × 10ⁿ that cuts `lo`…`hi` into at most
124
+ * `maxIntervals` steps. The share of the span each interval takes is reckoned
125
+ * so that a span past the largest double has one: −1.7E+308…1.7E+308 spans
126
+ * Infinity, and the search for its step never ended.
127
+ */
128
+ function stepFor(lo, hi, maxIntervals) {
72
129
  const most = Math.max(1, maxIntervals);
73
- let base = 10 ** Math.floor(Math.log10(span / most));
130
+ const span = hi - lo;
131
+ const share = Math.abs(Number.isFinite(span) ? span / most : hi / most - lo / most);
132
+ if (!(share > 0 && share < Infinity)) return 1;
133
+ let base = Math.max(Number.MIN_VALUE, 10 ** Math.floor(Math.log10(share)));
74
134
  for (;;) {
75
135
  for (const m of [
76
136
  1,
77
137
  2,
78
138
  5
79
- ]) if (span / (m * base) <= most + 1e-9) return m * base;
139
+ ]) if (share <= m * base * (1 + 1e-9)) return Math.min(m * base, Number.MAX_VALUE);
80
140
  base *= 10;
81
141
  }
82
142
  }
143
+ /** How many steps lie between a scale's ends — in halves where the span overflows. */
144
+ function intervalsOf(s) {
145
+ const span = s.max - s.min;
146
+ return Number.isFinite(span) ? span / s.step : (s.max / 2 - s.min / 2) / (s.step / 2);
147
+ }
148
+ /**
149
+ * How far along `s` the value `v` stands: 0 at its min, 1 at its max —
150
+ * reckoned in halves where a difference overflows, as it does between ends
151
+ * out at ±1.7E+308.
152
+ */
153
+ function fractionOf(v, s) {
154
+ const span = s.max - s.min;
155
+ const from = v - s.min;
156
+ return Number.isFinite(span) && Number.isFinite(from) ? from / span : (v / 2 - s.min / 2) / (s.max / 2 - s.min / 2);
157
+ }
83
158
  /**
84
- * Format an axis tick value, choosing decimal places from the tick `step` so
85
- * `0.25`-spaced ticks read `0.25` while integer steps drop the fraction.
159
+ * The most intervals Excel cuts a value axis into. A finer step than that is
160
+ * widened to a 500th of the axis: its own PDF ran 0…26.25 by 0.0525 for steps
161
+ * of 0.05, 0.01 and 0.001 alike, and kept 0.0525 itself (2026-10-02).
162
+ */
163
+ var MOST_INTERVALS = 500;
164
+ /** The most characters Excel gives a value-axis label under General, its sign among them. */
165
+ var AXIS_LABEL_CHARS = 9;
166
+ /**
167
+ * A value-axis tick as Excel labels it under General. An axis is not a cell:
168
+ * its General gives every label nine characters, the sign among them, on an
169
+ * axis 150pt or 500pt long, upright or lying alike — Excel's own PDF of 44
170
+ * axes (2026-10-01 and -02). Within them it is the cells' rule
171
+ * ({@link generalRenderings}): the number as it is where it fits (0.25 — we
172
+ * wrote 0.3 on an axis stepped by 0.25 — 500000000, −50000000, 0.0000003),
173
+ * else the finest of it rounded or in scientific notation (0.1234568,
174
+ * −0.123457, 123456771, 1E+09, 1.235E+09, −1.23E+09, 1.235E-05, 5E-151).
175
+ * Places counted from the step and written by `toFixed` threw past a hundred
176
+ * of them, which a step of 1E-150 asked for.
86
177
  *
87
- * @param v The tick value.
88
- * @param step The tick spacing (from {@link niceScale}).
178
+ * @param v The tick value.
89
179
  * @returns The label text.
90
180
  */
91
- function formatTick(v, step) {
92
- if (Number.isInteger(step) && Number.isInteger(v)) return String(v);
93
- const decimals = Math.max(0, -Math.floor(Math.log10(step)));
94
- const fixed = v.toFixed(decimals);
95
- return fixed.includes(".") ? fixed.replace(/0+$/, "").replace(/\.$/, "") : fixed;
181
+ function formatTick(v) {
182
+ return generalRenderings(v, AXIS_LABEL_CHARS, true).next().value ?? String(v);
96
183
  }
184
+ /**
185
+ * The values a scale labels: its min and every step up to its max. Counted,
186
+ * not summed: the sum drifted, and its tolerance was absolute, so a scale of
187
+ * 1E-150 ran on until it passed 1E-9 and asked for more ticks than an array
188
+ * holds. Never more than Excel's most intervals and one past them — rounding
189
+ * the ends out to a widened step can add that one.
190
+ */
97
191
  var ticks = (s) => {
192
+ const count = Math.min(Math.floor(intervalsOf(s) + 1e-9), 501);
98
193
  const out = [];
99
- for (let v = s.min; v <= s.max + 1e-9; v += s.step) out.push(Math.abs(v) < 1e-9 ? 0 : v);
194
+ for (let i = 0; i <= count; i++) {
195
+ const sum = s.min + i * s.step;
196
+ const v = Number.isFinite(sum) ? sum : 2 * (s.min / 2 + i * (s.step / 2));
197
+ out.push(Math.abs(v) < s.step * 1e-9 ? 0 : v);
198
+ }
100
199
  return out;
101
200
  };
102
201
  /**
103
202
  * The value axis's {@link Scale}: the ends the author fixed where they fixed
104
- * them (§21.2.2.157 `c:scaling/c:min|c:max`), the data's own range where they
105
- * did not. A fixed end is exact — nice-rounding it would move a number the
106
- * author chose — so only the tick step comes from the rounding pass.
203
+ * them (§21.2.2.157 `c:scaling/c:min|c:max`), the data's, padded as
204
+ * {@link niceScale} pads them, where they did not. A fixed end is exact —
205
+ * nice-rounding it would move a number the author chose.
107
206
  *
108
- * @param chart The chart (for its fixed ends).
109
- * @param dataMin The smallest value to cover.
110
- * @param dataMax The largest value to cover.
111
- * @param hPt The chart's height — how many ticks fit is a question about
112
- * the plot, not about the numbers.
207
+ * The step is the author's (§21.2.2.103 `c:majorUnit`), else the 1-2-5 step
208
+ * of the span between the ends — with both fixed, of the span they fix:
209
+ * −1E+300…1E+300 runs by 2E+299 in Excel, where padding the ends first
210
+ * stepped it by 5E+299. The automatic ends round out to an author's step
211
+ * from where the padding left them, not from where a step of our own would
212
+ * have: 10…25 by 0.1 runs 0…26.3, as in Excel, not 0…30. And no step cuts
213
+ * the axis finer than {@link MOST_INTERVALS}: Excel widens one that would to
214
+ * a 500th of the span and rounds the automatic ends out to the widened step
215
+ * (−10…25 by 0.01 runs −11.781…26.796 by 0.077). A step of 0.00001 asked for
216
+ * three million labels and ran out of stack.
217
+ *
218
+ * @param chart The chart (for its fixed ends and step).
219
+ * @param dataMin The smallest value to cover.
220
+ * @param dataMax The largest value to cover.
221
+ * @param extentPt The axis's length — how many ticks fit is a question
222
+ * about the plot, not about the numbers.
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}).
113
226
  * @returns The axis min/max and tick step.
114
227
  */
115
- function axisScale(chart, dataMin, dataMax, extentPt, horizontal = false) {
116
- const rounded = niceScale(chart.valAxisMin ?? dataMin, chart.valAxisMax ?? dataMax, intervalsThatFit(extentPt, horizontal));
117
- const step = chart.valAxisMajorUnit ?? rounded.step;
228
+ function axisScale(chart, dataMin, dataMax, extentPt, horizontal = false, pad = .05) {
229
+ const fixedMin = chart.valAxisMin;
230
+ const fixedMax = chart.valAxisMax;
231
+ const padded = paddedEnds(fixedMin ?? dataMin, fixedMax ?? dataMax, pad);
232
+ const lo = fixedMin ?? padded.lo;
233
+ const hi = fixedMax ?? padded.hi;
234
+ const rounded = (step) => ({
235
+ min: fixedMin ?? below(lo, step),
236
+ max: fixedMax ?? above(hi, step),
237
+ step
238
+ });
239
+ const scale = rounded(chart.valAxisMajorUnit ?? stepFor(lo, hi, intervalsThatFit(extentPt, horizontal)));
240
+ if (intervalsOf(scale) <= MOST_INTERVALS) return scale;
241
+ const span = hi - lo;
242
+ return rounded(Number.isFinite(span) ? span / MOST_INTERVALS : hi / MOST_INTERVALS - lo / MOST_INTERVALS);
243
+ }
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;
118
252
  return {
119
- min: chart.valAxisMin ?? (step === rounded.step ? rounded.min : Math.floor(rounded.min / step) * step),
120
- max: chart.valAxisMax ?? (step === rounded.step ? rounded.max : Math.ceil(rounded.max / step) * step),
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),
121
272
  step
122
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
+ };
123
280
  }
124
281
  /**
125
282
  * The chart title's lines. A title longer than the chart is wide wraps at its
@@ -135,12 +292,13 @@ function axisScale(chart, dataMin, dataMax, extentPt, horizontal = false) {
135
292
  */
136
293
  function titleLines(chart, wPt, measure) {
137
294
  if (!chart.title) return [];
295
+ const face = faceOf(chart, "title");
138
296
  const room = wPt * .8;
139
297
  const lines = [];
140
298
  let line = "";
141
299
  for (const word of chart.title.split(/\s+/).filter((w) => w.length > 0)) {
142
300
  const longer = line ? `${line} ${word}` : word;
143
- if (line && measure(longer, 13) > room) {
301
+ if (line && widthIn(measure, longer, face) > room) {
144
302
  lines.push(line);
145
303
  line = word;
146
304
  } else line = longer;
@@ -149,15 +307,15 @@ function titleLines(chart, wPt, measure) {
149
307
  return lines;
150
308
  }
151
309
  /** The band a title of `lines` takes at the top of the chart. */
152
- var titleHeight = (lines) => lines.length === 0 ? 0 : 13 * (1.6 + 1.2 * (lines.length - 1));
153
- function pushChartTitle(labels, lines, wPt, hPt) {
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");
154
313
  lines.forEach((text, i) => {
155
314
  labels.push({
156
315
  text,
157
316
  x: wPt / 2,
158
- y: hPt - 4 - 13 * (1 + 1.2 * i),
159
- sizePt: 13,
160
- colorHex: TITLE_COLOR,
317
+ y: hPt - 4 - face.sizePt * (1 + 1.2 * i),
318
+ ...face,
161
319
  align: "center"
162
320
  });
163
321
  });
@@ -169,37 +327,37 @@ function buildLegendBlock(chart, wPt, hPt, measure) {
169
327
  ...s.line?.none ? { marker: "box" } : isLineLike(s) ? { marker: "line" } : {}
170
328
  }));
171
329
  const marker = chart.type === "line" || chart.type === "scatter" ? "line" : "box";
172
- 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);
173
331
  }
174
- 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
+ };
175
337
  for (const v of tickVals) if (axis === "x") {
176
338
  const gx = at(v);
177
- gridlines.push({
339
+ if (rule) gridlines.push({
178
340
  points: [[gx, y0], [gx, y0 + plotH]],
179
- strokeHex: grid?.colorHex ?? GRID_COLOR,
180
- widthPt: .75
341
+ ...rule
181
342
  });
182
- labels.push({
343
+ if (labelled) labels.push({
183
344
  text: fmt(v),
184
345
  x: gx,
185
- y: atEnd ? y0 + plotH + 9 * .4 : y0 - 9,
186
- sizePt: 9,
187
- colorHex: LABEL_COLOR,
346
+ y: atEnd ? y0 + plotH + face.sizePt * .4 : y0 - face.sizePt,
347
+ ...face,
188
348
  align: "center"
189
349
  });
190
350
  } else {
191
351
  const gy = at(v);
192
- gridlines.push({
352
+ if (rule) gridlines.push({
193
353
  points: [[x0, gy], [x0 + plotW, gy]],
194
- strokeHex: grid?.colorHex ?? GRID_COLOR,
195
- widthPt: .75
354
+ ...rule
196
355
  });
197
- labels.push({
356
+ if (labelled) labels.push({
198
357
  text: fmt(v),
199
358
  x: atEnd ? x0 + plotW + 3 : x0 - 3,
200
- y: gy - 9 / 3,
201
- sizePt: 9,
202
- colorHex: LABEL_COLOR,
359
+ y: gy - face.sizePt / 3,
360
+ ...face,
203
361
  align: atEnd ? "left" : "right"
204
362
  });
205
363
  }
@@ -223,12 +381,16 @@ function axisStroke(style) {
223
381
  * the value axis — `cross.y` up a column chart, `cross.x` along a bar chart —
224
382
  * and the other stands at the plot's edge.
225
383
  */
226
- 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
+ };
227
389
  const val = axisStroke(chart.valAxisLine);
228
- const x = x0 + (cross.x ?? 0);
390
+ const x = x0 + (cross.x ?? 0) + up.dx;
229
391
  const y = y0 + (cross.y ?? 0);
230
392
  if (val) polylines.push({
231
- points: [[x, y0], [x, y0 + plotH]],
393
+ points: [[x, y0 + up.dy], [x, y0 + plotH + up.dy]],
232
394
  strokeHex: val.hex,
233
395
  widthPt: val.widthPt
234
396
  });
@@ -239,56 +401,197 @@ function pushAxisLines(polylines, x0, y0, plotW, plotH, chart, cross = {}) {
239
401
  widthPt: cat.widthPt
240
402
  });
241
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
+ }
242
524
  function buildFrame(chart, wPt, hPt, measure, horizontal, opts = {}) {
243
525
  const rects = [];
244
526
  const polylines = [];
245
527
  const gridlines = [];
246
528
  const labels = [];
247
- const nCats = Math.max(chart.categories.length, ...chart.series.map((s) => s.values.length), 1);
529
+ const nCats = catCount(chart);
248
530
  const onSecondary = chart.series.filter((s) => s.secondaryAxis);
249
531
  const allVals = (onSecondary.length > 0 ? chart.series.filter((s) => !s.secondaryAxis) : chart.series).flatMap((s) => s.values.slice(0, nCats));
250
- const [dataMin, dataMax] = opts.dataRange ?? (allVals.length > 0 ? [Math.min(...allVals), Math.max(...allVals)] : [0, 1]);
532
+ const [dataMin, dataMax] = opts.dataRange ?? extentOf(allVals) ?? [0, 1];
251
533
  const scale = opts.exactRange ? {
252
534
  min: dataMin,
253
535
  max: dataMax,
254
536
  step: niceScale(dataMin, dataMax, intervalsThatFit(horizontal ? wPt : hPt, horizontal)).step
255
- } : axisScale(chart, dataMin, dataMax, horizontal ? wPt : hPt, horizontal);
256
- const fmtVal = opts.formatValue ?? ((v) => formatTick(v, scale.step));
537
+ } : axisScale(chart, dataMin, dataMax, horizontal ? wPt : hPt, horizontal, opts.unpadded ? 0 : .05);
538
+ const fmtVal = opts.formatValue ?? formatTick;
257
539
  const tickVals = ticks(scale);
258
- const vals2 = onSecondary.flatMap((s) => s.values.slice(0, nCats));
259
- const scale2 = vals2.length > 0 ? niceScale(Math.min(...vals2), Math.max(...vals2), intervalsThatFit(hPt, false)) : void 0;
540
+ const range2 = extentOf(onSecondary.flatMap((s) => s.values.slice(0, nCats)));
541
+ const scale2 = range2 ? niceScale(range2[0], range2[1], intervalsThatFit(hPt, false)) : void 0;
260
542
  const tickVals2 = scale2 ? ticks(scale2) : [];
261
543
  const crosses = chart.catAxisCrosses;
262
544
  const crossValue = Math.min(Math.max(crosses === "min" ? scale.min : crosses === "max" ? scale.max : crosses ?? 0, scale.min), scale.max);
263
- const labelsAt = chart.catTickLabelPos === "none" ? "none" : chart.catTickLabelPos === "low" ? "start" : chart.catTickLabelPos === "high" ? "end" : crossValue <= scale.min ? "start" : crossValue >= scale.max ? "end" : "cross";
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";
264
547
  const catText = (c) => chart.categories[c] ?? String(c + 1);
265
- const widestCat = Math.max(0, ...Array.from({ length: nCats }, (_, c) => measure(catText(c), 9)));
266
- const catBand = 9 * 1.6;
548
+ const catFace = faceOf(chart, "catAxis");
549
+ const valFace = faceOf(chart, "valAxis");
550
+ const val2Face = faceOf(chart, "secondaryValAxis");
551
+ let widestCat = 0;
552
+ for (let c = 0; c < nCats; c++) widestCat = Math.max(widestCat, widthIn(measure, catText(c), catFace));
553
+ const catBand = catFace.sizePt * 1.6;
267
554
  const groupLevels = horizontal ? [] : chart.categoryGroups ?? [];
268
555
  const valAtEnd = chart.catAxisReversed === true ? chart.valAxisCrosses !== "max" : chart.valAxisCrosses === "max";
556
+ const valLabelled = chart.valAxisDeleted !== true && !(fromBelow && horizontal);
269
557
  const title = titleLines(chart, wPt, measure);
270
- 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);
271
559
  const legend = buildLegendBlock(chart, wPt, hPt, measure);
272
- const tick2W = scale2 && !horizontal ? Math.max(0, ...tickVals2.map((v) => measure(formatTick(v, scale2.step), 9))) + 4 + (chart.secondaryValAxisTitle ? 9 * 1.5 : 0) : 0;
273
- 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;
274
562
  const lastTickHalf = tickHalf(tickVals[tickVals.length - 1]);
275
563
  const catLabelW = Math.min(wPt * .4, widestCat + 6);
276
- 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;
277
565
  const plotRight = wPt - 4 - legend.rightWidth - tick2W - (horizontal && labelsAt === "end" ? catLabelW : lastTickHalf) - (!horizontal && valAtEnd ? tickLabelW : 0);
278
- const axisTitleBand = 9 * 1.5;
279
566
  const leftTitle = horizontal ? chart.catAxisTitle : chart.valAxisTitle;
280
567
  const footTitle = horizontal ? chart.valAxisTitle : chart.catAxisTitle;
281
- const x0 = 4 + (leftTitle ? axisTitleBand : 0) + (horizontal ? labelsAt === "start" ? catLabelW : tickHalf(tickVals[0]) : valAtEnd ? 0 : tickLabelW);
282
- const y0 = 4 + legend.bottomHeight + (footTitle ? axisTitleBand : 0) + ((horizontal ? !valAtEnd : labelsAt === "start") ? catBand : 0) + (!horizontal && labelsAt === "start" ? groupLevels.length * catBand : 0);
283
- const plotW = Math.max(1, plotRight - x0);
284
- const plotH = Math.max(1, hPt - top - y0);
285
- const valueOffset = (v) => (v - scale.min) / (scale.max - scale.min) * (horizontal ? plotW : plotH);
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;
588
+ const valueOffset = (v) => fractionOf(v, scale) * (horizontal ? plotW : plotH);
286
589
  const zeroOffset = valueOffset(crossValue);
287
590
  const plotBackground = chart.plotFillHex ?? chart.plotLine?.colorHex ? {
288
- x: x0,
289
- y: y0,
290
- w: plotW,
291
- h: plotH,
591
+ x: boxX0,
592
+ y: boxY0,
593
+ w: boxW,
594
+ h: boxH,
292
595
  ...chart.plotFillHex ? { fillHex: chart.plotFillHex } : {},
293
596
  ...chart.plotLine?.colorHex ? {
294
597
  strokeHex: chart.plotLine.colorHex,
@@ -296,13 +599,12 @@ function buildFrame(chart, wPt, hPt, measure, horizontal, opts = {}) {
296
599
  ...chart.plotLine.dash ? { strokeDash: chart.plotLine.dash } : {}
297
600
  } : {}
298
601
  } : void 0;
299
- pushChartTitle(labels, title, wPt, hPt);
602
+ pushChartTitle(labels, title, wPt, hPt, chart);
300
603
  if (leftTitle) labels.push({
301
604
  text: leftTitle,
302
- x: 12.1,
605
+ x: 4 + leftTitleFace.sizePt * .9,
303
606
  y: y0 + plotH / 2,
304
- sizePt: 9,
305
- colorHex: LABEL_COLOR,
607
+ ...leftTitleFace,
306
608
  align: "center",
307
609
  rotationDeg: 90
308
610
  });
@@ -310,20 +612,30 @@ function buildFrame(chart, wPt, hPt, measure, horizontal, opts = {}) {
310
612
  text: footTitle,
311
613
  x: x0 + plotW / 2,
312
614
  y: legend.bottomHeight + 2,
313
- sizePt: 9,
314
- colorHex: LABEL_COLOR,
615
+ ...footTitleFace,
315
616
  align: "center"
316
617
  });
317
- if (horizontal) pushGridTicks(gridlines, labels, tickVals, fmtVal, "x", (v) => x0 + valueOffset(v), x0, y0, plotW, plotH, chart.gridLine, valAtEnd);
318
- else pushGridTicks(gridlines, labels, tickVals, fmtVal, "y", (v) => y0 + valueOffset(v), x0, y0, plotW, plotH, chart.gridLine, valAtEnd);
319
- const valueOffset2 = scale2 && !horizontal ? (v) => (v - scale2.min) / (scale2.max - scale2.min) * plotH : void 0;
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
+ }
632
+ const valueOffset2 = scale2 && !horizontal ? (v) => fractionOf(v, scale2) * plotH : void 0;
320
633
  if (scale2 && valueOffset2) {
321
634
  for (const v of tickVals2) labels.push({
322
- text: formatTick(v, scale2.step),
635
+ text: formatTick(v),
323
636
  x: x0 + plotW + 3,
324
- y: y0 + valueOffset2(v) - 9 / 3,
325
- sizePt: 9,
326
- colorHex: LABEL_COLOR,
637
+ y: y0 + valueOffset2(v) - val2Face.sizePt / 3,
638
+ ...val2Face,
327
639
  align: "left"
328
640
  });
329
641
  const sec = axisStroke(chart.secondaryValAxisLine);
@@ -336,42 +648,41 @@ function buildFrame(chart, wPt, hPt, measure, horizontal, opts = {}) {
336
648
  text: chart.secondaryValAxisTitle,
337
649
  x: wPt - 4,
338
650
  y: y0 + plotH / 2,
339
- sizePt: 9,
340
- colorHex: LABEL_COLOR,
651
+ ...faceOf(chart, "secondaryValAxisTitle"),
341
652
  align: "center",
342
653
  rotationDeg: 90
343
654
  });
344
655
  }
345
656
  const slot = (horizontal ? plotH : plotW) / nCats;
346
- const need = horizontal ? 9 * 1.4 : widestCat + 4;
657
+ const need = horizontal ? catFace.sizePt * 1.4 : widestCat + 4;
347
658
  const step = Math.max(1, Math.ceil(need / Math.max(slot, .01)));
348
659
  const across = labelsAt === "start" ? 0 : labelsAt === "end" ? horizontal ? plotW : plotH : zeroOffset;
349
660
  const labelled = [];
350
- if (labelsAt !== "none") if (chart.catAxisReversed === true) for (let c = nCats - 1; c >= 0; c -= step) labelled.push(c);
351
- else for (let c = 0; c < nCats; c += step) labelled.push(c);
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
+ }
352
665
  for (const c of labelled) {
353
666
  const cat = catText(c);
354
667
  if (!cat) continue;
355
668
  const center = (horizontal ? y0 : x0) + c * slot + slot / 2;
356
669
  if (horizontal) labels.push({
357
670
  text: cat,
358
- x: labelsAt === "end" ? x0 + across + 3 : x0 + across - 3,
359
- y: center - 9 / 3,
360
- sizePt: 9,
361
- 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,
362
674
  align: labelsAt === "end" ? "left" : "right"
363
675
  });
364
676
  else labels.push({
365
677
  text: cat,
366
678
  x: center,
367
- y: labelsAt === "end" ? y0 + across + 9 * .4 : y0 + across - 9,
368
- sizePt: 9,
369
- colorHex: LABEL_COLOR,
679
+ y: labelsAt === "end" ? y0 + across + catFace.sizePt * .4 : y0 + across - catFace.sizePt,
680
+ ...catFace,
370
681
  align: "center"
371
682
  });
372
683
  }
373
684
  if (labelsAt !== "none" && labelsAt !== "end") groupLevels.forEach((groups, level) => {
374
- const rowY = y0 + across - 9 - (level + 1) * catBand;
685
+ const rowY = y0 + across - catFace.sizePt - (level + 1) * catBand;
375
686
  groups.forEach((group, i) => {
376
687
  const end = Math.min(nCats, groups[i + 1]?.start ?? nCats);
377
688
  if (end <= group.start) return;
@@ -379,24 +690,26 @@ function buildFrame(chart, wPt, hPt, measure, horizontal, opts = {}) {
379
690
  text: group.label,
380
691
  x: x0 + (group.start + end) / 2 * slot,
381
692
  y: rowY,
382
- sizePt: 9,
383
- colorHex: LABEL_COLOR,
693
+ ...catFace,
384
694
  align: "center"
385
695
  });
386
696
  if (i > 0) polylines.push({
387
- 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]],
388
698
  strokeHex: GRID_COLOR,
389
699
  widthPt: .75
390
700
  });
391
701
  });
392
702
  });
393
- 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 ? {
394
707
  x: zeroOffset,
395
708
  ...valAtEnd ? { y: plotH } : {}
396
709
  } : {
397
710
  y: zeroOffset,
398
711
  ...valAtEnd ? { x: plotW } : {}
399
- });
712
+ }, { upright });
400
713
  legend.emit(rects, labels);
401
714
  return {
402
715
  x0,
@@ -413,13 +726,38 @@ function buildFrame(chart, wPt, hPt, measure, horizontal, opts = {}) {
413
726
  polylines,
414
727
  gridlines,
415
728
  ...plotBackground ? { plotBackground } : {},
416
- labels
729
+ labels,
730
+ ...depth ? {
731
+ depth,
732
+ walls
733
+ } : {}
417
734
  };
418
735
  }
419
736
  var pctLabel = (v) => `${Math.round(v * 100)}%`;
420
737
  var fmtDataLabel = (chart, v) => chartValueFormatter(chart)?.(v) ?? (Number.isInteger(v) ? String(v) : String(Math.round(v * 100) / 100));
738
+ /**
739
+ * How many category slots a chart has: its labels, or its longest series —
740
+ * counted in a loop, as spreading a file's series into one call overflows
741
+ * the stack.
742
+ */
421
743
  function catCount(chart) {
422
- return Math.max(chart.categories.length, ...chart.series.map((s) => s.values.length), 1);
744
+ let n = Math.max(chart.categories.length, 1);
745
+ for (const s of chart.series) n = Math.max(n, s.values.length);
746
+ return n;
747
+ }
748
+ /**
749
+ * The least and the greatest of `values`, or undefined where none is a
750
+ * number. Read in a loop: `Math.min(...values)` throws on a series of 200 000
751
+ * points, as a call takes no more arguments than the stack holds.
752
+ */
753
+ function extentOf(values) {
754
+ let lo = Infinity;
755
+ let hi = -Infinity;
756
+ for (const v of values) {
757
+ if (v < lo) lo = v;
758
+ if (v > hi) hi = v;
759
+ }
760
+ return lo <= hi ? [lo, hi] : void 0;
423
761
  }
424
762
  function stackedTotals(chart, nCats) {
425
763
  let max = 0;
@@ -459,15 +797,72 @@ function groupingFrameOpts(chart, nCats) {
459
797
  }
460
798
  var CHART_FORMAT_ID = 1e6;
461
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
+ }
462
809
  function chartValueFormatter(chart) {
463
810
  const hit = formatterCache.get(chart);
464
811
  if (hit !== void 0) return hit ?? void 0;
465
812
  const code = chart.numberFormat;
466
- 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]]));
467
814
  formatterCache.set(chart, made);
468
815
  return made ?? void 0;
469
816
  }
470
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
+ /**
471
866
  * Lay out a bar/column {@link Chart} into a {@link ChartScene}. Honours
472
867
  * `chart.grouping` (clustered / stacked / percentStacked) and `chart.barDir`
473
868
  * (column vs horizontal bar), over the shared cartesian frame.
@@ -501,13 +896,39 @@ function buildBarScene(chart, wPt, hPt, measure) {
501
896
  const stacked = g === "stacked" || g === "percentStacked";
502
897
  const percent = g === "percentStacked";
503
898
  const horizontal = chart.barDir === "bar";
504
- const f = buildFrame(chart, wPt, hPt, measure, horizontal, groupingFrameOpts(chart, catCount(chart)));
899
+ const nCats = catCount(chart);
505
900
  const barIdx = [];
506
901
  const lineIdx = [];
507
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 = [];
508
930
  if (stacked) {
509
- const groupPad = f.slot * .15;
510
- const barW = f.slot - 2 * groupPad;
931
+ const groupPad = (f.slot - barW) / 2;
511
932
  for (let c = 0; c < f.nCats; c++) {
512
933
  const along = (horizontal ? f.y0 : f.x0) + c * f.slot + groupPad;
513
934
  const denom = percent ? barIdx.reduce((a, i) => a + Math.abs(chart.series[i].values[c] ?? 0), 0) || 1 : 1;
@@ -524,7 +945,7 @@ function buildBarScene(chart, wPt, hPt, measure) {
524
945
  const span = Math.abs(o1 - o0);
525
946
  const color = pointColor(series, c) ?? seriesColor(series, s, chart.seriesColorCycle);
526
947
  const outline = barOutline(series);
527
- if (horizontal) f.rects.push({
948
+ if (horizontal) put({
528
949
  x: f.x0 + lo,
529
950
  y: along,
530
951
  w: span,
@@ -532,7 +953,7 @@ function buildBarScene(chart, wPt, hPt, measure) {
532
953
  fillHex: color,
533
954
  ...outline
534
955
  });
535
- else f.rects.push({
956
+ else put({
536
957
  x: along,
537
958
  y: f.y0 + lo,
538
959
  w: barW,
@@ -540,90 +961,170 @@ function buildBarScene(chart, wPt, hPt, measure) {
540
961
  fillHex: color,
541
962
  ...outline
542
963
  });
543
- if (chart.showValues && span > 9) {
964
+ if (chart.showValues && span > faceOf(chart, "dataLabels").sizePt) {
544
965
  const raw = series.values[c] ?? 0;
545
- if (horizontal) f.labels.push(centeredLabel(fmtDataLabel(chart, raw), f.x0 + lo + span / 2, along + barW * .3));
546
- 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
+ });
547
971
  }
548
972
  if (v >= 0) cumPos = top;
549
973
  else cumNeg = top;
550
974
  }
551
975
  }
552
- pushComboLines(chart, f, lineIdx);
553
- return {
554
- rects: f.rects,
555
- polylines: f.polylines,
556
- gridlines: f.gridlines,
557
- ...f.plotBackground ? { plotBackground: f.plotBackground } : {},
558
- ...f.plotBackground ? { plotBackground: f.plotBackground } : {},
559
- wedges: [],
560
- labels: f.labels
561
- };
562
- }
563
- const nSer = Math.max(1, barIdx.length);
564
- const gap = (chart.gapPercent ?? 150) / 100;
565
- const barW = f.slot / (nSer + gap);
566
- const groupPad = (f.slot - barW * nSer) / 2;
567
- for (let c = 0; c < f.nCats; c++) {
568
- const slotStart = (horizontal ? f.y0 : f.x0) + c * f.slot + groupPad;
569
- for (let b = 0; b < barIdx.length; b++) {
570
- const s = barIdx[b];
571
- const series = chart.series[s];
572
- const len = f.valueOffset(series.values[c] ?? 0) - f.zeroOffset;
573
- const color = pointColor(series, c) ?? seriesColor(series, s, chart.seriesColorCycle);
574
- const along = slotStart + b * barW;
575
- const outline = barOutline(series);
576
- if (horizontal) {
577
- const bx = f.x0 + f.zeroOffset + Math.min(0, len);
578
- f.rects.push({
579
- 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),
580
990
  y: along,
581
991
  w: Math.abs(len),
582
992
  h: barW,
583
993
  fillHex: color,
584
994
  ...outline
585
995
  });
586
- } else {
587
- const by = f.y0 + f.zeroOffset + Math.min(0, len);
588
- f.rects.push({
996
+ else put({
589
997
  x: along,
590
- y: by,
998
+ y: f.y0 + f.zeroOffset + Math.min(0, len),
591
999
  w: barW,
592
1000
  h: Math.abs(len),
593
1001
  fillHex: color,
594
1002
  ...outline
595
1003
  });
596
- }
597
- if (chart.showValues) {
598
- const txt = fmtDataLabel(chart, series.values[c] ?? 0);
599
- if (horizontal) {
600
- const end = f.x0 + f.zeroOffset + len;
601
- f.labels.push({
602
- text: txt,
603
- x: end + (len >= 0 ? 3 : -3),
604
- y: along + barW * .25,
605
- sizePt: 9,
606
- colorHex: LABEL_COLOR,
607
- align: len >= 0 ? "left" : "right"
608
- });
609
- } else {
610
- const end = f.y0 + f.zeroOffset + len;
611
- 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
+ }
612
1027
  }
613
1028
  }
614
1029
  }
615
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);
616
1071
  pushComboLines(chart, f, lineIdx);
617
1072
  return {
618
1073
  rects: f.rects,
619
1074
  polylines: f.polylines,
620
1075
  gridlines: f.gridlines,
621
1076
  ...f.plotBackground ? { plotBackground: f.plotBackground } : {},
1077
+ ...f.walls && f.walls.length > 0 ? { walls: f.walls } : {},
1078
+ ...polygons.length > 0 ? { polygons } : {},
622
1079
  wedges: [],
623
1080
  labels: f.labels
624
1081
  };
625
1082
  }
626
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
+ /**
627
1128
  * The name a series shows in the legend: its own `c:tx`, or Excel's positional
628
1129
  * `SeriesN` when it has none. Exported because the SUBSET has to know it too —
629
1130
  * a name invented at draw time is a name no glyph collector ever saw, and
@@ -669,13 +1170,12 @@ function pushComboLines(chart, f, lineIdx) {
669
1170
  }
670
1171
  }
671
1172
  }
672
- function centeredLabel(text, x, y) {
1173
+ function centeredLabel(text, x, y, face) {
673
1174
  return {
674
1175
  text,
675
1176
  x,
676
1177
  y,
677
- sizePt: 9,
678
- colorHex: LABEL_COLOR,
1178
+ ...face,
679
1179
  align: "center"
680
1180
  };
681
1181
  }
@@ -768,22 +1268,55 @@ function buildScatterScene(chart, wPt, hPt, measure) {
768
1268
  wedges: [],
769
1269
  labels
770
1270
  };
771
- const xScale = niceScale(Math.min(...xs), Math.max(...xs), intervalsThatFit(wPt, true));
772
- const yScale = niceScale(Math.min(...ys), Math.max(...ys), intervalsThatFit(hPt, false));
1271
+ const [xLo, xHi] = extentOf(xs) ?? [0, 1];
1272
+ const [yLo, yHi] = extentOf(ys) ?? [0, 1];
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
+ });
773
1283
  const xTicks = ticks(xScale);
774
1284
  const yTicks = ticks(yScale);
1285
+ const fmtX = formatterOf(chart.xNumberFormat) ?? formatTick;
1286
+ const fmtY = formatterOf(chart.numberFormat) ?? formatTick;
775
1287
  const title = titleLines(chart, wPt, measure);
776
- const top = 4 + titleHeight(title);
1288
+ const top = 4 + titleHeight(title, chart);
777
1289
  const legend = buildLegendBlock(chart, wPt, hPt, measure);
778
- const x0 = 4 + (Math.max(0, ...yTicks.map((v) => measure(formatTick(v, yScale.step), 9))) + 4);
779
- const y0 = 4 + legend.bottomHeight + 9 * 1.6;
780
- const plotW = Math.max(1, wPt - 4 - legend.rightWidth - x0);
781
- const plotH = Math.max(1, hPt - top - y0);
782
- const xAt = (v) => x0 + (v - xScale.min) / (xScale.max - xScale.min) * plotW;
783
- const yAt = (v) => y0 + (v - yScale.min) / (yScale.max - yScale.min) * plotH;
784
- pushChartTitle(labels, title, wPt, hPt);
785
- pushGridTicks(gridlines, labels, yTicks, (v) => formatTick(v, yScale.step), "y", yAt, x0, y0, plotW, plotH, chart.gridLine);
786
- pushGridTicks(gridlines, labels, xTicks, (v) => formatTick(v, xScale.step), "x", xAt, x0, y0, plotW, plotH, chart.gridLine);
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);
1300
+ const xAt = (v) => x0 + fractionOf(v, xScale) * plotW;
1301
+ const yAt = (v) => y0 + fractionOf(v, yScale) * plotH;
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);
787
1320
  pushAxisLines(polylines, x0, y0, plotW, plotH, chart);
788
1321
  const style = chart.scatterStyle ?? "marker";
789
1322
  const joins = style === "line" || style === "lineMarker" || style.startsWith("smooth");
@@ -871,8 +1404,7 @@ function pushMarker(rects, wedges, marker, x, y, color) {
871
1404
  * @returns The positioned scene primitives.
872
1405
  */
873
1406
  function buildLineScene(chart, wPt, hPt, measure) {
874
- const allVals = chart.series.flatMap((s) => s.values);
875
- const range = [allVals.length > 0 ? Math.min(...allVals) : 0, allVals.length > 0 ? Math.max(...allVals) : 1];
1407
+ const range = extentOf(chart.series.flatMap((s) => s.values)) ?? [0, 1];
876
1408
  const lineFormat = chartValueFormatter(chart);
877
1409
  const f = buildFrame(chart, wPt, hPt, measure, false, {
878
1410
  dataRange: range,
@@ -888,7 +1420,7 @@ function buildLineScene(chart, wPt, hPt, measure) {
888
1420
  const y = f.y0 + f.valueOffset(series.values[c] ?? 0);
889
1421
  pts.push([x, y]);
890
1422
  if (chart.lineMarkers) pushMarker(f.rects, wedges, series.marker, x, y, color);
891
- 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")));
892
1424
  }
893
1425
  if (pts.length >= 2) f.polylines.push({
894
1426
  points: pts,
@@ -944,73 +1476,372 @@ function buildPieScene(chart, wPt, hPt, measure) {
944
1476
  labels
945
1477
  };
946
1478
  const title = titleLines(chart, wPt, measure);
947
- const top = 4 + titleHeight(title);
1479
+ const top = 4 + titleHeight(title, chart);
948
1480
  const legend = layoutLegend(values.map((_, i) => ({
949
1481
  name: chart.categories[i] ?? String(i + 1),
950
1482
  colorHex: sliceColor(series, i, chart.seriesColorCycle)
951
- })), chart.hasLegend, chart.legendPos ?? "r", wPt, hPt, measure);
952
- const availW = Math.max(1, wPt - 8 - legend.rightWidth);
953
- const availH = Math.max(1, hPt - top - 4 - legend.bottomHeight);
954
- const r = Math.max(1, Math.min(availW, availH) / 2 * .95);
955
- const cx = 4 + availW / 2;
956
- 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);
957
1576
  const holeR = chart.doughnut ? r * .5 : 0;
958
- const labelR = chart.doughnut ? (holeR + r) / 2 : r * .6;
959
- let ang = Math.PI / 2;
960
- for (let i = 0; i < values.length; i++) {
961
- const v = values[i];
962
- if (v <= 0) continue;
963
- const sweep = -(v / total) * 2 * Math.PI;
964
- 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({
965
1642
  cx,
966
1643
  cy,
967
- r,
968
- startRad: ang,
969
- sweepRad: sweep,
970
- fillHex: sliceColor(series, i, chart.seriesColorCycle),
971
- strokeHex: "FFFFFF"
1644
+ r: holeR,
1645
+ startRad: 0,
1646
+ sweepRad: -2 * Math.PI,
1647
+ fillHex: "FFFFFF"
972
1648
  });
973
- const mid = ang + sweep / 2;
974
- const pct = Math.round(v / total * 100);
975
- const own = chart.showValues ? fmtDataLabel(chart, v) : `${pct}%`;
976
- const custom = series.pointLabels?.find((l) => l.idx === i)?.text;
977
- if (custom !== void 0 || chart.showValues || pct >= 5) labels.push({
978
- text: custom ?? own,
979
- x: cx + Math.cos(mid) * labelR,
980
- y: cy + Math.sin(mid) * labelR - 9 / 3,
981
- sizePt: 9,
982
- colorHex: custom !== void 0 || chart.doughnut ? LABEL_COLOR : "FFFFFF",
983
- 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
+ }
984
1669
  });
985
- 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;
986
1673
  }
987
- if (holeR > 0) wedges.push({
988
- cx,
989
- cy,
990
- r: holeR,
991
- startRad: 0,
992
- sweepRad: -2 * Math.PI,
993
- fillHex: "FFFFFF"
994
- });
995
- 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);
996
1699
  legend.emit(rects, labels);
997
1700
  return {
998
1701
  rects,
999
- polylines: [],
1702
+ polylines,
1000
1703
  wedges,
1001
- labels
1704
+ labels,
1705
+ ...polygons.length > 0 ? { polygons } : {}
1002
1706
  };
1003
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
+ }
1004
1817
  function pointColor(series, idx) {
1005
- return series.pointColors?.find((p) => p.idx === idx)?.colorHex;
1818
+ return atPoint(series.pointColors, idx)?.colorHex;
1006
1819
  }
1007
- function layoutLegend(entries, hasLegend, pos, wPt, hPt, measure, marker = "box") {
1820
+ /** Each override list's entries by point index, built on first use. */
1821
+ var overridesByPoint = /* @__PURE__ */ new WeakMap();
1822
+ /**
1823
+ * The first of `overrides` for the point at `idx` — looked up, not searched
1824
+ * for: a series that overrides many of its points, searched through for every
1825
+ * one of them, took the square of their number.
1826
+ */
1827
+ function atPoint(overrides, idx) {
1828
+ if (!overrides) return void 0;
1829
+ let byPoint = overridesByPoint.get(overrides);
1830
+ if (!byPoint) {
1831
+ const index = /* @__PURE__ */ new Map();
1832
+ for (const o of overrides) if (!index.has(o.idx)) index.set(o.idx, o);
1833
+ overridesByPoint.set(overrides, index);
1834
+ byPoint = index;
1835
+ }
1836
+ return byPoint.get(idx);
1837
+ }
1838
+ function layoutLegend(entries, hasLegend, pos, wPt, hPt, measure, face, marker = "box") {
1008
1839
  if (!hasLegend || entries.length === 0) return {
1009
1840
  rightWidth: 0,
1010
1841
  bottomHeight: 0,
1011
1842
  emit: () => {}
1012
1843
  };
1013
- const sw = 9;
1844
+ const sw = face.sizePt;
1014
1845
  const gap = 4;
1015
1846
  const keyW = sw * 2.2;
1016
1847
  const keyH = sw * .55;
@@ -1027,9 +1858,9 @@ function layoutLegend(entries, hasLegend, pos, wPt, hPt, measure, marker = "box"
1027
1858
  h: keyH,
1028
1859
  fillHex: e.colorHex
1029
1860
  };
1030
- const entryW = (e) => keyW + 3 + measure(e.name, 9) + gap * 2;
1861
+ const entryW = (e) => keyW + 3 + widthIn(measure, e.name, face) + 8;
1031
1862
  if (pos === "r" || pos === "l") {
1032
- const colW = Math.max(...entries.map(entryW));
1863
+ const colW = entries.reduce((w, e) => Math.max(w, entryW(e)), 0);
1033
1864
  return {
1034
1865
  rightWidth: pos === "r" ? colW : 0,
1035
1866
  bottomHeight: 0,
@@ -1042,8 +1873,7 @@ function layoutLegend(entries, hasLegend, pos, wPt, hPt, measure, marker = "box"
1042
1873
  text: e.name,
1043
1874
  x: lx + keyW + 3,
1044
1875
  y: ly + 1,
1045
- sizePt: 9,
1046
- colorHex: LABEL_COLOR,
1876
+ ...face,
1047
1877
  align: "left"
1048
1878
  });
1049
1879
  ly -= sw + 4;
@@ -1063,9 +1893,8 @@ function layoutLegend(entries, hasLegend, pos, wPt, hPt, measure, marker = "box"
1063
1893
  labels.push({
1064
1894
  text: e.name,
1065
1895
  x: lx + keyW + 3,
1066
- y: ly + 1,
1067
- sizePt: 9,
1068
- colorHex: LABEL_COLOR,
1896
+ y: 3,
1897
+ ...face,
1069
1898
  align: "left"
1070
1899
  });
1071
1900
  lx += entryW(e);
@@ -1100,7 +1929,7 @@ function buildChartScene(chart, wPt, hPt, measure) {
1100
1929
  * in reverse.
1101
1930
  */
1102
1931
  function withReversedCategories(chart) {
1103
- const n = Math.max(chart.categories.length, ...chart.series.map((s) => s.values.length), 1);
1932
+ const n = catCount(chart);
1104
1933
  const flip = (i) => n - 1 - i;
1105
1934
  return {
1106
1935
  ...chart,
@@ -1115,6 +1944,10 @@ function withReversedCategories(chart) {
1115
1944
  ...s.pointLabels ? { pointLabels: s.pointLabels.map((p) => ({
1116
1945
  ...p,
1117
1946
  idx: flip(p.idx)
1947
+ })) } : {},
1948
+ ...s.pointLabelPlacements ? { pointLabelPlacements: s.pointLabelPlacements.map((p) => ({
1949
+ ...p,
1950
+ idx: flip(p.idx)
1118
1951
  })) } : {}
1119
1952
  }))
1120
1953
  };
@@ -1158,4 +1991,4 @@ function withFrame(scene, chart, wPt, hPt) {
1158
1991
  };
1159
1992
  }
1160
1993
  //#endregion
1161
- export { buildChartScene, legendSeriesName };
1994
+ export { SERIES_COLORS, buildAreaScene, buildBarScene, buildChartScene, buildLineScene, buildPieScene, buildScatterScene, formatTick, legendSeriesName, niceScale, seriesColor };