chartjs2img 0.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/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "chartjs2img",
3
+ "version": "0.3.0",
4
+ "description": "Render Chart.js charts to images via a TypeScript API, CLI, or HTTP server — headless Chromium under the hood. Chart.js 4.4 + 12 plugins bundled.",
5
+ "type": "module",
6
+ "main": "./dist/lib.js",
7
+ "module": "./dist/lib.js",
8
+ "types": "./dist/lib.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/lib.d.ts",
12
+ "import": "./dist/lib.js"
13
+ },
14
+ "./package.json": "./package.json"
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "sideEffects": false,
22
+ "bin": {
23
+ "chartjs2img": "src/index.ts"
24
+ },
25
+ "keywords": [
26
+ "chart",
27
+ "chartjs",
28
+ "chart.js",
29
+ "image",
30
+ "png",
31
+ "jpeg",
32
+ "webp",
33
+ "puppeteer",
34
+ "headless-chromium",
35
+ "visualization",
36
+ "llm"
37
+ ],
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/ideamans/chartjs2img.git"
41
+ },
42
+ "license": "MIT",
43
+ "scripts": {
44
+ "dev": "bun run src/index.ts serve",
45
+ "build": "bun build src/index.ts --compile --outfile chartjs2img",
46
+ "start": "bun run src/index.ts serve",
47
+ "cli": "bun run src/index.ts",
48
+ "typecheck": "tsc --noEmit",
49
+ "clean:dist": "rm -rf dist",
50
+ "build:lib": "bun run clean:dist && tsc -p tsconfig.build.json",
51
+ "prepublishOnly": "bun run build:lib",
52
+ "build-llms-txt": "bun scripts/build-llms-txt.ts",
53
+ "validate-plugin-skills": "bun scripts/validate-plugin-skills.ts",
54
+ "ai:regen": "bun scripts/build-llms-txt.ts",
55
+ "docs:examples": "bun scripts/build-docs-examples.ts",
56
+ "docs:diagrams": "bun scripts/build-docs-diagrams.ts",
57
+ "docs:dev": "bun run build-llms-txt && bun run docs:diagrams && vitepress dev docs",
58
+ "docs:build": "bun run build-llms-txt && bun run docs:diagrams && bun run docs:examples && vitepress build docs",
59
+ "docs:preview": "vitepress preview docs",
60
+ "docs:deploy": "bun run docs:build && rsync -av docs/.vitepress/dist/ web-g6:web/vhosts/chartjs2img.ideamans.com/html/"
61
+ },
62
+ "dependencies": {
63
+ "puppeteer-core": "^24.40.0"
64
+ },
65
+ "devDependencies": {
66
+ "@tailwindcss/vite": "^4.2.2",
67
+ "@types/bun": "latest",
68
+ "daisyui": "^5.5.19",
69
+ "tailwindcss": "^4.2.2",
70
+ "typescript": "^5.7.0",
71
+ "vitepress": "^2.0.0-alpha.17",
72
+ "vitepress-daisyui-theme": "^0.2.1",
73
+ "vue": "^3.5.0"
74
+ }
75
+ }
package/src/index.ts ADDED
@@ -0,0 +1,312 @@
1
+ #!/usr/bin/env bun
2
+ import { startServer } from './server'
3
+ import { cliRender, cliExamples } from './cli'
4
+ import { closeBrowser } from './renderer'
5
+ import { VERSION } from './version'
6
+ import { getLlmDocs } from './llm-docs'
7
+ import { parseArgs, CliArgError } from './cli-args'
8
+
9
+ function printUsage(): void {
10
+ console.log(`chartjs2img v${VERSION} - Render Chart.js charts to images using Playwright (headless Chromium)
11
+
12
+ Converts a Chart.js configuration JSON into a PNG or JPEG image. Works as an
13
+ HTTP server or a one-shot CLI. Chromium is installed automatically on first run.
14
+
15
+ COMMANDS
16
+ chartjs2img serve [options] Start HTTP server
17
+ chartjs2img render [options] Render a single chart image
18
+ chartjs2img examples [options] Generate all built-in example images
19
+ chartjs2img llm Print extended help for LLMs (Chart.js + plugin reference)
20
+ chartjs2img help Show this help
21
+
22
+ SERVE OPTIONS
23
+ --port, -p <port> Listen port (default: 3000, env: PORT)
24
+ --host <host> Bind address (default: 0.0.0.0, env: HOST)
25
+ --api-key <key> Require API key auth (env: API_KEY)
26
+
27
+ RENDER OPTIONS
28
+ --input, -i <file> Input JSON file, or "-" for stdin (default: stdin)
29
+ --output, -o <file> Output image file, or "-" for stdout (default: stdout)
30
+ --width, -w <px> Canvas width in pixels (default: 800)
31
+ --height, -h <px> Canvas height in pixels (default: 600)
32
+ --device-pixel-ratio <n> Retina scale factor (default: 2)
33
+ --background-color <color> CSS color or "transparent" (default: white)
34
+ --format, -f <fmt> png | jpeg (default: png)
35
+ --quality, -q <0-100> JPEG quality (default: 90)
36
+
37
+ EXAMPLES OPTIONS
38
+ --outdir, -o <dir> Output directory (default: ./examples)
39
+ --format, -f <fmt> png | jpeg (default: png)
40
+ --quality, -q <0-100> JPEG quality (default: 90)
41
+
42
+ ENVIRONMENT VARIABLES
43
+ PORT Server listen port (default: 3000)
44
+ HOST Server bind address (default: 0.0.0.0)
45
+ API_KEY API key for authentication (optional)
46
+ CONCURRENCY Max simultaneous renders (default: 8)
47
+ CACHE_MAX_ENTRIES Max cached images in memory (default: 1000)
48
+ CACHE_TTL_SECONDS Cache entry TTL in seconds (default: 3600)
49
+ MAX_RENDER_TIME_SECONDS Per-render upper bound for goto/waitForFunction (default: 30)
50
+ PAGE_TIMEOUT_SECONDS Override the safety-net force-close timer (default: MAX_RENDER_TIME * 2 + 10)
51
+
52
+ HTTP ENDPOINTS (serve mode)
53
+ POST /render Render chart. Body: JSON (see schema below). Returns image.
54
+ GET /render Same, but params via query string: ?chart={...}&width=800
55
+ GET /cache/<hash> Retrieve a previously rendered image by its cache hash.
56
+ GET /examples Interactive gallery page showing all built-in examples.
57
+ GET /health JSON status: browser, concurrency, cache stats.
58
+
59
+ Response headers from /render:
60
+ X-Cache-Hash SHA-256 hash (first 16 hex chars) of the render input
61
+ X-Cache-Url Full URL to GET this image from /cache/<hash>
62
+ X-Cache-Hit "true" if served from cache, "false" if freshly rendered
63
+ X-Chart-Messages JSON array of {level, message} from Chart.js (if any)
64
+
65
+ AUTHENTICATION (when API_KEY is set)
66
+ Requests to /render and /cache must include the key in one of:
67
+ - Header: Authorization: Bearer <key>
68
+ - Header: X-API-Key: <key>
69
+ - Query: ?api_key=<key>
70
+ /health is accessible without authentication.
71
+ /examples requires the API key as well (otherwise the page would leak
72
+ the key in its HTML since it calls /render from the browser).
73
+
74
+ INPUT JSON SCHEMA (for "render" CLI and POST /render)
75
+ The input is a JSON object. For CLI "render", the top-level object IS the
76
+ Chart.js config. For HTTP POST /render, wrap it in a "chart" field alongside
77
+ optional render settings.
78
+
79
+ JSON only — function / callback values (e.g. tooltip.callbacks, scale.ticks
80
+ callback, treemap labels.formatter) are silently dropped by JSON
81
+ serialization and never reach the browser. Use static values instead.
82
+
83
+ CLI stdin/file (Chart.js config directly):
84
+ {
85
+ "type": "bar",
86
+ "data": {
87
+ "labels": ["A", "B", "C"],
88
+ "datasets": [{ "label": "Series", "data": [10, 20, 30] }]
89
+ },
90
+ "options": { ... }
91
+ }
92
+
93
+ HTTP POST /render body:
94
+ {
95
+ "chart": { <Chart.js config as above> },
96
+ "width": 800, // optional, default 800
97
+ "height": 600, // optional, default 600
98
+ "devicePixelRatio": 2, // optional, default 2
99
+ "backgroundColor": "white",// optional, default "white"
100
+ "format": "png", // optional, "png" | "jpeg"
101
+ "quality": 90 // optional, 0-100 for jpeg
102
+ }
103
+
104
+ CHART.JS CONFIGURATION REFERENCE
105
+ The "chart" (or CLI input) follows the standard Chart.js config:
106
+ {
107
+ "type": "<chart-type>",
108
+ "data": {
109
+ "labels": [...],
110
+ "datasets": [{
111
+ "label": "...",
112
+ "data": [...],
113
+ "backgroundColor": "...",
114
+ "borderColor": "...",
115
+ ...dataset options
116
+ }]
117
+ },
118
+ "options": {
119
+ "responsive": true, // forced true internally
120
+ "plugins": {
121
+ "title": { "display": true, "text": "Title" },
122
+ "legend": { "position": "top" },
123
+ "datalabels": { "display": true, "color": "#333" },
124
+ "annotation": { "annotations": { ... } }
125
+ },
126
+ "scales": {
127
+ "x": { "type": "category", "title": { "display": true, "text": "X" } },
128
+ "y": { "type": "linear", "beginAtZero": true }
129
+ }
130
+ }
131
+ }
132
+
133
+ Supported "type" values (built-in):
134
+ bar, line, pie, doughnut, radar, polarArea, scatter, bubble
135
+
136
+ Additional types from plugins:
137
+ treemap, matrix, sankey, wordcloud, choropleth, bubbleMap,
138
+ graph, forceDirectedGraph, dendrogram, tree, venn, euler
139
+
140
+ BUILT-IN PLUGINS (all pre-loaded, no configuration needed)
141
+ chart.js 4.4.9 Core charting library
142
+ chartjs-plugin-datalabels 2.2.0 Show values on chart elements
143
+ chartjs-plugin-annotation 3.1.0 Lines, boxes, labels as overlays
144
+ chartjs-plugin-zoom 2.2.0 Pan & zoom (initial range for static)
145
+ chartjs-plugin-gradient 0.6.1 Gradient fills via simple config
146
+ chartjs-chart-matrix 2.0.1 Heatmap / matrix charts
147
+ chartjs-chart-sankey 0.12.1 Sankey flow diagrams
148
+ chartjs-chart-treemap 2.3.1 Treemap charts
149
+ chartjs-chart-wordcloud 4.4.3 Word cloud charts
150
+ chartjs-chart-geo 4.3.3 Choropleth & bubble map charts
151
+ chartjs-chart-graph 4.3.3 Network / force-directed graphs
152
+ chartjs-chart-venn 4.3.3 Venn & Euler diagrams
153
+ chartjs-adapter-date-fns 3.0.0 date-fns date adapter for time axes
154
+
155
+ Note: animation is always forced OFF for deterministic rendering.
156
+
157
+ ERROR FEEDBACK
158
+ Chart.js errors and warnings are captured from the browser console during
159
+ rendering and returned to the caller. This helps diagnose invalid configs.
160
+
161
+ CLI:
162
+ Errors/warnings are printed to stderr:
163
+ [chart ERROR] "nonexistent" is not a registered controller.
164
+ [chart WARN] Some warning message from Chart.js
165
+
166
+ HTTP API:
167
+ When messages exist, the response includes an X-Chart-Messages header:
168
+ X-Chart-Messages: [{"level":"error","message":"..."},{"level":"warn","message":"..."}]
169
+
170
+ Parse it as JSON to inspect issues:
171
+ curl -s -D- -X POST http://localhost:3000/render \\
172
+ -H 'Content-Type: application/json' \\
173
+ -d '{"chart":{"type":"invalid",...}}' -o /dev/null \\
174
+ | grep X-Chart-Messages
175
+
176
+ Note: rendering still completes even when errors occur (the image may be
177
+ blank or partial). Check messages to determine if the chart was valid.
178
+
179
+ USAGE EXAMPLES
180
+
181
+ # Start server on port 3000
182
+ chartjs2img serve
183
+
184
+ # Start with auth and custom port
185
+ chartjs2img serve --port 8080 --api-key s3cret
186
+
187
+ # Render bar chart from stdin to file
188
+ echo '{"type":"bar","data":{"labels":["A","B","C"],"datasets":[{"data":[1,2,3]}]}}' \\
189
+ | chartjs2img render -o chart.png
190
+
191
+ # Render from JSON file with custom size
192
+ chartjs2img render -i config.json -o wide.png -w 1200 -h 400
193
+
194
+ # Render as JPEG with quality setting
195
+ chartjs2img render -i config.json -o chart.jpg -f jpeg -q 85
196
+
197
+ # Generate all built-in examples as PNG
198
+ chartjs2img examples -o ./gallery
199
+
200
+ # Generate examples as JPEG
201
+ chartjs2img examples -o ./gallery -f jpeg -q 80
202
+
203
+ # HTTP: render via POST and save
204
+ curl -X POST http://localhost:3000/render \\
205
+ -H 'Content-Type: application/json' \\
206
+ -d '{"chart":{"type":"bar","data":{"labels":["A","B"],"datasets":[{"data":[1,2]}]}}}' \\
207
+ -o chart.png
208
+
209
+ # HTTP: render via POST with auth and options
210
+ curl -X POST http://localhost:3000/render \\
211
+ -H 'Content-Type: application/json' \\
212
+ -H 'X-API-Key: s3cret' \\
213
+ -d '{"chart":{"type":"line","data":{"labels":["Mon","Tue","Wed"],"datasets":[{"label":"Views","data":[100,200,150],"borderColor":"#36a2eb"}]}},"width":1200,"height":400,"format":"jpeg","quality":85}' \\
214
+ -o chart.jpg
215
+
216
+ # HTTP: use cached image (hash from X-Cache-Hash response header)
217
+ curl -o cached.png http://localhost:3000/cache/6b4cc4e8940fd921
218
+
219
+ # HTTP: embed in <img> tag via GET (URL-encode the chart JSON)
220
+ <img src="http://localhost:3000/render?chart=%7B%22type%22%3A%22pie%22%2C...%7D&width=400&height=400">
221
+
222
+ # Check server health
223
+ curl http://localhost:3000/health
224
+ `)
225
+ }
226
+
227
+ async function main(): Promise<void> {
228
+ const command = process.argv[2]
229
+ let args: Record<string, string | boolean>
230
+ try {
231
+ args = parseArgs(process.argv.slice(3))
232
+ } catch (err) {
233
+ if (err instanceof CliArgError) {
234
+ console.error(err.message)
235
+ process.exit(2)
236
+ }
237
+ throw err
238
+ }
239
+
240
+ if (command === '--version' || command === 'version') {
241
+ console.log(`chartjs2img v${VERSION}`)
242
+ process.exit(0)
243
+ }
244
+
245
+ if (!command || command === '--help' || command === '-h' || command === '-?' || command === 'help') {
246
+ printUsage()
247
+ process.exit(0)
248
+ }
249
+
250
+ if (command === 'llm') {
251
+ console.log(getLlmDocs())
252
+ process.exit(0)
253
+ }
254
+
255
+ if (command === 'serve') {
256
+ const port = Number(args['port'] ?? args['p'] ?? process.env.PORT ?? '3000')
257
+ const host = String(args['host'] ?? process.env.HOST ?? '0.0.0.0')
258
+ const apiKey = String(args['api-key'] ?? process.env.API_KEY ?? '') || undefined
259
+
260
+ const handle = await startServer({ port, host, apiKey })
261
+
262
+ // Signal handlers belong to the CLI (process entrypoint), not the
263
+ // server library. `once` avoids accumulating handlers if startServer
264
+ // is ever called multiple times in the same process.
265
+ let shuttingDown = false
266
+ const shutdown = async () => {
267
+ if (shuttingDown) return
268
+ shuttingDown = true
269
+ console.log('\nShutting down...')
270
+ try {
271
+ await handle.stop()
272
+ } catch (err) {
273
+ console.error('[cli] server stop failed:', err)
274
+ }
275
+ await closeBrowser()
276
+ process.exit(0)
277
+ }
278
+ process.once('SIGINT', shutdown)
279
+ process.once('SIGTERM', shutdown)
280
+ // Bun.serve keeps the event loop alive; main() returning is fine.
281
+ } else if (command === 'examples') {
282
+ const outdir = (args['outdir'] as string) ?? (args['o'] as string) ?? './examples'
283
+ const format = ((args['format'] ?? args['f']) as string) ?? 'png'
284
+ const quality = args['quality'] ? Number(args['quality']) : args['q'] ? Number(args['q']) : undefined
285
+
286
+ await cliExamples({
287
+ outdir,
288
+ format: format as 'png' | 'jpeg',
289
+ quality,
290
+ })
291
+ } else if (command === 'render') {
292
+ await cliRender({
293
+ input: args['input'] as string ?? args['i'] as string,
294
+ output: args['output'] as string ?? args['o'] as string,
295
+ width: args['width'] ? Number(args['width']) : args['w'] ? Number(args['w']) : undefined,
296
+ height: args['height'] ? Number(args['height']) : args['h'] ? Number(args['h']) : undefined,
297
+ devicePixelRatio: args['device-pixel-ratio'] ? Number(args['device-pixel-ratio']) : undefined,
298
+ backgroundColor: args['background-color'] as string,
299
+ format: (args['format'] ?? args['f']) as 'png' | 'jpeg' | undefined,
300
+ quality: args['quality'] ? Number(args['quality']) : args['q'] ? Number(args['q']) : undefined,
301
+ })
302
+ } else {
303
+ console.error(`Unknown command: ${command}`)
304
+ printUsage()
305
+ process.exit(1)
306
+ }
307
+ }
308
+
309
+ main().catch((err) => {
310
+ console.error(err)
311
+ process.exit(1)
312
+ })