@slickfast/mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +49 -0
  3. package/dist/index.js +1360 -0
  4. package/package.json +21 -0
package/dist/index.js ADDED
@@ -0,0 +1,1360 @@
1
+ #!/usr/bin/env node
2
+
3
+ // server.mjs
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import { z } from "zod";
7
+ import { readFileSync } from "node:fs";
8
+ import { fileURLToPath } from "node:url";
9
+ import { dirname, join } from "node:path";
10
+
11
+ // ../../packages/palette-core/tokens.json
12
+ var tokens_default = {
13
+ version: "1.0.0",
14
+ description: "Design tokens for BarGraphFast / PieChartFast / future data-viz sites and the rendering API. Language-agnostic: any consumer reads this verbatim. The color math that turns these into resolved palettes lives in palette-core (see README). Parity across consumers is enforced by golden-vectors.json.",
15
+ flatPalettes: [
16
+ { name: "Clean Corporate", colors: ["#2563eb", "#7c3aed", "#059669", "#d97706", "#dc2626", "#0891b2", "#65a30d", "#9333ea"] },
17
+ { name: "Pastel", colors: ["#93c5fd", "#c4b5fd", "#6ee7b7", "#fcd34d", "#fca5a5", "#67e8f9", "#bbf7d0", "#f0abfc"] },
18
+ { name: "Vibrant", colors: ["#f43f5e", "#f97316", "#eab308", "#22c55e", "#06b6d4", "#6366f1", "#a855f7", "#ec4899"] },
19
+ { name: "Monochrome", colors: ["#c8cdd4", "#a0a8b4", "#78838f", "#57606a", "#3d4752", "#6e8090", "#b0bac4", "#8a95a0"] },
20
+ { name: "Cyberpunk", colors: ["#ec4899", "#a855f7", "#22d3ee", "#d946ef", "#8b5cf6", "#2dd4bf", "#f472b6", "#0ea5e9"] },
21
+ { name: "Analogous Shift", colors: ["#6b2d5c", "#c8324b", "#f2785c", "#f2913d", "#f6b73c", "#fcd848", "#a3c940", "#2fa98c"] }
22
+ ],
23
+ nestedThemes: [
24
+ { name: "Modern Corporate", pie1: ["#14B8A6", "#0A2540", "#64748B"] },
25
+ { name: "Nordic Earth", pie1: ["#7C8A4E", "#C0683C", "#CBA14A"] },
26
+ { name: "Cyberpunk Glow", pie1: ["#A855F7", "#EC4899", "#22D3EE"], dark: true },
27
+ { name: "Sequential Rainbow", pie1: ["#F43F5E", "#2563EB", "#FACC15"], hueShift: 15, hueSpread: 25 },
28
+ { name: "Retro Editorial", tiers: [["#D4A017", "#C65D3B", "#2F7E8C"], ["#7A7C3F", "#C98B91", "#2C3E5C"], ["#A7B59A", "#F0C3A0", "#EDE4D0"]] },
29
+ { name: "Analogous Shift", tiers: [["#6B2D5C", "#C8324B", "#F2785C"], ["#F2913D", "#F6B73C", "#FCD848"], ["#A3C940", "#6FCF97", "#2FA98C"]] },
30
+ { name: "Classic Triadic", tiers: [["#4338CA", "#14B8A6", "#FB7185"], ["#374151", "#D4AF6A", "#6EE7B7"], ["#64748B", "#C4B5FD", "#FBCFE0"]] },
31
+ { name: "Sorbet Pastel", tiers: [["#C9B6E4", "#BFD8A4", "#F6C396"], ["#A9CCE3", "#F5E79E", "#F4B8D0"], ["#A8E0C8", "#D6CDEA", "#F6CBA0"]] },
32
+ { name: "Clean Corporate", pie1: ["#2563eb", "#7c3aed", "#059669"] },
33
+ { name: "Pastel", pie1: ["#93c5fd", "#c4b5fd", "#6ee7b7"] },
34
+ { name: "Vibrant", pie1: ["#f43f5e", "#f97316", "#eab308"] },
35
+ { name: "Monochrome", pie1: ["#c8cdd4", "#a0a8b4", "#78838f"] }
36
+ ],
37
+ rampConfig: {
38
+ comment: "Tunables for the generative color math. Mirror of the constants baked into palette-core's generators, surfaced here as design data. Changing these requires regenerating golden-vectors.json.",
39
+ monochromatic: { startLightness: 75, endLightness: 35 },
40
+ tier: {
41
+ depth1: { startLightnessClampMin: 46, startLightnessClampMax: 58, lightnessSpan: 24, saturationFloor: 42, saturationDrop: 4 },
42
+ depthN: { startLightness: 70, lightnessSpan: 16, saturationFloor: 32, saturationDrop: 20 },
43
+ lightnessCeiling: 92
44
+ },
45
+ fallback: { startLightnessOffset: 8, endLightnessStep: 8, lightnessCeiling: 92 }
46
+ }
47
+ };
48
+
49
+ // ../../packages/palette-core/palette-core.mjs
50
+ var RAMP = tokens_default.rampConfig;
51
+ function hexToRgb(hex) {
52
+ hex = String(hex).replace(/^#/, "");
53
+ if (hex.length === 3) hex = hex.split("").map((c) => c + c).join("");
54
+ const n = parseInt(hex, 16);
55
+ return { r: n >> 16 & 255, g: n >> 8 & 255, b: n & 255 };
56
+ }
57
+ function rgbToHsl(r2, g, b) {
58
+ r2 /= 255;
59
+ g /= 255;
60
+ b /= 255;
61
+ const mx = Math.max(r2, g, b), mn = Math.min(r2, g, b);
62
+ let h, s, l = (mx + mn) / 2;
63
+ if (mx === mn) {
64
+ h = s = 0;
65
+ } else {
66
+ const d = mx - mn;
67
+ s = l > 0.5 ? d / (2 - mx - mn) : d / (mx + mn);
68
+ switch (mx) {
69
+ case r2:
70
+ h = (g - b) / d + (g < b ? 6 : 0);
71
+ break;
72
+ case g:
73
+ h = (b - r2) / d + 2;
74
+ break;
75
+ default:
76
+ h = (r2 - g) / d + 4;
77
+ }
78
+ h /= 6;
79
+ }
80
+ return { h: h * 360, s: s * 100, l: l * 100 };
81
+ }
82
+ function hexToHsl(hex) {
83
+ const { r: r2, g, b } = hexToRgb(hex);
84
+ return rgbToHsl(r2, g, b);
85
+ }
86
+ function hslToHex(h, s, l) {
87
+ h = (h % 360 + 360) % 360;
88
+ s = Math.max(0, Math.min(100, s)) / 100;
89
+ l = Math.max(0, Math.min(100, l)) / 100;
90
+ const c = (1 - Math.abs(2 * l - 1)) * s, x = c * (1 - Math.abs(h / 60 % 2 - 1)), m = l - c / 2;
91
+ let r2, g, b;
92
+ if (h < 60) {
93
+ [r2, g, b] = [c, x, 0];
94
+ } else if (h < 120) {
95
+ [r2, g, b] = [x, c, 0];
96
+ } else if (h < 180) {
97
+ [r2, g, b] = [0, c, x];
98
+ } else if (h < 240) {
99
+ [r2, g, b] = [0, x, c];
100
+ } else if (h < 300) {
101
+ [r2, g, b] = [x, 0, c];
102
+ } else {
103
+ [r2, g, b] = [c, 0, x];
104
+ }
105
+ const to = (v) => ("0" + Math.round((v + m) * 255).toString(16)).slice(-2);
106
+ return "#" + to(r2) + to(g) + to(b);
107
+ }
108
+ function getLuminance(hex) {
109
+ const { r: r2, g, b } = hexToRgb(hex);
110
+ return [r2, g, b].reduce((sum, c, i) => {
111
+ c /= 255;
112
+ c = c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
113
+ return sum + c * [0.2126, 0.7152, 0.0722][i];
114
+ }, 0);
115
+ }
116
+ function contrastColor(hex) {
117
+ return getLuminance(hex) > 0.179 ? "#1a1a1a" : "#ffffff";
118
+ }
119
+ function generateMonochromaticPalette(baseHue, baseSaturation, sliceCount, startLightness = RAMP.monochromatic.startLightness, endLightness = RAMP.monochromatic.endLightness) {
120
+ const colors = [];
121
+ const step = (startLightness - endLightness) / Math.max(sliceCount - 1, 1);
122
+ for (let i = 0; i < sliceCount; i++) {
123
+ colors.push(hslToHex(baseHue, baseSaturation, startLightness - i * step));
124
+ }
125
+ return colors;
126
+ }
127
+ function tierPalette(rootHex, depth, count, opts = {}) {
128
+ const { hueShift = 0, hueSpread = 0 } = opts;
129
+ const d1 = RAMP.tier.depth1, dN = RAMP.tier.depthN, ceil = RAMP.tier.lightnessCeiling;
130
+ const base = hexToHsl(rootHex);
131
+ const startL = depth === 1 ? Math.min(Math.max(base.l, d1.startLightnessClampMin), d1.startLightnessClampMax) : dN.startLightness;
132
+ const spanL = depth === 1 ? d1.lightnessSpan : dN.lightnessSpan;
133
+ const S = depth === 1 ? Math.max(d1.saturationFloor, base.s - d1.saturationDrop) : Math.max(dN.saturationFloor, base.s - dN.saturationDrop);
134
+ const out = [];
135
+ for (let i = 0; i < count; i++) {
136
+ const t = count > 1 ? i / (count - 1) : 0;
137
+ out.push(hslToHex(base.h + hueShift * depth + hueSpread * t, S, Math.min(ceil, startL + t * spanL)));
138
+ }
139
+ return out;
140
+ }
141
+ function generateTierPaletteWithFallback(predefinedColors, sliceCount, rootHex = null) {
142
+ if (sliceCount <= predefinedColors.length) return predefinedColors.slice(0, sliceCount);
143
+ const fb = RAMP.fallback;
144
+ const result = predefinedColors.slice();
145
+ const remaining = sliceCount - predefinedColors.length;
146
+ const baseHsl = hexToHsl(predefinedColors[predefinedColors.length - 1]);
147
+ const baseHue = rootHex ? hexToHsl(rootHex).h : baseHsl.h;
148
+ const startLightness = baseHsl.l + fb.startLightnessOffset;
149
+ const endLightness = Math.min(fb.lightnessCeiling, baseHsl.l + remaining * fb.endLightnessStep);
150
+ const dynamic = generateMonochromaticPalette(baseHue, baseHsl.s, remaining + 1, startLightness, endLightness);
151
+ result.push(...dynamic.slice(1));
152
+ return result.slice(0, sliceCount);
153
+ }
154
+ var FLAT_PALETTES = tokens_default.flatPalettes;
155
+ var NESTED_THEMES = tokens_default.nestedThemes;
156
+ function resolveFlatPalette(name, count) {
157
+ const p = FLAT_PALETTES.find((p2) => p2.name === name) || FLAT_PALETTES[0];
158
+ return generateTierPaletteWithFallback(p.colors, count);
159
+ }
160
+ function resolveNestedTheme(themeName, pieIndex, count, rootHexOverride = null) {
161
+ const theme = NESTED_THEMES.find((t) => t.name === themeName) || NESTED_THEMES[0];
162
+ if (theme.tiers) {
163
+ const predefined = theme.tiers[Math.min(pieIndex, theme.tiers.length - 1)];
164
+ return generateTierPaletteWithFallback(predefined, count);
165
+ }
166
+ if (pieIndex === 0) return generateTierPaletteWithFallback(theme.pie1, count);
167
+ const root = rootHexOverride || theme.pie1[0];
168
+ return tierPalette(root, pieIndex, count, theme);
169
+ }
170
+
171
+ // ../../packages/fonts/fonts.mjs
172
+ var FONTS = {
173
+ Inter: "Inter, system-ui, -apple-system, sans-serif",
174
+ System: "system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif",
175
+ Serif: "Georgia, 'Times New Roman', serif",
176
+ Mono: "'JetBrains Mono', 'SF Mono', Menlo, monospace",
177
+ Rounded: "'Nunito', 'Segoe UI', system-ui, sans-serif",
178
+ Condensed: "'Roboto Condensed', 'Arial Narrow', sans-serif"
179
+ };
180
+ var DEFAULT_FONT = FONTS.Inter;
181
+ function resolveFont(spec = {}) {
182
+ if (spec.fontFamily) return spec.fontFamily;
183
+ if (spec.font && FONTS[spec.font]) return FONTS[spec.font];
184
+ return DEFAULT_FONT;
185
+ }
186
+
187
+ // ../../packages/render-core/render-core.mjs
188
+ function contrastFloor(color, bg, transparent) {
189
+ if (transparent || typeof color !== "string" || color[0] !== "#") return color;
190
+ const lc = getLuminance(color), lb = getLuminance(bg);
191
+ if (Math.abs(lc - lb) >= 0.22) return color;
192
+ const hsl = hexToHsl(color);
193
+ const dl = lb > 0.5 ? -30 : 30;
194
+ return hslToHex(hsl.h, hsl.s, Math.max(0, Math.min(100, hsl.l + dl)));
195
+ }
196
+ var esc = (s) => String(s ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
197
+ var r = (n) => Math.round(n * 100) / 100;
198
+ var fmt = (n) => (Number(n) || 0).toLocaleString("en-US");
199
+ function pct100(vals) {
200
+ const tot = vals.reduce((a, b) => a + b, 0);
201
+ if (tot <= 0) return vals.map(() => 0);
202
+ const exact = vals.map((v) => v / tot * 100);
203
+ const out = exact.map(Math.floor);
204
+ const rem = 100 - out.reduce((a, b) => a + b, 0);
205
+ const order = exact.map((v, i) => [i, v - Math.floor(v)]).sort((a, b) => b[1] - a[1]);
206
+ for (let k = 0; k < rem; k++) out[order[k % order.length][0]]++;
207
+ return out;
208
+ }
209
+ var txt = (spec, fallback) => spec.textColor || fallback;
210
+ var wAttr = (spec, base) => {
211
+ const w = spec.fontWeight ? esc(spec.fontWeight) : spec.bold === true ? Math.min(900, base + 100) : base;
212
+ return `font-weight="${w}"`;
213
+ };
214
+ var wOpt = (spec) => {
215
+ if (spec.fontWeight) return ` font-weight="${esc(spec.fontWeight)}"`;
216
+ if (spec.bold === true) return ' font-weight="700"';
217
+ return "";
218
+ };
219
+ function niceNum(x, round) {
220
+ const v = x > 0 ? x : 1;
221
+ const exp = Math.floor(Math.log10(v));
222
+ const f = v / Math.pow(10, exp);
223
+ const nf = round ? f < 1.5 ? 1 : f < 3 ? 2 : f < 7 ? 5 : 10 : f <= 1 ? 1 : f <= 2 ? 2 : f <= 5 ? 5 : 10;
224
+ return nf * Math.pow(10, exp);
225
+ }
226
+ function niceScale(max, ticks = 5) {
227
+ const m = max > 0 ? max : 1;
228
+ const step = niceNum(niceNum(m, false) / ticks, true);
229
+ return { step, niceMax: Math.ceil(m / step) * step };
230
+ }
231
+ function topRoundedBar(x, y, w, h, rad) {
232
+ const rr = Math.min(rad, w / 2, h);
233
+ return `M${r(x)},${r(y + h)} L${r(x)},${r(y + rr)} Q${r(x)},${r(y)} ${r(x + rr)},${r(y)} L${r(x + w - rr)},${r(y)} Q${r(x + w)},${r(y)} ${r(x + w)},${r(y + rr)} L${r(x + w)},${r(y + h)} Z`;
234
+ }
235
+ function rightRoundedBar(x, y, w, h, rad) {
236
+ const rr = Math.min(rad, h / 2, w);
237
+ return `M${r(x)},${r(y)} L${r(x + w - rr)},${r(y)} Q${r(x + w)},${r(y)} ${r(x + w)},${r(y + rr)} L${r(x + w)},${r(y + h - rr)} Q${r(x + w)},${r(y + h)} ${r(x + w - rr)},${r(y + h)} L${r(x)},${r(y + h)} Z`;
238
+ }
239
+ function bottomRoundedBar(x, y, w, h, rad) {
240
+ const rr = Math.min(rad, w / 2, h);
241
+ return `M${r(x)},${r(y)} L${r(x + w)},${r(y)} L${r(x + w)},${r(y + h - rr)} Q${r(x + w)},${r(y + h)} ${r(x + w - rr)},${r(y + h)} L${r(x + rr)},${r(y + h)} Q${r(x)},${r(y + h)} ${r(x)},${r(y + h - rr)} Z`;
242
+ }
243
+ function straightPath(pts) {
244
+ return pts.map((pt, i) => (i ? "L" : "M") + pt[0] + "," + pt[1]).join(" ");
245
+ }
246
+ function steppedPath(pts) {
247
+ let d = `M${pts[0][0]},${pts[0][1]}`;
248
+ for (let i = 1; i < pts.length; i++) d += ` L${pts[i][0]},${pts[i - 1][1]} L${pts[i][0]},${pts[i][1]}`;
249
+ return d;
250
+ }
251
+ function smoothPath(pts) {
252
+ if (pts.length < 3) return straightPath(pts);
253
+ let d = `M${pts[0][0]},${pts[0][1]}`;
254
+ for (let i = 0; i < pts.length - 1; i++) {
255
+ const p0 = pts[i - 1] || pts[i], p1 = pts[i], p2 = pts[i + 1], p3 = pts[i + 2] || p2;
256
+ const c1x = p1[0] + (p2[0] - p0[0]) / 6, c1y = p1[1] + (p2[1] - p0[1]) / 6;
257
+ const c2x = p2[0] - (p3[0] - p1[0]) / 6, c2y = p2[1] - (p3[1] - p1[1]) / 6;
258
+ d += ` C${r(c1x)},${r(c1y)} ${r(c2x)},${r(c2y)} ${p2[0]},${p2[1]}`;
259
+ }
260
+ return d;
261
+ }
262
+ function arcPath(cx, cy, rad, a0, a1, innerR) {
263
+ const x0 = r(cx + rad * Math.cos(a0)), y0 = r(cy + rad * Math.sin(a0));
264
+ const x1 = r(cx + rad * Math.cos(a1)), y1 = r(cy + rad * Math.sin(a1));
265
+ const large = a1 - a0 > Math.PI ? 1 : 0;
266
+ if (innerR > 0) {
267
+ const ix0 = r(cx + innerR * Math.cos(a0)), iy0 = r(cy + innerR * Math.sin(a0));
268
+ const ix1 = r(cx + innerR * Math.cos(a1)), iy1 = r(cy + innerR * Math.sin(a1));
269
+ return `M${x0},${y0} A${rad},${rad} 0 ${large} 1 ${x1},${y1} L${ix1},${iy1} A${innerR},${innerR} 0 ${large} 0 ${ix0},${iy0} Z`;
270
+ }
271
+ return `M${r(cx)},${r(cy)} L${x0},${y0} A${rad},${rad} 0 ${large} 1 ${x1},${y1} Z`;
272
+ }
273
+ function renderBar(spec) {
274
+ const W = spec.width || 800, H = spec.height || 450;
275
+ const bg = spec.background || "#ffffff";
276
+ const transparent = bg === "transparent" || bg === "none";
277
+ const isDark = !transparent && getLuminance(bg) < 0.35;
278
+ const fs = spec.fontSize ? Number(spec.fontSize) : Math.max(13, Math.round(13 * Math.sqrt(W * H) / 600));
279
+ const font = resolveFont(spec);
280
+ const title = spec.title || "";
281
+ const labels = spec.data.labels;
282
+ const values = spec.data.series[0].values.map((v) => Number(v) || 0);
283
+ const u = spec.valueUnit ? " " + spec.valueUnit : "";
284
+ const showValues = spec.showValues !== false;
285
+ const showTotal = spec.showTotal !== false;
286
+ const watermark = spec.watermark !== false;
287
+ const explicit = spec.data.series[0].colors;
288
+ const colors = Array.isArray(explicit) && explicit.length >= labels.length ? explicit : resolveFlatPalette(spec.palette || "Clean Corporate", labels.length);
289
+ const axisText = txt(spec, isDark ? "#94a3b8" : "#64748b");
290
+ const gridCol = isDark ? "#1e293b" : "#e2e8f0";
291
+ const catText = txt(spec, isDark ? "#cbd5e1" : "#475569");
292
+ const valText = txt(spec, isDark ? "#e2e8f0" : "#334155");
293
+ const titleCol = txt(spec, isDark ? "#f1f5f9" : "#0f172a");
294
+ const faint = isDark ? "#475569" : "#94a3b8";
295
+ const M = { left: 56, right: 24, top: title ? 54 : 28, bottom: 46 };
296
+ const plotW = W - M.left - M.right;
297
+ const plotH = H - M.top - M.bottom;
298
+ const maxV = Math.max(0, ...values);
299
+ const { step, niceMax } = niceScale(maxV);
300
+ const yPix = (v) => M.top + plotH - v / niceMax * plotH;
301
+ const band = plotW / labels.length;
302
+ const barW = Math.min(band * 0.62, 96);
303
+ const p = [];
304
+ p.push(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="${W}" height="${H}" font-family="${esc(font)}">`);
305
+ if (!transparent) p.push(`<rect x="0" y="0" width="${W}" height="${H}" fill="${bg}"/>`);
306
+ for (let v = 0; v <= niceMax + 1e-9; v += step) {
307
+ const y = r(yPix(v));
308
+ p.push(`<line x1="${M.left}" y1="${y}" x2="${r(M.left + plotW)}" y2="${y}" stroke="${gridCol}" stroke-width="1"/>`);
309
+ p.push(`<text x="${M.left - 8}" y="${r(y + 4)}" text-anchor="end" font-size="${fs - 2}"${wOpt(spec)} fill="${axisText}">${fmt(v)}</text>`);
310
+ }
311
+ values.forEach((v, i) => {
312
+ const x = M.left + i * band + (band - barW) / 2;
313
+ const y = yPix(v);
314
+ const h = M.top + plotH - y;
315
+ const cx = r(x + barW / 2);
316
+ p.push(`<path d="${topRoundedBar(x, y, barW, h, 5)}" fill="${colors[i]}"/>`);
317
+ if (showValues) p.push(`<text x="${cx}" y="${r(y - 7)}" text-anchor="middle" font-size="${fs - 1}" ${wAttr(spec, 600)} fill="${valText}">${esc(fmt(v) + u)}</text>`);
318
+ p.push(`<text x="${cx}" y="${r(M.top + plotH + 18)}" text-anchor="middle" font-size="${fs - 1}"${wOpt(spec)} fill="${catText}">${esc(labels[i])}</text>`);
319
+ });
320
+ if (showTotal) {
321
+ const total = values.reduce((s, v) => s + v, 0);
322
+ p.push(`<text x="${r(W - M.right)}" y="${title ? 50 : 20}" text-anchor="end" font-size="${fs - 1}"${wOpt(spec)} fill="${axisText}">Total: ${esc(fmt(total) + u)}</text>`);
323
+ }
324
+ if (title) p.push(`<text x="${r(W / 2)}" y="33" text-anchor="middle" font-size="${fs + 5}" ${wAttr(spec, 700)} fill="${titleCol}">${esc(title)}</text>`);
325
+ if (watermark) p.push(`<text x="${r(W - 8)}" y="${r(H - 8)}" text-anchor="end" font-size="10" fill="${faint}" opacity="0.7">slickfast.com</text>`);
326
+ p.push(`</svg>`);
327
+ return p.join("\n");
328
+ }
329
+ function renderBarGrouped(spec) {
330
+ const W = spec.width || 800, H = spec.height || 450;
331
+ const bg = spec.background || "#ffffff";
332
+ const transparent = bg === "transparent" || bg === "none";
333
+ const isDark = !transparent && getLuminance(bg) < 0.35;
334
+ const fs = spec.fontSize ? Number(spec.fontSize) : Math.max(13, Math.round(13 * Math.sqrt(W * H) / 600));
335
+ const font = resolveFont(spec);
336
+ const title = spec.title || "";
337
+ const labels = spec.data.labels;
338
+ const series = spec.data.series;
339
+ const u = spec.valueUnit ? " " + spec.valueUnit : "";
340
+ const showValues = spec.showValues !== false;
341
+ const watermark = spec.watermark !== false;
342
+ const cols = resolveFlatPalette(spec.palette || "Clean Corporate", 8);
343
+ const mid = Math.floor(cols.length / 2);
344
+ const seriesColor = (s, i) => s.color || (i === 0 ? cols[0] : i === 1 ? cols[mid] || cols[1] : cols[i % cols.length]);
345
+ const axisText = txt(spec, isDark ? "#94a3b8" : "#64748b");
346
+ const gridCol = isDark ? "#1e293b" : "#e2e8f0";
347
+ const catText = txt(spec, isDark ? "#cbd5e1" : "#475569");
348
+ const valText = txt(spec, isDark ? "#e2e8f0" : "#334155");
349
+ const titleCol = txt(spec, isDark ? "#f1f5f9" : "#0f172a");
350
+ const faint = isDark ? "#475569" : "#94a3b8";
351
+ const multi = series.length > 1;
352
+ const M = { left: 56, right: 24, top: title ? 54 : 28, bottom: multi ? 64 : 46 };
353
+ const plotW = W - M.left - M.right;
354
+ const plotH = H - M.top - M.bottom;
355
+ const maxV = Math.max(0, ...series.flatMap((s) => s.values.map((v) => Number(v) || 0)));
356
+ const { step, niceMax } = niceScale(maxV);
357
+ const yPix = (v) => M.top + plotH - v / niceMax * plotH;
358
+ const band = plotW / labels.length;
359
+ const groupW = Math.min(band * 0.72, 132);
360
+ const n = series.length;
361
+ const barW = groupW / n;
362
+ const p = [];
363
+ p.push(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="${W}" height="${H}" font-family="${esc(font)}">`);
364
+ if (!transparent) p.push(`<rect x="0" y="0" width="${W}" height="${H}" fill="${bg}"/>`);
365
+ for (let v = 0; v <= niceMax + 1e-9; v += step) {
366
+ const y = r(yPix(v));
367
+ p.push(`<line x1="${M.left}" y1="${y}" x2="${r(M.left + plotW)}" y2="${y}" stroke="${gridCol}" stroke-width="1"/>`);
368
+ p.push(`<text x="${M.left - 8}" y="${r(y + 4)}" text-anchor="end" font-size="${fs - 2}"${wOpt(spec)} fill="${axisText}">${fmt(v)}</text>`);
369
+ }
370
+ labels.forEach((lab, i) => {
371
+ const gx = M.left + i * band + (band - groupW) / 2;
372
+ series.forEach((s, si) => {
373
+ const v = Number(s.values[i]) || 0;
374
+ const x = gx + si * barW;
375
+ const y = yPix(v);
376
+ const h = M.top + plotH - y;
377
+ const cx = r(x + barW / 2);
378
+ p.push(`<path d="${topRoundedBar(x + 1, y, barW - 2, h, 4)}" fill="${seriesColor(s, si)}"/>`);
379
+ if (showValues && v > 0) p.push(`<text x="${cx}" y="${r(y - 6)}" text-anchor="middle" font-size="${fs - 3}" ${wAttr(spec, 600)} fill="${valText}">${esc(fmt(v) + u)}</text>`);
380
+ });
381
+ p.push(`<text x="${r(M.left + i * band + band / 2)}" y="${r(M.top + plotH + 18)}" text-anchor="middle" font-size="${fs - 1}"${wOpt(spec)} fill="${catText}">${esc(lab)}</text>`);
382
+ });
383
+ if (multi) {
384
+ const items = series.map((s, i) => ({ name: s.name || "Bar " + (i + 1), col: seriesColor(s, i) }));
385
+ const widths = items.map((it) => 16 + it.name.length * (fs * 0.56) + 18);
386
+ let lx = (W - widths.reduce((a, b) => a + b, 0)) / 2;
387
+ const ly = H - 16;
388
+ items.forEach((it, i) => {
389
+ p.push(`<rect x="${r(lx)}" y="${r(ly - 9)}" width="11" height="11" rx="2.5" fill="${it.col}"/>`);
390
+ p.push(`<text x="${r(lx + 17)}" y="${r(ly)}" font-size="${fs - 1}"${wOpt(spec)} fill="${catText}">${esc(it.name)}</text>`);
391
+ lx += widths[i];
392
+ });
393
+ }
394
+ if (title) p.push(`<text x="${r(W / 2)}" y="33" text-anchor="middle" font-size="${fs + 5}" ${wAttr(spec, 700)} fill="${titleCol}">${esc(title)}</text>`);
395
+ if (watermark) p.push(`<text x="${r(W - 8)}" y="${r(H - 8)}" text-anchor="end" font-size="10" fill="${faint}" opacity="0.7">slickfast.com</text>`);
396
+ p.push(`</svg>`);
397
+ return p.join("\n");
398
+ }
399
+ function renderBarStacked(spec) {
400
+ const W = spec.width || 800, H = spec.height || 450;
401
+ const bg = spec.background || "#ffffff";
402
+ const transparent = bg === "transparent" || bg === "none";
403
+ const isDark = !transparent && getLuminance(bg) < 0.35;
404
+ const fs = spec.fontSize ? Number(spec.fontSize) : Math.max(13, Math.round(13 * Math.sqrt(W * H) / 600));
405
+ const font = resolveFont(spec);
406
+ const title = spec.title || "";
407
+ const labels = spec.data.labels;
408
+ const series = spec.data.series;
409
+ const u = spec.valueUnit ? " " + spec.valueUnit : "";
410
+ const showValues = spec.showValues !== false;
411
+ const watermark = spec.watermark !== false;
412
+ const is100 = spec.type === "stacked100";
413
+ const cols = resolveFlatPalette(spec.palette || "Clean Corporate", Math.max(series.length, 2));
414
+ const segColor = (s, i) => s.color || cols[i % cols.length];
415
+ const axisText = txt(spec, isDark ? "#94a3b8" : "#64748b");
416
+ const gridCol = isDark ? "#1e293b" : "#e2e8f0";
417
+ const catText = txt(spec, isDark ? "#cbd5e1" : "#475569");
418
+ const titleCol = txt(spec, isDark ? "#f1f5f9" : "#0f172a");
419
+ const faint = isDark ? "#475569" : "#94a3b8";
420
+ const M = { left: 56, right: 24, top: title ? 54 : 28, bottom: 64 };
421
+ const plotW = W - M.left - M.right;
422
+ const plotH = H - M.top - M.bottom;
423
+ const raw = (si, i) => Math.max(0, Number(series[si].values[i]) || 0);
424
+ const colPct = is100 ? labels.map((_, i) => pct100(series.map((_s, si) => raw(si, i)))) : null;
425
+ const val = (si, i) => is100 ? colPct[i][si] : raw(si, i);
426
+ const totals = labels.map((_, i) => series.reduce((a, _s, si) => a + val(si, i), 0));
427
+ const maxV = is100 ? 100 : Math.max(0, ...totals);
428
+ const { step, niceMax } = is100 ? { step: 20, niceMax: 100 } : niceScale(maxV);
429
+ const yPix = (v) => M.top + plotH - v / niceMax * plotH;
430
+ const band = plotW / labels.length;
431
+ const barW = Math.min(band * 0.62, 96);
432
+ const p = [];
433
+ p.push(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="${W}" height="${H}" font-family="${esc(font)}">`);
434
+ if (!transparent) p.push(`<rect x="0" y="0" width="${W}" height="${H}" fill="${bg}"/>`);
435
+ for (let v = 0; v <= niceMax + 1e-9; v += step) {
436
+ const y = r(yPix(v));
437
+ p.push(`<line x1="${M.left}" y1="${y}" x2="${r(M.left + plotW)}" y2="${y}" stroke="${gridCol}" stroke-width="1"/>`);
438
+ p.push(`<text x="${M.left - 8}" y="${r(y + 4)}" text-anchor="end" font-size="${fs - 2}"${wOpt(spec)} fill="${axisText}">${fmt(v)}</text>`);
439
+ }
440
+ labels.forEach((lab, i) => {
441
+ const x = M.left + i * band + (band - barW) / 2;
442
+ const cx = r(x + barW / 2);
443
+ const barTotalH = totals[i] / niceMax * plotH || 1;
444
+ let topSeg = -1;
445
+ for (let k = series.length - 1; k >= 0; k--) {
446
+ if ((Number(series[k].values[i]) || 0) > 0) {
447
+ topSeg = k;
448
+ break;
449
+ }
450
+ }
451
+ let cum = 0;
452
+ series.forEach((s, si) => {
453
+ const v = val(si, i);
454
+ if (v <= 0) return;
455
+ const yTop = yPix(cum + v), yBot = yPix(cum), h = yBot - yTop;
456
+ const col = segColor(s, si);
457
+ if (si === topSeg) p.push(`<path d="${topRoundedBar(x, yTop, barW, h, 5)}" fill="${col}"/>`);
458
+ else p.push(`<rect x="${r(x)}" y="${r(yTop)}" width="${r(barW)}" height="${r(h)}" fill="${col}"/>`);
459
+ const tc = spec.textColor || contrastColor(col);
460
+ if (s.name && h >= 24 && h >= 0.12 * barTotalH) p.push(`<text x="${cx}" y="${r(yTop + 14)}" text-anchor="middle" font-size="${fs - 2}" ${wAttr(spec, 700)} fill="${tc}">${esc(s.name)}</text>`);
461
+ if (showValues && h >= 14) p.push(`<text x="${cx}" y="${r(yBot - 7)}" text-anchor="middle" font-size="${fs - 1}" ${wAttr(spec, 600)} fill="${tc}">${esc(fmt(v) + (is100 ? "%" : u))}</text>`);
462
+ cum += v;
463
+ });
464
+ p.push(`<text x="${cx}" y="${r(M.top + plotH + 18)}" text-anchor="middle" font-size="${fs - 1}"${wOpt(spec)} fill="${catText}">${esc(lab)}</text>`);
465
+ });
466
+ const items = series.map((s, i) => ({ name: s.name || "Series " + (i + 1), col: segColor(s, i) }));
467
+ const widths = items.map((it) => 16 + it.name.length * (fs * 0.56) + 18);
468
+ let lx = (W - widths.reduce((a, b) => a + b, 0)) / 2;
469
+ const ly = H - 16;
470
+ items.forEach((it, i) => {
471
+ p.push(`<rect x="${r(lx)}" y="${r(ly - 9)}" width="11" height="11" rx="2.5" fill="${it.col}"/>`);
472
+ p.push(`<text x="${r(lx + 17)}" y="${r(ly)}" font-size="${fs - 1}"${wOpt(spec)} fill="${catText}">${esc(it.name)}</text>`);
473
+ lx += widths[i];
474
+ });
475
+ if (title) p.push(`<text x="${r(W / 2)}" y="33" text-anchor="middle" font-size="${fs + 5}" ${wAttr(spec, 700)} fill="${titleCol}">${esc(title)}</text>`);
476
+ if (watermark) p.push(`<text x="${r(W - 8)}" y="${r(H - 8)}" text-anchor="end" font-size="10" fill="${faint}" opacity="0.7">slickfast.com</text>`);
477
+ p.push(`</svg>`);
478
+ return p.join("\n");
479
+ }
480
+ function renderBarStackedH(spec) {
481
+ const W = spec.width || 800, H = spec.height || 450;
482
+ const bg = spec.background || "#ffffff";
483
+ const transparent = bg === "transparent" || bg === "none";
484
+ const isDark = !transparent && getLuminance(bg) < 0.35;
485
+ const fs = spec.fontSize ? Number(spec.fontSize) : Math.max(13, Math.round(13 * Math.sqrt(W * H) / 600));
486
+ const font = resolveFont(spec);
487
+ const title = spec.title || "";
488
+ const labels = spec.data.labels;
489
+ const series = spec.data.series;
490
+ const u = spec.valueUnit ? " " + spec.valueUnit : "";
491
+ const showValues = spec.showValues !== false;
492
+ const watermark = spec.watermark !== false;
493
+ const cols = resolveFlatPalette(spec.palette || "Clean Corporate", Math.max(series.length, 2));
494
+ const segColor = (s, i) => s.color || cols[i % cols.length];
495
+ const axisText = txt(spec, isDark ? "#94a3b8" : "#64748b");
496
+ const gridCol = isDark ? "#1e293b" : "#e2e8f0";
497
+ const catText = txt(spec, isDark ? "#cbd5e1" : "#475569");
498
+ const titleCol = txt(spec, isDark ? "#f1f5f9" : "#0f172a");
499
+ const faint = isDark ? "#475569" : "#94a3b8";
500
+ const longest = Math.max(0, ...labels.map((l) => String(l).length));
501
+ const M = { left: Math.min(160, 24 + longest * (fs * 0.62)), right: 28, top: title ? 54 : 28, bottom: 64 };
502
+ const plotW = W - M.left - M.right;
503
+ const plotH = H - M.top - M.bottom;
504
+ const raw = (si, i) => Math.max(0, Number(series[si].values[i]) || 0);
505
+ const totals = labels.map((_, i) => series.reduce((a, _s, si) => a + raw(si, i), 0));
506
+ const maxV = Math.max(0, ...totals);
507
+ const { step, niceMax } = niceScale(maxV);
508
+ const xPix = (v) => M.left + v / niceMax * plotW;
509
+ const band = plotH / labels.length;
510
+ const barH = Math.min(band * 0.62, 64);
511
+ const p = [];
512
+ p.push(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="${W}" height="${H}" font-family="${esc(font)}">`);
513
+ if (!transparent) p.push(`<rect x="0" y="0" width="${W}" height="${H}" fill="${bg}"/>`);
514
+ for (let v = 0; v <= niceMax + 1e-9; v += step) {
515
+ const x = r(xPix(v));
516
+ p.push(`<line x1="${x}" y1="${M.top}" x2="${x}" y2="${r(M.top + plotH)}" stroke="${gridCol}" stroke-width="1"/>`);
517
+ p.push(`<text x="${x}" y="${r(M.top + plotH + 18)}" text-anchor="middle" font-size="${fs - 2}"${wOpt(spec)} fill="${axisText}">${fmt(v)}</text>`);
518
+ }
519
+ labels.forEach((lab, i) => {
520
+ const y = M.top + i * band + (band - barH) / 2;
521
+ const my = r(y + barH / 2 + 4);
522
+ let lastSeg = -1;
523
+ for (let k = series.length - 1; k >= 0; k--) {
524
+ if (raw(k, i) > 0) {
525
+ lastSeg = k;
526
+ break;
527
+ }
528
+ }
529
+ let cum = 0;
530
+ series.forEach((s, si) => {
531
+ const v = raw(si, i);
532
+ if (v <= 0) return;
533
+ const xL = xPix(cum), xR = xPix(cum + v), w = xR - xL;
534
+ const col = segColor(s, si);
535
+ if (si === lastSeg) p.push(`<path d="${rightRoundedBar(xL, y, w, barH, 5)}" fill="${col}"/>`);
536
+ else p.push(`<rect x="${r(xL)}" y="${r(y)}" width="${r(w)}" height="${r(barH)}" fill="${col}"/>`);
537
+ const tc = spec.textColor || contrastColor(col);
538
+ const cx = r(xL + w / 2);
539
+ if (s.name && w >= 58 && barH >= 30) {
540
+ p.push(`<text x="${cx}" y="${r(y + barH / 2 - 3)}" text-anchor="middle" font-size="${fs - 3}" ${wAttr(spec, 700)} fill="${tc}">${esc(s.name)}</text>`);
541
+ if (showValues) p.push(`<text x="${cx}" y="${r(y + barH / 2 + 13)}" text-anchor="middle" font-size="${fs - 1}" ${wAttr(spec, 600)} fill="${tc}">${esc(fmt(v) + u)}</text>`);
542
+ } else if (showValues && w >= 28) {
543
+ p.push(`<text x="${cx}" y="${my}" text-anchor="middle" font-size="${fs - 1}" ${wAttr(spec, 600)} fill="${tc}">${esc(fmt(v) + u)}</text>`);
544
+ }
545
+ cum += v;
546
+ });
547
+ p.push(`<text x="${r(M.left - 10)}" y="${my}" text-anchor="end" font-size="${fs - 1}"${wOpt(spec)} fill="${catText}">${esc(lab)}</text>`);
548
+ });
549
+ const items = series.map((s, i) => ({ name: s.name || "Series " + (i + 1), col: segColor(s, i) }));
550
+ const widths = items.map((it) => 16 + it.name.length * (fs * 0.56) + 18);
551
+ let lx = (W - widths.reduce((a, b) => a + b, 0)) / 2;
552
+ const ly = H - 16;
553
+ items.forEach((it, i) => {
554
+ p.push(`<rect x="${r(lx)}" y="${r(ly - 9)}" width="11" height="11" rx="2.5" fill="${it.col}"/>`);
555
+ p.push(`<text x="${r(lx + 17)}" y="${r(ly)}" font-size="${fs - 1}"${wOpt(spec)} fill="${catText}">${esc(it.name)}</text>`);
556
+ lx += widths[i];
557
+ });
558
+ if (title) p.push(`<text x="${r(W / 2)}" y="33" text-anchor="middle" font-size="${fs + 5}" ${wAttr(spec, 700)} fill="${titleCol}">${esc(title)}</text>`);
559
+ if (watermark) p.push(`<text x="${r(W - 8)}" y="${r(H - 8)}" text-anchor="end" font-size="10" fill="${faint}" opacity="0.7">slickfast.com</text>`);
560
+ p.push(`</svg>`);
561
+ return p.join("\n");
562
+ }
563
+ function renderBarH(spec) {
564
+ const W = spec.width || 800, H = spec.height || 450;
565
+ const bg = spec.background || "#ffffff";
566
+ const transparent = bg === "transparent" || bg === "none";
567
+ const isDark = !transparent && getLuminance(bg) < 0.35;
568
+ const fs = spec.fontSize ? Number(spec.fontSize) : Math.max(13, Math.round(13 * Math.sqrt(W * H) / 600));
569
+ const font = resolveFont(spec);
570
+ const title = spec.title || "";
571
+ const labels = spec.data.labels;
572
+ const values = spec.data.series[0].values.map((v) => Number(v) || 0);
573
+ const u = spec.valueUnit ? " " + spec.valueUnit : "";
574
+ const showValues = spec.showValues !== false;
575
+ const showTotal = spec.showTotal !== false;
576
+ const watermark = spec.watermark !== false;
577
+ const explicit = spec.data.series[0].colors;
578
+ const colors = Array.isArray(explicit) && explicit.length >= labels.length ? explicit : resolveFlatPalette(spec.palette || "Clean Corporate", labels.length);
579
+ const axisText = txt(spec, isDark ? "#94a3b8" : "#64748b");
580
+ const gridCol = isDark ? "#1e293b" : "#e2e8f0";
581
+ const catText = txt(spec, isDark ? "#cbd5e1" : "#475569");
582
+ const valText = txt(spec, isDark ? "#e2e8f0" : "#334155");
583
+ const titleCol = txt(spec, isDark ? "#f1f5f9" : "#0f172a");
584
+ const faint = isDark ? "#475569" : "#94a3b8";
585
+ const longest = Math.max(0, ...labels.map((l) => String(l).length));
586
+ const M = { left: Math.min(160, 24 + longest * (fs * 0.62)), right: 52, top: title ? 54 : 28, bottom: 46 };
587
+ const plotW = W - M.left - M.right;
588
+ const plotH = H - M.top - M.bottom;
589
+ const maxV = Math.max(0, ...values);
590
+ const { step, niceMax } = niceScale(maxV);
591
+ const xPix = (v) => M.left + v / niceMax * plotW;
592
+ const band = plotH / labels.length;
593
+ const barH = Math.min(band * 0.62, 56);
594
+ const p = [];
595
+ p.push(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="${W}" height="${H}" font-family="${esc(font)}">`);
596
+ if (!transparent) p.push(`<rect x="0" y="0" width="${W}" height="${H}" fill="${bg}"/>`);
597
+ for (let v = 0; v <= niceMax + 1e-9; v += step) {
598
+ const x = r(xPix(v));
599
+ p.push(`<line x1="${x}" y1="${M.top}" x2="${x}" y2="${r(M.top + plotH)}" stroke="${gridCol}" stroke-width="1"/>`);
600
+ p.push(`<text x="${x}" y="${r(M.top + plotH + 18)}" text-anchor="middle" font-size="${fs - 2}"${wOpt(spec)} fill="${axisText}">${fmt(v)}</text>`);
601
+ }
602
+ values.forEach((v, i) => {
603
+ const y = M.top + i * band + (band - barH) / 2;
604
+ const my = r(y + barH / 2 + 4);
605
+ const w = xPix(v) - M.left;
606
+ p.push(`<path d="${rightRoundedBar(M.left, y, Math.max(0, w), barH, 5)}" fill="${colors[i]}"/>`);
607
+ if (showValues) {
608
+ if (w >= 44) p.push(`<text x="${r(xPix(v) - 8)}" y="${my}" text-anchor="end" font-size="${fs - 1}" ${wAttr(spec, 600)} fill="${spec.textColor || contrastColor(colors[i])}">${esc(fmt(v) + u)}</text>`);
609
+ else p.push(`<text x="${r(xPix(v) + 6)}" y="${my}" text-anchor="start" font-size="${fs - 1}" ${wAttr(spec, 600)} fill="${valText}">${esc(fmt(v) + u)}</text>`);
610
+ }
611
+ p.push(`<text x="${r(M.left - 10)}" y="${my}" text-anchor="end" font-size="${fs - 1}"${wOpt(spec)} fill="${catText}">${esc(labels[i])}</text>`);
612
+ });
613
+ if (showTotal) {
614
+ const total = values.reduce((s, v) => s + v, 0);
615
+ p.push(`<text x="${r(W - M.right + 44)}" y="${title ? 50 : 20}" text-anchor="end" font-size="${fs - 1}"${wOpt(spec)} fill="${axisText}">Total: ${esc(fmt(total) + u)}</text>`);
616
+ }
617
+ if (title) p.push(`<text x="${r(W / 2)}" y="33" text-anchor="middle" font-size="${fs + 5}" ${wAttr(spec, 700)} fill="${titleCol}">${esc(title)}</text>`);
618
+ if (watermark) p.push(`<text x="${r(W - 8)}" y="${r(H - 8)}" text-anchor="end" font-size="10" fill="${faint}" opacity="0.7">slickfast.com</text>`);
619
+ p.push(`</svg>`);
620
+ return p.join("\n");
621
+ }
622
+ function renderDiverging(spec) {
623
+ const W = spec.width || 800, H = spec.height || 450;
624
+ const bg = spec.background || "#ffffff";
625
+ const transparent = bg === "transparent" || bg === "none";
626
+ const isDark = !transparent && getLuminance(bg) < 0.35;
627
+ const fs = spec.fontSize ? Number(spec.fontSize) : Math.max(13, Math.round(13 * Math.sqrt(W * H) / 600));
628
+ const font = resolveFont(spec);
629
+ const title = spec.title || "";
630
+ const labels = spec.data.labels;
631
+ const series = spec.data.series;
632
+ const u = spec.valueUnit ? " " + spec.valueUnit : "";
633
+ const showValues = spec.showValues !== false;
634
+ const watermark = spec.watermark !== false;
635
+ const cols = resolveFlatPalette(spec.palette || "Clean Corporate", 8);
636
+ const mid = Math.floor(cols.length / 2);
637
+ const A = series[0], B = series[1] || { name: "", values: [] };
638
+ const colA = A.color || cols[0];
639
+ const colB = B.color || cols[mid] || cols[1];
640
+ const axisText = txt(spec, isDark ? "#94a3b8" : "#64748b");
641
+ const gridCol = isDark ? "#1e293b" : "#e2e8f0";
642
+ const zeroCol = isDark ? "#64748b" : "#334155";
643
+ const catText = txt(spec, isDark ? "#cbd5e1" : "#475569");
644
+ const valText = txt(spec, isDark ? "#e2e8f0" : "#334155");
645
+ const titleCol = txt(spec, isDark ? "#f1f5f9" : "#0f172a");
646
+ const faint = isDark ? "#475569" : "#94a3b8";
647
+ const M = { left: 56, right: 24, top: title ? 54 : 28, bottom: 64 };
648
+ const plotW = W - M.left - M.right;
649
+ const plotH = H - M.top - M.bottom;
650
+ const aVals = labels.map((_, i) => Math.abs(Number(A.values[i]) || 0));
651
+ const bVals = labels.map((_, i) => Math.abs(Number(B.values[i]) || 0));
652
+ const S = Math.max(1, ...aVals, ...bVals);
653
+ const { step, niceMax } = niceScale(S);
654
+ const half = plotH / 2;
655
+ const yZero = M.top + half;
656
+ const yUp = (v) => yZero - v / niceMax * half;
657
+ const yDn = (v) => yZero + v / niceMax * half;
658
+ const band = plotW / labels.length;
659
+ const barW = Math.min(band * 0.5, 72);
660
+ const p = [];
661
+ p.push(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="${W}" height="${H}" font-family="${esc(font)}">`);
662
+ if (!transparent) p.push(`<rect x="0" y="0" width="${W}" height="${H}" fill="${bg}"/>`);
663
+ for (let v = step; v <= niceMax + 1e-9; v += step) {
664
+ [yUp(v), yDn(v)].forEach((y) => {
665
+ p.push(`<line x1="${M.left}" y1="${r(y)}" x2="${r(M.left + plotW)}" y2="${r(y)}" stroke="${gridCol}" stroke-width="1"/>`);
666
+ });
667
+ p.push(`<text x="${M.left - 8}" y="${r(yUp(v) + 4)}" text-anchor="end" font-size="${fs - 2}"${wOpt(spec)} fill="${axisText}">${fmt(v)}</text>`);
668
+ p.push(`<text x="${M.left - 8}" y="${r(yDn(v) + 4)}" text-anchor="end" font-size="${fs - 2}"${wOpt(spec)} fill="${axisText}">${fmt(v)}</text>`);
669
+ }
670
+ labels.forEach((lab, i) => {
671
+ const x = M.left + i * band + (band - barW) / 2;
672
+ const cx = r(x + barW / 2);
673
+ if (aVals[i] > 0) {
674
+ const yt = yUp(aVals[i]), h = yZero - yt;
675
+ p.push(`<path d="${topRoundedBar(x, yt, barW, h, 4)}" fill="${colA}"/>`);
676
+ if (showValues) p.push(`<text x="${cx}" y="${r(yt - 7)}" text-anchor="middle" font-size="${fs - 1}" ${wAttr(spec, 600)} fill="${valText}">${esc(fmt(aVals[i]) + u)}</text>`);
677
+ }
678
+ if (bVals[i] > 0) {
679
+ const h = yDn(bVals[i]) - yZero;
680
+ p.push(`<path d="${bottomRoundedBar(x, yZero, barW, h, 4)}" fill="${colB}"/>`);
681
+ if (showValues) p.push(`<text x="${cx}" y="${r(yDn(bVals[i]) + 16)}" text-anchor="middle" font-size="${fs - 1}" ${wAttr(spec, 600)} fill="${valText}">${esc(fmt(bVals[i]) + u)}</text>`);
682
+ }
683
+ });
684
+ p.push(`<line x1="${M.left}" y1="${r(yZero)}" x2="${r(M.left + plotW)}" y2="${r(yZero)}" stroke="${zeroCol}" stroke-width="1.75"/>`);
685
+ const pillFill = transparent ? isDark ? "#0b0f17" : "#ffffff" : bg;
686
+ labels.forEach((lab, i) => {
687
+ const cx = r(M.left + i * band + band / 2);
688
+ const lw = String(lab).length * (fs - 2) * 0.6 + 14;
689
+ p.push(`<rect x="${r(cx - lw / 2)}" y="${r(yZero - 9)}" width="${r(lw)}" height="18" rx="9" fill="${pillFill}"/>`);
690
+ p.push(`<text x="${cx}" y="${r(yZero + 4)}" text-anchor="middle" font-size="${fs - 2}" ${wAttr(spec, 600)} fill="${catText}">${esc(lab)}</text>`);
691
+ });
692
+ const items = [{ name: A.name || "Series A", col: colA }, { name: B.name || "Series B", col: colB }];
693
+ const widths = items.map((it) => 16 + it.name.length * (fs * 0.56) + 18);
694
+ let lx = (W - widths.reduce((a, b) => a + b, 0)) / 2;
695
+ const ly = H - 16;
696
+ items.forEach((it, i) => {
697
+ p.push(`<rect x="${r(lx)}" y="${r(ly - 9)}" width="11" height="11" rx="2.5" fill="${it.col}"/>`);
698
+ p.push(`<text x="${r(lx + 17)}" y="${r(ly)}" font-size="${fs - 1}"${wOpt(spec)} fill="${catText}">${esc(it.name)}</text>`);
699
+ lx += widths[i];
700
+ });
701
+ if (title) p.push(`<text x="${r(W / 2)}" y="33" text-anchor="middle" font-size="${fs + 5}" ${wAttr(spec, 700)} fill="${titleCol}">${esc(title)}</text>`);
702
+ if (watermark) p.push(`<text x="${r(W - 8)}" y="${r(H - 8)}" text-anchor="end" font-size="10" fill="${faint}" opacity="0.7">slickfast.com</text>`);
703
+ p.push(`</svg>`);
704
+ return p.join("\n");
705
+ }
706
+ function renderLollipop(spec) {
707
+ const W = spec.width || 800, H = spec.height || 450;
708
+ const bg = spec.background || "#ffffff";
709
+ const transparent = bg === "transparent" || bg === "none";
710
+ const isDark = !transparent && getLuminance(bg) < 0.35;
711
+ const fs = spec.fontSize ? Number(spec.fontSize) : Math.max(13, Math.round(13 * Math.sqrt(W * H) / 600));
712
+ const font = resolveFont(spec);
713
+ const title = spec.title || "";
714
+ const labels = spec.data.labels;
715
+ const values = spec.data.series[0].values.map((v) => Number(v) || 0);
716
+ const u = spec.valueUnit ? " " + spec.valueUnit : "";
717
+ const showValues = spec.showValues !== false;
718
+ const showTotal = spec.showTotal !== false;
719
+ const watermark = spec.watermark !== false;
720
+ const explicit = spec.data.series[0].colors;
721
+ const colors = Array.isArray(explicit) && explicit.length >= labels.length ? explicit : resolveFlatPalette(spec.palette || "Clean Corporate", labels.length);
722
+ const axisText = txt(spec, isDark ? "#94a3b8" : "#64748b");
723
+ const gridCol = isDark ? "#1e293b" : "#e2e8f0";
724
+ const catText = txt(spec, isDark ? "#cbd5e1" : "#475569");
725
+ const valText = txt(spec, isDark ? "#e2e8f0" : "#334155");
726
+ const titleCol = txt(spec, isDark ? "#f1f5f9" : "#0f172a");
727
+ const faint = isDark ? "#475569" : "#94a3b8";
728
+ const M = { left: 56, right: 24, top: title ? 54 : 28, bottom: 46 };
729
+ const plotW = W - M.left - M.right;
730
+ const plotH = H - M.top - M.bottom;
731
+ const maxV = Math.max(0, ...values);
732
+ const { step, niceMax } = niceScale(maxV);
733
+ const yPix = (v) => M.top + plotH - v / niceMax * plotH;
734
+ const band = plotW / labels.length;
735
+ const baseY = M.top + plotH;
736
+ const p = [];
737
+ p.push(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="${W}" height="${H}" font-family="${esc(font)}">`);
738
+ if (!transparent) p.push(`<rect x="0" y="0" width="${W}" height="${H}" fill="${bg}"/>`);
739
+ for (let v = 0; v <= niceMax + 1e-9; v += step) {
740
+ const y = r(yPix(v));
741
+ p.push(`<line x1="${M.left}" y1="${y}" x2="${r(M.left + plotW)}" y2="${y}" stroke="${gridCol}" stroke-width="1"/>`);
742
+ p.push(`<text x="${M.left - 8}" y="${r(y + 4)}" text-anchor="end" font-size="${fs - 2}"${wOpt(spec)} fill="${axisText}">${fmt(v)}</text>`);
743
+ }
744
+ values.forEach((v, i) => {
745
+ const cx = r(M.left + i * band + band / 2);
746
+ const y = yPix(v);
747
+ const mc = contrastFloor(colors[i], bg, transparent);
748
+ p.push(`<line x1="${cx}" y1="${r(baseY)}" x2="${cx}" y2="${r(y)}" stroke="${mc}" stroke-width="3" stroke-linecap="round"/>`);
749
+ p.push(`<circle cx="${cx}" cy="${r(y)}" r="7" fill="${mc}"/>`);
750
+ if (showValues) p.push(`<text x="${cx}" y="${r(y - 14)}" text-anchor="middle" font-size="${fs - 1}" ${wAttr(spec, 600)} fill="${valText}">${esc(fmt(v) + u)}</text>`);
751
+ p.push(`<text x="${cx}" y="${r(baseY + 18)}" text-anchor="middle" font-size="${fs - 1}"${wOpt(spec)} fill="${catText}">${esc(labels[i])}</text>`);
752
+ });
753
+ if (showTotal) {
754
+ const total = values.reduce((s, v) => s + v, 0);
755
+ p.push(`<text x="${r(W - M.right)}" y="${title ? 50 : 20}" text-anchor="end" font-size="${fs - 1}"${wOpt(spec)} fill="${axisText}">Total: ${esc(fmt(total) + u)}</text>`);
756
+ }
757
+ if (title) p.push(`<text x="${r(W / 2)}" y="33" text-anchor="middle" font-size="${fs + 5}" ${wAttr(spec, 700)} fill="${titleCol}">${esc(title)}</text>`);
758
+ if (watermark) p.push(`<text x="${r(W - 8)}" y="${r(H - 8)}" text-anchor="end" font-size="10" fill="${faint}" opacity="0.7">slickfast.com</text>`);
759
+ p.push(`</svg>`);
760
+ return p.join("\n");
761
+ }
762
+ function renderKpi(spec) {
763
+ const W = spec.width || 340, H = spec.height || 180;
764
+ const bg = spec.background || "#ffffff";
765
+ const transparent = bg === "transparent" || bg === "none";
766
+ const isDark = !transparent && getLuminance(bg) < 0.35;
767
+ const fs = spec.fontSize ? Number(spec.fontSize) : Math.max(13, Math.round(13 * Math.sqrt(W * H) / 600));
768
+ const font = resolveFont(spec);
769
+ const watermark = spec.watermark !== false;
770
+ const accent = resolveFlatPalette(spec.palette || "Clean Corporate", 1)[0];
771
+ const labelCol = txt(spec, isDark ? "#94a3b8" : "#64748b");
772
+ const valueCol = txt(spec, isDark ? "#f1f5f9" : "#0f172a");
773
+ const faint = isDark ? "#475569" : "#94a3b8";
774
+ const label = spec.label || "";
775
+ const valueStr = (spec.valuePrefix || "") + fmt(spec.value) + (spec.valueUnit ? " " + spec.valueUnit : "");
776
+ const hasDelta = spec.delta !== void 0 && spec.delta !== null;
777
+ const d = Number(spec.delta) || 0;
778
+ const goodDown = spec.deltaGoodWhen === "down";
779
+ const good = goodDown ? d < 0 : d > 0;
780
+ const bad = goodDown ? d > 0 : d < 0;
781
+ const arrow = d > 0 ? "\u25B2" : d < 0 ? "\u25BC" : "\u2013";
782
+ const dText = arrow + " " + fmt(Math.abs(d)) + (spec.deltaUnit || "%");
783
+ const pillBg = good ? isDark ? "#064e3b" : "#ecfdf5" : bad ? isDark ? "#7f1d1d" : "#fef2f2" : isDark ? "#334155" : "#f1f5f9";
784
+ const pillTx = good ? isDark ? "#6ee7b7" : "#059669" : bad ? isDark ? "#fca5a5" : "#dc2626" : labelCol;
785
+ const p = [];
786
+ p.push(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="${W}" height="${H}" font-family="${esc(font)}">`);
787
+ if (!transparent) p.push(`<rect x="0" y="0" width="${W}" height="${H}" rx="14" fill="${bg}"/>`);
788
+ const portrait = H > W;
789
+ if (!portrait) {
790
+ const cx = 40;
791
+ p.push(`<rect x="18" y="28" width="5" height="${r(H - 56)}" rx="2.5" fill="${accent}"/>`);
792
+ if (label) p.push(`<text x="${cx}" y="48" font-size="${fs + 1}" ${wAttr(spec, 600)} fill="${labelCol}">${esc(label)}</text>`);
793
+ p.push(`<text x="${cx}" y="104" font-size="42" ${wAttr(spec, 800)} fill="${valueCol}">${esc(valueStr)}</text>`);
794
+ if (hasDelta) {
795
+ const pillW = Math.round(dText.length * 7.3 + 18);
796
+ p.push(`<rect x="${cx}" y="124" width="${pillW}" height="24" rx="12" fill="${pillBg}"/>`);
797
+ p.push(`<text x="${r(cx + pillW / 2)}" y="140" text-anchor="middle" font-size="12.5" ${wAttr(spec, 700)} fill="${pillTx}">${esc(dText)}</text>`);
798
+ }
799
+ } else {
800
+ const hasLabel = !!label;
801
+ const px = Math.round(W * 0.09);
802
+ const valFont = Math.max(48, Math.min(Math.round(W * 0.16), Math.round((W - 2 * px) / Math.max(1, valueStr.length) * 1.7)));
803
+ const labelFont = Math.round(valFont * 0.32);
804
+ const pillFont = Math.round(valFont * 0.3);
805
+ const pillH = Math.round(pillFont * 1.9);
806
+ const gap1 = Math.round(valFont * 0.3), gap2 = Math.round(valFont * 0.34);
807
+ const blockH = (hasLabel ? labelFont + gap1 : 0) + valFont + (hasDelta ? gap2 + pillH : 0);
808
+ let y = Math.round((H - blockH) / 2);
809
+ const accW = Math.max(5, Math.round(W * 0.012));
810
+ p.push(`<rect x="${Math.round(W * 0.04)}" y="${r(y)}" width="${accW}" height="${r(blockH)}" rx="${r(accW / 2)}" fill="${accent}"/>`);
811
+ if (hasLabel) {
812
+ const by = y + labelFont;
813
+ p.push(`<text x="${px}" y="${r(by)}" font-size="${labelFont}" ${wAttr(spec, 600)} fill="${labelCol}">${esc(label)}</text>`);
814
+ y = by + gap1;
815
+ }
816
+ const vy = y + valFont;
817
+ p.push(`<text x="${px}" y="${r(vy)}" font-size="${valFont}" ${wAttr(spec, 800)} fill="${valueCol}">${esc(valueStr)}</text>`);
818
+ y = vy + gap2;
819
+ if (hasDelta) {
820
+ const pillW = Math.round(dText.length * pillFont * 0.62 + pillFont * 1.4);
821
+ p.push(`<rect x="${px}" y="${r(y)}" width="${pillW}" height="${pillH}" rx="${r(pillH / 2)}" fill="${pillBg}"/>`);
822
+ p.push(`<text x="${r(px + pillW / 2)}" y="${r(y + pillH * 0.68)}" text-anchor="middle" font-size="${pillFont}" ${wAttr(spec, 700)} fill="${pillTx}">${esc(dText)}</text>`);
823
+ }
824
+ }
825
+ if (watermark) p.push(`<text x="${W - 10}" y="${H - 9}" text-anchor="end" font-size="9" fill="${faint}" opacity="0.6">slickfast.com</text>`);
826
+ p.push(`</svg>`);
827
+ return p.join("\n");
828
+ }
829
+ function renderLine(spec) {
830
+ const W = spec.width || 800, H = spec.height || 450;
831
+ const bg = spec.background || "#ffffff";
832
+ const transparent = bg === "transparent" || bg === "none";
833
+ const isDark = !transparent && getLuminance(bg) < 0.35;
834
+ const fs = spec.fontSize ? Number(spec.fontSize) : Math.max(13, Math.round(13 * Math.sqrt(W * H) / 600));
835
+ const font = resolveFont(spec);
836
+ const watermark = spec.watermark !== false;
837
+ const title = spec.title || "";
838
+ const labels = spec.data.labels;
839
+ const series = spec.data.series;
840
+ const u = spec.valueUnit ? " " + spec.valueUnit : "";
841
+ const showValues = spec.showValues === true;
842
+ const showPoints = spec.showPoints !== false;
843
+ const smooth = spec.curve === "smooth" || spec.type === "smooth";
844
+ const stepped = spec.curve === "stepped" || spec.type === "stepped";
845
+ const area = spec.area === true || spec.type === "area";
846
+ const stackedArea = spec.stacked === true || spec.type === "stackedArea" || spec.type === "stacked-area";
847
+ const difference = spec.type === "difference";
848
+ const pal = resolveFlatPalette(spec.palette || "Clean Corporate", Math.max(series.length, 2));
849
+ const colorOf = (s, i) => s.color || pal[i % pal.length];
850
+ const axisText = txt(spec, isDark ? "#94a3b8" : "#64748b");
851
+ const gridCol = isDark ? "#1e293b" : "#e2e8f0";
852
+ const catText = txt(spec, isDark ? "#cbd5e1" : "#475569");
853
+ const valText = txt(spec, isDark ? "#e2e8f0" : "#334155");
854
+ const titleCol = txt(spec, isDark ? "#f1f5f9" : "#0f172a");
855
+ const faint = isDark ? "#475569" : "#94a3b8";
856
+ const multi = series.length > 1;
857
+ const M = { left: 56, right: 24, top: title ? 54 : 28, bottom: multi ? 64 : 46 };
858
+ const plotW = W - M.left - M.right;
859
+ const plotH = H - M.top - M.bottom;
860
+ const maxV = stackedArea ? Math.max(1, ...labels.map((_, i) => series.reduce((s, sr) => s + (Number(sr.values[i]) || 0), 0))) : Math.max(0, ...series.flatMap((s) => s.values.map((v) => Number(v) || 0)));
861
+ const { step, niceMax } = niceScale(maxV);
862
+ const yPix = (v) => M.top + plotH - v / niceMax * plotH;
863
+ const n = labels.length;
864
+ const xPix = (i) => M.left + (n > 1 ? i / (n - 1) : 0.5) * plotW;
865
+ const p = [];
866
+ p.push(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="${W}" height="${H}" font-family="${esc(font)}">`);
867
+ if (!transparent) p.push(`<rect x="0" y="0" width="${W}" height="${H}" fill="${bg}"/>`);
868
+ for (let v = 0; v <= niceMax + 1e-9; v += step) {
869
+ const y = r(yPix(v));
870
+ p.push(`<line x1="${M.left}" y1="${y}" x2="${r(M.left + plotW)}" y2="${y}" stroke="${gridCol}" stroke-width="1"/>`);
871
+ p.push(`<text x="${M.left - 8}" y="${r(y + 4)}" text-anchor="end" font-size="${fs - 2}"${wOpt(spec)} fill="${axisText}">${fmt(v)}</text>`);
872
+ }
873
+ labels.forEach((lab, i) => {
874
+ const anchor = i === 0 ? "start" : i === labels.length - 1 ? "end" : "middle";
875
+ p.push(`<text x="${r(xPix(i))}" y="${r(M.top + plotH + 18)}" text-anchor="${anchor}" font-size="${fs - 1}"${wOpt(spec)} fill="${catText}">${esc(lab)}</text>`);
876
+ });
877
+ const dot = (pt, col) => `<circle cx="${pt[0]}" cy="${pt[1]}" r="3.5" fill="${col}" stroke="${transparent ? col : bg}" stroke-width="1.5"/>`;
878
+ if (stackedArea) {
879
+ const cum = new Array(n).fill(0);
880
+ series.forEach((s, si) => {
881
+ const col = colorOf(s, si);
882
+ const top = cum.map((c, i) => c + (Number(s.values[i]) || 0));
883
+ const topPts = top.map((v, i) => [r(xPix(i)), r(yPix(v))]);
884
+ const botPts = cum.map((v, i) => [r(xPix(i)), r(yPix(v))]);
885
+ const topD = straightPath(topPts);
886
+ const botD = botPts.slice().reverse().map((pt) => "L" + pt[0] + "," + pt[1]).join(" ");
887
+ p.push(`<path d="${topD} ${botD} Z" fill="${col}" fill-opacity="0.8" stroke="none"/>`);
888
+ p.push(`<path d="${topD}" fill="none" stroke="${col}" stroke-width="2" stroke-linejoin="round"/>`);
889
+ if (showPoints) topPts.forEach((pt) => p.push(dot(pt, col)));
890
+ top.forEach((v, i) => {
891
+ cum[i] = v;
892
+ });
893
+ });
894
+ } else if (difference) {
895
+ const a = series[0], b = series[1] || series[0];
896
+ const colA = colorOf(a, 0), colB = colorOf(b, 1);
897
+ const aPts = labels.map((_, i) => [r(xPix(i)), r(yPix(Number(a.values[i]) || 0))]);
898
+ const bPts = labels.map((_, i) => [r(xPix(i)), r(yPix(Number(b.values[i]) || 0))]);
899
+ const aD = straightPath(aPts);
900
+ const bandBack = bPts.slice().reverse().map((pt) => "L" + pt[0] + "," + pt[1]).join(" ");
901
+ p.push(`<path d="${aD} ${bandBack} Z" fill="${isDark ? "#475569" : "#cbd5e1"}" fill-opacity="0.35" stroke="none"/>`);
902
+ p.push(`<path d="${aD}" fill="none" stroke="${colA}" stroke-width="2.5"/>`);
903
+ p.push(`<path d="${straightPath(bPts)}" fill="none" stroke="${colB}" stroke-width="2.5"/>`);
904
+ if (showPoints) {
905
+ aPts.forEach((pt) => p.push(dot(pt, colA)));
906
+ bPts.forEach((pt) => p.push(dot(pt, colB)));
907
+ }
908
+ labels.forEach((_, i) => {
909
+ const gap = (Number(a.values[i]) || 0) - (Number(b.values[i]) || 0);
910
+ const my = r((aPts[i][1] + bPts[i][1]) / 2 + 4);
911
+ p.push(`<text x="${r(xPix(i))}" y="${my}" text-anchor="middle" font-size="${fs - 1}" ${wAttr(spec, 700)} fill="${gap >= 0 ? "#059669" : "#dc2626"}">${(gap >= 0 ? "+" : "") + fmt(gap)}</text>`);
912
+ });
913
+ } else {
914
+ series.forEach((s, si) => {
915
+ const col = colorOf(s, si);
916
+ const vals = s.values.slice(0, n).map((v) => Number(v) || 0);
917
+ const pts = vals.map((v, i) => [r(xPix(i)), r(yPix(v))]);
918
+ const d = smooth ? smoothPath(pts) : stepped ? steppedPath(pts) : straightPath(pts);
919
+ if (area) {
920
+ const baseY = r(M.top + plotH);
921
+ p.push(`<path d="${d} L${pts[pts.length - 1][0]},${baseY} L${pts[0][0]},${baseY} Z" fill="${col}" fill-opacity="0.16" stroke="none"/>`);
922
+ }
923
+ p.push(`<path d="${d}" fill="none" stroke="${col}" stroke-width="2.5" stroke-linejoin="round" stroke-linecap="round"/>`);
924
+ if (showPoints) pts.forEach((pt) => p.push(dot(pt, col)));
925
+ if (showValues) vals.forEach((v, i) => p.push(`<text x="${r(xPix(i))}" y="${r(yPix(v) - 9)}" text-anchor="middle" font-size="${fs - 2}" ${wAttr(spec, 600)} fill="${valText}">${esc(fmt(v) + u)}</text>`));
926
+ });
927
+ }
928
+ if (multi) {
929
+ const items = series.map((s, i) => ({ name: s.name || "Series " + (i + 1), col: colorOf(s, i) }));
930
+ const widths = items.map((it) => 16 + it.name.length * (fs * 0.56) + 18);
931
+ const totalW = widths.reduce((a, b) => a + b, 0);
932
+ let lx = (W - totalW) / 2;
933
+ const ly = H - 16;
934
+ items.forEach((it, i) => {
935
+ p.push(`<circle cx="${r(lx + 5)}" cy="${r(ly - 4)}" r="5" fill="${it.col}"/>`);
936
+ p.push(`<text x="${r(lx + 16)}" y="${r(ly)}" font-size="${fs - 1}"${wOpt(spec)} fill="${catText}">${esc(it.name)}</text>`);
937
+ lx += widths[i];
938
+ });
939
+ }
940
+ if (title) p.push(`<text x="${r(W / 2)}" y="33" text-anchor="middle" font-size="${fs + 5}" ${wAttr(spec, 700)} fill="${titleCol}">${esc(title)}</text>`);
941
+ if (watermark) p.push(`<text x="${r(W - 8)}" y="${r(H - 8)}" text-anchor="end" font-size="10" fill="${faint}" opacity="0.7">slickfast.com</text>`);
942
+ p.push(`</svg>`);
943
+ return p.join("\n");
944
+ }
945
+ function renderSlope(spec) {
946
+ const W = spec.width || 800, H = spec.height || 450;
947
+ const bg = spec.background || "#ffffff";
948
+ const transparent = bg === "transparent" || bg === "none";
949
+ const isDark = !transparent && getLuminance(bg) < 0.35;
950
+ const fs = spec.fontSize ? Number(spec.fontSize) : Math.max(13, Math.round(13 * Math.sqrt(W * H) / 600));
951
+ const font = resolveFont(spec);
952
+ const watermark = spec.watermark !== false;
953
+ const title = spec.title || "";
954
+ const labels = spec.data.labels;
955
+ const series = spec.data.series;
956
+ const u = spec.valueUnit ? " " + spec.valueUnit : "";
957
+ const li = labels.length - 1;
958
+ const pal = resolveFlatPalette(spec.palette || "Clean Corporate", Math.max(series.length, 2));
959
+ const colorOf = (s, i) => s.color || pal[i % pal.length];
960
+ const guide = isDark ? "#1e293b" : "#e2e8f0";
961
+ const catText = txt(spec, isDark ? "#cbd5e1" : "#475569");
962
+ const valText = txt(spec, isDark ? "#e2e8f0" : "#334155");
963
+ const titleCol = txt(spec, isDark ? "#f1f5f9" : "#0f172a");
964
+ const faint = isDark ? "#475569" : "#94a3b8";
965
+ const multi = series.length > 1;
966
+ const M = { left: 72, right: 132, top: title ? 54 : 28, bottom: multi ? 64 : 44 };
967
+ const plotH = H - M.top - M.bottom;
968
+ const leftX = M.left, rightX = W - M.right, baseY = M.top + plotH;
969
+ const firstVals = series.map((s) => Number(s.values[0]) || 0);
970
+ const lastVals = series.map((s) => Number(s.values[li]) || 0);
971
+ const { niceMax } = niceScale(Math.max(1, ...firstVals, ...lastVals));
972
+ const yPix = (v) => M.top + plotH - v / niceMax * plotH;
973
+ const p = [];
974
+ p.push(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="${W}" height="${H}" font-family="${esc(font)}">`);
975
+ if (!transparent) p.push(`<rect x="0" y="0" width="${W}" height="${H}" fill="${bg}"/>`);
976
+ p.push(`<line x1="${leftX}" y1="${M.top}" x2="${leftX}" y2="${r(baseY)}" stroke="${guide}" stroke-width="1"/>`);
977
+ p.push(`<line x1="${rightX}" y1="${M.top}" x2="${rightX}" y2="${r(baseY)}" stroke="${guide}" stroke-width="1"/>`);
978
+ series.forEach((s, si) => {
979
+ const col = colorOf(s, si);
980
+ const y0 = r(yPix(firstVals[si])), y1 = r(yPix(lastVals[si]));
981
+ p.push(`<line x1="${leftX}" y1="${y0}" x2="${rightX}" y2="${y1}" stroke="${col}" stroke-width="2.5" stroke-linecap="round"/>`);
982
+ p.push(`<circle cx="${leftX}" cy="${y0}" r="4" fill="${col}"/>`);
983
+ p.push(`<circle cx="${rightX}" cy="${y1}" r="4" fill="${col}"/>`);
984
+ p.push(`<text x="${leftX - 10}" y="${r(y0 + 4)}" text-anchor="end" font-size="${fs - 1}" ${wAttr(spec, 600)} fill="${valText}">${esc(fmt(firstVals[si]) + u)}</text>`);
985
+ const delta = lastVals[si] - firstVals[si];
986
+ p.push(`<text x="${rightX + 10}" y="${r(y1 + 4)}" font-size="${fs - 1}" ${wAttr(spec, 600)} fill="${valText}">${esc(fmt(lastVals[si]) + u)} <tspan fill="${delta >= 0 ? "#059669" : "#dc2626"}">(${delta >= 0 ? "+" : ""}${esc(fmt(delta))})</tspan></text>`);
987
+ });
988
+ p.push(`<text x="${leftX}" y="${r(baseY + 22)}" text-anchor="middle" font-size="${fs - 1}" ${wAttr(spec, 600)} fill="${catText}">${esc(labels[0])}</text>`);
989
+ p.push(`<text x="${rightX}" y="${r(baseY + 22)}" text-anchor="middle" font-size="${fs - 1}" ${wAttr(spec, 600)} fill="${catText}">${esc(labels[li])}</text>`);
990
+ if (multi) {
991
+ const items = series.map((s, i) => ({ name: s.name || "Series " + (i + 1), col: colorOf(s, i) }));
992
+ const widths = items.map((it) => 16 + it.name.length * (fs * 0.56) + 18);
993
+ let lx = (W - widths.reduce((a, b) => a + b, 0)) / 2;
994
+ const ly = H - 16;
995
+ items.forEach((it, i) => {
996
+ p.push(`<circle cx="${r(lx + 5)}" cy="${r(ly - 4)}" r="5" fill="${it.col}"/>`);
997
+ p.push(`<text x="${r(lx + 16)}" y="${r(ly)}" font-size="${fs - 1}"${wOpt(spec)} fill="${catText}">${esc(it.name)}</text>`);
998
+ lx += widths[i];
999
+ });
1000
+ }
1001
+ if (title) p.push(`<text x="${r(W / 2)}" y="33" text-anchor="middle" font-size="${fs + 5}" ${wAttr(spec, 700)} fill="${titleCol}">${esc(title)}</text>`);
1002
+ if (watermark) p.push(`<text x="${r(W - 8)}" y="${r(H - 8)}" text-anchor="end" font-size="10" fill="${faint}" opacity="0.7">slickfast.com</text>`);
1003
+ p.push(`</svg>`);
1004
+ return p.join("\n");
1005
+ }
1006
+ function renderPie(spec) {
1007
+ const W = spec.width || 640, H = spec.height || 420;
1008
+ const bg = spec.background || "#ffffff";
1009
+ const transparent = bg === "transparent" || bg === "none";
1010
+ const isDark = !transparent && getLuminance(bg) < 0.35;
1011
+ const fs = spec.fontSize ? Number(spec.fontSize) : Math.max(13, Math.round(13 * Math.sqrt(W * H) / 600));
1012
+ const font = resolveFont(spec);
1013
+ const watermark = spec.watermark !== false;
1014
+ const title = spec.title || "";
1015
+ const donut = spec.type === "donut" || spec.donut === true;
1016
+ const labels = spec.data.labels;
1017
+ const values = spec.data.series[0].values.map((v) => Number(v) || 0);
1018
+ const u = spec.valueUnit ? " " + spec.valueUnit : "";
1019
+ const pre = spec.valuePrefix || "";
1020
+ const explicit = spec.data.series[0].colors;
1021
+ const colors = Array.isArray(explicit) && explicit.length >= labels.length ? explicit : resolveFlatPalette(spec.palette || "Clean Corporate", labels.length);
1022
+ const titleCol = txt(spec, isDark ? "#f1f5f9" : "#0f172a");
1023
+ const legendText = txt(spec, isDark ? "#cbd5e1" : "#475569");
1024
+ const faint = isDark ? "#475569" : "#94a3b8";
1025
+ const total = values.reduce((a2, b) => a2 + b, 0) || 1;
1026
+ const top = title ? 54 : 24;
1027
+ const bottomPad = 24;
1028
+ const portrait = H > W;
1029
+ const legendRowH = 26;
1030
+ const legendW = 184;
1031
+ const legendH = portrait ? labels.length * legendRowH + 14 : 0;
1032
+ const cx = portrait ? W / 2 : (W - legendW) / 2;
1033
+ const pieAreaH = portrait ? H - top - legendH - bottomPad : H - top - 24;
1034
+ const cy = top + pieAreaH / 2;
1035
+ const rad = r(Math.min(portrait ? W - 48 : W - legendW, pieAreaH) / 2 - 10);
1036
+ const innerR = r(donut ? rad * 0.58 : 0);
1037
+ const gapColor = transparent ? null : bg;
1038
+ const gapW = spec.sliceGap != null ? Number(spec.sliceGap) : Math.max(2, r(rad * 0.016));
1039
+ const gapAttr = gapColor && gapW > 0 ? ` stroke="${gapColor}" stroke-width="${gapW}" stroke-linejoin="round"` : "";
1040
+ const p = [];
1041
+ p.push(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="${W}" height="${H}" font-family="${esc(font)}">`);
1042
+ if (!transparent) p.push(`<rect x="0" y="0" width="${W}" height="${H}" fill="${bg}"/>`);
1043
+ let a = -Math.PI / 2;
1044
+ values.forEach((v, i) => {
1045
+ const frac = v / total;
1046
+ if (frac <= 0) return;
1047
+ const a1 = a + frac * Math.PI * 2;
1048
+ const col = colors[i];
1049
+ if (frac >= 0.9999) {
1050
+ if (innerR > 0) p.push(`<circle cx="${r(cx)}" cy="${r(cy)}" r="${r((rad + innerR) / 2)}" fill="none" stroke="${col}" stroke-width="${r(rad - innerR)}"/>`);
1051
+ else p.push(`<circle cx="${r(cx)}" cy="${r(cy)}" r="${rad}" fill="${col}"/>`);
1052
+ } else {
1053
+ p.push(`<path d="${arcPath(cx, cy, rad, a, a1, innerR)}" fill="${col}"${gapAttr}/>`);
1054
+ }
1055
+ if (frac >= 0.06) {
1056
+ const mid = (a + a1) / 2;
1057
+ const lr = innerR > 0 ? (rad + innerR) / 2 : rad * 0.62;
1058
+ p.push(`<text x="${r(cx + lr * Math.cos(mid))}" y="${r(cy + lr * Math.sin(mid) + 4)}" text-anchor="middle" font-size="${fs}" ${wAttr(spec, 700)} fill="${spec.textColor || contrastColor(col)}">${Math.round(frac * 100)}%</text>`);
1059
+ }
1060
+ a = a1;
1061
+ });
1062
+ if (donut) {
1063
+ p.push(`<text x="${r(cx)}" y="${r(cy - 2)}" text-anchor="middle" font-size="${fs + 10}" ${wAttr(spec, 800)} fill="${titleCol}">${esc(pre + fmt(total) + u)}</text>`);
1064
+ p.push(`<text x="${r(cx)}" y="${r(cy + 18)}" text-anchor="middle" font-size="${fs - 1}"${wOpt(spec)} fill="${legendText}">Total</text>`);
1065
+ }
1066
+ if (portrait) {
1067
+ const maxChars = Math.max(...labels.map((lab, i) => (String(lab) + " \xB7 " + pre + fmt(values[i]) + u).length));
1068
+ const blockW = 18 + maxChars * (fs - 1) * 0.55;
1069
+ const lx = r((W - blockW) / 2);
1070
+ let lgy = H - legendH - bottomPad + 20;
1071
+ labels.forEach((lab, i) => {
1072
+ p.push(`<rect x="${lx}" y="${r(lgy - 9)}" width="11" height="11" rx="2.5" fill="${colors[i]}"/>`);
1073
+ p.push(`<text x="${r(lx + 18)}" y="${r(lgy)}" font-size="${fs - 1}"${wOpt(spec)} fill="${legendText}">${esc(lab)} \xB7 ${esc(pre + fmt(values[i]) + u)}</text>`);
1074
+ lgy += legendRowH;
1075
+ });
1076
+ } else {
1077
+ const lgx = W - legendW + 8;
1078
+ let lgy = top + 8;
1079
+ labels.forEach((lab, i) => {
1080
+ p.push(`<rect x="${lgx}" y="${r(lgy - 9)}" width="11" height="11" rx="2.5" fill="${colors[i]}"/>`);
1081
+ p.push(`<text x="${lgx + 18}" y="${r(lgy)}" font-size="${fs - 1}"${wOpt(spec)} fill="${legendText}">${esc(lab)} \xB7 ${esc(pre + fmt(values[i]) + u)}</text>`);
1082
+ lgy += 24;
1083
+ });
1084
+ }
1085
+ if (title) p.push(`<text x="${r(W / 2)}" y="33" text-anchor="middle" font-size="${fs + 5}" ${wAttr(spec, 700)} fill="${titleCol}">${esc(title)}</text>`);
1086
+ if (watermark) p.push(`<text x="${r(W - 8)}" y="${r(H - 8)}" text-anchor="end" font-size="10" fill="${faint}" opacity="0.7">slickfast.com</text>`);
1087
+ p.push(`</svg>`);
1088
+ return p.join("\n");
1089
+ }
1090
+ function renderPieOfPie(spec) {
1091
+ const W = spec.width || 920, H = spec.height || 380;
1092
+ const bg = spec.background || "#ffffff";
1093
+ const transparent = bg === "transparent" || bg === "none";
1094
+ const isDark = !transparent && getLuminance(bg) < 0.35;
1095
+ const fs = spec.fontSize ? Number(spec.fontSize) : 13;
1096
+ const font = resolveFont(spec);
1097
+ const title = spec.title || "";
1098
+ const watermark = spec.watermark !== false;
1099
+ const donut = spec.donut !== false;
1100
+ const showValues = spec.showValues !== false;
1101
+ const titleCol = txt(spec, isDark ? "#f1f5f9" : "#0f172a");
1102
+ const subCol = txt(spec, isDark ? "#cbd5e1" : "#475569");
1103
+ const faint = isDark ? "#475569" : "#94a3b8";
1104
+ const sliceBorder = isDark ? "#0b0f17" : "#ffffff";
1105
+ const accent = isDark ? "#a78bfa" : "#8b5cf6";
1106
+ const cascade = spec.cascade !== false;
1107
+ const raw = Array.isArray(spec.pies) ? spec.pies : [];
1108
+ const pies = raw.map((pp) => {
1109
+ const labels = (pp.labels || []).map(String);
1110
+ const vals = (pp.values || []).map((v) => Number(v) || 0);
1111
+ const keep = vals.map((v, i) => [v, i]).filter((e) => e[0] > 0);
1112
+ const values = keep.map((e) => e[0]);
1113
+ const lbls = keep.map((e) => labels[e[1]] || "");
1114
+ return {
1115
+ title: pp.title || "",
1116
+ labels: lbls,
1117
+ values,
1118
+ total: values.reduce((a, b) => a + b, 0) || 1,
1119
+ explicit: Array.isArray(pp.colors) && pp.colors.length >= keep.length ? pp.colors : null,
1120
+ palette: pp.palette
1121
+ };
1122
+ }).filter((pie) => pie.values.length);
1123
+ const themeName = spec.palette || "Clean Corporate";
1124
+ const isNested = NESTED_THEMES.some((t) => t.name === themeName);
1125
+ pies.forEach((pie, i) => {
1126
+ const count = pie.values.length;
1127
+ if (pie.explicit) pie.colors = pie.explicit;
1128
+ else if (isNested && cascade) pie.colors = resolveNestedTheme(themeName, i, count, i > 0 ? pies[0].colors[0] : null);
1129
+ else if (i === 0 || !cascade) pie.colors = resolveFlatPalette(pie.palette || themeName, count);
1130
+ else pie.colors = tierPalette(pies[0].colors[0], i, count);
1131
+ });
1132
+ const n = pies.length;
1133
+ const p = [];
1134
+ p.push(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="${W}" height="${H}" font-family="${esc(font)}">`);
1135
+ if (!transparent) p.push(`<rect x="0" y="0" width="${W}" height="${H}" fill="${bg}"/>`);
1136
+ if (n === 0) {
1137
+ p.push(`</svg>`);
1138
+ return p.join("\n");
1139
+ }
1140
+ const SHRINK = 0.75, EXPL = 0.1, innerFrac = 0.58;
1141
+ const fac = (i) => Math.pow(SHRINK, i);
1142
+ const sumDiam = pies.reduce((s, _, i) => s + 2 * fac(i), 0);
1143
+ const sidePad = 28;
1144
+ const gap = Math.max(28, W * 0.035);
1145
+ const topMain = title ? 40 : 16;
1146
+ const perTitleH = 30;
1147
+ const botPad = 16 + (watermark ? 6 : 0);
1148
+ let R = Math.min((W - 2 * sidePad - (n - 1) * gap) / sumDiam, (H - topMain - perTitleH - botPad) / 2);
1149
+ R = Math.max(20, R);
1150
+ const cyAxis = topMain + perTitleH + R;
1151
+ const cxs = [];
1152
+ let cursor = sidePad;
1153
+ pies.forEach((_, i) => {
1154
+ const rad = R * fac(i);
1155
+ cxs.push(cursor + rad);
1156
+ cursor += 2 * rad + gap;
1157
+ });
1158
+ const rowRight = cxs[n - 1] + R * fac(n - 1);
1159
+ const center = (W - (rowRight - sidePad)) / 2 - sidePad;
1160
+ for (let i = 0; i < n; i++) cxs[i] += center;
1161
+ const clamp = (x) => Math.max(-Math.PI / 2, Math.min(Math.PI / 2, x));
1162
+ for (let i = 0; i < n - 1; i++) {
1163
+ const rad = R * fac(i), nrad = R * fac(i + 1), cx = cxs[i], ncx = cxs[i + 1];
1164
+ const frac0 = pies[i].values[0] / pies[i].total;
1165
+ const exCx = cx + rad * EXPL;
1166
+ const s0 = clamp(-frac0 * Math.PI), e0 = clamp(frac0 * Math.PI);
1167
+ const ex1 = exCx + rad * Math.cos(s0), ey1 = cyAxis + rad * Math.sin(s0);
1168
+ const ex2 = exCx + rad * Math.cos(e0), ey2 = cyAxis + rad * Math.sin(e0);
1169
+ const lx = ncx - Math.cos(Math.PI / 3) * nrad;
1170
+ const lyT = cyAxis - Math.sin(Math.PI / 3) * nrad, lyB = cyAxis + Math.sin(Math.PI / 3) * nrad;
1171
+ const col = pies[i].colors[0] || "#94a3b8";
1172
+ const sw = Math.max(1, r(rad * 0.012));
1173
+ p.push(`<line x1="${r(ex1)}" y1="${r(ey1)}" x2="${r(lx)}" y2="${r(lyT)}" stroke="${col}" stroke-width="${sw}" stroke-opacity="0.4"/>`);
1174
+ p.push(`<line x1="${r(ex2)}" y1="${r(ey2)}" x2="${r(lx)}" y2="${r(lyB)}" stroke="${col}" stroke-width="${sw}" stroke-opacity="0.4"/>`);
1175
+ }
1176
+ pies.forEach((pie, i) => {
1177
+ const rad = R * fac(i), cx = cxs[i];
1178
+ const innerR = donut ? r(rad * innerFrac) : 0;
1179
+ const isBridge = i < n - 1;
1180
+ const frac0 = pie.values[0] / pie.total;
1181
+ let a = isBridge ? -frac0 * Math.PI : -Math.PI / 2;
1182
+ pie.values.forEach((v, si) => {
1183
+ const frac = v / pie.total;
1184
+ const a1 = a + frac * Math.PI * 2;
1185
+ const col = pie.colors[si];
1186
+ const bridge = isBridge && si === 0;
1187
+ const drawCx = bridge ? cx + rad * EXPL : cx;
1188
+ const stroke = bridge ? ` stroke="${sliceBorder}" stroke-width="${Math.max(1.5, r(rad * 0.02))}"` : "";
1189
+ if (frac >= 0.9999) p.push(`<circle cx="${r(drawCx)}" cy="${r(cyAxis)}" r="${r(rad)}" fill="${col}"${stroke}/>`);
1190
+ else p.push(`<path d="${arcPath(drawCx, cyAxis, rad, a, a1, innerR)}" fill="${col}"${stroke}/>`);
1191
+ if (showValues && frac >= 0.07) {
1192
+ const mid = (a + a1) / 2;
1193
+ const lr = innerR > 0 ? (rad + innerR) / 2 : rad * 0.62;
1194
+ const lfs = Math.max(9, r(fs * Math.sqrt(fac(i))));
1195
+ p.push(`<text x="${r(drawCx + lr * Math.cos(mid))}" y="${r(cyAxis + lr * Math.sin(mid) + 4)}" text-anchor="middle" font-size="${lfs}" ${wAttr(spec, 700)} fill="${spec.textColor || contrastColor(col)}">${Math.round(frac * 100)}%</text>`);
1196
+ }
1197
+ a = a1;
1198
+ });
1199
+ if (pie.title) p.push(`<text x="${r(cx)}" y="${r(topMain + 13)}" text-anchor="middle" font-size="${fs}" ${wAttr(spec, 700)} fill="${subCol}">${esc(pie.title)}</text>`);
1200
+ const parentLabel = i > 0 ? pies[i - 1].labels[0] : "";
1201
+ if (parentLabel && parentLabel !== pie.title) {
1202
+ const sy = pie.title ? topMain + 27 : topMain + 14;
1203
+ p.push(`<text x="${r(cx)}" y="${r(sy)}" text-anchor="middle" font-size="${fs - 3}"${wOpt(spec)} fill="${accent}">\u21B3 ${esc(parentLabel)}</text>`);
1204
+ }
1205
+ });
1206
+ if (title) p.push(`<text x="${r(W / 2)}" y="26" text-anchor="middle" font-size="${fs + 5}" ${wAttr(spec, 700)} fill="${titleCol}">${esc(title)}</text>`);
1207
+ if (watermark) p.push(`<text x="${r(W - 8)}" y="${r(H - 8)}" text-anchor="end" font-size="10" fill="${faint}" opacity="0.7">slickfast.com</text>`);
1208
+ p.push(`</svg>`);
1209
+ return p.join("\n");
1210
+ }
1211
+ var RATIO_PRESETS = {
1212
+ "share card": [1200, 630],
1213
+ "sharecard": [1200, 630],
1214
+ "share-card": [1200, 630],
1215
+ "1.91:1": [1200, 630],
1216
+ "wide": [1280, 720],
1217
+ "16:9": [1280, 720],
1218
+ "square": [1080, 1080],
1219
+ "1:1": [1080, 1080],
1220
+ "portrait": [1080, 1350],
1221
+ "4:5": [1080, 1350],
1222
+ "tall": [1080, 1920],
1223
+ "9:16": [1080, 1920],
1224
+ "classic": [1200, 900],
1225
+ "4:3": [1200, 900]
1226
+ };
1227
+ function applyPreset(spec) {
1228
+ const key = String(spec.preset || spec.ratio || "").trim().toLowerCase();
1229
+ const p = RATIO_PRESETS[key];
1230
+ if (!p) return spec;
1231
+ return { ...spec, width: spec.width != null ? spec.width : p[0], height: spec.height != null ? spec.height : p[1] };
1232
+ }
1233
+ function renderSpec(spec) {
1234
+ spec = applyPreset(spec);
1235
+ switch (spec.type) {
1236
+ case "bar":
1237
+ return renderBar(spec);
1238
+ case "grouped":
1239
+ return renderBarGrouped(spec);
1240
+ case "stacked":
1241
+ case "stacked100":
1242
+ return renderBarStacked(spec);
1243
+ case "stackedh":
1244
+ return renderBarStackedH(spec);
1245
+ case "horizontal":
1246
+ return renderBarH(spec);
1247
+ case "diverging":
1248
+ return renderDiverging(spec);
1249
+ case "lollipop":
1250
+ return renderLollipop(spec);
1251
+ case "line":
1252
+ case "smooth":
1253
+ case "area":
1254
+ case "stepped":
1255
+ case "stackedArea":
1256
+ case "stacked-area":
1257
+ case "difference":
1258
+ return renderLine(spec);
1259
+ case "slope":
1260
+ return renderSlope(spec);
1261
+ case "pie":
1262
+ case "donut":
1263
+ return renderPie(spec);
1264
+ case "pieofpie":
1265
+ case "piepie":
1266
+ return renderPieOfPie(spec);
1267
+ case "kpi":
1268
+ return renderKpi(spec);
1269
+ default:
1270
+ throw new Error('render-core: unknown chart type "' + spec.type + '"');
1271
+ }
1272
+ }
1273
+
1274
+ // ../../packages/raster/raster.mjs
1275
+ import { Resvg } from "@resvg/resvg-js";
1276
+ function svgToPng(svg, opts = {}) {
1277
+ const fitTo = opts.width ? { mode: "width", value: opts.width } : { mode: "zoom", value: opts.scale || 2 };
1278
+ const resvg = new Resvg(svg, { fitTo, font: { loadSystemFonts: true } });
1279
+ return resvg.render().asPng();
1280
+ }
1281
+
1282
+ // server.mjs
1283
+ var here = dirname(fileURLToPath(import.meta.url));
1284
+ var SPEC_PATH = join(here, "../../packages/render-core/SPEC.md");
1285
+ var seriesShape = z.object({
1286
+ name: z.string().optional().describe("series label (legend)"),
1287
+ values: z.array(z.number()).describe("the numbers, aligned to data.labels"),
1288
+ color: z.string().optional().describe("per-series color override"),
1289
+ colors: z.array(z.string()).optional().describe("per-item color overrides")
1290
+ });
1291
+ var pieShape = z.object({
1292
+ title: z.string().optional().describe("this pie's heading"),
1293
+ labels: z.array(z.string()).describe("slice labels"),
1294
+ values: z.array(z.number()).describe("slice values, aligned to labels"),
1295
+ colors: z.array(z.string()).optional().describe("per-slice color overrides"),
1296
+ palette: z.string().optional().describe("per-pie palette override (rare)")
1297
+ });
1298
+ var inputSchema = {
1299
+ type: z.enum(["bar", "grouped", "stacked", "stacked100", "stackedh", "horizontal", "lollipop", "diverging", "line", "smooth", "area", "stepped", "stackedArea", "difference", "slope", "pie", "donut", "pieofpie", "kpi"]).describe("chart type"),
1300
+ title: z.string().optional(),
1301
+ data: z.object({ labels: z.array(z.string()), series: z.array(seriesShape) }).optional().describe("categories + series; required for every type except kpi and pieofpie"),
1302
+ pies: z.array(pieShape).optional().describe(`pieofpie ONLY: a list of pies; each pie's first slice ("bridge") drills down into the next. 2 pies = pie-of-pie, 3 = pie-of-pie-of-pie, N supported.`),
1303
+ cascade: z.boolean().optional().describe("pieofpie: child pies shade from the parent bridge hue (default true); false = flat palette per pie"),
1304
+ palette: z.string().optional().describe("Clean Corporate | Pastel | Vibrant | Monochrome | Cyberpunk | Analogous Shift. Use Monochrome for black & white / laser-printer output \u2014 a grey ramp that stays legible without color."),
1305
+ background: z.string().optional().describe('any hex color, or "transparent"'),
1306
+ font: z.string().optional().describe("Inter | System | Serif | Mono | Rounded | Condensed"),
1307
+ fontFamily: z.string().optional().describe("raw CSS font stack (overrides font)"),
1308
+ fontSize: z.number().optional(),
1309
+ textColor: z.string().optional().describe("force neutral text color (title/axis/labels/legend); omit for auto contrast. Semantic up/down colors are not affected"),
1310
+ bold: z.boolean().optional().describe("thicken all text"),
1311
+ fontWeight: z.string().optional().describe('exact weight for all text ("400"-"900" or "bold"); overrides bold'),
1312
+ valueUnit: z.string().optional().describe('appended to values, e.g. "$" or "%"'),
1313
+ width: z.number().optional(),
1314
+ height: z.number().optional(),
1315
+ preset: z.string().optional().describe('aspect-ratio preset (sets width/height): "Share Card" 1.91:1 (link/OG cards \u2014 Slack/X/LinkedIn), "Wide" 16:9, "Square" 1:1, "Portrait" 4:5 (IG/FB feed), "Tall" 9:16 (Stories/Reels/TikTok), "Classic" 4:3. Explicit width/height still win.'),
1316
+ ratio: z.string().optional().describe('alias for preset \u2014 accepts a ratio like "16:9", "1:1", "9:16", "4:5", "4:3", "1.91:1"'),
1317
+ watermark: z.boolean().optional().describe("tasteful slickfast.com mark (default true; off for the chart sites)"),
1318
+ showValues: z.boolean().optional(),
1319
+ showPoints: z.boolean().optional(),
1320
+ showTotal: z.boolean().optional(),
1321
+ curve: z.enum(["straight", "smooth", "stepped"]).optional().describe("line shape"),
1322
+ area: z.boolean().optional().describe("fill under the line"),
1323
+ stacked: z.boolean().optional().describe("stack area series"),
1324
+ donut: z.boolean().optional().describe("pie with a center hole"),
1325
+ label: z.string().optional().describe("kpi: the metric name"),
1326
+ value: z.number().optional().describe("kpi: the big number"),
1327
+ valuePrefix: z.string().optional().describe('prefix before values, e.g. "$" \u2014 used by kpi value and pie/donut legend + center total'),
1328
+ delta: z.number().optional().describe("kpi: the change (green up / red down)"),
1329
+ deltaUnit: z.string().optional().describe('kpi: delta unit, default "%"'),
1330
+ deltaGoodWhen: z.enum(["up", "down"]).optional().describe('kpi: which delta direction is GOOD (green). Default "up"; set "down" for lower-is-better metrics (churn, latency, cost)'),
1331
+ format: z.enum(["png", "svg"]).optional().describe('output format: "png" (default \u2014 shows inline in chat) or "svg" (scalable vector text)'),
1332
+ scale: z.number().optional().describe("png pixel-density multiplier (default 2 = retina)")
1333
+ };
1334
+ var server = new McpServer({ name: "slickfast", version: "0.1.0" });
1335
+ server.registerTool("render_chart", {
1336
+ title: "Render chart (SVG)",
1337
+ description: `Turn a chart spec into an SVG string with the SlickFast engine. Types: bar, grouped, stacked, stacked100, stackedh, horizontal, lollipop, diverging, line, smooth, area, stepped, stackedArea, difference, slope, pie, donut, pieofpie, kpi. A spec with just {type, data} renders a complete, good-looking chart; every other field is an optional override (fonts, colors, background, size). pieofpie uses {pies:[\u2026]} instead of data \u2014 each pie's first slice drills into the next; set palette to a nested theme like "Analogous Shift". Read the chart-spec resource for the full field contract.`,
1338
+ inputSchema
1339
+ }, async (spec) => {
1340
+ try {
1341
+ const svg = renderSpec(spec);
1342
+ if ((spec.format || "png") === "png") {
1343
+ const png = svgToPng(svg, { scale: spec.scale });
1344
+ return { content: [{ type: "image", data: png.toString("base64"), mimeType: "image/png" }] };
1345
+ }
1346
+ return { content: [{ type: "text", text: svg }] };
1347
+ } catch (e) {
1348
+ return { isError: true, content: [{ type: "text", text: "render error: " + (e?.message || String(e)) }] };
1349
+ }
1350
+ });
1351
+ server.registerResource("chart-spec", "spec://chart-spec", {
1352
+ title: "ChartSpec contract",
1353
+ description: "Full field reference for render_chart.",
1354
+ mimeType: "text/markdown"
1355
+ }, async (uri) => ({
1356
+ contents: [{ uri: uri.href, mimeType: "text/markdown", text: readFileSync(SPEC_PATH, "utf8") }]
1357
+ }));
1358
+ var transport = new StdioServerTransport();
1359
+ await server.connect(transport);
1360
+ console.error("slickfast MCP server ready (stdio) \u2014 tool: render_chart");