stella-coder 5.2.0 → 5.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.
@@ -0,0 +1,411 @@
1
+ import fs from "node:fs"
2
+ import path from "node:path"
3
+ import os from "node:os"
4
+ import { execSync } from "node:child_process"
5
+
6
+ const CHARTS_DIR = path.join(os.homedir(), ".stella", "charts")
7
+
8
+ function ensureDir() {
9
+ if (!fs.existsSync(CHARTS_DIR)) fs.mkdirSync(CHARTS_DIR, { recursive: true })
10
+ }
11
+
12
+ function openFile(filePath) {
13
+ try {
14
+ if (process.platform === "win32") {
15
+ execSync(`start "" "${filePath}"`, { shell: "cmd.exe", stdio: "ignore" })
16
+ } else if (process.platform === "darwin") {
17
+ execSync(`open "${filePath}"`, { stdio: "ignore" })
18
+ } else {
19
+ execSync(`xdg-open "${filePath}"`, { stdio: "ignore" })
20
+ }
21
+ } catch {}
22
+ }
23
+
24
+ function generateChartHTML(config) {
25
+ const {
26
+ type = "bar",
27
+ title = "Chart",
28
+ labels = [],
29
+ datasets = [],
30
+ width = 800,
31
+ height = 500,
32
+ xAxisLabel = "",
33
+ yAxisLabel = "",
34
+ backgroundColor = [],
35
+ borderColor = [],
36
+ showLegend = true,
37
+ showGrid = true,
38
+ animationDuration = 1000,
39
+ fontSize = 14,
40
+ darkMode = false,
41
+ } = config
42
+
43
+ const bg = darkMode ? "#1e1e2e" : "#ffffff"
44
+ const fg = darkMode ? "#cdd6f4" : "#333333"
45
+ const gridColor = darkMode ? "#45475a" : "#e0e0e0"
46
+
47
+ const chartDatasets = datasets.map((ds, i) => {
48
+ const dsConfig = {
49
+ label: ds.label || `Series ${i + 1}`,
50
+ data: ds.data || [],
51
+ }
52
+ if (type === "line" || type === "radar") {
53
+ dsConfig.borderColor = borderColor[i] || ds.borderColor || getColor(i)
54
+ dsConfig.backgroundColor = backgroundColor[i] || ds.backgroundColor || getColorAlpha(i, 0.2)
55
+ dsConfig.borderWidth = ds.borderWidth || 2
56
+ dsConfig.pointRadius = ds.pointRadius ?? 4
57
+ dsConfig.tension = ds.tension ?? 0.3
58
+ dsConfig.fill = ds.fill ?? false
59
+ } else if (type === "pie" || type === "doughnut") {
60
+ dsConfig.backgroundColor = (ds.data || []).map((_, j) => backgroundColor[j] || getColor(j))
61
+ dsConfig.borderColor = bg
62
+ dsConfig.borderWidth = 2
63
+ } else {
64
+ dsConfig.backgroundColor = (ds.data || []).map((_, j) => backgroundColor[j] || getColorAlpha(j, 0.7))
65
+ dsConfig.borderColor = (ds.data || []).map((_, j) => borderColor[j] || getColor(j))
66
+ dsConfig.borderWidth = ds.borderWidth ?? 1
67
+ dsConfig.borderRadius = ds.borderRadius ?? 4
68
+ }
69
+ return dsConfig
70
+ })
71
+
72
+ const chartConfig = {
73
+ type: type === "horizontalBar" ? "bar" : type,
74
+ data: {
75
+ labels,
76
+ datasets: chartDatasets,
77
+ },
78
+ options: {
79
+ responsive: true,
80
+ maintainAspectRatio: false,
81
+ animation: { duration: animationDuration },
82
+ plugins: {
83
+ title: {
84
+ display: !!title,
85
+ text: title,
86
+ color: fg,
87
+ font: { size: fontSize + 4, weight: "bold" },
88
+ },
89
+ legend: {
90
+ display: showLegend,
91
+ labels: { color: fg, font: { size: fontSize } },
92
+ },
93
+ },
94
+ scales: {},
95
+ },
96
+ }
97
+
98
+ if (!["pie", "doughnut", "radar", "polarArea"].includes(type)) {
99
+ chartConfig.options.indexAxis = type === "horizontalBar" ? "y" : "x"
100
+ chartConfig.options.scales = {
101
+ x: {
102
+ display: true,
103
+ title: { display: !!xAxisLabel, text: xAxisLabel, color: fg, font: { size: fontSize } },
104
+ ticks: { color: fg, font: { size: fontSize - 2 } },
105
+ grid: { display: showGrid, color: gridColor },
106
+ },
107
+ y: {
108
+ display: true,
109
+ title: { display: !!yAxisLabel, text: yAxisLabel, color: fg, font: { size: fontSize } },
110
+ ticks: { color: fg, font: { size: fontSize - 2 } },
111
+ grid: { display: showGrid, color: gridColor },
112
+ beginAtZero: true,
113
+ },
114
+ }
115
+ }
116
+
117
+ if (type === "radar") {
118
+ chartConfig.options.scales = {
119
+ r: {
120
+ angleLines: { color: gridColor },
121
+ grid: { color: gridColor },
122
+ pointLabels: { color: fg, font: { size: fontSize } },
123
+ ticks: { color: fg, backdropColor: "transparent" },
124
+ },
125
+ }
126
+ }
127
+
128
+ return `<!DOCTYPE html>
129
+ <html lang="en">
130
+ <head>
131
+ <meta charset="UTF-8">
132
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
133
+ <title>${title}</title>
134
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
135
+ <style>
136
+ * { margin: 0; padding: 0; box-sizing: border-box; }
137
+ body { background: ${bg}; color: ${fg}; font-family: 'Segoe UI', system-ui, sans-serif; display: flex; flex-direction: column; align-items: center; padding: 24px; min-height: 100vh; }
138
+ h1 { margin-bottom: 8px; font-size: 20px; }
139
+ .meta { color: ${gridColor}; font-size: 12px; margin-bottom: 16px; }
140
+ .chart-container { width: ${width}px; height: ${height}px; background: ${bg}; border-radius: 12px; padding: 16px; box-shadow: 0 2px 12px rgba(0,0,0,0.1); }
141
+ canvas { max-width: 100%; }
142
+ .export-btns { margin-top: 16px; display: flex; gap: 8px; }
143
+ .export-btns button { padding: 8px 16px; border: 1px solid ${gridColor}; border-radius: 6px; background: ${bg}; color: ${fg}; cursor: pointer; font-size: 13px; }
144
+ .export-btns button:hover { background: ${gridColor}; }
145
+ </style>
146
+ </head>
147
+ <body>
148
+ <h1>${title}</h1>
149
+ <div class="meta">Stella Coder Charts • ${new Date().toLocaleString()}</div>
150
+ <div class="chart-container">
151
+ <canvas id="chart"></canvas>
152
+ </div>
153
+ <div class="export-btns">
154
+ <button onclick="downloadPNG()">PNG</button>
155
+ <button onclick="downloadSVG()">SVG</button>
156
+ <button onclick="downloadCSV()">CSV</button>
157
+ </div>
158
+ <script>
159
+ const config = ${JSON.stringify(chartConfig, null, 2)};
160
+ const ctx = document.getElementById('chart').getContext('2d');
161
+ const chart = new Chart(ctx, config);
162
+
163
+ function downloadPNG() {
164
+ const link = document.createElement('a');
165
+ link.download = '${title.replace(/[^a-zA-Z0-9]/g, "_")}.png';
166
+ link.href = chart.toBase64Image('image/png', 1);
167
+ link.click();
168
+ }
169
+
170
+ function downloadSVG() {
171
+ const canvas = document.getElementById('chart');
172
+ const svgNS = 'http://www.w3.org/2000/svg';
173
+ const svg = document.createElementNS(svgNS, 'svg');
174
+ svg.setAttribute('width', canvas.width);
175
+ svg.setAttribute('height', canvas.height);
176
+ const image = document.createElementNS(svgNS, 'image');
177
+ image.setAttribute('href', canvas.toDataURL('image/png'));
178
+ image.setAttribute('width', canvas.width);
179
+ image.setAttribute('height', canvas.height);
180
+ svg.appendChild(image);
181
+ const blob = new Blob([svg.outerHTML], { type: 'image/svg+xml' });
182
+ const link = document.createElement('a');
183
+ link.download = '${title.replace(/[^a-zA-Z0-9]/g, "_")}.svg';
184
+ link.href = URL.createObjectURL(blob);
185
+ link.click();
186
+ }
187
+
188
+ function downloadCSV() {
189
+ const labels = ${JSON.stringify(labels)};
190
+ const datasets = ${JSON.stringify(datasets.map(ds => ({ label: ds.label, data: ds.data })))};
191
+ let csv = 'Label,' + datasets.map(d => d.label).join(',') + '\\n';
192
+ labels.forEach((l, i) => {
193
+ csv += '"' + l + '",' + datasets.map(d => d.data[i] ?? '').join(',') + '\\n';
194
+ });
195
+ const blob = new Blob([csv], { type: 'text/csv' });
196
+ const link = document.createElement('a');
197
+ link.download = '${title.replace(/[^a-zA-Z0-9]/g, "_")}.csv';
198
+ link.href = URL.createObjectURL(blob);
199
+ link.click();
200
+ }
201
+ </script>
202
+ </body>
203
+ </html>`
204
+ }
205
+
206
+ function getColor(index) {
207
+ const colors = [
208
+ "#7c3aed", "#2563eb", "#059669", "#d97706", "#dc2626",
209
+ "#8b5cf6", "#3b82f6", "#10b981", "#f59e0b", "#ef4444",
210
+ "#6d28d9", "#1d4ed8", "#047857", "#b45309", "#b91c1c",
211
+ "#a78bfa", "#60a5fa", "#34d399", "#fbbf24", "#f87171",
212
+ ]
213
+ return colors[index % colors.length]
214
+ }
215
+
216
+ function getColorAlpha(index, alpha) {
217
+ const hex = getColor(index)
218
+ const r = parseInt(hex.slice(1, 3), 16)
219
+ const g = parseInt(hex.slice(3, 5), 16)
220
+ const b = parseInt(hex.slice(5, 7), 16)
221
+ return `rgba(${r},${g},${b},${alpha})`
222
+ }
223
+
224
+ export class ChartGenerator {
225
+ constructor() {
226
+ ensureDir()
227
+ }
228
+
229
+ async createChart(config) {
230
+ const html = generateChartHTML(config)
231
+ const filename = `chart_${Date.now()}.html`
232
+ const filePath = path.join(CHARTS_DIR, filename)
233
+ fs.writeFileSync(filePath, html)
234
+ openFile(filePath)
235
+ return { success: true, path: filePath, filename }
236
+ }
237
+
238
+ async bar(title, labels, data, options = {}) {
239
+ return this.createChart({
240
+ type: "bar",
241
+ title,
242
+ labels,
243
+ datasets: [{ label: options.label || title, data }],
244
+ ...options,
245
+ })
246
+ }
247
+
248
+ async line(title, labels, datasets, options = {}) {
249
+ return this.createChart({
250
+ type: "line",
251
+ title,
252
+ labels,
253
+ datasets: datasets.map((d, i) => ({
254
+ label: d.label || `Series ${i + 1}`,
255
+ data: d.data || d,
256
+ borderColor: d.borderColor,
257
+ backgroundColor: d.backgroundColor,
258
+ fill: d.fill,
259
+ tension: d.tension,
260
+ ...d,
261
+ })),
262
+ ...options,
263
+ })
264
+ }
265
+
266
+ async pie(title, labels, data, options = {}) {
267
+ return this.createChart({
268
+ type: "pie",
269
+ title,
270
+ labels,
271
+ datasets: [{ label: title, data }],
272
+ showLegend: true,
273
+ showGrid: false,
274
+ ...options,
275
+ })
276
+ }
277
+
278
+ async doughnut(title, labels, data, options = {}) {
279
+ return this.createChart({
280
+ type: "doughnut",
281
+ title,
282
+ labels,
283
+ datasets: [{ label: title, data }],
284
+ showLegend: true,
285
+ showGrid: false,
286
+ ...options,
287
+ })
288
+ }
289
+
290
+ async radar(title, labels, datasets, options = {}) {
291
+ return this.createChart({
292
+ type: "radar",
293
+ title,
294
+ labels,
295
+ datasets: datasets.map((d, i) => ({
296
+ label: d.label || `Series ${i + 1}`,
297
+ data: d.data || d,
298
+ ...d,
299
+ })),
300
+ showGrid: true,
301
+ ...options,
302
+ })
303
+ }
304
+
305
+ async scatter(title, dataPoints, options = {}) {
306
+ const datasets = options.datasets || [{ label: title, data: dataPoints }]
307
+ return this.createChart({
308
+ type: "scatter",
309
+ title,
310
+ datasets: datasets.map((d, i) => ({
311
+ label: d.label || `Series ${i + 1}`,
312
+ data: d.data || d,
313
+ backgroundColor: getColorAlpha(i, 0.6),
314
+ borderColor: getColor(i),
315
+ pointRadius: 5,
316
+ ...d,
317
+ })),
318
+ ...options,
319
+ })
320
+ }
321
+
322
+ async horizontalBar(title, labels, data, options = {}) {
323
+ return this.createChart({
324
+ type: "horizontalBar",
325
+ title,
326
+ labels,
327
+ datasets: [{ label: options.label || title, data }],
328
+ ...options,
329
+ })
330
+ }
331
+
332
+ async polarArea(title, labels, data, options = {}) {
333
+ return this.createChart({
334
+ type: "polarArea",
335
+ title,
336
+ labels,
337
+ datasets: [{ label: title, data }],
338
+ showGrid: false,
339
+ ...options,
340
+ })
341
+ }
342
+
343
+ async bubble(title, dataPoints, options = {}) {
344
+ return this.createChart({
345
+ type: "bubble",
346
+ title,
347
+ datasets: [{
348
+ label: options.label || title,
349
+ data: dataPoints,
350
+ backgroundColor: getColorAlpha(0, 0.6),
351
+ borderColor: getColor(0),
352
+ }],
353
+ ...options,
354
+ })
355
+ }
356
+
357
+ async fromCSV(csvPath, options = {}) {
358
+ if (!fs.existsSync(csvPath)) return { success: false, error: "CSV file not found" }
359
+ const content = fs.readFileSync(csvPath, "utf8")
360
+ const lines = content.trim().split("\n")
361
+ if (lines.length < 2) return { success: false, error: "CSV too short" }
362
+
363
+ const headers = lines[0].split(",").map(h => h.trim().replace(/"/g, ""))
364
+ const labels = []
365
+ const datasets = headers.slice(1).map(h => ({ label: h, data: [] }))
366
+
367
+ for (let i = 1; i < lines.length; i++) {
368
+ const cols = lines[i].split(",").map(c => c.trim().replace(/"/g, ""))
369
+ labels.push(cols[0] || `Row ${i}`)
370
+ for (let j = 1; j < cols.length; j++) {
371
+ datasets[j - 1].data.push(parseFloat(cols[j]) || 0)
372
+ }
373
+ }
374
+
375
+ const chartType = options.type || (labels.length > 6 ? "line" : "bar")
376
+ return this.createChart({
377
+ type: chartType,
378
+ title: options.title || path.basename(csvPath, ".csv"),
379
+ labels,
380
+ datasets,
381
+ ...options,
382
+ })
383
+ }
384
+
385
+ async fromJSON(jsonPath, options = {}) {
386
+ if (!fs.existsSync(jsonPath)) return { success: false, error: "JSON file not found" }
387
+ const data = JSON.parse(fs.readFileSync(jsonPath, "utf8"))
388
+ return this.createChart({ ...data, ...options })
389
+ }
390
+
391
+ listCharts() {
392
+ if (!fs.existsSync(CHARTS_DIR)) return []
393
+ return fs.readdirSync(CHARTS_DIR)
394
+ .filter(f => f.endsWith(".html"))
395
+ .map(f => ({
396
+ name: f,
397
+ path: path.join(CHARTS_DIR, f),
398
+ created: fs.statSync(path.join(CHARTS_DIR, f)).birthtime,
399
+ }))
400
+ .sort((a, b) => b.created - a.created)
401
+ }
402
+
403
+ deleteChart(filename) {
404
+ const filePath = path.join(CHARTS_DIR, filename)
405
+ if (fs.existsSync(filePath)) {
406
+ fs.unlinkSync(filePath)
407
+ return { success: true }
408
+ }
409
+ return { success: false, error: "Not found" }
410
+ }
411
+ }