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