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.
- package/README.md +5 -4
- package/dist/esm/core/bidi/algorithm.js +2 -2
- package/dist/esm/core/bidi/char-types.js +1 -1
- package/dist/esm/core/bidi/index.js +3 -3
- package/dist/esm/core/bytes.js +1 -1
- package/dist/esm/core/converter/facade.js +3 -2
- package/dist/esm/core/converter/ream.js +12 -5
- package/dist/esm/core/crypto/asn1.js +2 -2
- package/dist/esm/core/crypto/cms.js +5 -3
- package/dist/esm/core/crypto/index.js +2 -1
- package/dist/esm/core/crypto/offcrypto.js +8 -7
- package/dist/esm/core/crypto/primitives.js +16 -16
- package/dist/esm/core/document-model/index.d.ts +1 -1
- package/dist/esm/core/document-model/types.d.ts +174 -2
- package/dist/esm/core/drawingml/chart-geometry.d.ts +24 -6
- package/dist/esm/core/drawingml/chart-geometry.js +905 -208
- package/dist/esm/core/drawingml/chart-parser.d.ts +5 -1
- package/dist/esm/core/drawingml/chart-parser.js +275 -19
- package/dist/esm/core/drawingml/chart-text.d.ts +41 -0
- package/dist/esm/core/drawingml/chart-text.js +93 -0
- package/dist/esm/core/drawingml/colors.js +5 -5
- package/dist/esm/core/drawingml/diagram/layout-engine.js +1 -4
- package/dist/esm/core/drawingml/diagram/run.js +2 -1
- package/dist/esm/core/drawingml/preset-geometry.js +4 -6
- package/dist/esm/core/drawingml/shape-render.js +1 -1
- package/dist/esm/core/drawingml/text-warp.js +4 -16
- package/dist/esm/core/drawingml/theme-parser.js +14 -8
- package/dist/esm/core/font/arabic-joining.js +2 -2
- package/dist/esm/core/font/binary-reader.js +2 -0
- package/dist/esm/core/font/font-registry.js +1 -0
- package/dist/esm/core/font/ligatures.js +1 -1
- package/dist/esm/core/font/opentype-layout.js +11 -5
- package/dist/esm/core/font/ttf-build.js +6 -10
- package/dist/esm/core/font/ttf-parser.js +2 -1
- package/dist/esm/core/font/ttf-subset.js +8 -8
- package/dist/esm/core/fonts/families.js +1 -1
- package/dist/esm/core/fonts/remote-fonts.js +4 -4
- package/dist/esm/core/hyphenation/index.js +1 -1
- package/dist/esm/core/images.js +8 -8
- package/dist/esm/core/ir/loss.js +1 -0
- package/dist/esm/core/line-breaker/cjk.js +1 -1
- package/dist/esm/core/line-breaker/index.js +4 -3
- package/dist/esm/core/links.js +1 -1
- package/dist/esm/core/lzw.js +1 -1
- package/dist/esm/core/metafile/blit.js +1 -1
- package/dist/esm/core/metafile/emf.js +5 -7
- package/dist/esm/core/metafile/picture.js +1 -1
- package/dist/esm/core/metafile/symbol-fonts.js +7 -7
- package/dist/esm/core/metafile/wmf.js +5 -7
- package/dist/esm/core/number-format.js +4 -6
- package/dist/esm/core/numbering/index.js +3 -2
- package/dist/esm/core/ole/cfb.js +4 -4
- package/dist/esm/core/ole/escher-blip.js +1 -1
- package/dist/esm/core/opc/index.js +5 -4
- package/dist/esm/core/opc/package.js +3 -1
- package/dist/esm/core/opc/xml-entities.js +1 -1
- package/dist/esm/core/outline.js +1 -1
- package/dist/esm/core/png-encode.js +3 -3
- package/dist/esm/core/po-helpers.js +1 -1
- package/dist/esm/core/spreadsheet-model/types.d.ts +6 -0
- package/dist/esm/core/style-cascade/index.js +4 -3
- package/dist/esm/core/tiff.js +3 -1
- package/dist/esm/excel/activex-parser.js +1 -1
- package/dist/esm/excel/conditional-format.js +7 -7
- package/dist/esm/excel/formula/functions.js +8 -4
- package/dist/esm/excel/formula/index.js +3 -3
- package/dist/esm/excel/formula/lexer.js +2 -2
- package/dist/esm/excel/formula/parser.js +4 -3
- package/dist/esm/excel/formula/value.js +1 -1
- package/dist/esm/excel/index.js +7 -6
- package/dist/esm/excel/print-model.js +4 -4
- package/dist/esm/excel/printer-settings.js +6 -7
- package/dist/esm/excel/rich-value.js +1 -1
- package/dist/esm/excel/sheet-shape-parser.js +4 -2
- package/dist/esm/excel/sheet-to-flow.js +13 -11
- package/dist/esm/excel/styles-parser.js +5 -3
- package/dist/esm/excel/tint.js +5 -5
- package/dist/esm/excel/vml-drawing.js +2 -2
- package/dist/esm/excel/worksheet-parser.js +5 -5
- package/dist/esm/excel/xls/biff-chart.js +1 -3
- package/dist/esm/excel/xls/biff-reader.js +10 -11
- package/dist/esm/excel/xls/xls-reader.js +1 -1
- package/dist/esm/excel/xlsx-reader.js +17 -4
- package/dist/esm/excel/xlsx-to-pdf.js +1 -1
- package/dist/esm/excel/xlsx-writer.js +2 -2
- package/dist/esm/html/html-writer.js +18 -10
- package/dist/esm/layout/line-paint.js +1 -1
- package/dist/esm/layout/math-layout.js +5 -8
- package/dist/esm/layout/styled-layout.js +103 -31
- package/dist/esm/layout/turned-section.js +2 -2
- package/dist/esm/markdown/markdown-writer.js +1 -1
- package/dist/esm/pdf/encryption.js +9 -9
- package/dist/esm/pdf/icc-profile.js +2 -2
- package/dist/esm/pdf/index.js +3 -2
- package/dist/esm/pdf/objects.js +7 -0
- package/dist/esm/pdf/serialize.js +1 -1
- package/dist/esm/pdf/struct-tree.js +3 -1
- package/dist/esm/pdf/styled-page-emitter.js +23 -15
- package/dist/esm/pdf/styled-page-renderer.js +9 -5
- package/dist/esm/pdf-reader/annot-draw.js +3 -3
- package/dist/esm/pdf-reader/ccitt.js +2 -1
- package/dist/esm/pdf-reader/cff-outline.js +1 -4
- package/dist/esm/pdf-reader/content.js +13 -15
- package/dist/esm/pdf-reader/decrypt.js +5 -4
- package/dist/esm/pdf-reader/document.js +6 -2
- package/dist/esm/pdf-reader/encodings.js +1 -1
- package/dist/esm/pdf-reader/flow-build.js +4 -6
- package/dist/esm/pdf-reader/font.js +3 -3
- package/dist/esm/pdf-reader/function.js +8 -7
- package/dist/esm/pdf-reader/icc.js +1 -1
- package/dist/esm/pdf-reader/images.js +1 -1
- package/dist/esm/pdf-reader/jbig2.js +36 -38
- package/dist/esm/pdf-reader/jpeg.js +14 -13
- package/dist/esm/pdf-reader/layout.js +12 -9
- package/dist/esm/pdf-reader/lexer.js +2 -1
- package/dist/esm/pdf-reader/math-rows.js +2 -2
- package/dist/esm/pdf-reader/optional-content.js +1 -1
- package/dist/esm/pdf-reader/page-numbers.js +1 -1
- package/dist/esm/pdf-reader/pattern-tint.js +1 -2
- package/dist/esm/pdf-reader/reader.js +2 -2
- package/dist/esm/pdf-reader/struct-tree.js +1 -1
- package/dist/esm/pdf-reader/tagged.js +3 -2
- package/dist/esm/pdf-reader/text.js +2 -2
- package/dist/esm/pdf-reader/type1-outline.js +1 -3
- package/dist/esm/pptx/embedded-fonts.js +3 -5
- package/dist/esm/pptx/placeholder-cascade.js +1 -1
- package/dist/esm/pptx/ppt/ppt-reader.js +5 -4
- package/dist/esm/pptx/ppt/ppt-text.js +9 -10
- package/dist/esm/pptx/pptx-reader.js +6 -5
- package/dist/esm/pptx/preset-table-styles.js +1 -1
- package/dist/esm/pptx/slide-parser.js +6 -4
- package/dist/esm/pptx/sp-helpers.js +1 -1
- package/dist/esm/pptx/table-style.js +2 -1
- package/dist/esm/svg/svg-writer.js +1 -1
- package/dist/esm/word/doc/doc-reader.js +2 -2
- package/dist/esm/word/doc/doc-text.js +20 -22
- package/dist/esm/word/document-parser.js +26 -21
- package/dist/esm/word/docx-reader.js +2 -2
- package/dist/esm/word/docx-to-pdf.js +3 -3
- package/dist/esm/word/docx-writer.js +10 -6
- package/dist/esm/word/drawing-parser.js +18 -18
- package/dist/esm/word/font-table.js +1 -1
- package/dist/esm/word/index.js +6 -5
- package/dist/esm/word/numbering-parser.js +4 -3
- package/dist/esm/word/omml-parser.js +1 -1
- package/dist/esm/word/paragraph-properties.js +11 -10
- package/dist/esm/word/run-properties.js +4 -4
- package/dist/esm/word/settings-parser.js +2 -1
- package/dist/esm/word/shading.js +1 -1
- package/dist/esm/word/styles-parser.js +5 -4
- package/dist/esm/word/table-parser.js +4 -4
- package/dist/esm/word/theme-fonts.js +1 -1
- 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
|
|
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)
|
|
71
|
-
|
|
72
|
-
|
|
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) *
|
|
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) *
|
|
107
|
+
lo: finite(lo - (to - lo) * pad),
|
|
85
108
|
hi: to
|
|
86
109
|
};
|
|
87
110
|
}
|
|
88
111
|
return {
|
|
89
|
-
lo: finite(lo - spread *
|
|
90
|
-
hi: finite(hi + spread *
|
|
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
|
|
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),
|
|
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.
|
|
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
|
|
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 :
|
|
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 -
|
|
254
|
-
|
|
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
|
-
|
|
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 +
|
|
281
|
-
|
|
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
|
-
|
|
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 -
|
|
296
|
-
|
|
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
|
|
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
|
|
362
|
-
const catBand =
|
|
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
|
|
369
|
-
const tickHalf = (v) => horizontal && v !== void 0 ? measure
|
|
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
|
|
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
|
|
378
|
-
const
|
|
379
|
-
const
|
|
380
|
-
const
|
|
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:
|
|
385
|
-
y:
|
|
386
|
-
w:
|
|
387
|
-
h:
|
|
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:
|
|
605
|
+
x: 4 + leftTitleFace.sizePt * .9,
|
|
399
606
|
y: y0 + plotH / 2,
|
|
400
|
-
|
|
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
|
-
|
|
410
|
-
colorHex: LABEL_COLOR,
|
|
615
|
+
...footTitleFace,
|
|
411
616
|
align: "center"
|
|
412
617
|
});
|
|
413
|
-
|
|
414
|
-
|
|
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) -
|
|
421
|
-
|
|
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
|
-
|
|
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 ?
|
|
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")
|
|
447
|
-
|
|
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 -
|
|
456
|
-
|
|
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 +
|
|
464
|
-
|
|
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 -
|
|
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
|
-
|
|
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 -
|
|
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,
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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 >
|
|
964
|
+
if (chart.showValues && span > faceOf(chart, "dataLabels").sizePt) {
|
|
661
965
|
const raw = series.values[c] ?? 0;
|
|
662
|
-
|
|
663
|
-
|
|
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
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
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
|
-
|
|
704
|
-
const by = f.y0 + f.zeroOffset + Math.min(0, len);
|
|
705
|
-
f.rects.push({
|
|
996
|
+
else put({
|
|
706
997
|
x: along,
|
|
707
|
-
y:
|
|
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
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
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
|
-
|
|
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 =
|
|
891
|
-
|
|
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
|
|
898
|
-
const
|
|
899
|
-
const
|
|
900
|
-
const
|
|
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
|
-
|
|
905
|
-
|
|
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
|
|
1071
|
-
const
|
|
1072
|
-
const
|
|
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
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
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:
|
|
1087
|
-
sweepRad:
|
|
1088
|
-
fillHex:
|
|
1089
|
-
strokeHex: "FFFFFF"
|
|
1644
|
+
r: holeR,
|
|
1645
|
+
startRad: 0,
|
|
1646
|
+
sweepRad: -2 * Math.PI,
|
|
1647
|
+
fillHex: "FFFFFF"
|
|
1090
1648
|
});
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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:
|
|
1203
|
-
|
|
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 };
|