@pixerate/editor 0.46.0 → 0.47.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/THIRD_PARTY_NOTICES.md +20 -0
- package/dist/agent/index.cjs +20 -19
- package/dist/agent/index.cjs.map +1 -1
- package/dist/agent/index.d.cts +2 -2
- package/dist/agent/index.d.ts +2 -2
- package/dist/agent/index.js +4 -3
- package/dist/agent/index.js.map +1 -1
- package/dist/{builtins-DghNQbT-.d.ts → builtins-D2nMtw2W.d.ts} +1 -1
- package/dist/{builtins-CZW-aYKS.d.cts → builtins-xs7rK6ys.d.cts} +1 -1
- package/dist/chart/index.cjs +3965 -821
- package/dist/chart/index.cjs.map +1 -1
- package/dist/chart/index.d.cts +964 -240
- package/dist/chart/index.d.ts +964 -240
- package/dist/chart/index.js +3922 -778
- package/dist/chart/index.js.map +1 -1
- package/dist/{chunk-PQM5W7ZN.js → chunk-4FX5ACVG.js} +29 -2
- package/dist/{chunk-PQM5W7ZN.js.map → chunk-4FX5ACVG.js.map} +1 -1
- package/dist/{chunk-3RPOW4CS.js → chunk-5VXI5GED.js} +12 -10
- package/dist/{chunk-3RPOW4CS.js.map → chunk-5VXI5GED.js.map} +1 -1
- package/dist/{chunk-HHQ4XJWT.js → chunk-AMWXSYCI.js} +2 -2
- package/dist/{chunk-U7VGDSNY.cjs → chunk-BBRK6MOI.cjs} +101 -99
- package/dist/chunk-BBRK6MOI.cjs.map +1 -0
- package/dist/chunk-C3NWJE3K.js +1182 -0
- package/dist/chunk-C3NWJE3K.js.map +1 -0
- package/dist/{chunk-N4MLNJPO.cjs → chunk-H4PICYNL.cjs} +59 -12
- package/dist/chunk-H4PICYNL.cjs.map +1 -0
- package/dist/chunk-LYGEEDNR.cjs +1895 -0
- package/dist/chunk-LYGEEDNR.cjs.map +1 -0
- package/dist/chunk-NEOO6QJD.cjs +1182 -0
- package/dist/chunk-NEOO6QJD.cjs.map +1 -0
- package/dist/{chunk-Q25ZS4F7.js → chunk-PQFAHT5O.js} +53 -6
- package/dist/chunk-PQFAHT5O.js.map +1 -0
- package/dist/{chunk-ZRNHGAQT.js → chunk-REAGGLXS.js} +43 -1891
- package/dist/chunk-REAGGLXS.js.map +1 -0
- package/dist/{chunk-EFDCLENW.cjs → chunk-RIWPYSKU.cjs} +29 -2
- package/dist/chunk-RIWPYSKU.cjs.map +1 -0
- package/dist/{chunk-EWCOFSCQ.cjs → chunk-SQC26VQV.cjs} +252 -2100
- package/dist/chunk-SQC26VQV.cjs.map +1 -0
- package/dist/{chunk-K2LVZ4FA.cjs → chunk-XFYUFVAX.cjs} +3 -3
- package/dist/{chunk-K2LVZ4FA.cjs.map → chunk-XFYUFVAX.cjs.map} +1 -1
- package/dist/chunk-ZFBBVGCI.js +1895 -0
- package/dist/chunk-ZFBBVGCI.js.map +1 -0
- package/dist/commands/index.cjs +93 -11
- package/dist/commands/index.cjs.map +1 -1
- package/dist/commands/index.d.cts +9 -7
- package/dist/commands/index.d.ts +9 -7
- package/dist/commands/index.js +92 -10
- package/dist/commands/index.js.map +1 -1
- package/dist/extensions/index.cjs +3 -2
- package/dist/extensions/index.cjs.map +1 -1
- package/dist/extensions/index.d.cts +3 -2
- package/dist/extensions/index.d.ts +3 -2
- package/dist/extensions/index.js +2 -1
- package/dist/generation/index.cjs +2 -2
- package/dist/generation/index.js +1 -1
- package/dist/{index-jwJ_onWf.d.ts → index-CSUCQYAH.d.ts} +23 -2
- package/dist/{index-CfOQf70b.d.cts → index-QrqMmZ75.d.cts} +23 -2
- package/dist/index.cjs +12 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -3
- package/dist/index.d.ts +9 -3
- package/dist/index.js +26 -23
- package/dist/index.js.map +1 -1
- package/dist/parameters/index.d.cts +1 -1
- package/dist/parameters/index.d.ts +1 -1
- package/dist/{registry-vz6HOiiy.d.cts → registry-CaR7LC6W.d.cts} +2 -0
- package/dist/{registry-BfVD-vVg.d.ts → registry-rVtFJjRM.d.ts} +2 -0
- package/dist/spreadsheet/index.cjs +5 -3
- package/dist/spreadsheet/index.cjs.map +1 -1
- package/dist/spreadsheet/index.js +24 -22
- package/dist/tools/index.cjs +3 -3
- package/dist/tools/index.js +2 -2
- package/dist/types-BVqWm3d1.d.cts +320 -0
- package/dist/types-BVqWm3d1.d.ts +320 -0
- package/dist/vector/index.d.cts +1 -1
- package/dist/vector/index.d.ts +1 -1
- package/package.json +3 -2
- package/dist/chunk-EFDCLENW.cjs.map +0 -1
- package/dist/chunk-EWCOFSCQ.cjs.map +0 -1
- package/dist/chunk-N4MLNJPO.cjs.map +0 -1
- package/dist/chunk-Q25ZS4F7.js.map +0 -1
- package/dist/chunk-U7VGDSNY.cjs.map +0 -1
- package/dist/chunk-ZRNHGAQT.js.map +0 -1
- /package/dist/{chunk-HHQ4XJWT.js.map → chunk-AMWXSYCI.js.map} +0 -0
|
@@ -0,0 +1,1182 @@
|
|
|
1
|
+
// src/chart/color.ts
|
|
2
|
+
var NAMED = {
|
|
3
|
+
black: "#000000",
|
|
4
|
+
white: "#ffffff",
|
|
5
|
+
red: "#ff0000",
|
|
6
|
+
green: "#008000",
|
|
7
|
+
blue: "#0000ff",
|
|
8
|
+
yellow: "#ffff00",
|
|
9
|
+
orange: "#ffa500",
|
|
10
|
+
purple: "#800080",
|
|
11
|
+
gray: "#808080",
|
|
12
|
+
grey: "#808080",
|
|
13
|
+
teal: "#008080",
|
|
14
|
+
navy: "#000080",
|
|
15
|
+
maroon: "#800000",
|
|
16
|
+
olive: "#808000",
|
|
17
|
+
silver: "#c0c0c0",
|
|
18
|
+
lime: "#00ff00",
|
|
19
|
+
aqua: "#00ffff",
|
|
20
|
+
cyan: "#00ffff",
|
|
21
|
+
fuchsia: "#ff00ff",
|
|
22
|
+
magenta: "#ff00ff",
|
|
23
|
+
pink: "#ffc0cb",
|
|
24
|
+
brown: "#a52a2a"
|
|
25
|
+
};
|
|
26
|
+
function parseChartColor(color) {
|
|
27
|
+
const text2 = color.trim().toLowerCase();
|
|
28
|
+
const hex2 = NAMED[text2] ?? text2;
|
|
29
|
+
let m = /^#([0-9a-f]{3})$/.exec(hex2);
|
|
30
|
+
if (m) return [...m[1]].map((c) => parseInt(c + c, 16) / 255);
|
|
31
|
+
m = /^#([0-9a-f]{6})([0-9a-f]{2})?$/.exec(hex2);
|
|
32
|
+
if (m) return [0, 2, 4].map((i) => parseInt(m[1].slice(i, i + 2), 16) / 255);
|
|
33
|
+
m = /^rgba?\(\s*([\d.]+)%?[\s,]+([\d.]+)%?[\s,]+([\d.]+)%?(?:[\s,/]+[\d.]+%?)?\s*\)$/.exec(text2);
|
|
34
|
+
if (m) {
|
|
35
|
+
const pct = text2.includes("%");
|
|
36
|
+
return [m[1], m[2], m[3]].map((v) => Math.max(0, Math.min(1, Number(v) / (pct ? 100 : 255))));
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
function chartRgbToHex(rgb) {
|
|
41
|
+
return `#${rgb.map((c) => Math.round(Math.max(0, Math.min(1, c)) * 255).toString(16).padStart(2, "0")).join("")}`;
|
|
42
|
+
}
|
|
43
|
+
function shadeChartColor(color, amount) {
|
|
44
|
+
const rgb = parseChartColor(color);
|
|
45
|
+
if (!rgb || !amount) return color;
|
|
46
|
+
const t = Math.max(-1, Math.min(1, amount));
|
|
47
|
+
return chartRgbToHex(rgb.map((c) => t > 0 ? c + (1 - c) * t : c * (1 + t)));
|
|
48
|
+
}
|
|
49
|
+
var CHART_PALETTE_SHADES = [0, 0.4, -0.25, 0.6, -0.5, 0.2];
|
|
50
|
+
function cyclePaletteColor(palette, index) {
|
|
51
|
+
const base = palette[index % palette.length];
|
|
52
|
+
const round = Math.floor(index / palette.length) % CHART_PALETTE_SHADES.length;
|
|
53
|
+
return shadeChartColor(base, CHART_PALETTE_SHADES[round]);
|
|
54
|
+
}
|
|
55
|
+
var toLinear = (c) => c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
|
56
|
+
var fromLinear = (c) => c <= 31308e-7 ? c * 12.92 : 1.055 * Math.pow(c, 1 / 2.4) - 0.055;
|
|
57
|
+
function chartLuminance(rgb) {
|
|
58
|
+
const [r, g, b] = rgb.map(toLinear);
|
|
59
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
60
|
+
}
|
|
61
|
+
function chartContrastRatio(a, b) {
|
|
62
|
+
const ra = parseChartColor(a);
|
|
63
|
+
const rb = parseChartColor(b);
|
|
64
|
+
if (!ra || !rb) return null;
|
|
65
|
+
const la = chartLuminance(ra);
|
|
66
|
+
const lb = chartLuminance(rb);
|
|
67
|
+
return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
|
|
68
|
+
}
|
|
69
|
+
function chartTextOn(fill, fallback, dark = "#0f172a", light = "#ffffff") {
|
|
70
|
+
const rgb = parseChartColor(fill);
|
|
71
|
+
if (!rgb) return fallback;
|
|
72
|
+
const darkRatio = chartContrastRatio(fill, dark);
|
|
73
|
+
const lightRatio = chartContrastRatio(fill, light);
|
|
74
|
+
return darkRatio >= lightRatio ? dark : light;
|
|
75
|
+
}
|
|
76
|
+
function chartRgbToOklab(rgb) {
|
|
77
|
+
const [r, g, b] = rgb.map(toLinear);
|
|
78
|
+
const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);
|
|
79
|
+
const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);
|
|
80
|
+
const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);
|
|
81
|
+
return [
|
|
82
|
+
0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s,
|
|
83
|
+
1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s,
|
|
84
|
+
0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s
|
|
85
|
+
];
|
|
86
|
+
}
|
|
87
|
+
function chartColorDistance(a, b) {
|
|
88
|
+
const p = chartRgbToOklab(a);
|
|
89
|
+
const q = chartRgbToOklab(b);
|
|
90
|
+
return Math.hypot(p[0] - q[0], p[1] - q[1], p[2] - q[2]);
|
|
91
|
+
}
|
|
92
|
+
var CVD = {
|
|
93
|
+
protanopia: [
|
|
94
|
+
[0.152286, 1.052583, -0.204868],
|
|
95
|
+
[0.114503, 0.786281, 0.099216],
|
|
96
|
+
[-3882e-6, -0.048116, 1.051998]
|
|
97
|
+
],
|
|
98
|
+
deuteranopia: [
|
|
99
|
+
[0.367322, 0.860646, -0.227968],
|
|
100
|
+
[0.280085, 0.672501, 0.047413],
|
|
101
|
+
[-0.01182, 0.04294, 0.968881]
|
|
102
|
+
],
|
|
103
|
+
tritanopia: [
|
|
104
|
+
[1.255528, -0.076749, -0.178779],
|
|
105
|
+
[-0.078411, 0.930809, 0.147602],
|
|
106
|
+
[4733e-6, 0.691367, 0.3039]
|
|
107
|
+
]
|
|
108
|
+
};
|
|
109
|
+
function simulateChartVision(rgb, vision) {
|
|
110
|
+
if (vision === "normal") return rgb;
|
|
111
|
+
const lin = rgb.map(toLinear);
|
|
112
|
+
const m = CVD[vision];
|
|
113
|
+
return m.map((row) => fromLinear(Math.max(0, Math.min(1, row[0] * lin[0] + row[1] * lin[1] + row[2] * lin[2]))));
|
|
114
|
+
}
|
|
115
|
+
function checkChartColors(colors, options = {}) {
|
|
116
|
+
const surface = options.surface ?? "#ffffff";
|
|
117
|
+
const minDistance = options.minDistance ?? 0.08;
|
|
118
|
+
const minContrast = options.minContrast ?? 1.5;
|
|
119
|
+
const visions = options.visions ?? ["normal", "protanopia", "deuteranopia", "tritanopia"];
|
|
120
|
+
const parsed = colors.map(parseChartColor);
|
|
121
|
+
const skipped = colors.filter((_, i) => !parsed[i]);
|
|
122
|
+
const issues = [];
|
|
123
|
+
const closest = {};
|
|
124
|
+
for (const vision of visions) {
|
|
125
|
+
const seen = parsed.map((c) => c ? simulateChartVision(c, vision) : null);
|
|
126
|
+
let best;
|
|
127
|
+
for (let a = 0; a < seen.length; a++) {
|
|
128
|
+
for (let b = a + 1; b < seen.length; b++) {
|
|
129
|
+
if (!seen[a] || !seen[b]) continue;
|
|
130
|
+
const distance = chartColorDistance(seen[a], seen[b]);
|
|
131
|
+
if (!best || distance < best.distance) best = { a, b, colors: [colors[a], colors[b]], distance: Math.round(distance * 1e3) / 1e3 };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (best) {
|
|
135
|
+
closest[vision] = best;
|
|
136
|
+
if (best.distance < minDistance) {
|
|
137
|
+
const who = vision === "normal" ? "" : ` with ${vision}`;
|
|
138
|
+
issues.push(`Colours ${best.a + 1} (${best.colors[0]}) and ${best.b + 1} (${best.colors[1]}) are hard to tell apart${who} (distance ${best.distance}, want at least ${minDistance}).`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const contrast = colors.map((c) => {
|
|
143
|
+
const ratio = chartContrastRatio(c, surface);
|
|
144
|
+
return ratio === null ? null : Math.round(ratio * 100) / 100;
|
|
145
|
+
});
|
|
146
|
+
contrast.forEach((ratio, i) => {
|
|
147
|
+
if (ratio !== null && ratio < minContrast) issues.push(`Colour ${i + 1} (${colors[i]}) barely stands out from the background ${surface} (contrast ${ratio}:1, want at least ${minContrast}:1).`);
|
|
148
|
+
});
|
|
149
|
+
return { ok: issues.length === 0, closest, contrast, issues, skipped };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/chart/spec.ts
|
|
153
|
+
var CHART_TYPES = ["bar", "line", "area", "scatter", "bubble", "pie", "donut", "radar", "waterfall", "histogram", "box", "funnel", "treemap"];
|
|
154
|
+
var CHART_MARKERS = ["circle", "square", "diamond", "triangle", "cross", "none"];
|
|
155
|
+
var CHART_TRENDLINE_TYPES = ["linear", "exponential", "logarithmic", "power", "polynomial", "moving-average"];
|
|
156
|
+
var CHART_LABEL_POSITIONS = ["auto", "inside", "outside", "center"];
|
|
157
|
+
var CHART_AXIS_KINDS = ["category", "date"];
|
|
158
|
+
var CHART_SERIES_TYPES = ["bar", "line", "area", "scatter"];
|
|
159
|
+
var CHART_STACKINGS = ["none", "stacked", "percent"];
|
|
160
|
+
var CHART_CURVES = ["linear", "smooth", "step"];
|
|
161
|
+
var CHART_ORIENTATIONS = ["vertical", "horizontal"];
|
|
162
|
+
var CHART_LEGEND_POSITIONS = ["top", "bottom", "left", "right"];
|
|
163
|
+
var CHART_NUMBER_STYLES = ["number", "percent", "currency", "compact"];
|
|
164
|
+
var CHART_TYPE_LABELS = {
|
|
165
|
+
bar: "Bar",
|
|
166
|
+
line: "Line",
|
|
167
|
+
area: "Area",
|
|
168
|
+
scatter: "Scatter",
|
|
169
|
+
bubble: "Bubble",
|
|
170
|
+
pie: "Pie",
|
|
171
|
+
donut: "Donut",
|
|
172
|
+
radar: "Radar",
|
|
173
|
+
waterfall: "Waterfall",
|
|
174
|
+
histogram: "Histogram",
|
|
175
|
+
box: "Box plot",
|
|
176
|
+
funnel: "Funnel",
|
|
177
|
+
treemap: "Treemap"
|
|
178
|
+
};
|
|
179
|
+
var DEFAULT_CHART_PALETTE = ["#4269d0", "#efb118", "#ff725c", "#6cc5b0", "#3ca951", "#ff8ab7", "#a463f2", "#97bbf5", "#9c6b4e", "#9498a0"];
|
|
180
|
+
var DEFAULT_CHART_THEME = {
|
|
181
|
+
background: "#ffffff",
|
|
182
|
+
surface: "#ffffff",
|
|
183
|
+
text: "#0f172a",
|
|
184
|
+
mutedText: "#64748b",
|
|
185
|
+
grid: "#e2e8f0",
|
|
186
|
+
axis: "#94a3b8",
|
|
187
|
+
selection: "#2563eb",
|
|
188
|
+
positive: "#3ca951",
|
|
189
|
+
negative: "#e5484d",
|
|
190
|
+
fontFamily: 'system-ui, -apple-system, "Segoe UI", sans-serif',
|
|
191
|
+
fontSize: 11,
|
|
192
|
+
titleSize: 15
|
|
193
|
+
};
|
|
194
|
+
var CSS_VARIABLE_CHART_THEME = {
|
|
195
|
+
...DEFAULT_CHART_THEME,
|
|
196
|
+
background: "transparent",
|
|
197
|
+
surface: "var(--background, #ffffff)",
|
|
198
|
+
text: "var(--foreground, #0f172a)",
|
|
199
|
+
mutedText: "var(--muted-foreground, #64748b)",
|
|
200
|
+
grid: "var(--border, #e2e8f0)",
|
|
201
|
+
axis: "var(--muted-foreground, #94a3b8)",
|
|
202
|
+
selection: "var(--primary, #2563eb)",
|
|
203
|
+
fontFamily: "inherit"
|
|
204
|
+
};
|
|
205
|
+
function isCartesianChart(type) {
|
|
206
|
+
return type === "bar" || type === "line" || type === "area" || type === "scatter";
|
|
207
|
+
}
|
|
208
|
+
function isXyChart(spec) {
|
|
209
|
+
if (spec.type === "bubble") return true;
|
|
210
|
+
return spec.type === "scatter" && spec.series.some((s) => !s.hidden && Array.isArray(s.x) && s.x.some((v) => v !== null));
|
|
211
|
+
}
|
|
212
|
+
function chartFamily(spec) {
|
|
213
|
+
if (isXyChart(spec)) return "xy";
|
|
214
|
+
switch (spec.type) {
|
|
215
|
+
case "pie":
|
|
216
|
+
case "donut":
|
|
217
|
+
return "pie";
|
|
218
|
+
case "radar":
|
|
219
|
+
case "waterfall":
|
|
220
|
+
case "histogram":
|
|
221
|
+
case "box":
|
|
222
|
+
case "funnel":
|
|
223
|
+
case "treemap":
|
|
224
|
+
return spec.type;
|
|
225
|
+
case "bubble":
|
|
226
|
+
return "xy";
|
|
227
|
+
default:
|
|
228
|
+
return "cartesian";
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function chartColorsByCategory(type) {
|
|
232
|
+
return type === "pie" || type === "donut" || type === "funnel" || type === "treemap";
|
|
233
|
+
}
|
|
234
|
+
function defaultChartAxis(value) {
|
|
235
|
+
return { show: true, title: "", min: null, max: null, step: null, gridlines: value, format: {} };
|
|
236
|
+
}
|
|
237
|
+
function createChartSpec(partial = {}) {
|
|
238
|
+
return normalizeChartSpec(partial).spec;
|
|
239
|
+
}
|
|
240
|
+
function chartSeriesColor(spec, seriesIndex) {
|
|
241
|
+
const series = spec.series[seriesIndex];
|
|
242
|
+
if (series?.color) return series.color;
|
|
243
|
+
return cyclePaletteColor(spec.palette.length ? spec.palette : DEFAULT_CHART_PALETTE, seriesIndex);
|
|
244
|
+
}
|
|
245
|
+
function chartSliceColor(spec, index) {
|
|
246
|
+
return cyclePaletteColor(spec.palette.length ? spec.palette : DEFAULT_CHART_PALETTE, index);
|
|
247
|
+
}
|
|
248
|
+
function chartPointColor(spec, seriesIndex, index) {
|
|
249
|
+
return spec.series[seriesIndex]?.pointColors?.[index] || chartSeriesColor(spec, seriesIndex);
|
|
250
|
+
}
|
|
251
|
+
function chartSeriesType(spec, series) {
|
|
252
|
+
if (series.type) return series.type;
|
|
253
|
+
if (spec.type === "bubble") return "scatter";
|
|
254
|
+
return isCartesianChart(spec.type) ? spec.type : "bar";
|
|
255
|
+
}
|
|
256
|
+
function uniqueChartSeriesId(series, base = "series") {
|
|
257
|
+
const used = new Set(series.map((s) => s.id));
|
|
258
|
+
for (let i = series.length + 1; ; i++) {
|
|
259
|
+
const id = `${base}-${i}`;
|
|
260
|
+
if (!used.has(id)) return id;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function parseChartValue(input) {
|
|
264
|
+
if (input === null || input === void 0) return null;
|
|
265
|
+
if (typeof input === "number") return Number.isFinite(input) ? input : void 0;
|
|
266
|
+
if (typeof input !== "string") return void 0;
|
|
267
|
+
let text2 = input.trim();
|
|
268
|
+
if (!text2 || text2 === "-" || text2 === "\u2014") return null;
|
|
269
|
+
let negative = false;
|
|
270
|
+
if (/^\(.*\)$/.test(text2)) {
|
|
271
|
+
negative = true;
|
|
272
|
+
text2 = text2.slice(1, -1).trim();
|
|
273
|
+
}
|
|
274
|
+
const percent = text2.endsWith("%");
|
|
275
|
+
if (percent) text2 = text2.slice(0, -1).trim();
|
|
276
|
+
text2 = text2.replace(/^[$€£¥]/, "").replace(/,/g, "").replace(/\s+/g, "");
|
|
277
|
+
if (!/^[-+]?(\d+\.?\d*|\.\d+)(e[-+]?\d+)?$/i.test(text2)) return void 0;
|
|
278
|
+
let value = Number(text2);
|
|
279
|
+
if (percent) value /= 100;
|
|
280
|
+
if (negative) value = -value;
|
|
281
|
+
return Number.isFinite(value) ? value : void 0;
|
|
282
|
+
}
|
|
283
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
284
|
+
function pick(value, options, fallback, path2, issues) {
|
|
285
|
+
if (value === void 0) return fallback;
|
|
286
|
+
if (typeof value === "string" && options.includes(value)) return value;
|
|
287
|
+
issues.push({ path: path2, message: `${path2} must be one of ${options.map((o) => `"${o}"`).join(", ")}; got ${JSON.stringify(value)}.` });
|
|
288
|
+
return fallback;
|
|
289
|
+
}
|
|
290
|
+
function text(value, fallback, path2, issues) {
|
|
291
|
+
if (value === void 0 || value === null) return fallback;
|
|
292
|
+
if (typeof value === "string") return value;
|
|
293
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
294
|
+
issues.push({ path: path2, message: `${path2} must be text.` });
|
|
295
|
+
return fallback;
|
|
296
|
+
}
|
|
297
|
+
function flag(value, fallback, path2, issues) {
|
|
298
|
+
if (value === void 0) return fallback;
|
|
299
|
+
if (typeof value === "boolean") return value;
|
|
300
|
+
issues.push({ path: path2, message: `${path2} must be true or false.` });
|
|
301
|
+
return fallback;
|
|
302
|
+
}
|
|
303
|
+
function bound(value, path2, issues) {
|
|
304
|
+
if (value === void 0 || value === null || value === "") return null;
|
|
305
|
+
const n = typeof value === "string" ? Number(value) : value;
|
|
306
|
+
if (typeof n === "number" && Number.isFinite(n)) return n;
|
|
307
|
+
issues.push({ path: path2, message: `${path2} must be a number or null.` });
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
function normalizeFormat(value, path2, issues) {
|
|
311
|
+
if (value === void 0 || value === null) return {};
|
|
312
|
+
if (!isRecord(value)) {
|
|
313
|
+
issues.push({ path: path2, message: `${path2} must be an object.` });
|
|
314
|
+
return {};
|
|
315
|
+
}
|
|
316
|
+
const out = {};
|
|
317
|
+
if (value.style !== void 0) out.style = pick(value.style, CHART_NUMBER_STYLES, "number", `${path2}.style`, issues);
|
|
318
|
+
if (value.decimals !== void 0) {
|
|
319
|
+
const d = Number(value.decimals);
|
|
320
|
+
if (Number.isInteger(d) && d >= 0 && d <= 10) out.decimals = d;
|
|
321
|
+
else issues.push({ path: `${path2}.decimals`, message: `${path2}.decimals must be a whole number from 0 to 10.` });
|
|
322
|
+
}
|
|
323
|
+
for (const key2 of ["currency", "prefix", "suffix", "code"]) {
|
|
324
|
+
if (value[key2] !== void 0) out[key2] = text(value[key2], "", `${path2}.${key2}`, issues);
|
|
325
|
+
}
|
|
326
|
+
return out;
|
|
327
|
+
}
|
|
328
|
+
function normalizeChartAxis(value, fallback, path2, issues = []) {
|
|
329
|
+
if (value === void 0) return { ...fallback, format: { ...fallback.format } };
|
|
330
|
+
if (!isRecord(value)) {
|
|
331
|
+
issues.push({ path: path2, message: `${path2} must be an object.` });
|
|
332
|
+
return { ...fallback, format: { ...fallback.format } };
|
|
333
|
+
}
|
|
334
|
+
let min = "min" in value ? bound(value.min, `${path2}.min`, issues) : fallback.min;
|
|
335
|
+
let max = "max" in value ? bound(value.max, `${path2}.max`, issues) : fallback.max;
|
|
336
|
+
if (min !== null && max !== null && min >= max) {
|
|
337
|
+
issues.push({ path: `${path2}.max`, message: `${path2}.max (${max}) must be greater than ${path2}.min (${min}).` });
|
|
338
|
+
min = fallback.min;
|
|
339
|
+
max = fallback.max;
|
|
340
|
+
}
|
|
341
|
+
let step = "step" in value ? bound(value.step, `${path2}.step`, issues) : fallback.step;
|
|
342
|
+
if (step !== null && !(step > 0)) {
|
|
343
|
+
issues.push({ path: `${path2}.step`, message: `${path2}.step must be greater than 0 (or null for automatic).` });
|
|
344
|
+
step = fallback.step;
|
|
345
|
+
}
|
|
346
|
+
const axis = {
|
|
347
|
+
show: flag(value.show, fallback.show, `${path2}.show`, issues),
|
|
348
|
+
title: text(value.title, fallback.title, `${path2}.title`, issues),
|
|
349
|
+
min,
|
|
350
|
+
max,
|
|
351
|
+
step,
|
|
352
|
+
gridlines: flag(value.gridlines, fallback.gridlines, `${path2}.gridlines`, issues),
|
|
353
|
+
format: "format" in value ? normalizeFormat(value.format, `${path2}.format`, issues) : { ...fallback.format }
|
|
354
|
+
};
|
|
355
|
+
const kind = "kind" in value ? value.kind === null ? void 0 : pick(value.kind, CHART_AXIS_KINDS, "category", `${path2}.kind`, issues) : fallback.kind;
|
|
356
|
+
if (kind && kind !== "category") axis.kind = kind;
|
|
357
|
+
return axis;
|
|
358
|
+
}
|
|
359
|
+
function normalizeLegend(value, issues) {
|
|
360
|
+
const fallback = { show: true, position: "bottom" };
|
|
361
|
+
if (value === void 0) return fallback;
|
|
362
|
+
if (!isRecord(value)) {
|
|
363
|
+
issues.push({ path: "legend", message: "legend must be an object." });
|
|
364
|
+
return fallback;
|
|
365
|
+
}
|
|
366
|
+
return {
|
|
367
|
+
show: flag(value.show, fallback.show, "legend.show", issues),
|
|
368
|
+
position: pick(value.position, CHART_LEGEND_POSITIONS, fallback.position, "legend.position", issues)
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
function normalizeChartValues(value, length, path2, issues = []) {
|
|
372
|
+
if (value === void 0) return length ? Array(length).fill(null) : [];
|
|
373
|
+
if (!Array.isArray(value)) {
|
|
374
|
+
issues.push({ path: path2, message: `${path2} must be an array of numbers.` });
|
|
375
|
+
return length ? Array(length).fill(null) : [];
|
|
376
|
+
}
|
|
377
|
+
const out = value.map((v, i) => {
|
|
378
|
+
const parsed = parseChartValue(v);
|
|
379
|
+
if (parsed === void 0) {
|
|
380
|
+
issues.push({ path: `${path2}[${i}]`, message: `${path2}[${i}] is not a number: ${JSON.stringify(v)}.` });
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
return parsed;
|
|
384
|
+
});
|
|
385
|
+
if (length === void 0) return out;
|
|
386
|
+
if (out.length > length) return out.slice(0, length);
|
|
387
|
+
while (out.length < length) out.push(null);
|
|
388
|
+
return out;
|
|
389
|
+
}
|
|
390
|
+
function normalizeSeries(value, length, used, index, issues) {
|
|
391
|
+
const path2 = `series[${index}]`;
|
|
392
|
+
if (!isRecord(value)) {
|
|
393
|
+
issues.push({ path: path2, message: `${path2} must be an object with a name and values.` });
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
let id = typeof value.id === "string" && value.id.trim() ? value.id.trim() : "";
|
|
397
|
+
if (!id || used.has(id)) {
|
|
398
|
+
if (id) issues.push({ path: `${path2}.id`, message: `Series id "${id}" is used twice; the second was renamed.` });
|
|
399
|
+
for (let n = index + 1; ; n++) {
|
|
400
|
+
id = `series-${n}`;
|
|
401
|
+
if (!used.has(id)) break;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
used.add(id);
|
|
405
|
+
const series = {
|
|
406
|
+
id,
|
|
407
|
+
name: text(value.name, `Series ${index + 1}`, `${path2}.name`, issues),
|
|
408
|
+
values: normalizeChartValues(value.values, length, `${path2}.values`, issues)
|
|
409
|
+
};
|
|
410
|
+
if (typeof value.color === "string" && value.color.trim()) series.color = value.color.trim();
|
|
411
|
+
if (value.type !== void 0 && value.type !== null) series.type = pick(value.type, CHART_SERIES_TYPES, "bar", `${path2}.type`, issues);
|
|
412
|
+
if (value.hidden === true) series.hidden = true;
|
|
413
|
+
if (value.axis !== void 0 && value.axis !== null && pick(value.axis, ["primary", "secondary"], "primary", `${path2}.axis`, issues) === "secondary") series.axis = "secondary";
|
|
414
|
+
for (const key2 of ["x", "sizes"]) {
|
|
415
|
+
if (value[key2] === void 0 || value[key2] === null) continue;
|
|
416
|
+
const list = normalizeChartValues(value[key2], length, `${path2}.${key2}`, issues);
|
|
417
|
+
if (list.some((v) => v !== null)) series[key2] = list;
|
|
418
|
+
}
|
|
419
|
+
if (key(value, "sizes") && series.sizes?.some((v) => v !== null && v < 0)) {
|
|
420
|
+
issues.push({ path: `${path2}.sizes`, message: `${path2}.sizes must not be negative.` });
|
|
421
|
+
series.sizes = series.sizes.map((v) => v !== null && v < 0 ? null : v);
|
|
422
|
+
}
|
|
423
|
+
if (value.pointColors !== void 0 && value.pointColors !== null) {
|
|
424
|
+
if (!Array.isArray(value.pointColors)) issues.push({ path: `${path2}.pointColors`, message: `${path2}.pointColors must be an array of colours or nulls.` });
|
|
425
|
+
else {
|
|
426
|
+
const colors = Array.from({ length }, (_, i) => {
|
|
427
|
+
const c = value.pointColors[i];
|
|
428
|
+
return typeof c === "string" && c.trim() ? c.trim() : null;
|
|
429
|
+
});
|
|
430
|
+
if (colors.some((c) => c !== null)) series.pointColors = colors;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (value.marker !== void 0 && value.marker !== null) series.marker = pick(value.marker, CHART_MARKERS, "circle", `${path2}.marker`, issues);
|
|
434
|
+
if (value.curve !== void 0 && value.curve !== null) series.curve = pick(value.curve, CHART_CURVES, "linear", `${path2}.curve`, issues);
|
|
435
|
+
if (value.dataLabels !== void 0 && value.dataLabels !== null) series.dataLabels = flag(value.dataLabels, false, `${path2}.dataLabels`, issues);
|
|
436
|
+
if (value.format !== void 0 && value.format !== null) {
|
|
437
|
+
const format = normalizeFormat(value.format, `${path2}.format`, issues);
|
|
438
|
+
if (Object.keys(format).length) series.format = format;
|
|
439
|
+
}
|
|
440
|
+
if (value.trendlines !== void 0 && value.trendlines !== null) {
|
|
441
|
+
if (!Array.isArray(value.trendlines)) issues.push({ path: `${path2}.trendlines`, message: `${path2}.trendlines must be an array.` });
|
|
442
|
+
else {
|
|
443
|
+
const lines = value.trendlines.map((t, i) => normalizeChartTrendline(t, `${path2}.trendlines[${i}]`, issues)).filter((t) => t !== null);
|
|
444
|
+
if (lines.length) series.trendlines = lines;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return series;
|
|
448
|
+
}
|
|
449
|
+
var key = (value, name) => value[name] !== void 0 && value[name] !== null;
|
|
450
|
+
function normalizeChartTrendline(value, path2, issues = []) {
|
|
451
|
+
if (!isRecord(value)) {
|
|
452
|
+
issues.push({ path: path2, message: `${path2} must be an object with a type.` });
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
const type = pick(value.type, CHART_TRENDLINE_TYPES, "linear", `${path2}.type`, issues);
|
|
456
|
+
const line = { type };
|
|
457
|
+
const whole = (name, min, max) => {
|
|
458
|
+
if (!key(value, name)) return;
|
|
459
|
+
const n = Number(value[name]);
|
|
460
|
+
if (Number.isInteger(n) && n >= min && n <= max) line[name] = n;
|
|
461
|
+
else issues.push({ path: `${path2}.${name}`, message: `${path2}.${name} must be a whole number from ${min} to ${max}.` });
|
|
462
|
+
};
|
|
463
|
+
if (type === "polynomial") whole("order", 2, 6);
|
|
464
|
+
if (type === "moving-average") whole("period", 2, 1e3);
|
|
465
|
+
for (const name of ["forward", "backward"]) {
|
|
466
|
+
if (!key(value, name)) continue;
|
|
467
|
+
const n = Number(value[name]);
|
|
468
|
+
if (Number.isFinite(n) && n >= 0) {
|
|
469
|
+
if (n > 0) line[name] = n;
|
|
470
|
+
} else issues.push({ path: `${path2}.${name}`, message: `${path2}.${name} must be a number of x units, 0 or more.` });
|
|
471
|
+
}
|
|
472
|
+
if (key(value, "intercept")) {
|
|
473
|
+
const n = Number(value.intercept);
|
|
474
|
+
if (!Number.isFinite(n)) issues.push({ path: `${path2}.intercept`, message: `${path2}.intercept must be a number or null.` });
|
|
475
|
+
else if (type === "linear" || type === "polynomial" || type === "exponential") {
|
|
476
|
+
if (type === "exponential" && n <= 0) issues.push({ path: `${path2}.intercept`, message: `${path2}.intercept must be greater than 0 for an exponential trendline.` });
|
|
477
|
+
else line.intercept = n;
|
|
478
|
+
} else issues.push({ path: `${path2}.intercept`, message: `A ${type} trendline cannot have a fixed intercept; use linear, polynomial or exponential.` });
|
|
479
|
+
}
|
|
480
|
+
for (const name of ["showEquation", "showR2"]) if (key(value, name) && flag(value[name], false, `${path2}.${name}`, issues)) line[name] = true;
|
|
481
|
+
for (const name of ["name", "color"]) {
|
|
482
|
+
if (!key(value, name)) continue;
|
|
483
|
+
const t = text(value[name], "", `${path2}.${name}`, issues).trim();
|
|
484
|
+
if (t) line[name] = t;
|
|
485
|
+
}
|
|
486
|
+
return line;
|
|
487
|
+
}
|
|
488
|
+
function normalizeChartSpec(input) {
|
|
489
|
+
const issues = [];
|
|
490
|
+
const value = isRecord(input) ? input : {};
|
|
491
|
+
if (input !== void 0 && !isRecord(input)) issues.push({ path: "", message: "A chart must be an object." });
|
|
492
|
+
const type = pick(value.type, CHART_TYPES, "bar", "type", issues);
|
|
493
|
+
const rawSeries = Array.isArray(value.series) ? value.series : [];
|
|
494
|
+
if (value.series !== void 0 && !Array.isArray(value.series)) issues.push({ path: "series", message: "series must be an array." });
|
|
495
|
+
let categories;
|
|
496
|
+
if (Array.isArray(value.categories)) categories = value.categories.map((c, i) => text(c, "", `categories[${i}]`, issues));
|
|
497
|
+
else {
|
|
498
|
+
if (value.categories !== void 0) issues.push({ path: "categories", message: "categories must be an array of labels." });
|
|
499
|
+
const longest = rawSeries.reduce((n, s) => Math.max(n, isRecord(s) && Array.isArray(s.values) ? s.values.length : 0), 0);
|
|
500
|
+
categories = Array.from({ length: longest }, (_, i) => `${i + 1}`);
|
|
501
|
+
}
|
|
502
|
+
const used = /* @__PURE__ */ new Set();
|
|
503
|
+
const series = rawSeries.map((s, i) => normalizeSeries(s, categories.length, used, i, issues)).filter((s) => s !== null);
|
|
504
|
+
let palette = [...DEFAULT_CHART_PALETTE];
|
|
505
|
+
if (Array.isArray(value.palette)) {
|
|
506
|
+
const colors = value.palette.filter((c) => typeof c === "string" && c.trim() !== "");
|
|
507
|
+
if (colors.length) palette = colors;
|
|
508
|
+
else issues.push({ path: "palette", message: "palette must list at least one colour." });
|
|
509
|
+
} else if (value.palette !== void 0) issues.push({ path: "palette", message: "palette must be an array of colours." });
|
|
510
|
+
const fraction = (name, fallback, min, max) => {
|
|
511
|
+
if (value[name] === void 0) return fallback;
|
|
512
|
+
const r = Number(value[name]);
|
|
513
|
+
if (value[name] !== null && Number.isFinite(r) && r >= min && r <= max) return r;
|
|
514
|
+
issues.push({ path: name, message: `${name} must be a number from ${min} to ${max}.` });
|
|
515
|
+
return fallback;
|
|
516
|
+
};
|
|
517
|
+
const innerRadius = fraction("innerRadius", 0.55, 0, 0.9);
|
|
518
|
+
const gapWidth = fraction("gapWidth", 0.28, 0, 0.9);
|
|
519
|
+
const overlap = fraction("overlap", -0.12, -1, 1);
|
|
520
|
+
let bins = null;
|
|
521
|
+
if (value.bins !== void 0 && value.bins !== null) {
|
|
522
|
+
const b = Number(value.bins);
|
|
523
|
+
if (Number.isInteger(b) && b >= 1 && b <= 500) bins = b;
|
|
524
|
+
else issues.push({ path: "bins", message: "bins must be a whole number from 1 to 500, or null for automatic." });
|
|
525
|
+
}
|
|
526
|
+
const spec = {
|
|
527
|
+
version: 1,
|
|
528
|
+
type,
|
|
529
|
+
title: text(value.title, "", "title", issues),
|
|
530
|
+
subtitle: text(value.subtitle, "", "subtitle", issues),
|
|
531
|
+
categories,
|
|
532
|
+
series,
|
|
533
|
+
xAxis: normalizeChartAxis(value.xAxis, defaultChartAxis(false), "xAxis", issues),
|
|
534
|
+
yAxis: normalizeChartAxis(value.yAxis, defaultChartAxis(true), "yAxis", issues),
|
|
535
|
+
yAxis2: normalizeChartAxis(value.yAxis2, defaultChartAxis(false), "yAxis2", issues),
|
|
536
|
+
legend: normalizeLegend(value.legend, issues),
|
|
537
|
+
stacking: pick(value.stacking, CHART_STACKINGS, "none", "stacking", issues),
|
|
538
|
+
orientation: pick(value.orientation, CHART_ORIENTATIONS, "vertical", "orientation", issues),
|
|
539
|
+
curve: pick(value.curve, CHART_CURVES, "linear", "curve", issues),
|
|
540
|
+
points: flag(value.points, true, "points", issues),
|
|
541
|
+
dataLabels: flag(value.dataLabels, false, "dataLabels", issues),
|
|
542
|
+
labelPosition: pick(value.labelPosition, CHART_LABEL_POSITIONS, "auto", "labelPosition", issues),
|
|
543
|
+
palette,
|
|
544
|
+
innerRadius,
|
|
545
|
+
gapWidth,
|
|
546
|
+
overlap,
|
|
547
|
+
bins,
|
|
548
|
+
showTotal: flag(value.showTotal, true, "showTotal", issues)
|
|
549
|
+
};
|
|
550
|
+
return { spec, issues };
|
|
551
|
+
}
|
|
552
|
+
function cloneChartSpec(spec) {
|
|
553
|
+
return {
|
|
554
|
+
...spec,
|
|
555
|
+
categories: [...spec.categories],
|
|
556
|
+
series: spec.series.map(cloneChartSeries),
|
|
557
|
+
xAxis: { ...spec.xAxis, format: { ...spec.xAxis.format } },
|
|
558
|
+
yAxis: { ...spec.yAxis, format: { ...spec.yAxis.format } },
|
|
559
|
+
yAxis2: { ...spec.yAxis2, format: { ...spec.yAxis2.format } },
|
|
560
|
+
legend: { ...spec.legend },
|
|
561
|
+
palette: [...spec.palette]
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
function cloneChartSeries(series) {
|
|
565
|
+
const copy = { ...series, values: [...series.values] };
|
|
566
|
+
if (series.x) copy.x = [...series.x];
|
|
567
|
+
if (series.sizes) copy.sizes = [...series.sizes];
|
|
568
|
+
if (series.pointColors) copy.pointColors = [...series.pointColors];
|
|
569
|
+
if (series.format) copy.format = { ...series.format };
|
|
570
|
+
if (series.trendlines) copy.trendlines = series.trendlines.map((t) => ({ ...t }));
|
|
571
|
+
return copy;
|
|
572
|
+
}
|
|
573
|
+
function chartDataFromTable(rows, options = {}) {
|
|
574
|
+
let grid = rows.map((r) => [...r]).filter((r) => r.some((c) => c !== null && c !== void 0 && String(c).trim() !== ""));
|
|
575
|
+
if (options.seriesIn === "rows") {
|
|
576
|
+
const width2 = Math.max(0, ...grid.map((r) => r.length));
|
|
577
|
+
grid = Array.from({ length: width2 }, (_, c) => grid.map((r) => r[c]));
|
|
578
|
+
}
|
|
579
|
+
const issues = [];
|
|
580
|
+
if (!grid.length) return { categories: [], series: [], issues };
|
|
581
|
+
const width = Math.max(...grid.map((r) => r.length));
|
|
582
|
+
const header = options.header === "auto" ? grid[0].slice(1).some((c) => c !== null && c !== void 0 && String(c).trim() !== "" && parseChartValue(c) === void 0) : options.header ?? true;
|
|
583
|
+
const names = header ? grid[0] : [];
|
|
584
|
+
const body = header ? grid.slice(1) : grid;
|
|
585
|
+
const categories = body.map((r, i) => {
|
|
586
|
+
const c = r[0];
|
|
587
|
+
return c === null || c === void 0 || String(c).trim() === "" ? `${i + 1}` : String(c).trim();
|
|
588
|
+
});
|
|
589
|
+
const series = [];
|
|
590
|
+
for (let col = 1; col < width; col++) {
|
|
591
|
+
const name = names[col] !== void 0 && String(names[col]).trim() ? String(names[col]).trim() : `Series ${col}`;
|
|
592
|
+
const values = body.map((r, i) => {
|
|
593
|
+
const parsed = parseChartValue(r[col] ?? null);
|
|
594
|
+
if (parsed === void 0) {
|
|
595
|
+
issues.push({ path: `${name}[${i}]`, message: `"${String(r[col])}" in ${name} (${categories[i]}) is not a number; it was left blank.` });
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
598
|
+
return parsed;
|
|
599
|
+
});
|
|
600
|
+
series.push({ id: `series-${col}`, name, values });
|
|
601
|
+
}
|
|
602
|
+
if (options.xFromFirstColumn) {
|
|
603
|
+
const x = body.map((r, i) => {
|
|
604
|
+
const parsed = parseChartValue(r[0] ?? null);
|
|
605
|
+
if (parsed === void 0) issues.push({ path: `x[${i}]`, message: `"${String(r[0])}" in the first column is not a number; that point has no x.` });
|
|
606
|
+
return parsed ?? null;
|
|
607
|
+
});
|
|
608
|
+
for (const s of series) s.x = [...x];
|
|
609
|
+
}
|
|
610
|
+
return { categories, series, issues };
|
|
611
|
+
}
|
|
612
|
+
function chartDataToTable(spec, options = {}) {
|
|
613
|
+
return [
|
|
614
|
+
[options.categoryHeader ?? "", ...spec.series.map((s) => s.name)],
|
|
615
|
+
...spec.categories.map((category, i) => [category, ...spec.series.map((s) => s.values[i] ?? null)])
|
|
616
|
+
];
|
|
617
|
+
}
|
|
618
|
+
function chartColorInputValue(color) {
|
|
619
|
+
if (/^#[0-9a-f]{6}$/i.test(color)) return color.toLowerCase();
|
|
620
|
+
if (/^#[0-9a-f]{3}$/i.test(color)) return `#${[...color.slice(1)].map((c) => c + c).join("")}`.toLowerCase();
|
|
621
|
+
return "#000000";
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// src/chart/ooxml.ts
|
|
625
|
+
var CHART_OOXML_NS = "http://schemas.openxmlformats.org/drawingml/2006/chart";
|
|
626
|
+
var CHART_OOXML_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.drawingml.chart+xml";
|
|
627
|
+
var CHART_OOXML_RELATIONSHIP = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart";
|
|
628
|
+
var MAX_POINTS = 1e5;
|
|
629
|
+
function parser(options) {
|
|
630
|
+
const P = options.DOMParser ?? globalThis.DOMParser;
|
|
631
|
+
if (!P) throw new Error("Reading chart XML needs a DOMParser. In Node, pass one (e.g. from jsdom or @xmldom/xmldom) as { DOMParser }.");
|
|
632
|
+
return P;
|
|
633
|
+
}
|
|
634
|
+
var kids = (el, name) => el ? Array.from(el.childNodes).filter((n) => n.nodeType === 1 && (!name || n.localName === name)) : [];
|
|
635
|
+
var kid = (el, name) => kids(el, name)[0];
|
|
636
|
+
var val = (el, name) => kid(el, name)?.getAttribute("val") ?? void 0;
|
|
637
|
+
var path = (el, ...names) => names.reduce((e, n) => kid(e, n), el ?? void 0);
|
|
638
|
+
var find = (el, name) => {
|
|
639
|
+
if (!el) return void 0;
|
|
640
|
+
for (const c of kids(el)) {
|
|
641
|
+
if (c.localName === name) return c;
|
|
642
|
+
const deep = find(c, name);
|
|
643
|
+
if (deep) return deep;
|
|
644
|
+
}
|
|
645
|
+
return void 0;
|
|
646
|
+
};
|
|
647
|
+
var bool = (v, fallback = false) => v === void 0 ? fallback : v === "1" || v === "true";
|
|
648
|
+
var numberOf = (v) => {
|
|
649
|
+
if (v === void 0) return null;
|
|
650
|
+
const n = Number(v);
|
|
651
|
+
return Number.isFinite(n) ? n : null;
|
|
652
|
+
};
|
|
653
|
+
function strings(e) {
|
|
654
|
+
if (!e) return [];
|
|
655
|
+
const cache = find(e, "strCache") ?? find(e, "numCache") ?? find(e, "strLit") ?? find(e, "numLit");
|
|
656
|
+
if (cache) {
|
|
657
|
+
const n = Math.min(MAX_POINTS, Number(val(cache, "ptCount") ?? 0) || 0);
|
|
658
|
+
const out = Array(n).fill("");
|
|
659
|
+
for (const pt of kids(cache, "pt").slice(0, MAX_POINTS)) {
|
|
660
|
+
const i = Number(pt.getAttribute("idx") ?? 0);
|
|
661
|
+
if (!Number.isInteger(i) || i < 0 || i >= MAX_POINTS) continue;
|
|
662
|
+
while (out.length <= i) out.push("");
|
|
663
|
+
out[i] = kid(pt, "v")?.textContent ?? "";
|
|
664
|
+
}
|
|
665
|
+
return out;
|
|
666
|
+
}
|
|
667
|
+
const multi = find(e, "multiLvlStrCache");
|
|
668
|
+
if (multi) {
|
|
669
|
+
const lvl = kid(multi, "lvl");
|
|
670
|
+
return kids(lvl, "pt").map((p) => kid(p, "v")?.textContent ?? "");
|
|
671
|
+
}
|
|
672
|
+
const v = kid(e, "v") ?? find(e, "v");
|
|
673
|
+
return v ? [v.textContent ?? ""] : [];
|
|
674
|
+
}
|
|
675
|
+
var numbers = (e) => strings(e).map((s) => s.trim() === "" ? null : numberOf(s.trim()));
|
|
676
|
+
function formatCodeOf(e) {
|
|
677
|
+
return find(e, "numCache") ? kid(find(e, "numCache"), "formatCode")?.textContent ?? void 0 : void 0;
|
|
678
|
+
}
|
|
679
|
+
function richText(t) {
|
|
680
|
+
if (!t) return void 0;
|
|
681
|
+
const tx = kid(t, "tx");
|
|
682
|
+
const rich2 = kid(tx, "rich");
|
|
683
|
+
if (rich2) return kids(rich2, "p").map((p) => Array.from(p.getElementsByTagNameNS("*", "t")).map((x) => x.textContent ?? "").join("")).join("\n");
|
|
684
|
+
const v = find(tx, "v");
|
|
685
|
+
return v?.textContent ?? void 0;
|
|
686
|
+
}
|
|
687
|
+
function solidColor(spPr) {
|
|
688
|
+
const fill = kid(spPr, "solidFill") ?? kid(kid(spPr, "ln"), "solidFill");
|
|
689
|
+
const srgb = kid(fill, "srgbClr");
|
|
690
|
+
const v = srgb?.getAttribute("val");
|
|
691
|
+
return v && /^[0-9a-f]{6}$/i.test(v) ? `#${v.toLowerCase()}` : void 0;
|
|
692
|
+
}
|
|
693
|
+
var MARKERS = { circle: "circle", dot: "circle", square: "square", diamond: "diamond", triangle: "triangle", x: "cross", plus: "cross", star: "cross", dash: "square", none: "none" };
|
|
694
|
+
var TREND_IN = { linear: "linear", exp: "exponential", log: "logarithmic", poly: "polynomial", power: "power", movingAvg: "moving-average" };
|
|
695
|
+
var TREND_OUT = { linear: "linear", exponential: "exp", logarithmic: "log", polynomial: "poly", power: "power", "moving-average": "movingAvg" };
|
|
696
|
+
var LEGEND_IN = { b: "bottom", t: "top", l: "left", r: "right", tr: "right" };
|
|
697
|
+
function chartFromOoxml(xml, options = {}) {
|
|
698
|
+
const issues = [];
|
|
699
|
+
const unsupported = [];
|
|
700
|
+
let doc;
|
|
701
|
+
try {
|
|
702
|
+
doc = new (parser(options))().parseFromString(xml, "application/xml");
|
|
703
|
+
} catch (error) {
|
|
704
|
+
return { spec: normalizeChartSpec({}).spec, issues: [{ path: "", message: `The chart XML could not be read: ${error.message}` }], unsupported, lossless: false };
|
|
705
|
+
}
|
|
706
|
+
const root = doc.documentElement;
|
|
707
|
+
if (!root || root.localName !== "chartSpace" || doc.getElementsByTagName("parsererror").length) {
|
|
708
|
+
return { spec: normalizeChartSpec({}).spec, issues: [{ path: "", message: "This is not a chart part: expected a c:chartSpace (or cx:chartSpace) root element." }], unsupported, lossless: false };
|
|
709
|
+
}
|
|
710
|
+
if (root.namespaceURI?.includes("chartex") || kid(root, "chartData")) return readChartex(root, issues, unsupported);
|
|
711
|
+
const chart = kid(root, "chart");
|
|
712
|
+
const plot = kid(chart, "plotArea");
|
|
713
|
+
const input = { type: "bar" };
|
|
714
|
+
const titleEl = kid(chart, "title");
|
|
715
|
+
if (titleEl && !bool(val(chart, "autoTitleDeleted"))) input.title = richText(titleEl) ?? "";
|
|
716
|
+
const legend = kid(chart, "legend");
|
|
717
|
+
input.legend = { show: !!legend, position: LEGEND_IN[val(legend, "legendPos") ?? "r"] ?? "right" };
|
|
718
|
+
if (kid(root, "roundedCorners") && bool(val(root, "roundedCorners"))) unsupported.push("rounded corners");
|
|
719
|
+
const valAxes = kids(plot, "valAx");
|
|
720
|
+
const vertical = (a) => ["l", "r"].includes(val(a, "axPos") ?? "");
|
|
721
|
+
const secondaryPlaced = (a) => val(a, "crosses") === "max" || ["r", "t"].includes(val(a, "axPos") ?? "");
|
|
722
|
+
const groupValAxis = (g) => {
|
|
723
|
+
const ids = kids(g, "axId").map((a) => a.getAttribute("val"));
|
|
724
|
+
const axes = ids.map((id) => valAxes.find((a) => val(a, "axId") === id)).filter((a) => !!a);
|
|
725
|
+
return axes.find(vertical) ?? axes[0];
|
|
726
|
+
};
|
|
727
|
+
const groups = kids(plot).filter((g) => g.localName.endsWith("Chart"));
|
|
728
|
+
const used = groups.map(groupValAxis).filter((a) => !!a);
|
|
729
|
+
const primaryAxis = used.find((a) => !secondaryPlaced(a)) ?? used[0];
|
|
730
|
+
const series = [];
|
|
731
|
+
let categories = [];
|
|
732
|
+
const types = [];
|
|
733
|
+
let stacking = "none";
|
|
734
|
+
let orientation = "vertical";
|
|
735
|
+
let dataLabels = false;
|
|
736
|
+
let points = false;
|
|
737
|
+
for (const g of groups) {
|
|
738
|
+
const name = g.localName;
|
|
739
|
+
const grouping = val(g, "grouping") ?? "clustered";
|
|
740
|
+
let type;
|
|
741
|
+
if (/3D/.test(name)) unsupported.push(`3-D ${name.replace(/3DChart$/, "")} chart (drawn flat)`);
|
|
742
|
+
switch (name) {
|
|
743
|
+
case "barChart":
|
|
744
|
+
case "bar3DChart":
|
|
745
|
+
type = "bar";
|
|
746
|
+
if (val(g, "barDir") === "bar") orientation = "horizontal";
|
|
747
|
+
break;
|
|
748
|
+
case "lineChart":
|
|
749
|
+
case "line3DChart":
|
|
750
|
+
type = "line";
|
|
751
|
+
if (kids(g, "ser").some((s) => (val(kid(s, "marker"), "symbol") ?? (bool(val(g, "marker"), true) ? "auto" : "none")) !== "none")) points = true;
|
|
752
|
+
break;
|
|
753
|
+
case "areaChart":
|
|
754
|
+
case "area3DChart":
|
|
755
|
+
type = "area";
|
|
756
|
+
break;
|
|
757
|
+
case "pieChart":
|
|
758
|
+
case "pie3DChart":
|
|
759
|
+
case "ofPieChart":
|
|
760
|
+
type = "pie";
|
|
761
|
+
if (name === "ofPieChart") unsupported.push("pie-of-pie (drawn as one pie)");
|
|
762
|
+
break;
|
|
763
|
+
case "doughnutChart":
|
|
764
|
+
type = "donut";
|
|
765
|
+
input.innerRadius = Math.min(0.9, Math.max(0, (numberOf(val(g, "holeSize")) ?? 50) / 100));
|
|
766
|
+
break;
|
|
767
|
+
case "scatterChart":
|
|
768
|
+
type = "scatter";
|
|
769
|
+
if (/line/i.test(val(g, "scatterStyle") ?? "")) points = true;
|
|
770
|
+
break;
|
|
771
|
+
case "bubbleChart":
|
|
772
|
+
type = "bubble";
|
|
773
|
+
break;
|
|
774
|
+
case "radarChart":
|
|
775
|
+
type = "radar";
|
|
776
|
+
break;
|
|
777
|
+
default:
|
|
778
|
+
unsupported.push(`${name.replace(/Chart$/, "")} chart`);
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
if (grouping === "stacked") stacking = "stacked";
|
|
782
|
+
else if (grouping === "percentStacked") stacking = "percent";
|
|
783
|
+
const gap = numberOf(val(g, "gapWidth"));
|
|
784
|
+
const overlap = numberOf(val(g, "overlap"));
|
|
785
|
+
if (type === "bar") {
|
|
786
|
+
const n = Math.max(1, kids(g, "ser").length);
|
|
787
|
+
const g100 = (gap ?? 150) / 100;
|
|
788
|
+
const o = Math.max(-1, Math.min(1, (overlap ?? (grouping === "clustered" ? 0 : 100)) / 100));
|
|
789
|
+
const barsWidth = grouping === "clustered" ? n - (n - 1) * o : 1;
|
|
790
|
+
input.gapWidth = Math.round(Math.max(0, Math.min(0.9, g100 / (barsWidth + g100))) * 100) / 100;
|
|
791
|
+
if (grouping === "clustered") input.overlap = o;
|
|
792
|
+
}
|
|
793
|
+
if (bool(val(kid(g, "dLbls"), "showVal"))) dataLabels = true;
|
|
794
|
+
types.push(type);
|
|
795
|
+
const axis = groupValAxis(g);
|
|
796
|
+
const secondary = !!axis && !!primaryAxis && axis !== primaryAxis;
|
|
797
|
+
for (const s of kids(g, "ser").slice(0, 1e3)) {
|
|
798
|
+
const out = { id: `series-${series.length + 1}`, name: strings(kid(s, "tx"))[0] ?? `Series ${series.length + 1}`, values: [] };
|
|
799
|
+
const color = solidColor(kid(s, "spPr"));
|
|
800
|
+
if (color) out.color = color;
|
|
801
|
+
out.type = type === "bubble" ? void 0 : type;
|
|
802
|
+
if (secondary) out.axis = "secondary";
|
|
803
|
+
const marker = val(kid(s, "marker"), "symbol");
|
|
804
|
+
if (marker && MARKERS[marker] && MARKERS[marker] !== "circle" && (type === "scatter" || MARKERS[marker] !== "none") && (type === "line" || type === "scatter")) out.marker = MARKERS[marker];
|
|
805
|
+
if (bool(val(s, "smooth"))) out.curve = "smooth";
|
|
806
|
+
const sl = kid(s, "dLbls");
|
|
807
|
+
if (sl && val(sl, "showVal") !== void 0) out.dataLabels = bool(val(sl, "showVal"));
|
|
808
|
+
const labelCode = [kid(sl, "numFmt"), kid(kid(g, "dLbls"), "numFmt")].find((n) => n && n.getAttribute("sourceLinked") !== "1")?.getAttribute("formatCode");
|
|
809
|
+
const valueEl = kid(s, "val") ?? kid(s, "yVal");
|
|
810
|
+
const code = labelCode ?? formatCodeOf(valueEl);
|
|
811
|
+
if (code && code !== "General") out.format = { code };
|
|
812
|
+
const catEl = kid(s, "cat") ?? kid(s, "xVal");
|
|
813
|
+
const cats = strings(catEl);
|
|
814
|
+
if (type === "scatter" || type === "bubble") {
|
|
815
|
+
const xs = cats.map((c) => numberOf(c.trim()));
|
|
816
|
+
if (xs.some((x) => x !== null)) out.x = xs;
|
|
817
|
+
}
|
|
818
|
+
if (!categories.length && cats.length) categories = cats;
|
|
819
|
+
out.values = numbers(valueEl);
|
|
820
|
+
if (type === "bubble") out.sizes = numbers(kid(s, "bubbleSize"));
|
|
821
|
+
const dPts = kids(s, "dPt");
|
|
822
|
+
if (dPts.length) {
|
|
823
|
+
const pc = [];
|
|
824
|
+
for (const dp of dPts) {
|
|
825
|
+
const i = Number(val(dp, "idx"));
|
|
826
|
+
const c = solidColor(kid(dp, "spPr"));
|
|
827
|
+
if (Number.isInteger(i) && i >= 0 && i < MAX_POINTS && c) {
|
|
828
|
+
while (pc.length <= i) pc.push(null);
|
|
829
|
+
pc[i] = c;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
if (pc.some(Boolean)) out.pointColors = pc;
|
|
833
|
+
}
|
|
834
|
+
const trends = [];
|
|
835
|
+
for (const t of kids(s, "trendline")) {
|
|
836
|
+
const kind = TREND_IN[val(t, "trendlineType") ?? ""];
|
|
837
|
+
if (!kind) continue;
|
|
838
|
+
const line = { type: kind };
|
|
839
|
+
const order = numberOf(val(t, "order"));
|
|
840
|
+
const period = numberOf(val(t, "period"));
|
|
841
|
+
if (kind === "polynomial" && order) line.order = order;
|
|
842
|
+
if (kind === "moving-average" && period) line.period = period;
|
|
843
|
+
for (const k of ["forward", "backward", "intercept"]) {
|
|
844
|
+
const n = numberOf(val(t, k));
|
|
845
|
+
if (n !== null && (k === "intercept" || n > 0)) line[k] = n;
|
|
846
|
+
}
|
|
847
|
+
if (bool(val(t, "dispEq"))) line.showEquation = true;
|
|
848
|
+
if (bool(val(t, "dispRSqr"))) line.showR2 = true;
|
|
849
|
+
const tName = kid(t, "name")?.textContent;
|
|
850
|
+
if (tName) line.name = tName;
|
|
851
|
+
const tc = solidColor(kid(t, "spPr"));
|
|
852
|
+
if (tc) line.color = tc;
|
|
853
|
+
trends.push(line);
|
|
854
|
+
}
|
|
855
|
+
if (trends.length) out.trendlines = trends;
|
|
856
|
+
if (kids(s, "errBars").length) unsupported.push("error bars");
|
|
857
|
+
series.push(out);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
if (!types.length) {
|
|
861
|
+
issues.push({ path: "plotArea", message: unsupported.length ? `The chart type is not supported: ${unsupported.join(", ")}.` : "The chart has no plot groups." });
|
|
862
|
+
return { spec: normalizeChartSpec(input).spec, issues, unsupported, lossless: false };
|
|
863
|
+
}
|
|
864
|
+
const first = types[0];
|
|
865
|
+
const combo = types.some((t) => t !== first);
|
|
866
|
+
input.type = combo ? types.includes("bar") ? "bar" : first : first;
|
|
867
|
+
if (!combo) for (const s of series) delete s.type;
|
|
868
|
+
else for (const s of series) if (s.type === input.type) delete s.type;
|
|
869
|
+
if (!combo) {
|
|
870
|
+
for (const s of series) if (s.axis) issues.push({ path: "series", message: `"${s.name}" is on a second value axis.` });
|
|
871
|
+
}
|
|
872
|
+
if (types.some((t) => !isCartesianChart(t)) && combo) unsupported.push("a pie or radar mixed with other chart types");
|
|
873
|
+
input.series = series;
|
|
874
|
+
input.categories = categories.length ? categories : Array.from({ length: Math.max(0, ...series.map((s) => s.values.length)) }, (_, i) => `${i + 1}`);
|
|
875
|
+
input.stacking = stacking;
|
|
876
|
+
input.orientation = orientation;
|
|
877
|
+
input.dataLabels = dataLabels;
|
|
878
|
+
input.points = points || first !== "line";
|
|
879
|
+
if (bool(val(groups[0], "varyColors")) && (first === "bar" || first === "line") && series.length === 1) unsupported.push("one colour per point (vary colours)");
|
|
880
|
+
const readAxis = (a, fallback) => {
|
|
881
|
+
const axis = { ...fallback, format: {} };
|
|
882
|
+
if (!a) return axis;
|
|
883
|
+
axis.show = !bool(val(a, "delete"));
|
|
884
|
+
axis.title = richText(kid(a, "title")) ?? "";
|
|
885
|
+
axis.gridlines = !!kid(a, "majorGridlines");
|
|
886
|
+
const scaling = kid(a, "scaling");
|
|
887
|
+
axis.min = numberOf(val(scaling, "min"));
|
|
888
|
+
axis.max = numberOf(val(scaling, "max"));
|
|
889
|
+
if (val(scaling, "orientation") === "maxMin") unsupported.push("a reversed axis");
|
|
890
|
+
if (kid(scaling, "logBase")) unsupported.push("a logarithmic axis");
|
|
891
|
+
axis.step = numberOf(val(a, "majorUnit"));
|
|
892
|
+
const fmt = kid(a, "numFmt");
|
|
893
|
+
const code = fmt?.getAttribute("formatCode");
|
|
894
|
+
if (code && code !== "General" && fmt?.getAttribute("sourceLinked") !== "1") axis.format = { code };
|
|
895
|
+
return axis;
|
|
896
|
+
};
|
|
897
|
+
const xy = first === "scatter" || first === "bubble";
|
|
898
|
+
const catAxis = kid(plot, "catAx") ?? kid(plot, "dateAx") ?? (xy ? valAxes.find((a) => !vertical(a)) : void 0);
|
|
899
|
+
input.xAxis = readAxis(catAxis, defaultChartAxis(false));
|
|
900
|
+
if (kid(plot, "dateAx")) input.xAxis.kind = "date";
|
|
901
|
+
input.yAxis = readAxis(primaryAxis ?? valAxes.find(vertical) ?? valAxes[0], defaultChartAxis(true));
|
|
902
|
+
const secondaryAxis = valAxes.find((a) => a !== primaryAxis && (xy ? vertical(a) && a !== primaryAxis : true) && a !== catAxis);
|
|
903
|
+
if (series.some((s) => s.axis === "secondary") && secondaryAxis) input.yAxis2 = readAxis(secondaryAxis, defaultChartAxis(false));
|
|
904
|
+
const { spec, issues: specIssues } = normalizeChartSpec(input);
|
|
905
|
+
issues.push(...specIssues);
|
|
906
|
+
const uniq = [...new Set(unsupported)];
|
|
907
|
+
return { spec, issues, unsupported: uniq, lossless: uniq.length === 0 && specIssues.length === 0 };
|
|
908
|
+
}
|
|
909
|
+
function readChartex(root, issues, unsupported) {
|
|
910
|
+
const data = /* @__PURE__ */ new Map();
|
|
911
|
+
for (const d of kids(kid(root, "chartData"), "data")) {
|
|
912
|
+
const id = d.getAttribute("id") ?? String(data.size);
|
|
913
|
+
const strDim = kids(d, "strDim")[0];
|
|
914
|
+
const numDim = kids(d, "numDim")[0];
|
|
915
|
+
const lvl = (dim) => kids(dim, "lvl")[0];
|
|
916
|
+
const cats = kids(lvl(strDim), "pt").map((p) => p.textContent ?? "");
|
|
917
|
+
const values = [];
|
|
918
|
+
const nl = lvl(numDim);
|
|
919
|
+
for (const p of kids(nl, "pt")) {
|
|
920
|
+
const i = Number(p.getAttribute("idx") ?? values.length);
|
|
921
|
+
while (values.length < i) values.push(null);
|
|
922
|
+
values[i] = numberOf((p.textContent ?? "").trim());
|
|
923
|
+
}
|
|
924
|
+
data.set(id, { categories: cats, values, code: nl?.getAttribute("formatCode") ?? void 0 });
|
|
925
|
+
}
|
|
926
|
+
const chart = kid(root, "chart");
|
|
927
|
+
const region = path(chart, "plotArea", "plotAreaRegion");
|
|
928
|
+
const seriesEls = kids(region, "series");
|
|
929
|
+
const layouts = { waterfall: "waterfall", funnel: "funnel", treemap: "treemap", sunburst: "treemap", boxWhisker: "box", clusteredColumn: "histogram", paretoLine: "histogram" };
|
|
930
|
+
const layout = seriesEls[0]?.getAttribute("layoutId") ?? "";
|
|
931
|
+
const type = layouts[layout];
|
|
932
|
+
if (!type) {
|
|
933
|
+
issues.push({ path: "plotArea", message: `The chart layout "${layout || "unknown"}" is not supported.` });
|
|
934
|
+
return { spec: normalizeChartSpec({}).spec, issues, unsupported: [layout || "unknown chartex layout"], lossless: false };
|
|
935
|
+
}
|
|
936
|
+
if (layout === "sunburst") unsupported.push("sunburst (drawn as a treemap of the innermost level)");
|
|
937
|
+
if (layout === "paretoLine") unsupported.push("pareto line");
|
|
938
|
+
const series = [];
|
|
939
|
+
let categories = [];
|
|
940
|
+
for (const s of seriesEls) {
|
|
941
|
+
if (s.getAttribute("layoutId") === "paretoLine") continue;
|
|
942
|
+
const ref = kid(s, "dataId")?.getAttribute("val") ?? "0";
|
|
943
|
+
const d = data.get(ref);
|
|
944
|
+
if (!d) continue;
|
|
945
|
+
if (!categories.length) categories = d.categories;
|
|
946
|
+
const out = { id: `series-${series.length + 1}`, name: strings(kid(s, "tx"))[0] ?? `Series ${series.length + 1}`, values: d.values };
|
|
947
|
+
if (d.code && d.code !== "General") out.format = { code: d.code };
|
|
948
|
+
series.push(out);
|
|
949
|
+
}
|
|
950
|
+
const title = richText(kid(chart, "title"));
|
|
951
|
+
const longest = Math.max(0, ...series.map((s) => s.values.length));
|
|
952
|
+
const input = {
|
|
953
|
+
type,
|
|
954
|
+
title: title ?? "",
|
|
955
|
+
series,
|
|
956
|
+
categories: categories.length >= longest ? categories : Array.from({ length: longest }, (_, i) => categories[i] ?? `${i + 1}`)
|
|
957
|
+
};
|
|
958
|
+
if (type === "waterfall") input.showTotal = false;
|
|
959
|
+
const { spec, issues: specIssues } = normalizeChartSpec(input);
|
|
960
|
+
issues.push(...specIssues);
|
|
961
|
+
return { spec, issues, unsupported, lossless: unsupported.length === 0 && specIssues.length === 0 };
|
|
962
|
+
}
|
|
963
|
+
var esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
964
|
+
var hex = (color) => {
|
|
965
|
+
if (!color) return null;
|
|
966
|
+
const rgb = parseChartColor(color);
|
|
967
|
+
return rgb ? chartRgbToHex(rgb).slice(1).toUpperCase() : null;
|
|
968
|
+
};
|
|
969
|
+
var fillXml = (color, line = false) => {
|
|
970
|
+
const h = hex(color);
|
|
971
|
+
if (!h) return "";
|
|
972
|
+
return line ? `<c:spPr><a:ln w="28575" cap="rnd"><a:solidFill><a:srgbClr val="${h}"/></a:solidFill><a:round/></a:ln></c:spPr>` : `<c:spPr><a:solidFill><a:srgbClr val="${h}"/></a:solidFill></c:spPr>`;
|
|
973
|
+
};
|
|
974
|
+
var rich = (text2) => `<c:tx><c:rich><a:bodyPr/><a:lstStyle/><a:p><a:r><a:t>${esc(text2)}</a:t></a:r></a:p></c:rich></c:tx><c:overlay val="0"/>`;
|
|
975
|
+
var strLit = (items) => `<c:strLit><c:ptCount val="${items.length}"/>${items.map((s, i) => `<c:pt idx="${i}"><c:v>${esc(s)}</c:v></c:pt>`).join("")}</c:strLit>`;
|
|
976
|
+
var numLit = (items, code = "General") => `<c:numLit><c:formatCode>${esc(code)}</c:formatCode><c:ptCount val="${items.length}"/>${items.map((v, i) => v === null || !Number.isFinite(v) ? "" : `<c:pt idx="${i}"><c:v>${v}</c:v></c:pt>`).join("")}</c:numLit>`;
|
|
977
|
+
var CURRENCY_SYMBOL = { USD: "$", EUR: "\u20AC", GBP: "\xA3", JPY: "\xA5", CNY: "\xA5", INR: "\u20B9" };
|
|
978
|
+
function chartFormatToCode(format) {
|
|
979
|
+
if (format.code) return format.code;
|
|
980
|
+
const d = format.decimals;
|
|
981
|
+
const frac = (n, fallback) => {
|
|
982
|
+
const k = n ?? fallback;
|
|
983
|
+
return k > 0 ? `.${"0".repeat(k)}` : "";
|
|
984
|
+
};
|
|
985
|
+
const wrap = (body) => `${format.prefix ? `"${format.prefix.replace(/"/g, "")}"` : ""}${body}${format.suffix ? `"${format.suffix.replace(/"/g, "")}"` : ""}`;
|
|
986
|
+
switch (format.style) {
|
|
987
|
+
case "percent":
|
|
988
|
+
return wrap(`#,##0${frac(d, 0)}%`);
|
|
989
|
+
case "currency": {
|
|
990
|
+
const code = (format.currency || "USD").toUpperCase();
|
|
991
|
+
const sym = CURRENCY_SYMBOL[code] ?? `${code} `;
|
|
992
|
+
return wrap(`"${sym}"#,##0${frac(d, 0)}`);
|
|
993
|
+
}
|
|
994
|
+
case "compact":
|
|
995
|
+
return wrap('#,##0.0,,"M"');
|
|
996
|
+
default:
|
|
997
|
+
return d !== void 0 || format.prefix || format.suffix ? wrap(`#,##0${frac(d, 0)}`) : "General";
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
var MARKER_OUT = { circle: "circle", square: "square", diamond: "diamond", triangle: "triangle", cross: "x", none: "none" };
|
|
1001
|
+
function chartToOoxml(spec) {
|
|
1002
|
+
const issues = [];
|
|
1003
|
+
const family = chartFamily(spec);
|
|
1004
|
+
let type = spec.type;
|
|
1005
|
+
if (family === "waterfall" || family === "histogram" || family === "box" || family === "funnel") {
|
|
1006
|
+
issues.push(`${spec.type} charts are written as bar charts: DrawingML has no ${spec.type} type (Office 2016 stores it as chartex).`);
|
|
1007
|
+
type = "bar";
|
|
1008
|
+
} else if (family === "treemap") {
|
|
1009
|
+
issues.push("Treemaps are written as pie charts: DrawingML has no treemap type.");
|
|
1010
|
+
type = "pie";
|
|
1011
|
+
}
|
|
1012
|
+
const visible = spec.series.filter((s) => !s.hidden);
|
|
1013
|
+
if (visible.length < spec.series.length) issues.push(`${spec.series.length - visible.length} hidden series ${spec.series.length - visible.length === 1 ? "is" : "are"} left out.`);
|
|
1014
|
+
const grouping = spec.stacking === "stacked" ? "stacked" : spec.stacking === "percent" ? "percentStacked" : "standard";
|
|
1015
|
+
const ids = { cat: 500000001, val: 500000002, cat2: 500000003, val2: 500000004 };
|
|
1016
|
+
const xy = family === "xy" || type === "scatter" || type === "bubble";
|
|
1017
|
+
const pieLike = type === "pie" || type === "donut";
|
|
1018
|
+
const typed = (s) => pieLike || type === "radar" ? type : type === "bubble" ? "bubble" : xy ? "scatter" : chartSeriesType({ type }, s);
|
|
1019
|
+
const groups = [];
|
|
1020
|
+
visible.forEach((s) => {
|
|
1021
|
+
const kind = typed(s);
|
|
1022
|
+
const secondary = !pieLike && s.axis === "secondary";
|
|
1023
|
+
let g = groups.find((x) => x.kind === kind && x.secondary === secondary);
|
|
1024
|
+
if (!g) groups.push(g = { kind, secondary, series: [] });
|
|
1025
|
+
g.series.push({ s, index: spec.series.indexOf(s) });
|
|
1026
|
+
});
|
|
1027
|
+
if (pieLike && visible.length > 1) issues.push("Only the first series of a pie is drawn; the others are kept in the part.");
|
|
1028
|
+
const catLit = strLit(spec.categories);
|
|
1029
|
+
let order = 0;
|
|
1030
|
+
const serXml = (kind, s, index) => {
|
|
1031
|
+
const parts = [`<c:idx val="${index}"/>`, `<c:order val="${order++}"/>`, `<c:tx><c:v>${esc(s.name)}</c:v></c:tx>`];
|
|
1032
|
+
const lineLike = kind === "line" || kind === "scatter" || kind === "radar";
|
|
1033
|
+
parts.push(fillXml(s.color, lineLike));
|
|
1034
|
+
if (kind === "bar" || kind === "bubble") parts.push('<c:invertIfNegative val="0"/>');
|
|
1035
|
+
if (lineLike) {
|
|
1036
|
+
const marker = s.marker ?? (spec.points || kind === "scatter" ? "circle" : "none");
|
|
1037
|
+
parts.push(`<c:marker><c:symbol val="${MARKER_OUT[marker]}"/>${marker !== "none" ? '<c:size val="5"/>' : ""}</c:marker>`);
|
|
1038
|
+
}
|
|
1039
|
+
(s.pointColors ?? []).forEach((c, i) => {
|
|
1040
|
+
const h = hex(c ?? void 0);
|
|
1041
|
+
if (h) parts.push(`<c:dPt><c:idx val="${i}"/>${kind === "bar" ? '<c:invertIfNegative val="0"/>' : ""}<c:bubble3D val="0"/><c:spPr><a:solidFill><a:srgbClr val="${h}"/></a:solidFill></c:spPr></c:dPt>`);
|
|
1042
|
+
});
|
|
1043
|
+
const labels = s.dataLabels ?? spec.dataLabels;
|
|
1044
|
+
const code = s.format ? chartFormatToCode(s.format) : void 0;
|
|
1045
|
+
if (s.dataLabels !== void 0 || code)
|
|
1046
|
+
parts.push(`<c:dLbls>${code ? `<c:numFmt formatCode="${esc(code)}" sourceLinked="0"/>` : ""}<c:showLegendKey val="0"/><c:showVal val="${labels ? 1 : 0}"/><c:showCatName val="0"/><c:showSerName val="0"/><c:showPercent val="0"/><c:showBubbleSize val="0"/></c:dLbls>`);
|
|
1047
|
+
if (!pieLike && kind !== "radar")
|
|
1048
|
+
for (const t of s.trendlines ?? []) {
|
|
1049
|
+
let x = `<c:trendline>${t.name ? `<c:name>${esc(t.name)}</c:name>` : ""}${fillXml(t.color, true)}<c:trendlineType val="${TREND_OUT[t.type]}"/>`;
|
|
1050
|
+
if (t.type === "polynomial") x += `<c:order val="${t.order ?? 2}"/>`;
|
|
1051
|
+
if (t.type === "moving-average") x += `<c:period val="${t.period ?? 2}"/>`;
|
|
1052
|
+
if (t.forward) x += `<c:forward val="${t.forward}"/>`;
|
|
1053
|
+
if (t.backward) x += `<c:backward val="${t.backward}"/>`;
|
|
1054
|
+
if (t.intercept !== void 0 && t.intercept !== null) x += `<c:intercept val="${t.intercept}"/>`;
|
|
1055
|
+
x += `<c:dispRSqr val="${t.showR2 ? 1 : 0}"/><c:dispEq val="${t.showEquation ? 1 : 0}"/></c:trendline>`;
|
|
1056
|
+
parts.push(x);
|
|
1057
|
+
}
|
|
1058
|
+
const valCode = chartFormatToCode(s.format ?? (s.axis === "secondary" ? spec.yAxis2.format : spec.yAxis.format));
|
|
1059
|
+
if (kind === "scatter" || kind === "bubble") {
|
|
1060
|
+
const xs = s.x ?? spec.categories.map((_, i) => i + 1);
|
|
1061
|
+
parts.push(`<c:xVal>${numLit(xs)}</c:xVal><c:yVal>${numLit(s.values, valCode)}</c:yVal>`);
|
|
1062
|
+
if (kind === "bubble") parts.push(`<c:bubbleSize>${numLit(s.sizes ?? s.values.map(() => 1))}</c:bubbleSize><c:bubble3D val="0"/>`);
|
|
1063
|
+
else parts.push(`<c:smooth val="${(s.curve ?? spec.curve) === "smooth" ? 1 : 0}"/>`);
|
|
1064
|
+
} else {
|
|
1065
|
+
parts.push(`<c:cat>${catLit}</c:cat><c:val>${numLit(s.values, valCode)}</c:val>`);
|
|
1066
|
+
if (kind === "line") parts.push(`<c:smooth val="${(s.curve ?? spec.curve) === "smooth" ? 1 : 0}"/>`);
|
|
1067
|
+
}
|
|
1068
|
+
if ((s.curve ?? spec.curve) === "step" && lineLike) issues.push(`"${s.name}" uses step lines, which DrawingML cannot express; they are written straight.`);
|
|
1069
|
+
if ((s.curve ?? spec.curve) === "smooth" && kind === "area") issues.push(`"${s.name}" is a smoothed area, which DrawingML cannot express; it is written straight.`);
|
|
1070
|
+
return `<c:ser>${parts.join("")}</c:ser>`;
|
|
1071
|
+
};
|
|
1072
|
+
const dLbls = `<c:dLbls><c:showLegendKey val="0"/><c:showVal val="${spec.dataLabels ? 1 : 0}"/><c:showCatName val="0"/><c:showSerName val="0"/><c:showPercent val="0"/><c:showBubbleSize val="0"/></c:dLbls>`;
|
|
1073
|
+
const groupXml = groups.map((g) => {
|
|
1074
|
+
const axes2 = g.secondary ? `<c:axId val="${ids.cat2}"/><c:axId val="${ids.val2}"/>` : `<c:axId val="${ids.cat}"/><c:axId val="${ids.val}"/>`;
|
|
1075
|
+
const sers = g.series.map(({ s, index }) => serXml(g.kind, s, index)).join("");
|
|
1076
|
+
switch (g.kind) {
|
|
1077
|
+
case "bar": {
|
|
1078
|
+
const n = g.series.length;
|
|
1079
|
+
const barsWidth = spec.stacking === "none" ? n - (n - 1) * spec.overlap : 1;
|
|
1080
|
+
const gapPct = Math.round(Math.min(500, spec.gapWidth / Math.max(0.01, 1 - spec.gapWidth) * barsWidth * 100));
|
|
1081
|
+
const overlapPct = spec.stacking === "none" ? Math.round(spec.overlap * 100) : 100;
|
|
1082
|
+
return `<c:barChart><c:barDir val="${spec.orientation === "horizontal" ? "bar" : "col"}"/><c:grouping val="${spec.stacking === "none" ? "clustered" : grouping}"/><c:varyColors val="0"/>${sers}${dLbls}<c:gapWidth val="${gapPct}"/><c:overlap val="${overlapPct}"/>${axes2}</c:barChart>`;
|
|
1083
|
+
}
|
|
1084
|
+
case "line":
|
|
1085
|
+
return `<c:lineChart><c:grouping val="${spec.stacking === "none" ? "standard" : "standard"}"/><c:varyColors val="0"/>${sers}${dLbls}<c:marker val="1"/>${axes2}</c:lineChart>`;
|
|
1086
|
+
case "area":
|
|
1087
|
+
return `<c:areaChart><c:grouping val="${grouping}"/><c:varyColors val="0"/>${sers}${dLbls}${axes2}</c:areaChart>`;
|
|
1088
|
+
case "scatter":
|
|
1089
|
+
return `<c:scatterChart><c:scatterStyle val="lineMarker"/><c:varyColors val="0"/>${sers}${dLbls}${axes2}</c:scatterChart>`;
|
|
1090
|
+
case "bubble":
|
|
1091
|
+
return `<c:bubbleChart><c:varyColors val="0"/>${sers}${dLbls}<c:bubbleScale val="100"/>${axes2}</c:bubbleChart>`;
|
|
1092
|
+
case "radar":
|
|
1093
|
+
return `<c:radarChart><c:radarStyle val="marker"/><c:varyColors val="0"/>${sers}${dLbls}${axes2}</c:radarChart>`;
|
|
1094
|
+
case "donut":
|
|
1095
|
+
return `<c:doughnutChart><c:varyColors val="1"/>${sers}${dLbls}<c:firstSliceAng val="0"/><c:holeSize val="${Math.round(Math.max(10, Math.min(90, spec.innerRadius * 100)))}"/></c:doughnutChart>`;
|
|
1096
|
+
default:
|
|
1097
|
+
return `<c:pieChart><c:varyColors val="1"/>${sers}${dLbls}<c:firstSliceAng val="0"/></c:pieChart>`;
|
|
1098
|
+
}
|
|
1099
|
+
});
|
|
1100
|
+
const scaling = (a) => `<c:scaling><c:orientation val="minMax"/>${a.max !== null ? `<c:max val="${a.max}"/>` : ""}${a.min !== null ? `<c:min val="${a.min}"/>` : ""}</c:scaling>`;
|
|
1101
|
+
const title = (a) => a.title ? `<c:title>${rich(a.title)}</c:title>` : "";
|
|
1102
|
+
const fmt = (a) => {
|
|
1103
|
+
const code = chartFormatToCode(a.format);
|
|
1104
|
+
return code !== "General" ? `<c:numFmt formatCode="${esc(code)}" sourceLinked="0"/>` : `<c:numFmt formatCode="${spec.stacking === "percent" ? "0%" : "General"}" sourceLinked="1"/>`;
|
|
1105
|
+
};
|
|
1106
|
+
const horizontal = spec.orientation === "horizontal";
|
|
1107
|
+
const catPos = horizontal ? "l" : "b";
|
|
1108
|
+
const valPos = horizontal ? "b" : "l";
|
|
1109
|
+
const valAx = (id, cross, a, pos, crosses, catKind = false) => `<c:valAx><c:axId val="${id}"/>${scaling(a)}<c:delete val="${a.show ? 0 : 1}"/><c:axPos val="${pos}"/>${a.gridlines ? "<c:majorGridlines/>" : ""}${title(a)}${fmt(a)}<c:majorTickMark val="out"/><c:minorTickMark val="none"/><c:tickLblPos val="nextTo"/><c:crossAx val="${cross}"/><c:crosses val="${crosses}"/><c:crossBetween val="${catKind ? "midCat" : "between"}"/>${a.step ? `<c:majorUnit val="${a.step}"/>` : ""}</c:valAx>`;
|
|
1110
|
+
const catAx = (id, cross, a, pos, hidden = false) => `<c:${a.kind === "date" ? "dateAx" : "catAx"}><c:axId val="${id}"/><c:scaling><c:orientation val="minMax"/></c:scaling><c:delete val="${a.show && !hidden ? 0 : 1}"/><c:axPos val="${pos}"/>${a.gridlines ? "<c:majorGridlines/>" : ""}${title(a)}<c:numFmt formatCode="General" sourceLinked="1"/><c:majorTickMark val="out"/><c:minorTickMark val="none"/><c:tickLblPos val="nextTo"/><c:crossAx val="${cross}"/><c:crosses val="autoZero"/><c:auto val="1"/>${a.kind === "date" ? '<c:lblOffset val="100"/><c:baseTimeUnit val="days"/>' : '<c:lblAlgn val="ctr"/><c:lblOffset val="100"/><c:noMultiLvlLbl val="0"/>'}</c:${a.kind === "date" ? "dateAx" : "catAx"}>`;
|
|
1111
|
+
let axes = "";
|
|
1112
|
+
if (!pieLike) {
|
|
1113
|
+
if (xy) axes = valAx(ids.cat, ids.val, spec.xAxis, "b", "autoZero", true) + valAx(ids.val, ids.cat, spec.yAxis, "l", "autoZero", true);
|
|
1114
|
+
else axes = catAx(ids.cat, ids.val, spec.xAxis, catPos) + valAx(ids.val, ids.cat, spec.yAxis, valPos, "autoZero");
|
|
1115
|
+
if (groups.some((g) => g.secondary)) axes += catAx(ids.cat2, ids.val2, spec.xAxis, catPos, true) + valAx(ids.val2, ids.cat2, spec.yAxis2, horizontal ? "t" : "r", "max");
|
|
1116
|
+
}
|
|
1117
|
+
const legendPos = { bottom: "b", top: "t", left: "l", right: "r" }[spec.legend.position];
|
|
1118
|
+
const legend = spec.legend.show ? `<c:legend><c:legendPos val="${legendPos}"/><c:overlay val="0"/></c:legend>` : "";
|
|
1119
|
+
const titleXml = spec.title ? `<c:title>${rich(spec.title)}</c:title><c:autoTitleDeleted val="0"/>` : '<c:autoTitleDeleted val="1"/>';
|
|
1120
|
+
if (spec.subtitle) issues.push("The subtitle is left out: DrawingML charts have one title.");
|
|
1121
|
+
if (family === "radar" && spec.series.some((s) => s.trendlines?.length)) issues.push("Trendlines on a radar chart are left out.");
|
|
1122
|
+
const xml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><c:chartSpace xmlns:c="${CHART_OOXML_NS}" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"><c:roundedCorners val="0"/><c:chart>${titleXml}<c:plotArea><c:layout/>${groupXml.join("")}${axes}</c:plotArea>${legend}<c:plotVisOnly val="1"/><c:dispBlanksAs val="gap"/></c:chart><c:txPr><a:bodyPr/><a:lstStyle/><a:p><a:pPr><a:defRPr sz="${Math.round(10 * 100)}"/></a:pPr><a:endParaRPr lang="en-US"/></a:p></c:txPr></c:chartSpace>`;
|
|
1123
|
+
return { xml, issues: [...new Set(issues)] };
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
export {
|
|
1127
|
+
parseChartColor,
|
|
1128
|
+
chartRgbToHex,
|
|
1129
|
+
shadeChartColor,
|
|
1130
|
+
CHART_PALETTE_SHADES,
|
|
1131
|
+
cyclePaletteColor,
|
|
1132
|
+
chartLuminance,
|
|
1133
|
+
chartContrastRatio,
|
|
1134
|
+
chartTextOn,
|
|
1135
|
+
chartRgbToOklab,
|
|
1136
|
+
chartColorDistance,
|
|
1137
|
+
simulateChartVision,
|
|
1138
|
+
checkChartColors,
|
|
1139
|
+
CHART_TYPES,
|
|
1140
|
+
CHART_MARKERS,
|
|
1141
|
+
CHART_TRENDLINE_TYPES,
|
|
1142
|
+
CHART_LABEL_POSITIONS,
|
|
1143
|
+
CHART_AXIS_KINDS,
|
|
1144
|
+
CHART_SERIES_TYPES,
|
|
1145
|
+
CHART_STACKINGS,
|
|
1146
|
+
CHART_CURVES,
|
|
1147
|
+
CHART_ORIENTATIONS,
|
|
1148
|
+
CHART_LEGEND_POSITIONS,
|
|
1149
|
+
CHART_NUMBER_STYLES,
|
|
1150
|
+
CHART_TYPE_LABELS,
|
|
1151
|
+
DEFAULT_CHART_PALETTE,
|
|
1152
|
+
DEFAULT_CHART_THEME,
|
|
1153
|
+
CSS_VARIABLE_CHART_THEME,
|
|
1154
|
+
isCartesianChart,
|
|
1155
|
+
isXyChart,
|
|
1156
|
+
chartFamily,
|
|
1157
|
+
chartColorsByCategory,
|
|
1158
|
+
defaultChartAxis,
|
|
1159
|
+
createChartSpec,
|
|
1160
|
+
chartSeriesColor,
|
|
1161
|
+
chartSliceColor,
|
|
1162
|
+
chartPointColor,
|
|
1163
|
+
chartSeriesType,
|
|
1164
|
+
uniqueChartSeriesId,
|
|
1165
|
+
parseChartValue,
|
|
1166
|
+
normalizeChartAxis,
|
|
1167
|
+
normalizeChartValues,
|
|
1168
|
+
normalizeChartTrendline,
|
|
1169
|
+
normalizeChartSpec,
|
|
1170
|
+
cloneChartSpec,
|
|
1171
|
+
cloneChartSeries,
|
|
1172
|
+
chartDataFromTable,
|
|
1173
|
+
chartDataToTable,
|
|
1174
|
+
chartColorInputValue,
|
|
1175
|
+
CHART_OOXML_NS,
|
|
1176
|
+
CHART_OOXML_CONTENT_TYPE,
|
|
1177
|
+
CHART_OOXML_RELATIONSHIP,
|
|
1178
|
+
chartFromOoxml,
|
|
1179
|
+
chartFormatToCode,
|
|
1180
|
+
chartToOoxml
|
|
1181
|
+
};
|
|
1182
|
+
//# sourceMappingURL=chunk-C3NWJE3K.js.map
|