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