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/README.md ADDED
@@ -0,0 +1,607 @@
1
+ # chartjs2img
2
+
3
+ Server-side Chart.js rendering service. Takes a Chart.js configuration as JSON, renders it to an image using headless Chromium (via `puppeteer-core`), and returns the result. Ships as an HTTP API, a CLI, and a TypeScript / Node library.
4
+
5
+ Built for generating charts in contexts where a browser isn't available — email campaigns, PowerPoint generation, PDF reports, Slack bots, LLM tool calls, etc.
6
+
7
+ Full documentation (EN / JA): <https://chartjs2img.ideamans.com>
8
+
9
+ ## Features
10
+
11
+ - **Chart.js 4.4 + 11 plugins + date-fns adapter** built-in (see [Included Plugins](#included-plugins))
12
+ - **HTTP API** — POST JSON, get an image back
13
+ - **CLI** — pipe JSON in, get an image out
14
+ - **Library API** — `import { renderChart } from 'chartjs2img'` from any Bun / Node program
15
+ - **Claude Code plugin** — ships three Agent Skills (`/chartjs2img-render`, `/chartjs2img-author`, `/chartjs2img-install`) under `plugins/chartjs2img/`
16
+ - **Hash-based caching** — identical requests return cached images instantly
17
+ - **Concurrency control** — configurable semaphore prevents resource exhaustion
18
+ - **Browser lifecycle management** — auto-restart on crash, orphaned page cleanup
19
+ - **API key authentication** — optional, via header or query param
20
+ - **Japanese text support** — Noto Sans CJK included in Docker image (no tofu)
21
+ - **Error feedback** — Chart.js errors/warnings captured and returned via header (HTTP) or stderr (CLI)
22
+ - **LLM integration** — `chartjs2img llm` outputs a full Chart.js + plugin reference in Markdown; `docs/public/llms.txt` / `llms-full.txt` are published for retrieval agents
23
+ - **Examples gallery** — built-in `/examples` page (35 configurations) for visual verification
24
+ - **Single binary** — compile with `bun build --compile` for easy distribution
25
+
26
+ ## Prerequisites
27
+
28
+ You need [Bun](https://bun.sh) installed. If you haven't installed it yet:
29
+
30
+ ```bash
31
+ # macOS / Linux
32
+ curl -fsSL https://bun.sh/install | bash
33
+
34
+ # Then restart your terminal, or run:
35
+ source ~/.bashrc # or ~/.zshrc
36
+ ```
37
+
38
+ Verify it's working:
39
+
40
+ ```bash
41
+ bun --version
42
+ ```
43
+
44
+ ## Chrome / Chromium
45
+
46
+ chartjs2img requires Chrome or Chromium to render charts. On first run, it searches for an existing installation in this order:
47
+
48
+ 1. `CHROMIUM_PATH` environment variable
49
+ 2. `ms-playwright` browser cache (`~/Library/Caches/ms-playwright/` etc. — reused if a prior Playwright install is present)
50
+ 3. System-installed Chrome/Chromium (`/Applications/Google Chrome.app`, `/usr/bin/google-chrome`, etc.)
51
+ 4. **Auto-download** — if nothing is found, [Chrome for Testing](https://googlechromelabs.github.io/chrome-for-testing/) is downloaded automatically to the user cache directory (no sudo required)
52
+
53
+ Auto-download is available for **macOS (x64/arm64)**, **Windows (x64/x86)**, and **Linux (x64)**.
54
+
55
+ > **Linux ARM64:** Chrome for Testing does not provide linux-arm64 builds. You must install Chromium manually:
56
+ > ```bash
57
+ > # Debian/Ubuntu
58
+ > sudo apt install chromium-browser
59
+ > # or
60
+ > sudo apt install chromium
61
+ > ```
62
+ > Then either let chartjs2img detect it automatically, or set `CHROMIUM_PATH`:
63
+ > ```bash
64
+ > export CHROMIUM_PATH=/usr/bin/chromium-browser
65
+ > ```
66
+
67
+ ## Quick Start
68
+
69
+ ### 1. Install dependencies
70
+
71
+ ```bash
72
+ cd chartjs2img
73
+ bun install
74
+ ```
75
+
76
+ This installs the Node.js packages. Bun uses `node_modules` just like npm, but it's much faster.
77
+
78
+ ### 2. Start the development server
79
+
80
+ > **Zero-config:** You do **not** need to install Chromium manually. On first run, if Chromium is not found, it will be downloaded automatically (~250 MB one-time download). Just run the command below and wait for the install to complete.
81
+
82
+ ```bash
83
+ bun run dev
84
+ ```
85
+
86
+ This starts the HTTP server on `http://localhost:3000`. You should see:
87
+
88
+ ```
89
+ chartjs2img server listening on http://0.0.0.0:3000
90
+ POST /render - render chart from JSON body
91
+ GET /render - render chart from query params
92
+ GET /cache/:hash - retrieve cached image
93
+ GET /examples - examples gallery
94
+ GET /health - health check + stats
95
+ ```
96
+
97
+ > `bun run dev` is equivalent to `bun run src/index.ts serve`. You can also pass options directly:
98
+ > ```bash
99
+ > bun run src/index.ts serve --port 8080 --api-key mysecret
100
+ > ```
101
+
102
+ ### 3. Open the examples gallery
103
+
104
+ Visit [http://localhost:3000/examples](http://localhost:3000/examples) in your browser to see all chart types rendered live.
105
+
106
+ ### 4. Render your first chart
107
+
108
+ ```bash
109
+ curl -X POST http://localhost:3000/render \
110
+ -H 'Content-Type: application/json' \
111
+ -d '{
112
+ "chart": {
113
+ "type": "bar",
114
+ "data": {
115
+ "labels": ["Jan", "Feb", "Mar", "Apr"],
116
+ "datasets": [{
117
+ "label": "Sales",
118
+ "data": [12, 19, 3, 5],
119
+ "backgroundColor": "rgba(54, 162, 235, 0.7)"
120
+ }]
121
+ }
122
+ }
123
+ }' \
124
+ -o chart.png
125
+ ```
126
+
127
+ ## npm Scripts Reference
128
+
129
+ | Command | What it does |
130
+ |---------|-------------|
131
+ | `bun run dev` | Start the HTTP server (development) |
132
+ | `bun run start` | Same as `bun run dev` |
133
+ | `bun run cli -- <cmd>` | Run any CLI subcommand (e.g., `bun run cli -- llm`) |
134
+ | `bun run typecheck` | Run `tsc --noEmit` |
135
+ | `bun run build` | Compile to a single binary `./chartjs2img` |
136
+ | `bun run build:lib` | Emit the library bundle to `dist/` (used by `prepublishOnly`) |
137
+ | `bun run ai:regen` | Rebuild `docs/public/llms.txt` / `llms-full.txt` from `src/llm-docs/` + `docs/en/` |
138
+ | `bun run validate-plugin-skills` | Validate the SKILL.md files under `plugins/chartjs2img/skills/` |
139
+ | `bun run docs:dev` | VitePress dev server for the documentation site |
140
+ | `bun run docs:build` | Full docs build (llms.txt + diagrams + example images + VitePress) |
141
+ | `bun run docs:examples` | Regenerate the PNG / JSON pairs under `docs/public/examples/` |
142
+ | `bun run docs:diagrams` | Regenerate the architecture diagrams under `docs/public/diagrams/` |
143
+ | `bun run docs:preview` | Preview the built docs site |
144
+
145
+ > **Tip:** You can always run TypeScript files directly with Bun — no compilation step needed for development:
146
+ > ```bash
147
+ > bun run src/index.ts serve --port 8080
148
+ > ```
149
+
150
+ > **AI-facing artifacts are generated.** `docs/public/llms.txt`, `docs/public/llms-full.txt`, and `docs/public/examples/**` are derived from `src/llm-docs/`, `src/examples.ts`, and `docs/en/**`. Do not hand-edit them — run `bun run ai:regen` (or `/regen-ai`) after changing the sources. See `.claude/rules/ai-artifacts-policy.md`.
151
+
152
+ ## HTTP API
153
+
154
+ ### `POST /render`
155
+
156
+ Render a chart from a JSON body.
157
+
158
+ **Request body:**
159
+
160
+ ```json
161
+ {
162
+ "chart": { },
163
+ "width": 800,
164
+ "height": 600,
165
+ "devicePixelRatio": 2,
166
+ "backgroundColor": "white",
167
+ "format": "png",
168
+ "quality": 90
169
+ }
170
+ ```
171
+
172
+ | Field | Type | Default | Description |
173
+ |-------|------|---------|-------------|
174
+ | `chart` | object | *required* | Chart.js configuration (type, data, options, plugins) |
175
+ | `width` | number | 800 | Image width in pixels |
176
+ | `height` | number | 600 | Image height in pixels |
177
+ | `devicePixelRatio` | number | 2 | Retina scaling factor |
178
+ | `backgroundColor` | string | `"white"` | CSS background color (`"transparent"` supported) |
179
+ | `format` | string | `"png"` | Output format: `png` or `jpeg` |
180
+ | `quality` | number | 90 | JPEG quality (0-100) |
181
+
182
+ **Response headers:**
183
+
184
+ | Header | Description |
185
+ |--------|-------------|
186
+ | `X-Cache-Hash` | Unique hash for this chart configuration |
187
+ | `X-Cache-Url` | Full URL to retrieve this image from cache |
188
+ | `X-Cache-Hit` | `"true"` if served from cache, `"false"` if freshly rendered |
189
+ | `X-Chart-Messages` | JSON array of `{level, message}` from Chart.js (only present if errors/warnings occurred) |
190
+
191
+ ### `GET /render`
192
+
193
+ Same as POST, but pass parameters as query strings. Useful for `<img>` tags.
194
+
195
+ ```
196
+ GET /render?chart={"type":"bar","data":{...}}&width=400&height=300
197
+ ```
198
+
199
+ ### `GET /cache/:hash`
200
+
201
+ Retrieve a previously rendered image by its cache hash. The hash is returned in the `X-Cache-Hash` response header of `/render`.
202
+
203
+ ```bash
204
+ # Render and get the cache hash
205
+ HASH=$(curl -s -D- -X POST http://localhost:3000/render \
206
+ -H 'Content-Type: application/json' \
207
+ -d '{"chart":{"type":"bar","data":{"labels":["A","B"],"datasets":[{"data":[1,2]}]}}}' \
208
+ -o /dev/null | grep -i x-cache-hash | awk '{print $2}' | tr -d '\r')
209
+
210
+ # Access the cached image later
211
+ curl -o chart.png "http://localhost:3000/cache/$HASH"
212
+ ```
213
+
214
+ ### `GET /health`
215
+
216
+ Returns server status, renderer stats, and cache info.
217
+
218
+ ```json
219
+ {
220
+ "status": "ok",
221
+ "renderer": {
222
+ "browserConnected": true,
223
+ "concurrency": { "max": 8, "active": 2, "pending": 0 },
224
+ "activePages": 2,
225
+ "pageTimeoutSeconds": 60
226
+ },
227
+ "cache": {
228
+ "size": 42,
229
+ "maxEntries": 1000,
230
+ "ttlSeconds": 3600
231
+ }
232
+ }
233
+ ```
234
+
235
+ ### `GET /examples`
236
+
237
+ Built-in gallery page showing 35 chart examples rendered in real time. Useful for visual verification and as a reference for building chart configurations.
238
+
239
+ ## Authentication
240
+
241
+ API key authentication is optional. When enabled, every request to `/render` and `/cache/:hash` must include the key in one of these ways:
242
+
243
+ ```bash
244
+ # Authorization header
245
+ curl -H 'Authorization: Bearer YOUR_KEY' ...
246
+
247
+ # X-API-Key header
248
+ curl -H 'X-API-Key: YOUR_KEY' ...
249
+
250
+ # Query parameter
251
+ curl 'http://localhost:3000/render?api_key=YOUR_KEY&chart=...'
252
+ ```
253
+
254
+ Set the key via CLI flag or environment variable:
255
+
256
+ ```bash
257
+ bun run src/index.ts serve --api-key YOUR_KEY
258
+ # or
259
+ API_KEY=YOUR_KEY bun run dev
260
+ ```
261
+
262
+ ## CLI Usage
263
+
264
+ ### LLM Help
265
+
266
+ Print extended documentation for LLMs — covers Chart.js core and all plugin parameters in Markdown:
267
+
268
+ ```bash
269
+ chartjs2img llm
270
+ # or
271
+ bun run cli -- llm
272
+ ```
273
+
274
+ This outputs ~1440 lines of structured Markdown reference, organized per module:
275
+
276
+ - **Usage guide** — input format (CLI / HTTP), constraints (JSON only, no functions)
277
+ - **Chart.js core** — all chart types, dataset properties, scales, title/legend/tooltip
278
+ - **11 plugins + date adapter** — datalabels, annotation, zoom, gradient, treemap, matrix, sankey, wordcloud, geo, graph, venn, and the date-fns adapter
279
+
280
+ Each section includes option tables (property, type, default, description) and JSON examples.
281
+
282
+ **Use cases:**
283
+
284
+ ```bash
285
+ # Feed as system prompt context to an LLM
286
+ chartjs2img llm | pbcopy # copy to clipboard (macOS)
287
+
288
+ # Save to a file for reuse
289
+ chartjs2img llm > chartjs2img-reference.md
290
+
291
+ # Pipe directly into an LLM CLI tool
292
+ chartjs2img llm | llm -s "Generate a bar chart config for monthly sales data"
293
+ ```
294
+
295
+ The output includes a disclaimer noting that the documentation may contain inaccuracies. LLMs should prioritize Chart.js error messages (returned via [Error Feedback](#error-feedback)) over this reference when debugging.
296
+
297
+ **Maintaining the docs:** Each module's documentation lives in its own file under `src/llm-docs/`. When a plugin is added or removed, add/remove the corresponding file and update `src/llm-docs/index.ts`. Then regenerate the published `llms.txt` / `llms-full.txt` with `bun run ai:regen`.
298
+
299
+ ### Rendering
300
+
301
+ Render charts directly from the command line without starting a server.
302
+
303
+ ```bash
304
+ # From a JSON file
305
+ bun run src/index.ts render -i chart.json -o chart.png
306
+
307
+ # From stdin
308
+ echo '{"type":"bar","data":{"labels":["A","B"],"datasets":[{"data":[1,2]}]}}' \
309
+ | bun run src/index.ts render -o chart.png
310
+
311
+ # With options
312
+ bun run src/index.ts render -i chart.json -o chart.png -w 1200 -h 400 -f jpeg -q 85
313
+ ```
314
+
315
+ | Flag | Description |
316
+ |------|-------------|
317
+ | `-i, --input <file>` | Input JSON file (default: stdin) |
318
+ | `-o, --output <file>` | Output image file (default: stdout) |
319
+ | `-w, --width <px>` | Width (default: 800) |
320
+ | `-h, --height <px>` | Height (default: 600) |
321
+ | `--device-pixel-ratio <n>` | DPR (default: 2) |
322
+ | `--background-color <color>` | Background (default: white) |
323
+ | `-f, --format <fmt>` | png, jpeg (default: png) |
324
+ | `-q, --quality <0-100>` | JPEG quality (default: 90) |
325
+
326
+ ### Batch rendering built-in examples
327
+
328
+ ```bash
329
+ # PNG files into ./gallery
330
+ chartjs2img examples -o ./gallery
331
+
332
+ # JPEG at quality 80
333
+ chartjs2img examples -o ./gallery -f jpeg -q 80
334
+ ```
335
+
336
+ This iterates the 35 bundled chart configs (the same set shown by `GET /examples`) and writes one image per entry. Useful for smoke-testing a new plugin bundle or for regenerating reference images.
337
+
338
+ ### Other subcommands
339
+
340
+ | Command | Description |
341
+ |---------|-------------|
342
+ | `chartjs2img help` / `--help` / `-h` | Show the full usage banner |
343
+ | `chartjs2img version` / `--version` | Print the version number |
344
+
345
+ ## Library API
346
+
347
+ Use chartjs2img programmatically from any Bun or Node program:
348
+
349
+ ```ts
350
+ import { renderChart, closeBrowser, computeHash, BUNDLED_LIBS, VERSION } from 'chartjs2img'
351
+
352
+ const result = await renderChart({
353
+ chart: {
354
+ type: 'bar',
355
+ data: {
356
+ labels: ['A', 'B', 'C'],
357
+ datasets: [{ data: [1, 2, 3] }],
358
+ },
359
+ },
360
+ width: 800,
361
+ height: 600,
362
+ format: 'png',
363
+ })
364
+
365
+ await Bun.write('chart.png', result.buffer)
366
+ if (result.messages.length) console.warn(result.messages)
367
+
368
+ // Call once on process shutdown to release the shared browser
369
+ await closeBrowser()
370
+ ```
371
+
372
+ Exports:
373
+
374
+ | Symbol | Purpose |
375
+ |--------|---------|
376
+ | `renderChart(options)` | Render a single chart using a lazily-created default `Renderer` |
377
+ | `closeBrowser()` | Close the shared headless browser on shutdown |
378
+ | `rendererStats()` | Browser / concurrency / page counters (same shape as `/health`) |
379
+ | `Renderer` | Class for advanced callers that want isolated browser pools / concurrency |
380
+ | `computeHash(options)` | Deterministic hash of a render input (for your own cache layer) |
381
+ | `BUNDLED_LIBS` | Frozen table of Chart.js + plugin versions baked into the page |
382
+ | `VERSION`, `NAME` | Package identification |
383
+
384
+ Types: `RenderOptions`, `RenderResult`, `ConsoleMessage`, `RendererConfig`, `RendererStats`.
385
+
386
+ ## Error Feedback
387
+
388
+ Chart.js errors and warnings are captured from the browser console during rendering and returned to the caller. This helps diagnose invalid configurations without guessing.
389
+
390
+ ### CLI
391
+
392
+ Errors and warnings are printed to stderr:
393
+
394
+ ```bash
395
+ $ echo '{"type":"invalid","data":{"labels":["A"],"datasets":[{"data":[1]}]}}' \
396
+ | chartjs2img render -o chart.png
397
+ [chart ERROR] "invalid" is not a registered controller.
398
+ Written to chart.png (hash: ...)
399
+ ```
400
+
401
+ ### HTTP API
402
+
403
+ When messages are present, the response includes an `X-Chart-Messages` header containing a JSON array:
404
+
405
+ ```bash
406
+ $ curl -s -D- -X POST http://localhost:3000/render \
407
+ -H 'Content-Type: application/json' \
408
+ -d '{"chart":{"type":"invalid","data":{"labels":["A"],"datasets":[{"data":[1]}]}}}' \
409
+ -o /dev/null | grep X-Chart-Messages
410
+
411
+ X-Chart-Messages: [{"level":"error","message":"\"invalid\" is not a registered controller."}]
412
+ ```
413
+
414
+ Each message has:
415
+
416
+ | Field | Values | Description |
417
+ |-------|--------|-------------|
418
+ | `level` | `"error"`, `"warn"` | Severity level |
419
+ | `message` | string | Message text from Chart.js |
420
+
421
+ > **Note:** Rendering still completes even when errors occur — the resulting image may be blank or partial. Always check `X-Chart-Messages` to determine if the chart configuration was valid.
422
+
423
+ ## Environment Variables
424
+
425
+ All settings can be configured via environment variables, making it easy to configure in Docker or CI.
426
+
427
+ | Variable | Default | Description |
428
+ |----------|---------|-------------|
429
+ | `PORT` | `3000` | HTTP server port |
430
+ | `HOST` | `0.0.0.0` | HTTP server bind address |
431
+ | `API_KEY` | *(none)* | API key for authentication |
432
+ | `CONCURRENCY` | `8` | Max simultaneous renders |
433
+ | `CACHE_MAX_ENTRIES` | `1000` | Max cached images in memory |
434
+ | `CACHE_TTL_SECONDS` | `3600` | Cache entry lifetime (seconds) |
435
+ | `MAX_RENDER_TIME_SECONDS` | `30` | Per-render upper bound (goto + waitForFunction timeout) |
436
+ | `PAGE_TIMEOUT_SECONDS` | *(derived)* | Override the safety-net force-close timer. Defaults to `MAX_RENDER_TIME_SECONDS * 2 + 10s` |
437
+
438
+ ## Included Plugins
439
+
440
+ All plugins are loaded from CDN inside the headless browser. No extra installation needed.
441
+
442
+ ### Core
443
+
444
+ | Plugin | Version | Description |
445
+ |--------|---------|-------------|
446
+ | [chart.js](https://www.chartjs.org/) | 4.4.9 | Chart.js core |
447
+
448
+ ### Plugins
449
+
450
+ | Plugin | Version | Description |
451
+ |--------|---------|-------------|
452
+ | [chartjs-plugin-datalabels](https://chartjs-plugin-datalabels.netlify.app/) | 2.2.0 | Display values on chart elements |
453
+ | [chartjs-plugin-annotation](https://www.chartjs.org/chartjs-plugin-annotation/) | 3.1.0 | Threshold lines, boxes, labels |
454
+ | [chartjs-plugin-zoom](https://www.chartjs.org/chartjs-plugin-zoom/) | 2.2.0 | Zoom and pan (initial range) |
455
+ | [chartjs-plugin-gradient](https://github.com/kurkle/chartjs-plugin-gradient) | 0.6.1 | Easy gradient fills |
456
+
457
+ ### Additional Chart Types
458
+
459
+ | Plugin | Version | Description |
460
+ |--------|---------|-------------|
461
+ | [chartjs-chart-matrix](https://chartjs-chart-matrix.pages.dev/) | 2.0.1 | Heatmaps and matrix charts |
462
+ | [chartjs-chart-sankey](https://github.com/kurkle/chartjs-chart-sankey) | 0.12.1 | Sankey diagrams |
463
+ | [chartjs-chart-treemap](https://chartjs-chart-treemap.pages.dev/) | 2.3.1 | Treemap charts |
464
+ | [chartjs-chart-wordcloud](https://github.com/sgratzl/chartjs-chart-wordcloud) | 4.4.3 | Word clouds |
465
+ | [chartjs-chart-geo](https://github.com/sgratzl/chartjs-chart-geo) | 4.3.3 | Choropleth and bubble maps |
466
+ | [chartjs-chart-graph](https://github.com/sgratzl/chartjs-chart-graph) | 4.3.3 | Network graphs |
467
+ | [chartjs-chart-venn](https://github.com/sgratzl/chartjs-chart-venn) | 4.3.3 | Venn and Euler diagrams |
468
+
469
+ ### Date Adapter
470
+
471
+ | Plugin | Version | Description |
472
+ |--------|---------|-------------|
473
+ | [chartjs-adapter-date-fns](https://github.com/chartjs/chartjs-adapter-date-fns) | 3.0.0 | date-fns adapter for time-series axes (bundled build — date-fns included) |
474
+
475
+ ## Docker
476
+
477
+ ### Build
478
+
479
+ ```bash
480
+ docker build -t chartjs2img .
481
+ ```
482
+
483
+ ### Run
484
+
485
+ ```bash
486
+ docker run -p 3000:3000 chartjs2img
487
+
488
+ # With configuration
489
+ docker run -p 3000:3000 \
490
+ -e API_KEY=mysecret \
491
+ -e CONCURRENCY=4 \
492
+ chartjs2img
493
+ ```
494
+
495
+ The Docker image includes:
496
+ - Bun runtime
497
+ - Playwright + Chromium (headless)
498
+ - Noto Sans CJK fonts (Japanese, Chinese, Korean — no tofu characters)
499
+
500
+ ### Docker Compose
501
+
502
+ ```yaml
503
+ services:
504
+ chartjs2img:
505
+ build: .
506
+ ports:
507
+ - "3000:3000"
508
+ environment:
509
+ - API_KEY=mysecret
510
+ - CONCURRENCY=8
511
+ - CACHE_MAX_ENTRIES=2000
512
+ - CACHE_TTL_SECONDS=7200
513
+ ```
514
+
515
+ ## Building a Single Binary
516
+
517
+ Bun can compile the entire project into a standalone executable:
518
+
519
+ ```bash
520
+ bun run build
521
+ # or directly:
522
+ bun build src/index.ts --compile --outfile chartjs2img
523
+ ```
524
+
525
+ This produces a `./chartjs2img` binary that can be distributed without requiring Bun or Node.js on the target machine.
526
+
527
+ > **Note:** Playwright's Chromium browser is **not** bundled into the binary, but it will be **downloaded automatically** on first run if not found. Just distribute the binary — everything else is handled.
528
+
529
+ ```bash
530
+ # Use the compiled binary — Chromium auto-installs on first run
531
+ ./chartjs2img serve --port 3000
532
+ ./chartjs2img render -i chart.json -o chart.png
533
+ ```
534
+
535
+ ## Claude Code Plugin
536
+
537
+ `plugins/chartjs2img/` is a self-contained [Claude Code plugin](https://docs.claude.com/en/docs/claude-code/plugins) that exposes three Agent Skills:
538
+
539
+ | Skill | Purpose |
540
+ |-------|---------|
541
+ | `/chartjs2img-render` | Render a Chart.js config JSON to PNG / JPEG, surfacing `X-Chart-Messages` / stderr warnings |
542
+ | `/chartjs2img-author` | Compose a new Chart.js config from a natural-language description, then validate via render-and-iterate |
543
+ | `/chartjs2img-install` | Install or update the `chartjs2img` CLI from the GitHub Releases of `ideamans/chartjs2img` |
544
+
545
+ Install into Claude Code by pointing at this repository's `plugins/chartjs2img/` directory (or by publishing / distributing that folder on its own). The plugin manifest lives at `plugins/chartjs2img/.claude-plugin/plugin.json` and its version is kept in sync with `package.json`.
546
+
547
+ When editing skill bodies, regenerate the AI-facing artifacts with `bun run ai:regen` and validate with `bun run validate-plugin-skills`.
548
+
549
+ ## Documentation Site
550
+
551
+ Full English + Japanese docs are built with VitePress and deployed to <https://chartjs2img.ideamans.com>:
552
+
553
+ - `docs/en/**`, `docs/ja/**` — hand-written guide, CLI / HTTP / Docker references, examples gallery, developer notes
554
+ - `docs/public/llms.txt`, `docs/public/llms-full.txt` — LLM-oriented index + full bundle (generated by `bun run ai:regen`)
555
+ - `docs/public/examples/**` — PNG + JSON for every built-in example (generated by `bun run docs:examples`)
556
+
557
+ Run `bun run docs:dev` to preview locally. Do **not** hand-edit the generated files — see `.claude/rules/ai-artifacts-policy.md` for the SSOT mapping.
558
+
559
+ ## Architecture
560
+
561
+ ```
562
+ Request flow:
563
+
564
+ HTTP Request
565
+
566
+
567
+ ┌─────────┐ ┌──────────┐
568
+ │ Auth │───▶│ Cache │──▶ Cache Hit → return image
569
+ │ Check │ │ Lookup │
570
+ └─────────┘ └──────────┘
571
+ │ Cache Miss
572
+
573
+ ┌──────────┐
574
+ │ Semaphore │──▶ Wait if at max concurrency
575
+ └──────────┘
576
+ │ Acquired
577
+
578
+ ┌──────────┐
579
+ │ Ensure │──▶ Launch browser if not running
580
+ │ Browser │ Auto-restart if crashed
581
+ └──────────┘
582
+
583
+
584
+ ┌──────────┐
585
+ │ New Page │──▶ Fresh tab with 60s timeout
586
+ │ (Tab) │
587
+ └──────────┘
588
+
589
+
590
+ ┌──────────┐
591
+ │ Render │──▶ Load HTML + Chart.js + plugins
592
+ │ Chart │ Screenshot canvas element
593
+ └──────────┘
594
+
595
+
596
+ ┌──────────┐
597
+ │ Store │──▶ Cache by SHA-256 hash
598
+ │ Cache │ Return image + cache headers
599
+ └──────────┘
600
+
601
+
602
+ Close page, release semaphore
603
+ ```
604
+
605
+ ## License
606
+
607
+ MIT
@@ -0,0 +1,16 @@
1
+ import type { RenderOptions } from './template';
2
+ interface CacheEntry {
3
+ buffer: Buffer;
4
+ contentType: string;
5
+ createdAt: number;
6
+ }
7
+ export declare function computeHash(options: RenderOptions): string;
8
+ export declare function getCache(hash: string): CacheEntry | undefined;
9
+ export declare function setCache(hash: string, buffer: Buffer, contentType: string): void;
10
+ export declare function cacheStats(): {
11
+ size: number;
12
+ maxEntries: number;
13
+ ttlSeconds: number;
14
+ };
15
+ export {};
16
+ //# sourceMappingURL=cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE/C,UAAU,UAAU;IAClB,MAAM,EAAE,MAAM,CAAA;IACd,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;CAClB;AA8BD,wBAAgB,WAAW,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CAW1D;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAQ7D;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI,CAOhF;AAED,wBAAgB,UAAU,IAAI;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAErF"}
package/dist/cache.js ADDED
@@ -0,0 +1,60 @@
1
+ import { createHash } from 'crypto';
2
+ const store = new Map();
3
+ /** Max cache entries (env: CACHE_MAX_ENTRIES, default: 1000) */
4
+ const MAX_ENTRIES = Number(process.env.CACHE_MAX_ENTRIES ?? '1000');
5
+ /** Cache TTL in ms (env: CACHE_TTL_SECONDS, default: 3600) */
6
+ const TTL_MS = Number(process.env.CACHE_TTL_SECONDS ?? '3600') * 1000;
7
+ /**
8
+ * JSON stringifier with a stable key order. Two semantically identical
9
+ * objects ({a:1,b:2} and {b:2,a:1}) must produce the same output so
10
+ * the cache hash depends only on meaning, not on how the caller happened
11
+ * to construct the object. Arrays keep their order (meaningful); object
12
+ * keys are sorted.
13
+ */
14
+ function stableStringify(v) {
15
+ if (v === null || typeof v !== 'object')
16
+ return JSON.stringify(v);
17
+ if (Array.isArray(v))
18
+ return '[' + v.map(stableStringify).join(',') + ']';
19
+ const keys = Object.keys(v).sort();
20
+ return ('{' +
21
+ keys
22
+ .map((k) => JSON.stringify(k) + ':' + stableStringify(v[k]))
23
+ .join(',') +
24
+ '}');
25
+ }
26
+ export function computeHash(options) {
27
+ const json = stableStringify({
28
+ chart: options.chart,
29
+ width: options.width ?? 800,
30
+ height: options.height ?? 600,
31
+ devicePixelRatio: options.devicePixelRatio ?? 2,
32
+ backgroundColor: options.backgroundColor ?? 'white',
33
+ format: options.format ?? 'png',
34
+ quality: options.quality ?? 90,
35
+ });
36
+ return createHash('sha256').update(json).digest('hex').slice(0, 16);
37
+ }
38
+ export function getCache(hash) {
39
+ const entry = store.get(hash);
40
+ if (!entry)
41
+ return undefined;
42
+ if (Date.now() - entry.createdAt > TTL_MS) {
43
+ store.delete(hash);
44
+ return undefined;
45
+ }
46
+ return entry;
47
+ }
48
+ export function setCache(hash, buffer, contentType) {
49
+ // Evict oldest if at capacity
50
+ if (store.size >= MAX_ENTRIES) {
51
+ const oldest = store.keys().next().value;
52
+ if (oldest)
53
+ store.delete(oldest);
54
+ }
55
+ store.set(hash, { buffer, contentType, createdAt: Date.now() });
56
+ }
57
+ export function cacheStats() {
58
+ return { size: store.size, maxEntries: MAX_ENTRIES, ttlSeconds: TTL_MS / 1000 };
59
+ }
60
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"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"}