sdocs-dev 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +149 -0
- package/bin/sdocs-dev.js +282 -38
- package/package.json +6 -3
- package/public/{brotli-wasm.js → brotli-wasm-v1.js} +5 -6
- package/public/css/layout.css +3 -4
- package/public/css/mobile.css +7 -0
- package/public/css/panel.css +1 -1
- package/public/css/rendered.css +184 -1
- package/public/index.html +137 -43
- package/public/sdoc.md +542 -0
- package/public/sdocs-app.js +222 -63
- package/public/sdocs-charts.js +905 -0
- package/public/sdocs-controls.js +135 -53
- package/public/sdocs-export.js +939 -61
- package/public/sdocs-slugify.js +12 -0
- package/public/sdocs-state.js +14 -0
- package/public/sdocs-styles.js +180 -254
- package/public/sdocs-theme.js +21 -19
- package/public/sdocs-write.js +4 -1
- package/public/sw.js +5 -3
- package/public/vendor/purify.min.js +3 -0
- package/server.js +51 -3
- package/public/default.md +0 -255
|
@@ -0,0 +1,905 @@
|
|
|
1
|
+
/* ═══════════════════════════════════════════════════
|
|
2
|
+
SDocs Charts — render ```chart code blocks as Chart.js charts
|
|
3
|
+
Lazy-loads Chart.js from CDN on first use.
|
|
4
|
+
|
|
5
|
+
Supported types:
|
|
6
|
+
pie, doughnut, bar, horizontal_bar, stacked_bar, stacked_horizontal_bar,
|
|
7
|
+
line, area, stacked_area, radar, polarArea, scatter, bubble, mixed
|
|
8
|
+
|
|
9
|
+
Options:
|
|
10
|
+
title, subtitle, labels, values, datasets, colors,
|
|
11
|
+
xAxis, yAxis, y2Axis, legend, aspectRatio,
|
|
12
|
+
format (currency/percent/number), stacked,
|
|
13
|
+
min, max, stepSize, beginAtZero,
|
|
14
|
+
annotations (horizontal/vertical reference lines)
|
|
15
|
+
═══════════════════════════════════════════════════ */
|
|
16
|
+
(function () {
|
|
17
|
+
var S = window.SDocs;
|
|
18
|
+
var chartJsLoaded = false;
|
|
19
|
+
var chartJsLoading = false;
|
|
20
|
+
var pendingCallbacks = [];
|
|
21
|
+
var CDN_CHART = 'https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js';
|
|
22
|
+
var CDN_LABELS = 'https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2/dist/chartjs-plugin-datalabels.min.js';
|
|
23
|
+
var activeCharts = [];
|
|
24
|
+
|
|
25
|
+
// ── Fallback palette (used when no accent is set) ──
|
|
26
|
+
var DEFAULT_PALETTE = [
|
|
27
|
+
'#3b82f6', '#ef4444', '#22c55e', '#f59e0b', '#8b5cf6',
|
|
28
|
+
'#ec4899', '#14b8a6', '#f97316', '#6366f1', '#84cc16',
|
|
29
|
+
'#06b6d4', '#d946ef', '#0ea5e9', '#a3e635', '#fb923c',
|
|
30
|
+
'#e11d48', '#2dd4bf', '#a78bfa', '#fbbf24', '#34d399'
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
// ── HSL helpers (shared from sdocs-styles.js) ──
|
|
34
|
+
var hexToHsl = SDocStyles.hexToHsl;
|
|
35
|
+
var hslToHex = SDocStyles.hslToHex;
|
|
36
|
+
|
|
37
|
+
// ── Palette generation ──
|
|
38
|
+
// Modes: complementary, monochrome, analogous, triadic, warm, cool, pastel, earth
|
|
39
|
+
function generatePalette(accent, mode, count) {
|
|
40
|
+
var hsl = hexToHsl(accent);
|
|
41
|
+
var h = hsl[0], s = hsl[1], l = hsl[2];
|
|
42
|
+
var colors = [];
|
|
43
|
+
var i;
|
|
44
|
+
|
|
45
|
+
switch (mode) {
|
|
46
|
+
case 'monochrome':
|
|
47
|
+
case 'mono':
|
|
48
|
+
// Same hue, spread lightness from dark to light
|
|
49
|
+
for (i = 0; i < count; i++) {
|
|
50
|
+
var li = 25 + (50 * i / Math.max(count - 1, 1)); // 25% to 75%
|
|
51
|
+
colors.push(hslToHex(h, s, li));
|
|
52
|
+
}
|
|
53
|
+
break;
|
|
54
|
+
|
|
55
|
+
case 'analogous':
|
|
56
|
+
// ±40° spread around the accent hue
|
|
57
|
+
var spread = 40;
|
|
58
|
+
for (i = 0; i < count; i++) {
|
|
59
|
+
var offset = -spread + (2 * spread * i / Math.max(count - 1, 1));
|
|
60
|
+
colors.push(hslToHex(h + offset, s, l));
|
|
61
|
+
}
|
|
62
|
+
break;
|
|
63
|
+
|
|
64
|
+
case 'triadic':
|
|
65
|
+
// Three base hues 120° apart, then vary lightness
|
|
66
|
+
for (i = 0; i < count; i++) {
|
|
67
|
+
var baseH = h + (i % 3) * 120;
|
|
68
|
+
var li2 = l + (Math.floor(i / 3) * 10 - 10);
|
|
69
|
+
colors.push(hslToHex(baseH, s, li2));
|
|
70
|
+
}
|
|
71
|
+
break;
|
|
72
|
+
|
|
73
|
+
case 'warm':
|
|
74
|
+
for (i = 0; i < count; i++) {
|
|
75
|
+
colors.push(hslToHex(i * (60 / count), 70 + (i % 3) * 10, 50 + (i % 2) * 10));
|
|
76
|
+
}
|
|
77
|
+
break;
|
|
78
|
+
|
|
79
|
+
case 'cool':
|
|
80
|
+
for (i = 0; i < count; i++) {
|
|
81
|
+
colors.push(hslToHex(180 + i * (80 / count), 60 + (i % 3) * 10, 45 + (i % 2) * 10));
|
|
82
|
+
}
|
|
83
|
+
break;
|
|
84
|
+
|
|
85
|
+
case 'pastel':
|
|
86
|
+
for (i = 0; i < count; i++) {
|
|
87
|
+
colors.push(hslToHex(h + i * (360 / count), 55, 75));
|
|
88
|
+
}
|
|
89
|
+
break;
|
|
90
|
+
|
|
91
|
+
case 'earth':
|
|
92
|
+
var earthHues = [30, 45, 20, 60, 15, 35, 50, 10, 40, 25];
|
|
93
|
+
for (i = 0; i < count; i++) {
|
|
94
|
+
colors.push(hslToHex(earthHues[i % earthHues.length], 45 + (i % 3) * 10, 40 + (i % 4) * 8));
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
|
|
98
|
+
case 'complementary':
|
|
99
|
+
default:
|
|
100
|
+
// Spread hues evenly around the wheel, starting from accent
|
|
101
|
+
for (i = 0; i < count; i++) {
|
|
102
|
+
colors.push(hslToHex(h + i * (360 / count), s, l));
|
|
103
|
+
}
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return colors;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── Get active palette (reads CSS vars or per-chart overrides) ──
|
|
111
|
+
function getActivePalette(data, count) {
|
|
112
|
+
// Per-chart colors override everything
|
|
113
|
+
if (data.colors) return data.colors;
|
|
114
|
+
|
|
115
|
+
// Per-chart accent + mode
|
|
116
|
+
var accent = data.accent || null;
|
|
117
|
+
var mode = data.palette || null;
|
|
118
|
+
|
|
119
|
+
// Fall back to front matter chart styles (persisted on S.chartStyles)
|
|
120
|
+
if (!accent && S.chartStyles) {
|
|
121
|
+
accent = S.chartStyles.accent || null;
|
|
122
|
+
if (!mode) mode = S.chartStyles.palette || null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Fall back to CSS vars from style panel
|
|
126
|
+
if (!accent) {
|
|
127
|
+
var rendered = document.getElementById('rendered');
|
|
128
|
+
if (rendered) {
|
|
129
|
+
var cs = getComputedStyle(rendered);
|
|
130
|
+
accent = cs.getPropertyValue('--md-chart-accent').trim() || null;
|
|
131
|
+
if (!mode) mode = cs.getPropertyValue('--md-chart-palette').trim() || null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// No accent set — use the default static palette
|
|
136
|
+
if (!accent) return DEFAULT_PALETTE.slice(0, Math.max(count, 1));
|
|
137
|
+
|
|
138
|
+
return generatePalette(accent, mode || 'monochrome', count);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function paletteColor(data, i, count) {
|
|
142
|
+
var pal = getActivePalette(data, count || 10);
|
|
143
|
+
return pal[i % pal.length];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function loadScript(url, cb) {
|
|
147
|
+
var s = document.createElement('script');
|
|
148
|
+
s.src = url;
|
|
149
|
+
s.onload = cb;
|
|
150
|
+
s.onerror = function () { console.error('SDocs: failed to load ' + url); };
|
|
151
|
+
document.head.appendChild(s);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function ensureChartJs(cb) {
|
|
155
|
+
if (chartJsLoaded) return cb();
|
|
156
|
+
pendingCallbacks.push(cb);
|
|
157
|
+
if (chartJsLoading) return;
|
|
158
|
+
chartJsLoading = true;
|
|
159
|
+
loadScript(CDN_CHART, function () {
|
|
160
|
+
loadScript(CDN_LABELS, function () {
|
|
161
|
+
Chart.register(ChartDataLabels);
|
|
162
|
+
chartJsLoaded = true;
|
|
163
|
+
chartJsLoading = false;
|
|
164
|
+
pendingCallbacks.forEach(function (fn) { fn(); });
|
|
165
|
+
pendingCallbacks = [];
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ── Theme ──
|
|
171
|
+
function isDark() {
|
|
172
|
+
return document.documentElement.dataset.theme === 'dark';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function getDocFont() {
|
|
176
|
+
var rendered = document.getElementById('rendered');
|
|
177
|
+
if (!rendered) return '';
|
|
178
|
+
return getComputedStyle(rendered).getPropertyValue('--md-font-family').trim() || '';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function cssVar(name) {
|
|
182
|
+
var rendered = document.getElementById('rendered');
|
|
183
|
+
if (!rendered) return '';
|
|
184
|
+
return getComputedStyle(rendered).getPropertyValue(name).trim();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function theme() {
|
|
188
|
+
var dark = isDark();
|
|
189
|
+
var chartText = cssVar('--md-chart-text');
|
|
190
|
+
var chartBg = cssVar('--md-chart-bg');
|
|
191
|
+
var textColor = chartText || (dark ? '#A8A29E' : '#78716c');
|
|
192
|
+
var titleColor = chartText || (dark ? '#E7E5E2' : '#1C1917');
|
|
193
|
+
// Grid: semi-transparent version of the text color
|
|
194
|
+
var gridColor;
|
|
195
|
+
if (chartText) {
|
|
196
|
+
gridColor = hexToRgba(chartText, 0.15);
|
|
197
|
+
} else {
|
|
198
|
+
gridColor = dark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.06)';
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
font: getDocFont(),
|
|
202
|
+
text: textColor,
|
|
203
|
+
grid: gridColor,
|
|
204
|
+
title: titleColor,
|
|
205
|
+
tooltipBg: chartBg || (dark ? '#292524' : '#fff'),
|
|
206
|
+
tooltipBorder: chartText ? hexToRgba(chartText, 0.2) : (dark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'),
|
|
207
|
+
annotationColor: chartText ? hexToRgba(chartText, 0.4) : (dark ? 'rgba(255,255,255,0.3)' : 'rgba(0,0,0,0.2)'),
|
|
208
|
+
annotationLabel: titleColor
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ── Parse ──
|
|
213
|
+
function parseChartData(text) {
|
|
214
|
+
try { return JSON.parse(text); } catch (e) { return null; }
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ── Number formatting ──
|
|
218
|
+
function makeTickCallback(fmt, prefix, suffix) {
|
|
219
|
+
if (!fmt && !prefix && !suffix) return null;
|
|
220
|
+
return function (value) {
|
|
221
|
+
var v = value;
|
|
222
|
+
if (fmt === 'currency' || fmt === 'dollar' || fmt === 'usd')
|
|
223
|
+
return (prefix || '$') + v.toLocaleString() + (suffix || '');
|
|
224
|
+
if (fmt === 'euro') return (prefix || '€') + v.toLocaleString() + (suffix || '');
|
|
225
|
+
if (fmt === 'pound') return (prefix || '£') + v.toLocaleString() + (suffix || '');
|
|
226
|
+
if (fmt === 'percent' || fmt === 'percentage')
|
|
227
|
+
return (prefix || '') + v + (suffix || '%');
|
|
228
|
+
if (fmt === 'number' || fmt === 'comma')
|
|
229
|
+
return (prefix || '') + v.toLocaleString() + (suffix || '');
|
|
230
|
+
return (prefix || '') + v + (suffix || '');
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// ── Normalize type aliases ──
|
|
235
|
+
function normalizeType(raw) {
|
|
236
|
+
var t = (raw || 'bar').toLowerCase().replace(/[\s-]/g, '_');
|
|
237
|
+
var map = {
|
|
238
|
+
pie_chart: 'pie', piechart: 'pie',
|
|
239
|
+
bar_chart: 'bar', barchart: 'bar',
|
|
240
|
+
line_chart: 'line', linechart: 'line',
|
|
241
|
+
donut: 'doughnut', donut_chart: 'doughnut', doughnut_chart: 'doughnut',
|
|
242
|
+
horizontal_bar: 'horizontalBar', hbar: 'horizontalBar',
|
|
243
|
+
horizontal_bar_chart: 'horizontalBar', hbarchart: 'horizontalBar',
|
|
244
|
+
stacked_bar: 'stackedBar', stackedbar: 'stackedBar',
|
|
245
|
+
stacked_bar_chart: 'stackedBar', stackedbarchart: 'stackedBar',
|
|
246
|
+
stacked_horizontal_bar: 'stackedHBar', stacked_hbar: 'stackedHBar',
|
|
247
|
+
area: 'area', area_chart: 'area', areachart: 'area',
|
|
248
|
+
stacked_area: 'stackedArea', stackedarea: 'stackedArea',
|
|
249
|
+
stacked_line: 'stackedArea',
|
|
250
|
+
radar_chart: 'radar', radarchart: 'radar', spider: 'radar',
|
|
251
|
+
polararea: 'polarArea', polar_area: 'polarArea', polar: 'polarArea', polar_area_chart: 'polarArea',
|
|
252
|
+
scatter_chart: 'scatter', scatterchart: 'scatter', scatter_plot: 'scatter',
|
|
253
|
+
bubble_chart: 'bubble', bubblechart: 'bubble',
|
|
254
|
+
doughnut: 'doughnut',
|
|
255
|
+
combo: 'mixed', mixed_chart: 'mixed', combination: 'mixed'
|
|
256
|
+
};
|
|
257
|
+
return map[t] || t;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ── Build datasets ──
|
|
261
|
+
function buildDatasets(data, chartType) {
|
|
262
|
+
var isRadial = chartType === 'pie' || chartType === 'doughnut' || chartType === 'polarArea';
|
|
263
|
+
var isLine = chartType === 'line' || chartType === 'area' || chartType === 'stackedArea';
|
|
264
|
+
var isBubble = chartType === 'bubble';
|
|
265
|
+
var isScatter = chartType === 'scatter';
|
|
266
|
+
var isFill = chartType === 'area' || chartType === 'stackedArea';
|
|
267
|
+
var isMixed = chartType === 'mixed';
|
|
268
|
+
|
|
269
|
+
if (isRadial) {
|
|
270
|
+
var values = data.values || (data.datasets && data.datasets[0] && data.datasets[0].values) || [];
|
|
271
|
+
// Single-color pie: auto-generate monochrome shades
|
|
272
|
+
var radialColors;
|
|
273
|
+
if (data.color && !data.colors) {
|
|
274
|
+
radialColors = generatePalette(data.color, 'monochrome', values.length);
|
|
275
|
+
} else {
|
|
276
|
+
radialColors = getActivePalette(data, values.length);
|
|
277
|
+
}
|
|
278
|
+
return [{
|
|
279
|
+
data: values,
|
|
280
|
+
backgroundColor: radialColors,
|
|
281
|
+
borderWidth: isDark() ? 1 : 2,
|
|
282
|
+
borderColor: isDark() ? 'rgba(0,0,0,0.3)' : '#fff'
|
|
283
|
+
}];
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
var dsCount = data.datasets ? data.datasets.length : 1;
|
|
287
|
+
|
|
288
|
+
if (data.values && !data.datasets) {
|
|
289
|
+
// Simple single-dataset
|
|
290
|
+
var c0 = data.color || paletteColor(data, 0, dsCount);
|
|
291
|
+
var ds = {
|
|
292
|
+
label: data.label || '',
|
|
293
|
+
data: data.values,
|
|
294
|
+
backgroundColor: isLine ? undefined : (data.colors || c0),
|
|
295
|
+
borderColor: isLine || isScatter ? c0 : undefined,
|
|
296
|
+
borderWidth: isLine ? 2.5 : 0,
|
|
297
|
+
tension: data.tension != null ? data.tension : 0.35,
|
|
298
|
+
fill: isFill,
|
|
299
|
+
pointRadius: isLine ? 3 : undefined,
|
|
300
|
+
pointHoverRadius: isLine ? 5 : undefined
|
|
301
|
+
};
|
|
302
|
+
if (isFill) {
|
|
303
|
+
ds.backgroundColor = hexToRgba(c0, 0.15);
|
|
304
|
+
ds.borderColor = c0;
|
|
305
|
+
}
|
|
306
|
+
return [ds];
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (data.datasets) {
|
|
310
|
+
return data.datasets.map(function (ds, i) {
|
|
311
|
+
var color = ds.color || paletteColor(data, i, dsCount);
|
|
312
|
+
var dsType = isMixed ? (ds.type || 'bar') : undefined;
|
|
313
|
+
var isLineLike = isLine || dsType === 'line' || chartType === 'radar';
|
|
314
|
+
var result = {
|
|
315
|
+
label: ds.label || '',
|
|
316
|
+
data: ds.values || ds.data || [],
|
|
317
|
+
backgroundColor: isLineLike && isFill ? hexToRgba(color, 0.15) : (isLineLike ? undefined : (ds.colors || color)),
|
|
318
|
+
borderColor: isLineLike || isScatter || isBubble ? color : undefined,
|
|
319
|
+
borderWidth: isLineLike ? 2.5 : 0,
|
|
320
|
+
tension: ds.tension != null ? ds.tension : 0.35,
|
|
321
|
+
fill: ds.fill != null ? ds.fill : isFill,
|
|
322
|
+
pointRadius: isLineLike ? 3 : undefined,
|
|
323
|
+
pointHoverRadius: isLineLike ? 5 : undefined,
|
|
324
|
+
order: ds.order != null ? ds.order : undefined
|
|
325
|
+
};
|
|
326
|
+
if (isMixed && dsType) result.type = dsType;
|
|
327
|
+
if (ds.yAxisID) result.yAxisID = ds.yAxisID;
|
|
328
|
+
if (isBubble && !ds.data) {
|
|
329
|
+
// Convert separate arrays to {x, y, r} format
|
|
330
|
+
if (ds.x && ds.y && ds.r) {
|
|
331
|
+
result.data = ds.x.map(function (xv, j) {
|
|
332
|
+
return { x: xv, y: ds.y[j], r: ds.r[j] || 5 };
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return result;
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ── Hex to rgba ──
|
|
344
|
+
function hexToRgba(hex, alpha) {
|
|
345
|
+
if (!hex || hex.charAt(0) !== '#') return hex;
|
|
346
|
+
var r = parseInt(hex.slice(1, 3), 16);
|
|
347
|
+
var g = parseInt(hex.slice(3, 5), 16);
|
|
348
|
+
var b = parseInt(hex.slice(5, 7), 16);
|
|
349
|
+
return 'rgba(' + r + ',' + g + ',' + b + ',' + alpha + ')';
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ── Build scales ──
|
|
353
|
+
function buildScales(data, chartType, th) {
|
|
354
|
+
var noScales = chartType === 'pie' || chartType === 'doughnut' ||
|
|
355
|
+
chartType === 'polarArea' || chartType === 'radar';
|
|
356
|
+
if (noScales) return undefined;
|
|
357
|
+
|
|
358
|
+
var isHorizontal = chartType === 'horizontalBar' || chartType === 'stackedHBar';
|
|
359
|
+
var isStacked = chartType === 'stackedBar' || chartType === 'stackedHBar' ||
|
|
360
|
+
chartType === 'stackedArea' || data.stacked;
|
|
361
|
+
|
|
362
|
+
var tickCb = makeTickCallback(data.format, data.prefix, data.suffix);
|
|
363
|
+
|
|
364
|
+
var xScale = {
|
|
365
|
+
title: {
|
|
366
|
+
display: !!(data.xAxis || data.xLabel),
|
|
367
|
+
text: data.xAxis || data.xLabel || '',
|
|
368
|
+
color: th.text
|
|
369
|
+
},
|
|
370
|
+
ticks: { color: th.text },
|
|
371
|
+
grid: { color: th.grid },
|
|
372
|
+
stacked: isStacked || undefined
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
var yScale = {
|
|
376
|
+
title: {
|
|
377
|
+
display: !!(data.yAxis || data.yLabel),
|
|
378
|
+
text: data.yAxis || data.yLabel || '',
|
|
379
|
+
color: th.text
|
|
380
|
+
},
|
|
381
|
+
ticks: { color: th.text },
|
|
382
|
+
grid: { color: th.grid },
|
|
383
|
+
beginAtZero: data.beginAtZero !== false,
|
|
384
|
+
stacked: isStacked || undefined
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
// Axis-specific options
|
|
388
|
+
var valueAxis = isHorizontal ? xScale : yScale;
|
|
389
|
+
if (tickCb) valueAxis.ticks.callback = tickCb;
|
|
390
|
+
if (data.min != null) valueAxis.min = data.min;
|
|
391
|
+
if (data.max != null) valueAxis.max = data.max;
|
|
392
|
+
if (data.stepSize != null) valueAxis.ticks.stepSize = data.stepSize;
|
|
393
|
+
|
|
394
|
+
var scales = { x: xScale, y: yScale };
|
|
395
|
+
|
|
396
|
+
// Dual y-axis
|
|
397
|
+
if (data.y2Axis || data.y2Label || data.dualAxis) {
|
|
398
|
+
scales.y2 = {
|
|
399
|
+
position: 'right',
|
|
400
|
+
title: {
|
|
401
|
+
display: !!(data.y2Axis || data.y2Label),
|
|
402
|
+
text: data.y2Axis || data.y2Label || '',
|
|
403
|
+
color: th.text
|
|
404
|
+
},
|
|
405
|
+
ticks: { color: th.text },
|
|
406
|
+
grid: { drawOnChartArea: false },
|
|
407
|
+
beginAtZero: data.beginAtZero !== false
|
|
408
|
+
};
|
|
409
|
+
var tickCb2 = makeTickCallback(data.y2Format, data.y2Prefix, data.y2Suffix);
|
|
410
|
+
if (tickCb2) scales.y2.ticks.callback = tickCb2;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return scales;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// ── Build annotation plugin config ──
|
|
417
|
+
function buildAnnotations(data, th) {
|
|
418
|
+
if (!data.annotations || !data.annotations.length) return undefined;
|
|
419
|
+
var annots = {};
|
|
420
|
+
data.annotations.forEach(function (a, i) {
|
|
421
|
+
var isHorizontal = a.axis === 'y' || a.type === 'horizontal' || a.y != null;
|
|
422
|
+
annots['ann' + i] = {
|
|
423
|
+
type: 'line',
|
|
424
|
+
scaleID: isHorizontal ? 'y' : 'x',
|
|
425
|
+
value: isHorizontal ? (a.y || a.value) : (a.x || a.value),
|
|
426
|
+
borderColor: a.color || th.annotationColor,
|
|
427
|
+
borderWidth: a.width || 2,
|
|
428
|
+
borderDash: a.dashed !== false ? [6, 4] : [],
|
|
429
|
+
label: a.label ? {
|
|
430
|
+
display: true,
|
|
431
|
+
content: a.label,
|
|
432
|
+
position: a.position || 'end',
|
|
433
|
+
backgroundColor: 'transparent',
|
|
434
|
+
color: a.labelColor || th.annotationLabel,
|
|
435
|
+
font: { size: 12, weight: '500' }
|
|
436
|
+
} : undefined
|
|
437
|
+
};
|
|
438
|
+
});
|
|
439
|
+
return { annotations: annots };
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ── Build datalabels plugin config ──
|
|
443
|
+
function buildDatalabels(data, chartType, th) {
|
|
444
|
+
if (data.dataLabels === false) return { display: false };
|
|
445
|
+
|
|
446
|
+
var isRadial = chartType === 'pie' || chartType === 'doughnut' || chartType === 'polarArea';
|
|
447
|
+
var isScatterLike = chartType === 'scatter' || chartType === 'bubble';
|
|
448
|
+
var isRadar = chartType === 'radar';
|
|
449
|
+
|
|
450
|
+
// Hide labels on scatter/bubble — too cluttered
|
|
451
|
+
if (isScatterLike) return { display: false };
|
|
452
|
+
|
|
453
|
+
if (isRadial) {
|
|
454
|
+
return {
|
|
455
|
+
display: true,
|
|
456
|
+
color: '#fff',
|
|
457
|
+
font: { weight: '600', size: 12 },
|
|
458
|
+
textShadowColor: 'rgba(0,0,0,0.3)',
|
|
459
|
+
textShadowBlur: 4,
|
|
460
|
+
formatter: function (value, ctx) {
|
|
461
|
+
var total = ctx.dataset.data.reduce(function (a, b) { return a + b; }, 0);
|
|
462
|
+
var pct = Math.round(value / total * 100);
|
|
463
|
+
if (pct < 5) return ''; // hide tiny slices
|
|
464
|
+
return pct + '%';
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (isRadar) {
|
|
470
|
+
return {
|
|
471
|
+
display: true,
|
|
472
|
+
color: th.text,
|
|
473
|
+
font: { size: 10 },
|
|
474
|
+
align: 'end',
|
|
475
|
+
offset: 4,
|
|
476
|
+
formatter: function (value) { return value; }
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Bar, line, area — show values
|
|
481
|
+
var tickCb = makeTickCallback(data.format, data.prefix, data.suffix);
|
|
482
|
+
return {
|
|
483
|
+
display: true,
|
|
484
|
+
color: th.text,
|
|
485
|
+
font: { size: 11, weight: '500' },
|
|
486
|
+
anchor: 'end',
|
|
487
|
+
align: 'end',
|
|
488
|
+
offset: 2,
|
|
489
|
+
clip: false,
|
|
490
|
+
formatter: function (value) {
|
|
491
|
+
if (tickCb) return tickCb(value);
|
|
492
|
+
return value;
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// ── Build Chart.js config ──
|
|
498
|
+
function buildConfig(data) {
|
|
499
|
+
var rawType = normalizeType(data.type);
|
|
500
|
+
var th = theme();
|
|
501
|
+
|
|
502
|
+
// Map our types to Chart.js types + options
|
|
503
|
+
var chartJsType = rawType;
|
|
504
|
+
var isHorizontal = false;
|
|
505
|
+
if (rawType === 'horizontalBar' || rawType === 'stackedHBar') {
|
|
506
|
+
chartJsType = 'bar';
|
|
507
|
+
isHorizontal = true;
|
|
508
|
+
} else if (rawType === 'stackedBar') {
|
|
509
|
+
chartJsType = 'bar';
|
|
510
|
+
} else if (rawType === 'area' || rawType === 'stackedArea') {
|
|
511
|
+
chartJsType = 'line';
|
|
512
|
+
} else if (rawType === 'mixed') {
|
|
513
|
+
chartJsType = 'bar'; // base type for mixed, datasets override individually
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
var datasets = buildDatasets(data, rawType);
|
|
517
|
+
if (!datasets) return null;
|
|
518
|
+
|
|
519
|
+
var isRadial = rawType === 'pie' || rawType === 'doughnut' || rawType === 'polarArea';
|
|
520
|
+
var isRadar = rawType === 'radar';
|
|
521
|
+
var showLegend = data.legend !== false && (isRadial || datasets.length > 1 || rawType === 'mixed');
|
|
522
|
+
|
|
523
|
+
// Legend position
|
|
524
|
+
var legendPos = data.legendPosition || 'bottom';
|
|
525
|
+
|
|
526
|
+
var config = {
|
|
527
|
+
type: chartJsType,
|
|
528
|
+
data: { labels: data.labels || [], datasets: datasets },
|
|
529
|
+
options: {
|
|
530
|
+
responsive: true,
|
|
531
|
+
maintainAspectRatio: true,
|
|
532
|
+
animation: false,
|
|
533
|
+
layout: { padding: { top: 20 } },
|
|
534
|
+
font: th.font ? { family: th.font } : undefined,
|
|
535
|
+
aspectRatio: data.aspectRatio || undefined,
|
|
536
|
+
indexAxis: isHorizontal ? 'y' : undefined,
|
|
537
|
+
plugins: {
|
|
538
|
+
title: {
|
|
539
|
+
display: !!data.title,
|
|
540
|
+
text: data.title || '',
|
|
541
|
+
color: th.title,
|
|
542
|
+
font: { size: 15, weight: '600', family: th.font || undefined },
|
|
543
|
+
padding: { bottom: data.subtitle ? 2 : 23 }
|
|
544
|
+
},
|
|
545
|
+
subtitle: {
|
|
546
|
+
display: !!data.subtitle,
|
|
547
|
+
text: data.subtitle || '',
|
|
548
|
+
color: th.text,
|
|
549
|
+
font: { size: 12, weight: '400' },
|
|
550
|
+
padding: { bottom: 24 }
|
|
551
|
+
},
|
|
552
|
+
legend: {
|
|
553
|
+
display: showLegend,
|
|
554
|
+
position: legendPos,
|
|
555
|
+
labels: { color: th.text, usePointStyle: true, padding: 16 }
|
|
556
|
+
},
|
|
557
|
+
tooltip: {
|
|
558
|
+
backgroundColor: th.tooltipBg,
|
|
559
|
+
titleColor: th.title,
|
|
560
|
+
bodyColor: th.text,
|
|
561
|
+
borderColor: th.tooltipBorder,
|
|
562
|
+
borderWidth: 1,
|
|
563
|
+
cornerRadius: 6,
|
|
564
|
+
padding: 10
|
|
565
|
+
},
|
|
566
|
+
datalabels: buildDatalabels(data, rawType, th)
|
|
567
|
+
},
|
|
568
|
+
scales: buildScales(data, rawType, th)
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
// Radar scale styling
|
|
573
|
+
if (isRadar) {
|
|
574
|
+
config.options.scales = {
|
|
575
|
+
r: {
|
|
576
|
+
ticks: { color: th.text, backdropColor: 'transparent' },
|
|
577
|
+
grid: { color: th.grid },
|
|
578
|
+
pointLabels: { color: th.text, font: { size: 12 } },
|
|
579
|
+
beginAtZero: data.beginAtZero !== false
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Annotations (requires annotation plugin — use inline plugin)
|
|
585
|
+
var annots = buildAnnotations(data, th);
|
|
586
|
+
if (annots) {
|
|
587
|
+
config.options.plugins.annotation = annots;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
return config;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// ── Destroy all active charts (called before re-render) ──
|
|
594
|
+
function destroyAll() {
|
|
595
|
+
activeCharts.forEach(function (c) { c.destroy(); });
|
|
596
|
+
activeCharts = [];
|
|
597
|
+
chartDataStore = [];
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// ── Process rendered HTML: find chart code blocks, replace with canvases ──
|
|
601
|
+
function processCharts(container) {
|
|
602
|
+
var chartBlocks = container.querySelectorAll('code.language-chart');
|
|
603
|
+
if (!chartBlocks.length) return;
|
|
604
|
+
|
|
605
|
+
ensureChartJs(function () {
|
|
606
|
+
chartBlocks.forEach(function (codeEl) {
|
|
607
|
+
var pre = codeEl.closest('pre');
|
|
608
|
+
if (!pre) return;
|
|
609
|
+
|
|
610
|
+
var data = parseChartData(codeEl.textContent);
|
|
611
|
+
if (!data) {
|
|
612
|
+
pre.classList.add('sdoc-chart-error');
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
var config = buildConfig(data);
|
|
617
|
+
if (!config) return;
|
|
618
|
+
|
|
619
|
+
var wrapper = document.createElement('div');
|
|
620
|
+
wrapper.className = 'sdoc-chart';
|
|
621
|
+
var canvas = document.createElement('canvas');
|
|
622
|
+
wrapper.appendChild(canvas);
|
|
623
|
+
|
|
624
|
+
var preWrapper = pre.closest('.pre-wrapper');
|
|
625
|
+
var target = preWrapper || pre;
|
|
626
|
+
target.parentNode.replaceChild(wrapper, target);
|
|
627
|
+
|
|
628
|
+
var chart = new Chart(canvas, config);
|
|
629
|
+
activeCharts.push(chart);
|
|
630
|
+
});
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// ── Re-render charts when palette controls change ──
|
|
635
|
+
// Store chart data alongside instances so we can rebuild with new colors
|
|
636
|
+
var chartDataStore = [];
|
|
637
|
+
|
|
638
|
+
var _origProcess = processCharts;
|
|
639
|
+
processCharts = function (container) {
|
|
640
|
+
chartDataStore = [];
|
|
641
|
+
var chartBlocks = container.querySelectorAll('code.language-chart');
|
|
642
|
+
if (!chartBlocks.length) return;
|
|
643
|
+
|
|
644
|
+
ensureChartJs(function () {
|
|
645
|
+
chartBlocks.forEach(function (codeEl) {
|
|
646
|
+
var pre = codeEl.closest('pre');
|
|
647
|
+
if (!pre) return;
|
|
648
|
+
var data = parseChartData(codeEl.textContent);
|
|
649
|
+
if (!data) { pre.classList.add('sdoc-chart-error'); return; }
|
|
650
|
+
var config = buildConfig(data);
|
|
651
|
+
if (!config) return;
|
|
652
|
+
|
|
653
|
+
var chartIndex = chartDataStore.length;
|
|
654
|
+
var wrapper = document.createElement('div');
|
|
655
|
+
wrapper.className = 'sdoc-chart';
|
|
656
|
+
wrapper.setAttribute('data-chart-index', chartIndex);
|
|
657
|
+
var canvas = document.createElement('canvas');
|
|
658
|
+
wrapper.appendChild(canvas);
|
|
659
|
+
wrapper.appendChild(buildChartMenu(data, chartIndex));
|
|
660
|
+
|
|
661
|
+
var preWrapper = pre.closest('.pre-wrapper');
|
|
662
|
+
var target = preWrapper || pre;
|
|
663
|
+
target.parentNode.replaceChild(wrapper, target);
|
|
664
|
+
|
|
665
|
+
var chart = new Chart(canvas, config);
|
|
666
|
+
activeCharts.push(chart);
|
|
667
|
+
chartDataStore.push({ chart: chart, data: data, canvas: canvas, wrapper: wrapper });
|
|
668
|
+
});
|
|
669
|
+
});
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
// ── Type families for type switching ──
|
|
673
|
+
var TYPE_FAMILIES = {
|
|
674
|
+
bar: [['bar', 'Bar'], ['horizontal_bar', 'Horizontal'], ['stacked_bar', 'Stacked']],
|
|
675
|
+
horizontalBar: [['bar', 'Bar'], ['horizontal_bar', 'Horizontal'], ['stacked_bar', 'Stacked']],
|
|
676
|
+
stackedBar: [['bar', 'Bar'], ['horizontal_bar', 'Horizontal'], ['stacked_bar', 'Stacked']],
|
|
677
|
+
line: [['line', 'Line'], ['area', 'Area'], ['stacked_area', 'Stacked']],
|
|
678
|
+
area: [['line', 'Line'], ['area', 'Area'], ['stacked_area', 'Stacked']],
|
|
679
|
+
stackedArea: [['line', 'Line'], ['area', 'Area'], ['stacked_area', 'Stacked']],
|
|
680
|
+
pie: [['pie', 'Pie'], ['doughnut', 'Doughnut'], ['polarArea', 'Polar']],
|
|
681
|
+
doughnut: [['pie', 'Pie'], ['doughnut', 'Doughnut'], ['polarArea', 'Polar']],
|
|
682
|
+
polarArea: [['pie', 'Pie'], ['doughnut', 'Doughnut'], ['polarArea', 'Polar']],
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
function _el(tag, cls, text) {
|
|
686
|
+
var e = document.createElement(tag);
|
|
687
|
+
if (cls) e.className = cls;
|
|
688
|
+
if (text) e.textContent = text;
|
|
689
|
+
return e;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function buildChartMenu(data, chartIndex) {
|
|
693
|
+
var frag = document.createDocumentFragment();
|
|
694
|
+
var btn = _el('button', 'chart-menu-btn');
|
|
695
|
+
btn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" stroke="none"><circle cx="12" cy="5" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="12" cy="19" r="2"/></svg>';
|
|
696
|
+
btn.title = 'Chart options';
|
|
697
|
+
btn.setAttribute('data-chart-index', chartIndex);
|
|
698
|
+
frag.appendChild(btn);
|
|
699
|
+
|
|
700
|
+
var menu = _el('div', 'chart-menu');
|
|
701
|
+
menu.setAttribute('data-chart-index', chartIndex);
|
|
702
|
+
|
|
703
|
+
var copyBtn = _el('button', 'chart-menu-item', 'Copy as image');
|
|
704
|
+
copyBtn.setAttribute('data-action', 'copy-png');
|
|
705
|
+
menu.appendChild(copyBtn);
|
|
706
|
+
var dlBtn = _el('button', 'chart-menu-item', 'Download as PNG');
|
|
707
|
+
dlBtn.setAttribute('data-action', 'download-png');
|
|
708
|
+
menu.appendChild(dlBtn);
|
|
709
|
+
menu.appendChild(_el('div', 'chart-menu-sep'));
|
|
710
|
+
|
|
711
|
+
var rawType = normalizeType(data.type);
|
|
712
|
+
var isRadial = rawType === 'pie' || rawType === 'doughnut' || rawType === 'polarArea';
|
|
713
|
+
|
|
714
|
+
if (!isRadial) {
|
|
715
|
+
var lbl = document.createElement('label');
|
|
716
|
+
lbl.className = 'chart-menu-toggle';
|
|
717
|
+
var cb1 = document.createElement('input');
|
|
718
|
+
cb1.type = 'checkbox'; cb1.setAttribute('data-field', 'dataLabels'); cb1.checked = data.dataLabels !== false;
|
|
719
|
+
lbl.appendChild(cb1); lbl.appendChild(document.createTextNode(' Data labels'));
|
|
720
|
+
menu.appendChild(lbl);
|
|
721
|
+
}
|
|
722
|
+
var lbl2 = document.createElement('label');
|
|
723
|
+
lbl2.className = 'chart-menu-toggle';
|
|
724
|
+
var cb2 = document.createElement('input');
|
|
725
|
+
cb2.type = 'checkbox'; cb2.setAttribute('data-field', 'legend'); cb2.checked = data.legend !== false;
|
|
726
|
+
lbl2.appendChild(cb2); lbl2.appendChild(document.createTextNode(' Legend'));
|
|
727
|
+
menu.appendChild(lbl2);
|
|
728
|
+
menu.appendChild(_el('div', 'chart-menu-sep'));
|
|
729
|
+
|
|
730
|
+
var family = TYPE_FAMILIES[rawType];
|
|
731
|
+
if (family) {
|
|
732
|
+
var tg = _el('div', 'chart-menu-types');
|
|
733
|
+
family.forEach(function (pair) {
|
|
734
|
+
var tb = _el('button', 'chart-type-btn', pair[1]);
|
|
735
|
+
tb.setAttribute('data-type', pair[0]);
|
|
736
|
+
if (normalizeType(pair[0]) === rawType) tb.classList.add('active');
|
|
737
|
+
tg.appendChild(tb);
|
|
738
|
+
});
|
|
739
|
+
menu.appendChild(tg);
|
|
740
|
+
menu.appendChild(_el('div', 'chart-menu-sep'));
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
var ti = document.createElement('input');
|
|
744
|
+
ti.className = 'chart-menu-input'; ti.setAttribute('data-field', 'title');
|
|
745
|
+
ti.placeholder = 'Title'; ti.value = data.title || '';
|
|
746
|
+
menu.appendChild(ti);
|
|
747
|
+
var si = document.createElement('input');
|
|
748
|
+
si.className = 'chart-menu-input'; si.setAttribute('data-field', 'subtitle');
|
|
749
|
+
si.placeholder = 'Subtitle'; si.value = data.subtitle || '';
|
|
750
|
+
menu.appendChild(si);
|
|
751
|
+
|
|
752
|
+
frag.appendChild(menu);
|
|
753
|
+
return frag;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// ── Replace the Nth ```chart block in markdown ──
|
|
757
|
+
function replaceChartBlock(body, index, newJson) {
|
|
758
|
+
var count = -1;
|
|
759
|
+
return body.replace(/```chart\n([\s\S]*?)```/g, function (match) {
|
|
760
|
+
count++;
|
|
761
|
+
if (count === index) return '```chart\n' + newJson + '\n```';
|
|
762
|
+
return match;
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function rebuildChart(index) {
|
|
767
|
+
var entry = chartDataStore[index];
|
|
768
|
+
if (!entry) return;
|
|
769
|
+
entry.chart.destroy();
|
|
770
|
+
var config = buildConfig(entry.data);
|
|
771
|
+
entry.chart = new Chart(entry.canvas, config);
|
|
772
|
+
activeCharts = chartDataStore.map(function (e) { return e.chart; });
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
function persistChartChange(index) {
|
|
776
|
+
var entry = chartDataStore[index];
|
|
777
|
+
if (!entry) return;
|
|
778
|
+
var json = JSON.stringify(entry.data, null, 2);
|
|
779
|
+
S.currentBody = replaceChartBlock(S.currentBody, index, json);
|
|
780
|
+
S.currentMeta = Object.assign({}, S.currentMeta, { styles: S.collectStyles() });
|
|
781
|
+
S.rawEl.value = window.SDocYaml.serializeFrontMatter(S.currentMeta) + '\n' + S.currentBody;
|
|
782
|
+
S._isDefaultState = false;
|
|
783
|
+
S.syncAll('load');
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// ── Chart menu event delegation ──
|
|
787
|
+
document.addEventListener('click', function (e) {
|
|
788
|
+
var menuBtn = e.target.closest('.chart-menu-btn');
|
|
789
|
+
if (menuBtn) {
|
|
790
|
+
e.stopPropagation();
|
|
791
|
+
var menu = menuBtn.parentElement.querySelector('.chart-menu');
|
|
792
|
+
var isOpen = menu.classList.contains('open');
|
|
793
|
+
document.querySelectorAll('.chart-menu.open').forEach(function (m) { m.classList.remove('open'); });
|
|
794
|
+
if (!isOpen) menu.classList.add('open');
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
var item = e.target.closest('.chart-menu-item');
|
|
798
|
+
if (item) {
|
|
799
|
+
e.stopPropagation();
|
|
800
|
+
var action = item.getAttribute('data-action');
|
|
801
|
+
var idx = parseInt(item.closest('.chart-menu').getAttribute('data-chart-index'));
|
|
802
|
+
var entry = chartDataStore[idx];
|
|
803
|
+
if (!entry) return;
|
|
804
|
+
if (action === 'copy-png') {
|
|
805
|
+
entry.canvas.toBlob(function (blob) {
|
|
806
|
+
navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]).then(function () {
|
|
807
|
+
item.textContent = 'Copied!';
|
|
808
|
+
setTimeout(function () { item.textContent = 'Copy as image'; }, 1500);
|
|
809
|
+
});
|
|
810
|
+
});
|
|
811
|
+
} else if (action === 'download-png') {
|
|
812
|
+
var link = document.createElement('a');
|
|
813
|
+
link.download = (entry.data.title || 'chart').replace(/[^a-zA-Z0-9]/g, '_') + '.png';
|
|
814
|
+
link.href = entry.canvas.toDataURL('image/png');
|
|
815
|
+
link.click();
|
|
816
|
+
}
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
var typeBtn = e.target.closest('.chart-type-btn');
|
|
820
|
+
if (typeBtn) {
|
|
821
|
+
e.stopPropagation();
|
|
822
|
+
var idx = parseInt(typeBtn.closest('.chart-menu').getAttribute('data-chart-index'));
|
|
823
|
+
var entry = chartDataStore[idx];
|
|
824
|
+
if (!entry) return;
|
|
825
|
+
entry.data.type = typeBtn.getAttribute('data-type');
|
|
826
|
+
rebuildChart(idx);
|
|
827
|
+
persistChartChange(idx);
|
|
828
|
+
typeBtn.closest('.chart-menu-types').querySelectorAll('.chart-type-btn').forEach(function (b) {
|
|
829
|
+
b.classList.toggle('active', b.getAttribute('data-type') === entry.data.type);
|
|
830
|
+
});
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
if (!e.target.closest('.chart-menu')) {
|
|
834
|
+
document.querySelectorAll('.chart-menu.open').forEach(function (m) { m.classList.remove('open'); });
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
document.addEventListener('change', function (e) {
|
|
839
|
+
if (e.target.type === 'checkbox' && e.target.closest('.chart-menu-toggle')) {
|
|
840
|
+
var field = e.target.getAttribute('data-field');
|
|
841
|
+
var idx = parseInt(e.target.closest('.chart-menu').getAttribute('data-chart-index'));
|
|
842
|
+
var entry = chartDataStore[idx];
|
|
843
|
+
if (!entry) return;
|
|
844
|
+
if (e.target.checked) delete entry.data[field];
|
|
845
|
+
else entry.data[field] = false;
|
|
846
|
+
rebuildChart(idx);
|
|
847
|
+
persistChartChange(idx);
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
if (e.target.classList && e.target.classList.contains('chart-menu-input')) {
|
|
851
|
+
var field = e.target.getAttribute('data-field');
|
|
852
|
+
var idx = parseInt(e.target.closest('.chart-menu').getAttribute('data-chart-index'));
|
|
853
|
+
var entry = chartDataStore[idx];
|
|
854
|
+
if (!entry) return;
|
|
855
|
+
if (e.target.value.trim()) entry.data[field] = e.target.value.trim();
|
|
856
|
+
else delete entry.data[field];
|
|
857
|
+
rebuildChart(idx);
|
|
858
|
+
persistChartChange(idx);
|
|
859
|
+
}
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
function refreshChartColors() {
|
|
863
|
+
chartDataStore.forEach(function (entry) {
|
|
864
|
+
entry.chart.destroy();
|
|
865
|
+
var config = buildConfig(entry.data);
|
|
866
|
+
entry.chart = new Chart(entry.canvas, config);
|
|
867
|
+
});
|
|
868
|
+
// Update activeCharts
|
|
869
|
+
activeCharts = chartDataStore.map(function (e) { return e.chart; });
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
['ctrl-chart-accent', 'ctrl-chart-palette'].forEach(function (id) {
|
|
873
|
+
var el = document.getElementById(id);
|
|
874
|
+
if (el) {
|
|
875
|
+
el.addEventListener('input', refreshChartColors);
|
|
876
|
+
el.addEventListener('change', refreshChartColors);
|
|
877
|
+
}
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
// ── Public API ──
|
|
881
|
+
S.destroyCharts = destroyAll;
|
|
882
|
+
S.processCharts = processCharts;
|
|
883
|
+
S.refreshChartColors = refreshChartColors;
|
|
884
|
+
S.replaceChartBlock = replaceChartBlock;
|
|
885
|
+
S.getChartImages = function () {
|
|
886
|
+
return chartDataStore.map(function (entry) {
|
|
887
|
+
var chart = entry.chart;
|
|
888
|
+
var prevDpr = chart.options.devicePixelRatio;
|
|
889
|
+
var dataUrl;
|
|
890
|
+
try {
|
|
891
|
+
// Temporarily boost devicePixelRatio for crisper PDF export
|
|
892
|
+
chart.options.devicePixelRatio = (window.devicePixelRatio || 1) * 2.5;
|
|
893
|
+
chart.resize();
|
|
894
|
+
dataUrl = chart.toBase64Image('image/png', 1);
|
|
895
|
+
} catch (e) {
|
|
896
|
+
dataUrl = chart.toBase64Image();
|
|
897
|
+
} finally {
|
|
898
|
+
// Restore
|
|
899
|
+
chart.options.devicePixelRatio = prevDpr;
|
|
900
|
+
chart.resize();
|
|
901
|
+
}
|
|
902
|
+
return { wrapper: entry.wrapper, dataUrl: dataUrl };
|
|
903
|
+
});
|
|
904
|
+
};
|
|
905
|
+
})();
|