sdocs-dev 1.3.0 → 1.4.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.
@@ -1,905 +0,0 @@
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
- })();