@taylorwong/ichartjs 2.0.3 → 2.0.5
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/CHANGELOG.md +27 -0
- package/README.md +14 -6
- package/docs/agent/README.md +2 -1
- package/docs/agent/coding-agent-integration.md +8 -4
- package/docs/agent/development/2.0-release-readiness.md +2 -0
- package/docs/agent/development/iteration-10.md +32 -0
- package/docs/agent/development/release-sop.md +2 -2
- package/docs/agent/development/roadmap.md +4 -3
- package/docs/agent/frontend-integration.md +5 -1
- package/docs/agent/quickstart.md +9 -6
- package/docs/agent/runtime-contract.md +5 -5
- package/docs/agent/usage-scenarios.md +161 -0
- package/docs/agent/zh-CN/README.md +1 -0
- package/docs/agent/zh-CN/coding-agent-integration.md +1 -1
- package/docs/agent/zh-CN/diagram-scenario.md +3 -3
- package/docs/agent/zh-CN/frontend-integration.md +1 -1
- package/docs/agent/zh-CN/quickstart.md +6 -6
- package/docs/agent/zh-CN/runtime-contract.md +5 -5
- package/docs/agent/zh-CN/usage-scenarios.md +152 -0
- package/docs/manifests/capabilities.json +5 -4
- package/package.json +1 -1
- package/skills/ichartjs/SKILL.md +12 -2
- package/src/capabilities.mjs +3 -3
- package/src/index.mjs +68 -48
- package/src/renderer.mjs +3 -3
- package/src/spec.mjs +3 -0
- package/types/index.d.ts +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.0.5 - 2026-09-18
|
|
4
|
+
|
|
5
|
+
- Hardened JSON, SVG, PNG, and JPEG export contracts with deterministic type errors, JSON output representations, and an explicit optional Node `canvas` path through `exportAsync()`.
|
|
6
|
+
- Aligned Canvas and SVG background, fill, stroke, and transparent-paint behavior; normalized `theme.branding` into the shared branding contract.
|
|
7
|
+
- Hardened the Playground server's port detection, path containment, malformed URI handling, and escaped 404 responses.
|
|
8
|
+
- Added bilingual usage-scenario guidance for Runtime integration, Coding Agents, the official Skill, one-off artifacts, project/diagram workflows, and CI reports.
|
|
9
|
+
- Expanded export, headless Canvas, branding, documentation, and package regression coverage.
|
|
10
|
+
|
|
11
|
+
## 2.0.4 - 2026-09-18
|
|
12
|
+
|
|
13
|
+
- Synchronized current Agent documentation, scoped package paths, GitHub fallback instructions, Skill links, Playground links, and release status.
|
|
14
|
+
- Corrected Runtime and Playground version displays to `2.0.4`.
|
|
15
|
+
- Marked the original `2.0.0` release-readiness document as historical and added stronger documentation consistency checks.
|
|
16
|
+
|
|
17
|
+
## 2.0.3 - 2026-09-17
|
|
18
|
+
|
|
19
|
+
- Completed Iteration 8 acceptance documentation and added the author-only release SOP.
|
|
20
|
+
- Added README and official Skill release guardrails for the scoped npm package and controlled publishing workflow.
|
|
21
|
+
|
|
22
|
+
## 2.0.2 - 2026-09-17
|
|
23
|
+
|
|
24
|
+
- Fixed Line and Area rendering so `fill: 'none'` does not create an unintended filled area.
|
|
25
|
+
|
|
26
|
+
## 2.0.1 - 2026-09-17
|
|
27
|
+
|
|
28
|
+
- Added dual-engine raster export, branding signature support, and synchronized documentation and TypeScript declarations.
|
|
29
|
+
|
|
3
30
|
## 2.0.0 - 2026-09-16
|
|
4
31
|
|
|
5
32
|
- Added Iteration 9's lightweight visual style system with adaptive modes, semantic presets and palettes, live switching, contrast diagnostics, Agent recommendations, bilingual guidance, and Theme Gallery while retaining version `2.0.0`.
|
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
iChart.js is an Agent-first, renderer-independent visualization runtime. An Agent can inspect data, choose an appropriate chart, generate and validate a JSON-friendly Spec, render it with SVG or Canvas, and explain the result through public APIs.
|
|
4
4
|
|
|
5
|
+
Use it in one of three ways: integrate the Runtime into a web project, let a Coding Agent modify a project, or install the official Skill so an Agent can orchestrate chart selection and delivery. The Runtime produces interactive pages, SVG/PNG/JPEG artifacts, JSON checkpoints, and integration code; the Skill does not replace the Runtime.
|
|
6
|
+
|
|
5
7
|
## For AI Agents
|
|
6
8
|
|
|
7
9
|
Do not guess chart types or configuration fields from source code. Use the public planning contract:
|
|
@@ -20,10 +22,11 @@ getCapabilities
|
|
|
20
22
|
|
|
21
23
|
| Need | Entry point |
|
|
22
24
|
| --- | --- |
|
|
23
|
-
| Runtime and Agent planning APIs |
|
|
24
|
-
| Machine-readable capability manifest |
|
|
25
|
-
| Intent and chart recipes |
|
|
25
|
+
| Runtime and Agent planning APIs | `@taylorwong/ichartjs` |
|
|
26
|
+
| Machine-readable capability manifest | `@taylorwong/ichartjs/capabilities.json` |
|
|
27
|
+
| Intent and chart recipes | `@taylorwong/ichartjs/recipes/*` |
|
|
26
28
|
| Agent quickstart | [`docs/agent/quickstart.md`](docs/agent/quickstart.md) |
|
|
29
|
+
| Usage scenarios and output formats | [`docs/agent/usage-scenarios.md`](docs/agent/usage-scenarios.md) |
|
|
27
30
|
| Chinese quickstart | [`docs/agent/zh-CN/quickstart.md`](docs/agent/zh-CN/quickstart.md) |
|
|
28
31
|
| Coding Agent integration | [`docs/agent/coding-agent-integration.md`](docs/agent/coding-agent-integration.md) |
|
|
29
32
|
| Frontend integration | [`docs/agent/frontend-integration.md`](docs/agent/frontend-integration.md) |
|
|
@@ -36,7 +39,7 @@ getCapabilities
|
|
|
36
39
|
npm install @taylorwong/ichartjs@^2
|
|
37
40
|
```
|
|
38
41
|
|
|
39
|
-
As a fallback for environments without npm access, install directly from GitHub: `npm install github:wanghetommy/ichartjs#v2.0.
|
|
42
|
+
As a fallback for environments without npm access, install directly from GitHub: `npm install github:wanghetommy/ichartjs#v2.0.5`.
|
|
40
43
|
|
|
41
44
|
### Optional Agent Skill
|
|
42
45
|
|
|
@@ -52,7 +55,7 @@ From a repository checkout, install it into Codex with:
|
|
|
52
55
|
cp -R skills/ichartjs "${CODEX_HOME:-$HOME/.codex}/skills/"
|
|
53
56
|
```
|
|
54
57
|
|
|
55
|
-
WorkBuddy users can import or register the same `skills/ichartjs` folder through the host's Skill interface. Package consumers can copy it from `node_modules/ichartjs/skills/ichartjs` into their Agent host's Skill directory. After installation, invoke it as `$ichartjs` when the host supports named Skill invocation, or select the `ichartjs` Skill in the host UI.
|
|
58
|
+
WorkBuddy users can import or register the same `skills/ichartjs` folder through the host's Skill interface. Package consumers can copy it from `node_modules/@taylorwong/ichartjs/skills/ichartjs` into their Agent host's Skill directory. After installation, invoke it as `$ichartjs` when the host supports named Skill invocation, or select the `ichartjs` Skill in the host UI.
|
|
56
59
|
|
|
57
60
|
### Agent workflow
|
|
58
61
|
|
|
@@ -140,9 +143,14 @@ npm run playground
|
|
|
140
143
|
- Playground Home: `http://localhost:3000/playground/index.html`
|
|
141
144
|
- Agent Workbench: `http://localhost:3000/playground/agent-workbench.html`
|
|
142
145
|
- Complete Gallery: `http://localhost:3000/playground/project-gallery.html`
|
|
146
|
+
- Foundational Gallery: `http://localhost:3000/playground/foundational-gallery.html`
|
|
143
147
|
- Theme Gallery: `http://localhost:3000/playground/theme-gallery.html`
|
|
148
|
+
- Business Editing: `http://localhost:3000/playground/editing.html`
|
|
144
149
|
- Project Intelligence: `http://localhost:3000/playground/project-intelligence.html`
|
|
145
150
|
- Diagram Editor: `http://localhost:3000/playground/diagram-editor.html`
|
|
151
|
+
- Interaction Lab: `http://localhost:3000/playground/interaction-lab.html`
|
|
152
|
+
- Accessibility Lab: `http://localhost:3000/playground/accessibility-lab.html`
|
|
153
|
+
- Performance Lab: `http://localhost:3000/playground/performance-lab.html`
|
|
146
154
|
|
|
147
155
|
## For Developers
|
|
148
156
|
|
|
@@ -199,7 +207,7 @@ Contributors should:
|
|
|
199
207
|
1. Open PRs / commit only to `develop`
|
|
200
208
|
2. Ensure `npm run agent:check` and `npm test` are green
|
|
201
209
|
|
|
202
|
-
Full author-only release SOP (
|
|
210
|
+
Full author-only release SOP (preconditions, release sequence, must-not rules, and rollback procedure):
|
|
203
211
|
👉 [`docs/agent/development/release-sop.md`](docs/agent/development/release-sop.md)
|
|
204
212
|
|
|
205
213
|
Start with [`docs/agent/quickstart.md`](docs/agent/quickstart.md). Detailed runtime, charting, project, diagram, and editing contracts live in [`docs/agent/`](docs/agent/).
|
package/docs/agent/README.md
CHANGED
|
@@ -11,6 +11,7 @@ This is the user-facing Agent entry point for iChart.js 2.0. Read this file firs
|
|
|
11
11
|
## Start Here
|
|
12
12
|
|
|
13
13
|
- [Agent Quickstart](quickstart.md): install, import, plan, validate, render, explain, and self-check.
|
|
14
|
+
- [Usage Scenarios](usage-scenarios.md): choose Runtime, Coding Agent, Skill, one-off artifact, project, diagram, or CI usage and select the right output.
|
|
14
15
|
- [Coding Agent Integration](coding-agent-integration.md): use from Codex and similar code-editing Agents.
|
|
15
16
|
- [Frontend Integration](frontend-integration.md): use from ordinary JavaScript applications.
|
|
16
17
|
- [Visual Style and Themes](theme-guide.md): automatic matching, presets, palettes, switching, and contrast checks.
|
|
@@ -21,7 +22,7 @@ This is the user-facing Agent entry point for iChart.js 2.0. Read this file firs
|
|
|
21
22
|
- [Editing Contract](editing-contract.md): schemas, commands, preview, commit, and undo/redo.
|
|
22
23
|
- Machine-readable capability manifests are in `docs/manifests/` and should be loaded on demand.
|
|
23
24
|
|
|
24
|
-
The 2.0 API is Spec-first. Import from
|
|
25
|
+
The 2.0 API is Spec-first. Import from `@taylorwong/ichartjs`, inspect data, plan a chart, create a JSON-friendly Spec, validate it, render it, and self-check the explanation and runtime state.
|
|
25
26
|
|
|
26
27
|
## Recommended flow
|
|
27
28
|
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
Use this guide with Codex, WorkBuddy, and similar Agents that can read a repository, edit JavaScript, execute tests, and open a local browser preview.
|
|
4
4
|
|
|
5
|
+
For the complete choice between project integration, one-off artifacts, Skill orchestration, and CI output, read [Usage Scenarios](usage-scenarios.md).
|
|
6
|
+
|
|
5
7
|
## Responsibility Boundary
|
|
6
8
|
|
|
7
|
-
iChart.js is a JavaScript UI component library. The coding Agent writes or updates the host application and calls the public
|
|
9
|
+
iChart.js is a JavaScript UI component library. The coding Agent writes or updates the host application and calls the public `@taylorwong/ichartjs` ESM API. The optional Skill provides workflow guidance only; it does not introduce a second runtime or service.
|
|
8
10
|
|
|
9
11
|
Do not add a CLI, MCP server, HTTP API, or Python adapter to complete an ordinary charting task.
|
|
10
12
|
|
|
@@ -16,7 +18,7 @@ Inside a consumer project:
|
|
|
16
18
|
npm install @taylorwong/ichartjs@^2
|
|
17
19
|
```
|
|
18
20
|
|
|
19
|
-
Do not install the unscoped npm registry package named `ichartjs`; it is
|
|
21
|
+
Do not install the unscoped npm registry package named `ichartjs`; it is an npm security holding package and is not this project. The published package is `@taylorwong/ichartjs`.
|
|
20
22
|
|
|
21
23
|
Inside this repository:
|
|
22
24
|
|
|
@@ -46,7 +48,7 @@ If the host supports named Skill invocation, the request may start with `Use $ic
|
|
|
46
48
|
## Required Workflow
|
|
47
49
|
|
|
48
50
|
1. Inspect existing application structure and local instructions.
|
|
49
|
-
2. Import public APIs from
|
|
51
|
+
2. Import public APIs from `@taylorwong/ichartjs`.
|
|
50
52
|
3. Call `getCapabilities()`, `inspectData()`, and `planChart()`.
|
|
51
53
|
4. Stop or ask for input when required fields are missing.
|
|
52
54
|
5. Build a JSON-friendly Spec and call `validateSpec()`.
|
|
@@ -80,9 +82,11 @@ Use `examples/agent-workflow.mjs` for the full executable lifecycle.
|
|
|
80
82
|
|
|
81
83
|
## Acceptance
|
|
82
84
|
|
|
83
|
-
- The application imports only
|
|
85
|
+
- The application imports only `@taylorwong/ichartjs`, not source internals.
|
|
84
86
|
- The chosen chart exists in capabilities.
|
|
85
87
|
- Validation passes before rendering.
|
|
86
88
|
- Warnings and assumptions remain visible.
|
|
87
89
|
- Stable source IDs remain in explanation lineage.
|
|
88
90
|
- The user receives a working preview URL.
|
|
91
|
+
|
|
92
|
+
The final Agent response should also state whether the deliverable is a live component, code change, SVG/PNG/JPEG artifact, JSON checkpoint, or a combination of these.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# iChart.js 2.0 Final Release Readiness
|
|
2
2
|
|
|
3
|
+
> Historical record. This document describes the original `2.0.0` source-release decision and is superseded by the published `2.0.x` release line. Use `release-sop.md` for current releases.
|
|
4
|
+
|
|
3
5
|
Date: 2026-09-16
|
|
4
6
|
|
|
5
7
|
## Decision
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Iteration 10 — Export Contract Hardening
|
|
2
|
+
|
|
3
|
+
Iteration 10A hardens the v2.0 export contract without adding chart types or changing chart behavior. The work targets `v2.0.5`, stays on the `2.0.x` line, and keeps the existing dual-engine, single-Scene-Graph architecture.
|
|
4
|
+
|
|
5
|
+
## 10A — Export Contract Hardening
|
|
6
|
+
|
|
7
|
+
- Keep JSON, SVG, PNG, and JPEG export type detection deterministic.
|
|
8
|
+
- Return stable structured errors for unknown formats and unavailable headless raster paths.
|
|
9
|
+
- Support JSON `string`, `object`, `dataurl`, and `blob` representations.
|
|
10
|
+
- Provide an explicit `exportAsync()` path for optional Node `canvas` raster export.
|
|
11
|
+
- Prevent headless Canvas calls from throwing when no canvas surface is mounted.
|
|
12
|
+
- Keep Canvas and SVG fill, stroke, background, and transparent-paint semantics aligned.
|
|
13
|
+
- Resolve `theme.branding` into the normalized top-level branding contract.
|
|
14
|
+
- Fix playground port detection, path traversal checks, malformed URI handling, and escaped 404 paths.
|
|
15
|
+
- Add regression coverage for export representations, unsupported types, headless Canvas, and theme branding.
|
|
16
|
+
|
|
17
|
+
## Acceptance
|
|
18
|
+
|
|
19
|
+
- `npm run agent:check` passes.
|
|
20
|
+
- `npm run example:agent` passes.
|
|
21
|
+
- `git diff --check` passes.
|
|
22
|
+
- Browser preview remains available at `http://localhost:3000/playground/project-gallery.html` through `npm run playground`.
|
|
23
|
+
- No new public chart type or chart behavior is introduced by 10A.
|
|
24
|
+
|
|
25
|
+
## v2.0.5 Release Readiness
|
|
26
|
+
|
|
27
|
+
- Package, Runtime, Playground, documentation home, GitHub fallback instructions, Changelog, and Roadmap identify `2.0.5` consistently.
|
|
28
|
+
- `npm run agent:check` passes with 61 tests, 16 chart types, 24 edit commands, and 7 business schemas.
|
|
29
|
+
- `npm run example:agent` and `git diff --check` pass.
|
|
30
|
+
- `npm pack --dry-run` reports `@taylorwong/ichartjs@2.0.5`, 83 package entries, and includes the English/Chinese usage guides plus this Iteration 10 record.
|
|
31
|
+
- npm identity is `taylorwong`, Registry is `https://registry.npmjs.org/`, and the currently published package remains `2.0.4` before release.
|
|
32
|
+
- Author-only commit, tag, push, npm publish, and `develop` to `master` merge remain pending under `release-sop.md`.
|
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
>
|
|
9
9
|
> **Agent Policy:** An Agent MUST NOT initiate any step in this SOP on its own.
|
|
10
10
|
> It MAY execute the sequence **only when explicitly instructed** by the author
|
|
11
|
-
> (e.g. "publish
|
|
11
|
+
> (e.g. "publish vX.Y.Z", "commit and release patch", or equivalent intent).
|
|
12
12
|
|
|
13
13
|
## 0. Preconditions (MUST ALL be green)
|
|
14
14
|
|
|
15
15
|
0. `git status -sb` → on `develop`, working tree clean
|
|
16
16
|
1. `npm run check` → exit 0
|
|
17
|
-
2. `npm test` → exit 0 (
|
|
17
|
+
2. `npm test` → exit 0 (all current tests pass)
|
|
18
18
|
3. `npm run docs:check` → exit 0
|
|
19
19
|
4. `npm run agent:check` → exit 0
|
|
20
20
|
5. `npm whoami` → output = `taylorwong`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# iChart.js 2.0 Roadmap
|
|
2
2
|
|
|
3
|
-
> Roadmap baseline: 2026-09-14. Geographic charts and 3D rendering remain out of scope until explicitly reintroduced.
|
|
3
|
+
> Roadmap baseline: 2026-09-14. Current release status: `v2.0.4` is published and `v2.0.5` is prepared as the Iteration 10A export-contract and usage-guidance patch release. Geographic charts and 3D rendering remain out of scope until explicitly reintroduced.
|
|
4
4
|
|
|
5
5
|
## Current Status
|
|
6
6
|
|
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
- Iteration 6 project intelligence local runtime and browser acceptance are complete.
|
|
12
12
|
- Iteration 7 local runtime and browser acceptance are complete for foundational composition, Heatmap, and Radar; physical-device checks remain host integration evidence.
|
|
13
13
|
- Iteration 8A–8D is complete and accepted in automated tests, Chromium, Firefox 144, WebKit 26, native Safari 26.6.2, and a 390 px touch viewport. Physical iOS/Android and representative release-host measurements remain post-release host/device follow-up. No new public chart type was introduced.
|
|
14
|
-
- Iteration 9A–9D
|
|
15
|
-
-
|
|
14
|
+
- Iteration 9A–9D implemented the lightweight visual style system, adaptive theme planning, runtime switching, renderer integration, and bilingual Agent guidance without adding a chart type; these capabilities remain in the `2.0.x` line.
|
|
15
|
+
- Iteration 10A Export Contract Hardening is implemented on `develop`: export representations and type errors are deterministic, optional Node raster export uses `exportAsync()`, Canvas/SVG paint semantics are aligned, and the playground server has safer port/path handling. No chart behavior or public chart type was added.
|
|
16
|
+
- The original `2.0.0` readiness record is historical and superseded by the published `2.0.x` releases. Current release checks are defined by `docs/agent/development/release-sop.md`.
|
|
16
17
|
|
|
17
18
|
## Iteration 4 — Agent Data Contract and Business Editing
|
|
18
19
|
|
|
@@ -2,13 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
Use iChart.js as an ordinary JavaScript UI component inside a browser application. The host application owns data loading, authentication, persistence, routing, and any assistant or natural-language interface.
|
|
4
4
|
|
|
5
|
+
This is the production component path. For one-off files or Agent-led repository changes, use [Usage Scenarios](usage-scenarios.md) and the [Coding Agent Integration](coding-agent-integration.md) guide instead.
|
|
6
|
+
|
|
5
7
|
## Install
|
|
6
8
|
|
|
7
9
|
```bash
|
|
8
10
|
npm install @taylorwong/ichartjs@^2
|
|
9
11
|
```
|
|
10
12
|
|
|
11
|
-
For environments without npm registry access, install from GitHub as a fallback: `npm install github:wanghetommy/ichartjs#v2.0.
|
|
13
|
+
For environments without npm registry access, install from GitHub as a fallback: `npm install github:wanghetommy/ichartjs#v2.0.5`.
|
|
12
14
|
|
|
13
15
|
Use the package through a bundler or another environment that resolves npm ESM imports:
|
|
14
16
|
|
|
@@ -64,3 +66,5 @@ Add these only in an application that has a demonstrated requirement. Do not dup
|
|
|
64
66
|
## Preview
|
|
65
67
|
|
|
66
68
|
Within this repository, run `npm run playground` and open `http://localhost:3000/playground/project-gallery.html` to inspect every public chart type.
|
|
69
|
+
|
|
70
|
+
In a consumer application, use that application's own development server and return its URL. The Playground is an example and acceptance surface, not a required production dependency.
|
package/docs/agent/quickstart.md
CHANGED
|
@@ -4,6 +4,8 @@ Use this guide when an Agent needs to turn user data or project information into
|
|
|
4
4
|
|
|
5
5
|
## Import Surface
|
|
6
6
|
|
|
7
|
+
Choose the integration mode before starting: use the Runtime for a product component, the Coding Agent guide when modifying a repository, or the official Skill when an Agent must repeatedly plan and deliver charts. See [Usage Scenarios](usage-scenarios.md) for the input/output contract.
|
|
8
|
+
|
|
7
9
|
```js
|
|
8
10
|
import {
|
|
9
11
|
createChart,
|
|
@@ -15,14 +17,14 @@ import {
|
|
|
15
17
|
} from '@taylorwong/ichartjs';
|
|
16
18
|
```
|
|
17
19
|
|
|
18
|
-
Agents and developers use the same
|
|
20
|
+
Agents and developers use the same `@taylorwong/ichartjs` ESM entry. Agent behavior comes from the public planning APIs and optional Skill, not from a second runtime.
|
|
19
21
|
|
|
20
22
|
Additional package resources:
|
|
21
23
|
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
24
|
+
- `@taylorwong/ichartjs/capabilities.json`: machine-readable catalog, including per-chart exports, branding, and interaction declarations.
|
|
25
|
+
- `@taylorwong/ichartjs/recipes/foundational-analysis`: foundational chart recipes.
|
|
26
|
+
- `@taylorwong/ichartjs/recipes/project-management`: project intelligence recipes.
|
|
27
|
+
- `@taylorwong/ichartjs/recipes/diagrams/workflow`: diagram editing recipe.
|
|
26
28
|
- `skills/ichartjs/SKILL.md`: optional workflow adapter for Codex, WorkBuddy, and other Agent Skills-compatible hosts.
|
|
27
29
|
|
|
28
30
|
The Skill is not the runtime. Install or register it only when the Agent host supports Skills; it must still call the package's public APIs and capability contract.
|
|
@@ -140,10 +142,11 @@ const jsonString = chart.export({ type: 'json' });
|
|
|
140
142
|
// SVG vector export (zero-dependency, browser + headless)
|
|
141
143
|
const svgString = chart.export({ type: 'svg' });
|
|
142
144
|
|
|
143
|
-
// PNG raster (browser
|
|
145
|
+
// PNG raster (browser sync; Node headless uses the optional canvas package)
|
|
144
146
|
const pngDataUrl = typeof document !== 'undefined'
|
|
145
147
|
? chart.toDataURL('image/png')
|
|
146
148
|
: null;
|
|
149
|
+
const headlessPng = await chart.exportAsync({ type: 'png' });
|
|
147
150
|
|
|
148
151
|
// Browser-only convenience downloads
|
|
149
152
|
if (typeof document !== 'undefined') {
|
|
@@ -50,7 +50,7 @@ When `branding:false`, no signature text appears on the chart or any exported ar
|
|
|
50
50
|
## Exports and Downloads
|
|
51
51
|
|
|
52
52
|
iChart.js exports use a **dual-engine single-source architecture**. Every artifact shares the same Scene Graph produced by `buildScene()`, and the on-screen renderer is fully decoupled from the export backend:
|
|
53
|
-
1. **PNG / JPEG (raster)**: always drawn by a `CanvasRenderer` replaying the shared Scene Graph
|
|
53
|
+
1. **PNG / JPEG (raster)**: always drawn by a `CanvasRenderer` replaying the shared Scene Graph. Browser export is synchronous; Node headless export uses `exportAsync()` with the optional `canvas` npm package.
|
|
54
54
|
2. **SVG (vector)**: serialized from an `SVGRenderer` DOM tree (browser) or assembled as a pure string with zero dependencies (headless). Includes XML 1.0 header, expanded font properties, and accessibility attributes.
|
|
55
55
|
3. **JSON (reconstructable)**: serializes the current `spec` plus `getState()`, used for persistence, agent self-check, and cross-environment rebuild.
|
|
56
56
|
|
|
@@ -64,21 +64,21 @@ The signature gate inside `buildScene()` guarantees that live rendering, SVG exp
|
|
|
64
64
|
|-------------|------------------------|--------------------------|------------------------------|
|
|
65
65
|
| JSON | ✅ | ✅ | ✅ |
|
|
66
66
|
| SVG | ✅ | ✅ | ✅ |
|
|
67
|
-
| PNG / JPEG | ✅ true raster, sync | ❌ structured `HEADLESS_EXPORT_UNSUPPORTED` | ✅ |
|
|
67
|
+
| PNG / JPEG | ✅ true raster, sync | ❌ structured `HEADLESS_EXPORT_UNSUPPORTED` | ✅ via `exportAsync()` |
|
|
68
68
|
|
|
69
69
|
### Public Export APIs
|
|
70
70
|
|
|
71
71
|
- `chart.toDataURL(type='image/png')` → data URL string or structured ExportError.
|
|
72
72
|
- `chart.toBlob(type='image/png')` → Blob or ExportError (headless returns `BLOB_HEADLESS`).
|
|
73
73
|
- `chart.export({ type, as })` → sync string / JSON object / Blob / ExportError. `as` accepts `string`, `dataurl`, `blob`, `object` (JSON only).
|
|
74
|
-
- `chart.exportAsync({ type, as })` → Promise
|
|
74
|
+
- `chart.exportAsync({ type, as })` → Promise export path; enables optional Node `canvas` raster output.
|
|
75
75
|
- `chart.download({ type })` / `downloadPNG()` / `downloadSVG()` / `downloadJSON()` → triggers a browser save-as. In headless, falls back to returning the raw string or structured error.
|
|
76
76
|
|
|
77
77
|
### Structured Error Shape
|
|
78
78
|
|
|
79
79
|
All export/download methods return a stable `{ valid:false, code, message?, suggestion?, rasterCode? }` object on failure for reliable agent automation. Common `code` values:
|
|
80
|
-
- `HEADLESS_EXPORT_UNSUPPORTED`: raster export
|
|
81
|
-
- `BLOB_HEADLESS`: `toBlob` or `as=blob`
|
|
80
|
+
- `HEADLESS_EXPORT_UNSUPPORTED`: synchronous raster export in headless, or raster export without a usable canvas adapter.
|
|
81
|
+
- `BLOB_HEADLESS`: synchronous `toBlob` or headless SVG `as=blob` has no mounted/browser serialization surface; use `exportAsync()` for Node raster or `as=dataurl`/`as=string`.
|
|
82
82
|
- `DOWNLOAD_HEADLESS`: `chart.download*()` in headless; use `chart.export()` instead.
|
|
83
83
|
- `EXPORT_TYPE_UNSUPPORTED`: unrecognized export type.
|
|
84
84
|
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# iChart.js Usage Scenarios
|
|
2
|
+
|
|
3
|
+
iChart.js has three layers:
|
|
4
|
+
|
|
5
|
+
1. **Runtime** — `@taylorwong/ichartjs` executes planning, validation, rendering, interaction, editing, and export.
|
|
6
|
+
2. **Skill** — `skills/ichartjs/SKILL.md` teaches an Agent how to choose a chart, preserve data lineage, validate a Spec, and report the result.
|
|
7
|
+
3. **Host project** — the user's web app, Node script, CI job, or Agent workspace provides data, lifecycle, routing, storage, and delivery.
|
|
8
|
+
|
|
9
|
+
The Skill is not a second renderer or service. A Skill-enabled Agent still needs a JavaScript host to render an interactive chart or create a file.
|
|
10
|
+
|
|
11
|
+
## Choose a Scenario
|
|
12
|
+
|
|
13
|
+
| Need | Use | Runtime location | Typical output |
|
|
14
|
+
| --- | --- | --- | --- |
|
|
15
|
+
| Add a chart to a product | Frontend integration | User's browser app | Interactive SVG or Canvas chart |
|
|
16
|
+
| Ask Codex to change an existing app | Coding Agent + Runtime | Repository and app dev server | Code changes, Spec, tests, preview URL |
|
|
17
|
+
| Reuse charting guidance across Agents | Skill + Runtime | Agent workspace and target project | Validated code, artifacts, explanation |
|
|
18
|
+
| Create a one-off visualization | Node script or standalone HTML | Node.js or browser | SVG, PNG/JPEG, JSON, or HTML |
|
|
19
|
+
| Build project or delivery views | Project scenario + Runtime | Project dashboard | Gantt, Burndown, Timeline, analytics |
|
|
20
|
+
| Build a process or architecture view | Diagram scenario + Runtime | Web app or document workflow | Flow/Swimlane UI, SVG, JSON |
|
|
21
|
+
| Generate scheduled reports | Node script + Runtime | CI or report job | SVG/PNG files and JSON checkpoints |
|
|
22
|
+
|
|
23
|
+
## Output Contract
|
|
24
|
+
|
|
25
|
+
| Output | Best for | API or delivery |
|
|
26
|
+
| --- | --- | --- |
|
|
27
|
+
| Interactive page | Product dashboards, editors, analysis screens | `createChart()` mounted into a DOM container |
|
|
28
|
+
| SVG DOM | Accessibility, keyboard interaction, diagrams, printing | `renderer: 'svg'` |
|
|
29
|
+
| SVG file/string | Documents, email, vector handoff, zero-dependency headless output | `chart.export({ type: 'svg' })` |
|
|
30
|
+
| PNG/JPEG | Presentations, chat attachments, image reports | Browser `toDataURL()` or Node `exportAsync()` with optional `canvas` |
|
|
31
|
+
| JSON Spec/state | Persistence, Agent checkpoints, cross-environment rebuild | `chart.export({ type: 'json' })` |
|
|
32
|
+
| Data URL or Blob | Embedding or browser download | `as: 'dataurl'` or browser download APIs |
|
|
33
|
+
| Code snippet | Developer handoff or generated page | `createChart()` integration code |
|
|
34
|
+
| Explanation/state | Agent self-check and user-facing audit | `chart.explain()` and `chart.getState()` |
|
|
35
|
+
|
|
36
|
+
JSON is the machine-readable source of truth. SVG and PNG/JPEG are presentation artifacts. Code is the integration artifact. An interactive page is the product artifact.
|
|
37
|
+
|
|
38
|
+
## Scenario 1: Integrate into a Web Project
|
|
39
|
+
|
|
40
|
+
Install the runtime in the host application:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm install @taylorwong/ichartjs@^2
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Use the normal component lifecycle:
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
import { createChart } from '@taylorwong/ichartjs';
|
|
50
|
+
|
|
51
|
+
const chart = createChart({
|
|
52
|
+
container: '#chart',
|
|
53
|
+
type: 'line',
|
|
54
|
+
renderer: 'svg',
|
|
55
|
+
data: { values: rows },
|
|
56
|
+
encoding: {
|
|
57
|
+
x: { field: 'month', type: 'category' },
|
|
58
|
+
y: { field: 'sales', type: 'quantitative' }
|
|
59
|
+
},
|
|
60
|
+
accessibility: { enabled: true }
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The host application owns data loading, authentication, routing, persistence, and state management. Call `setData()` for row changes, `update()` for Spec changes, and `destroy()` before replacing the component.
|
|
65
|
+
|
|
66
|
+
Use this scenario for dashboards, admin pages, project management products, editors, and embedded analytics.
|
|
67
|
+
|
|
68
|
+
## Scenario 2: Ask a Coding Agent to Modify a Project
|
|
69
|
+
|
|
70
|
+
Give the Agent the intent, data source, desired output, and acceptance requirement:
|
|
71
|
+
|
|
72
|
+
```text
|
|
73
|
+
Use @taylorwong/ichartjs in the current project. Inspect the order data,
|
|
74
|
+
choose and validate a monthly sales chart, add it to the existing analysis
|
|
75
|
+
page, run focused tests, and return the exact preview URL. Keep assumptions,
|
|
76
|
+
warnings, record lineage, and unsupported requests visible.
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The Agent should return:
|
|
80
|
+
|
|
81
|
+
- changed files and the validated Spec;
|
|
82
|
+
- field mappings and chart-selection reasons;
|
|
83
|
+
- assumptions, warnings, and unsupported requests;
|
|
84
|
+
- focused test results and self-check state;
|
|
85
|
+
- the host project's exact preview URL and manual acceptance steps.
|
|
86
|
+
|
|
87
|
+
The repository Playground is only for iChart.js examples:
|
|
88
|
+
`npm run playground` → `http://localhost:3000/playground/project-gallery.html`.
|
|
89
|
+
For a consumer project, return that project's own development URL.
|
|
90
|
+
|
|
91
|
+
## Scenario 3: Use the Official Skill
|
|
92
|
+
|
|
93
|
+
Install or select `skills/ichartjs` in Codex, WorkBuddy, or another Agent Skills-compatible host. Then ask:
|
|
94
|
+
|
|
95
|
+
```text
|
|
96
|
+
Use the iChart.js Skill. Read this dataset, create a project Burndown,
|
|
97
|
+
validate the Spec, generate a browser preview, and also export SVG and JSON.
|
|
98
|
+
Return the preview URL, artifact paths, warnings, assumptions, and lineage.
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The Skill routes the task to the public Runtime API and scenario guides. It does not replace the package, add a service endpoint, or create a second chart-selection implementation.
|
|
102
|
+
|
|
103
|
+
If the host can edit and run a JavaScript project, the output can be an interactive page. If it cannot run JavaScript, request an SVG, JSON, or code artifact instead.
|
|
104
|
+
|
|
105
|
+
## Scenario 4: Create a One-Off Artifact
|
|
106
|
+
|
|
107
|
+
For a single chart, an Agent can create a small ESM script or standalone HTML page.
|
|
108
|
+
|
|
109
|
+
Recommended outputs:
|
|
110
|
+
|
|
111
|
+
- `chart.svg` for vector documents and zero-dependency headless generation;
|
|
112
|
+
- `chart.json` for a reproducible Spec and runtime checkpoint;
|
|
113
|
+
- `chart.png` or `chart.jpeg` for presentations and image sharing;
|
|
114
|
+
- `index.html` plus JavaScript when the recipient needs interaction.
|
|
115
|
+
|
|
116
|
+
Headless SVG and JSON work without an extra production dependency. Headless PNG/JPEG uses `chart.exportAsync()` and the optional `canvas` package. Do not add Python, MCP, HTTP, or a CLI merely to create one chart.
|
|
117
|
+
|
|
118
|
+
## Scenario 5: Project and Diagram Workflows
|
|
119
|
+
|
|
120
|
+
Use the project and diagram contracts when the data has project semantics rather than generic numeric series:
|
|
121
|
+
|
|
122
|
+
- `gantt`, `timeline`, `milestone`, and `burndown` for delivery schedules;
|
|
123
|
+
- project analytics for capacity, velocity, release forecast, risk, and issue aging;
|
|
124
|
+
- `flow` and `swimlane` for process, ownership, responsibility, groups, ports, and controlled editing.
|
|
125
|
+
|
|
126
|
+
Preserve stable record, node, edge, lane, group, and port IDs. For business edits, return a preview before commit and include the audit result.
|
|
127
|
+
|
|
128
|
+
## Scenario 6: CI and Scheduled Reports
|
|
129
|
+
|
|
130
|
+
Use a Node ESM script in the report job:
|
|
131
|
+
|
|
132
|
+
```js
|
|
133
|
+
const svg = chart.export({ type: 'svg' });
|
|
134
|
+
const json = chart.export({ type: 'json' });
|
|
135
|
+
const png = await chart.exportAsync({ type: 'png' });
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Store JSON as the reproducible checkpoint and SVG/PNG as presentation artifacts. Report export errors as structured results instead of silently substituting another format.
|
|
139
|
+
|
|
140
|
+
## Boundaries
|
|
141
|
+
|
|
142
|
+
- iChart.js is a JavaScript UI/runtime package, not a data service.
|
|
143
|
+
- A Skill gives Agents workflow instructions; it does not render without a JavaScript host.
|
|
144
|
+
- The host application owns authentication, storage, sharing, and application routing.
|
|
145
|
+
- There is no need for a core CLI, MCP server, HTTP service, or Python API for ordinary usage.
|
|
146
|
+
- If data is prepared in Python, export JSON/CSV and let a JavaScript process render the chart.
|
|
147
|
+
- Do not generate maps or 3D charts unless `getCapabilities()` explicitly declares them.
|
|
148
|
+
|
|
149
|
+
## Agent Delivery Checklist
|
|
150
|
+
|
|
151
|
+
Every Agent response should state:
|
|
152
|
+
|
|
153
|
+
1. selected chart/view and the reason;
|
|
154
|
+
2. input fields, transforms, and stable IDs;
|
|
155
|
+
3. validated Spec or structured repair request;
|
|
156
|
+
4. assumptions, warnings, unsupported requests, and accessibility choices;
|
|
157
|
+
5. explanation/state self-check results;
|
|
158
|
+
6. exact preview URL, code files, or artifact paths;
|
|
159
|
+
7. export format and any optional dependency required.
|
|
160
|
+
|
|
161
|
+
Related guides: [Quickstart](quickstart.md), [Frontend Integration](frontend-integration.md), [Coding Agent Integration](coding-agent-integration.md), [Runtime Contract](runtime-contract.md), [Project Scenario](project-scenario.md), and [Diagram Scenario](diagram-scenario.md).
|
|
@@ -14,7 +14,7 @@ iChart.js 是 JavaScript UI 图表组件库。编码 Agent 负责修改宿主前
|
|
|
14
14
|
npm install @taylorwong/ichartjs@^2
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
npm Registry 中无作用域的 `ichartjs`
|
|
17
|
+
npm Registry 中无作用域的 `ichartjs` 是安全占位包,并非本项目。正式包名是 `@taylorwong/ichartjs`,优先从 npm 安装。
|
|
18
18
|
|
|
19
19
|
Codex、WorkBuddy 和其他兼容 Agent Skills 的宿主可共同使用 `skills/ichartjs`。从仓库检出目录安装到 Codex:
|
|
20
20
|
|
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
|
|
10
10
|
## 当前能力
|
|
11
11
|
|
|
12
|
-
已支持节点、边、泳道、Group、Port
|
|
12
|
+
已支持节点、边、泳道、Group、Port、四种布局、三种路由、节点拖动、多选、对齐、网格吸附、键盘移动、Copy/Paste、Group 折叠展开、Port 键盘连线和 Undo/Redo。
|
|
13
13
|
|
|
14
14
|
当前限制:
|
|
15
15
|
|
|
16
16
|
- Group 只支持平级 Group,不支持嵌套。
|
|
17
|
-
-
|
|
18
|
-
-
|
|
17
|
+
- Group bounds 由成员几何和 `group.padding` 推导;`resizeGroup` 会缩放成员位置和尺寸,不持久化第二个 Group 矩形。
|
|
18
|
+
- Canvas 提供基础无障碍文本,SVG 提供更丰富的 Diagram 语义。
|
|
19
19
|
|
|
20
20
|
## Agent 流程
|
|
21
21
|
|
|
@@ -6,7 +6,7 @@ iChart.js 应作为普通 JavaScript UI 组件运行在浏览器应用中。数
|
|
|
6
6
|
npm install @taylorwong/ichartjs@^2
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
无法访问 npm Registry 的环境请用 GitHub 源作为后备:`npm install github:wanghetommy/ichartjs#v2.0.
|
|
9
|
+
无法访问 npm Registry 的环境请用 GitHub 源作为后备:`npm install github:wanghetommy/ichartjs#v2.0.5`。
|
|
10
10
|
|
|
11
11
|
```js
|
|
12
12
|
import { createChart } from '@taylorwong/ichartjs';
|
|
@@ -15,9 +15,9 @@ import {
|
|
|
15
15
|
} from '@taylorwong/ichartjs';
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
18
|
+
- `@taylorwong/ichartjs`:统一的 Agent 规划与 Runtime API。
|
|
19
|
+
- `@taylorwong/ichartjs/capabilities.json`:机器可读能力清单,包含逐图表导出、署名和交互声明。
|
|
20
|
+
- `@taylorwong/ichartjs/recipes/*`:基础分析、项目管理和 Diagram Recipes。
|
|
21
21
|
- `skills/ichartjs/SKILL.md`:适用于 Codex、WorkBuddy 等 Agent Skills 兼容宿主的可选编排层。
|
|
22
22
|
|
|
23
23
|
Agent 与开发者使用同一个 ESM 入口。编码 Agent 的完整方式见 [编码 Agent 集成](coding-agent-integration.md),普通应用集成见 [前端项目集成](frontend-integration.md)。
|
|
@@ -53,10 +53,10 @@ const obj = chart.export({ type: 'json', as: 'object' });
|
|
|
53
53
|
const svg = chart.export({ type: 'svg' });
|
|
54
54
|
const svgDataUrl = chart.export({ type: 'svg', as: 'dataurl' });
|
|
55
55
|
|
|
56
|
-
// 3. PNG / JPEG
|
|
57
|
-
//
|
|
58
|
-
// 无头:需安装 npm i canvas,否则返回结构化错误 code=HEADLESS_EXPORT_UNSUPPORTED
|
|
56
|
+
// 3. PNG / JPEG:浏览器同步真光栅;Node 无头使用异步导出
|
|
57
|
+
// Node 无头:安装 npm i canvas 后,exportAsync() 可生成真光栅
|
|
59
58
|
const png = typeof document !== 'undefined' ? chart.toDataURL('image/png') : null;
|
|
59
|
+
const headlessPng = await chart.exportAsync({ type: 'png' });
|
|
60
60
|
```
|
|
61
61
|
|
|
62
62
|
## Agent 输出要求
|
|
@@ -48,7 +48,7 @@ iChart.js 默认在所有图表右下角显示低对比度的品牌署名(`Pow
|
|
|
48
48
|
## 导出与下载
|
|
49
49
|
|
|
50
50
|
iChart.js 导出采用**双底层单源架构**,所有产物共享 `buildScene()` 生成的同一份 Scene Graph,**画面显示用的 renderer 和导出底层完全解耦**:
|
|
51
|
-
1. **PNG/JPEG(光栅)**:底层用 `CanvasRenderer` 重绘 Scene Graph
|
|
51
|
+
1. **PNG/JPEG(光栅)**:底层用 `CanvasRenderer` 重绘 Scene Graph;浏览器同步输出真光栅文件,无头环境通过 `exportAsync()` 加载可选的 `canvas` npm 包。
|
|
52
52
|
2. **SVG(矢量)**:底层用 `SVGRenderer` DOM 序列化(浏览器)或纯字符串拼装(无头零依赖),支持 XML 1.0 头部、字体拆分、无障碍属性。
|
|
53
53
|
3. **JSON(可重建)**:序列化当前 `spec` + `getState()` 结果,用于持久化、Agent 自检和跨端重建。
|
|
54
54
|
|
|
@@ -62,21 +62,21 @@ iChart.js 导出采用**双底层单源架构**,所有产物共享 `buildScene
|
|
|
62
62
|
|------------|----------------------------|----------------|--------------------------|
|
|
63
63
|
| JSON | ✅ | ✅ | ✅ |
|
|
64
64
|
| SVG | ✅ | ✅ | ✅ |
|
|
65
|
-
| PNG / JPEG | ✅ 同步真光栅 | ❌ 返回结构化 `HEADLESS_EXPORT_UNSUPPORTED` | ✅ |
|
|
65
|
+
| PNG / JPEG | ✅ 同步真光栅 | ❌ 返回结构化 `HEADLESS_EXPORT_UNSUPPORTED` | ✅ 通过 `exportAsync()` |
|
|
66
66
|
|
|
67
67
|
### 公共导出 API
|
|
68
68
|
|
|
69
69
|
- `chart.toDataURL(type='image/png')` → data URL 字符串或结构化 ExportError。
|
|
70
|
-
- `chart.toBlob(type='image/png')` → Blob 或 ExportError
|
|
70
|
+
- `chart.toBlob(type='image/png')` → Blob 或 ExportError(无头同步路径不提供光栅 Blob)。
|
|
71
71
|
- `chart.export({ type, as })` → 同步返回字符串 / JSON 对象 / Blob / ExportError,`as` 支持 `string`、`dataurl`、`blob`、`object`(仅 JSON)。
|
|
72
|
-
- `chart.exportAsync({ type, as })` → Promise
|
|
72
|
+
- `chart.exportAsync({ type, as })` → Promise 导出路径;Node 无头环境可通过可选 `canvas` 依赖生成 PNG/JPEG。
|
|
73
73
|
- `chart.download({ type })` / `downloadPNG()` / `downloadSVG()` / `downloadJSON()` → 触发浏览器保存(无头回落到返回字符串或结构化错误)。
|
|
74
74
|
|
|
75
75
|
### 错误结构
|
|
76
76
|
|
|
77
77
|
所有导出/下载方法失败时统一返回 `{ valid:false, code, message?, suggestion?, rasterCode? }` 稳定结构,便于 Agent 自动化判断,常见 `code`:
|
|
78
78
|
- `HEADLESS_EXPORT_UNSUPPORTED`:当前无头环境缺少光栅所需依赖(`canvas`)。
|
|
79
|
-
- `BLOB_HEADLESS
|
|
79
|
+
- `BLOB_HEADLESS`:同步 `toBlob` 无法在当前无头 renderer 上生成光栅 Blob;Node 光栅场景改用 `exportAsync()`。
|
|
80
80
|
- `DOWNLOAD_HEADLESS`:`chart.download*()` 仅在浏览器有 DOM 时可用,无头用 `export`。
|
|
81
81
|
- `EXPORT_TYPE_UNSUPPORTED`:不支持的导出类型。
|
|
82
82
|
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# iChart.js 使用场景
|
|
2
|
+
|
|
3
|
+
iChart.js 可以分成三层:
|
|
4
|
+
|
|
5
|
+
1. **Runtime**:`@taylorwong/ichartjs`,负责规划、校验、渲染、交互、编辑和导出。
|
|
6
|
+
2. **Skill**:`skills/ichartjs/SKILL.md`,负责指导 Agent 选择图表、保留数据 lineage、校验 Spec 和交付结果。
|
|
7
|
+
3. **宿主项目**:用户自己的 Web 项目、Node 脚本、CI 任务或 Agent 工作区,负责数据、生命周期、路由、存储和交付。
|
|
8
|
+
|
|
9
|
+
Skill 不是第二套渲染器,也不是服务端。使用 Skill 的 Agent 仍然需要 JavaScript 宿主,才能生成交互页面或文件。
|
|
10
|
+
|
|
11
|
+
## 场景选择
|
|
12
|
+
|
|
13
|
+
| 需求 | 使用方式 | 运行位置 | 常见输出 |
|
|
14
|
+
| --- | --- | --- | --- |
|
|
15
|
+
| 在产品中增加图表 | 前端项目集成 | 用户自己的浏览器应用 | 交互式 SVG 或 Canvas 图表 |
|
|
16
|
+
| 让 Codex 修改已有项目 | Coding Agent + Runtime | 代码仓库和项目开发服务器 | 代码、Spec、测试结果、预览地址 |
|
|
17
|
+
| 让多个 Agent 复用图表能力 | Skill + Runtime | Agent 工作区和目标项目 | 校验后的代码、文件、解释结果 |
|
|
18
|
+
| 一次性生成可视化 | Node 脚本或独立 HTML | Node.js 或浏览器 | SVG、PNG/JPEG、JSON、HTML |
|
|
19
|
+
| 项目进度和交付分析 | 项目场景 + Runtime | 项目看板 | Gantt、Burndown、Timeline、分析结果 |
|
|
20
|
+
| 流程或架构图 | Diagram 场景 + Runtime | Web 应用或文档流程 | Flow/Swimlane 页面、SVG、JSON |
|
|
21
|
+
| 定时生成报告 | Node 脚本 + Runtime | CI 或报表任务 | SVG/PNG 文件和 JSON 快照 |
|
|
22
|
+
|
|
23
|
+
## 输出契约
|
|
24
|
+
|
|
25
|
+
| 输出 | 适用场景 | API 或交付方式 |
|
|
26
|
+
| --- | --- | --- |
|
|
27
|
+
| 交互式页面 | 产品看板、编辑器、分析页面 | `createChart()` 挂载到 DOM |
|
|
28
|
+
| SVG DOM | 无障碍、键盘交互、Diagram、打印 | `renderer: 'svg'` |
|
|
29
|
+
| SVG 文件/字符串 | 文档、邮件、矢量交付、无头环境 | `chart.export({ type: 'svg' })` |
|
|
30
|
+
| PNG/JPEG | 汇报、聊天附件、图片报告 | 浏览器 `toDataURL()`,或 Node `exportAsync()` + 可选 `canvas` |
|
|
31
|
+
| JSON Spec/state | 持久化、Agent 快照、跨环境重建 | `chart.export({ type: 'json' })` |
|
|
32
|
+
| Data URL/Blob | 页面嵌入或浏览器下载 | `as: 'dataurl'` 或浏览器下载 API |
|
|
33
|
+
| 代码片段 | 开发者集成或生成页面 | `createChart()` 集成代码 |
|
|
34
|
+
| 解释和状态 | Agent 自检和用户审计 | `chart.explain()`、`chart.getState()` |
|
|
35
|
+
|
|
36
|
+
JSON 是机器可读的事实来源;SVG 和 PNG/JPEG 是视觉交付物;代码是集成交付物;交互页面是产品交付物。
|
|
37
|
+
|
|
38
|
+
## 场景一:集成到 Web 项目
|
|
39
|
+
|
|
40
|
+
在宿主项目中安装 Runtime:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm install @taylorwong/ichartjs@^2
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
import { createChart } from '@taylorwong/ichartjs';
|
|
48
|
+
|
|
49
|
+
const chart = createChart({
|
|
50
|
+
container: '#chart',
|
|
51
|
+
type: 'line',
|
|
52
|
+
renderer: 'svg',
|
|
53
|
+
data: { values: rows },
|
|
54
|
+
encoding: {
|
|
55
|
+
x: { field: 'month', type: 'category' },
|
|
56
|
+
y: { field: 'sales', type: 'quantitative' }
|
|
57
|
+
},
|
|
58
|
+
accessibility: { enabled: true }
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
宿主项目负责数据加载、认证、路由、持久化和状态管理。数据变化使用 `setData()`,Spec 变化使用 `update()`,替换组件前调用 `destroy()`。
|
|
63
|
+
|
|
64
|
+
适用于管理后台、数据看板、项目管理、流程编辑和嵌入式分析页面。
|
|
65
|
+
|
|
66
|
+
## 场景二:让 Coding Agent 修改项目
|
|
67
|
+
|
|
68
|
+
可以这样向 Codex 提需求:
|
|
69
|
+
|
|
70
|
+
```text
|
|
71
|
+
在当前项目中使用 @taylorwong/ichartjs。检查订单数据,选择并校验月度销售图表,
|
|
72
|
+
把它加入现有分析页面,运行相关测试,并返回准确的预览地址。保留假设、警告、
|
|
73
|
+
record lineage 和不支持请求。
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Agent 应返回:
|
|
77
|
+
|
|
78
|
+
- 修改文件和已校验的 Spec;
|
|
79
|
+
- 字段映射和选图原因;
|
|
80
|
+
- 假设、警告和不支持请求;
|
|
81
|
+
- 测试结果和运行时自检;
|
|
82
|
+
- 宿主项目的准确预览地址和人工验收步骤。
|
|
83
|
+
|
|
84
|
+
仓库内的 Playground 只用于 iChart.js 示例:`npm run playground` → `http://localhost:3000/playground/project-gallery.html`。如果修改的是其他项目,应返回其他项目自己的开发地址。
|
|
85
|
+
|
|
86
|
+
## 场景三:作为官方 Skill 使用
|
|
87
|
+
|
|
88
|
+
在 Codex、WorkBuddy 或其他支持 Agent Skills 的宿主中安装或选择 `skills/ichartjs`:
|
|
89
|
+
|
|
90
|
+
```text
|
|
91
|
+
使用 iChart.js Skill。读取这份数据,生成项目 Burndown,校验 Spec,
|
|
92
|
+
生成浏览器预览,并额外导出 SVG 和 JSON。返回预览地址、文件路径、
|
|
93
|
+
警告、假设和 lineage。
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Skill 会把任务路由到公开 Runtime API 和场景文档。它不会替代 npm 包、增加服务端点,也不会维护第二套选图逻辑。
|
|
97
|
+
|
|
98
|
+
如果宿主可以编辑并运行 JavaScript 项目,输出可以是交互页面;如果不能运行 JavaScript,应要求输出 SVG、JSON 或代码文件。
|
|
99
|
+
|
|
100
|
+
## 场景四:一次性生成文件
|
|
101
|
+
|
|
102
|
+
Agent 可以创建一个 ESM 脚本或独立 HTML 页面,输出:
|
|
103
|
+
|
|
104
|
+
- `chart.svg`:矢量文档和零额外依赖的无头输出;
|
|
105
|
+
- `chart.json`:可复现的 Spec 和运行时快照;
|
|
106
|
+
- `chart.png` 或 `chart.jpeg`:汇报和图片分享;
|
|
107
|
+
- `index.html` 加 JavaScript:需要交互时使用。
|
|
108
|
+
|
|
109
|
+
无头 SVG 和 JSON 不需要额外生产依赖;无头 PNG/JPEG 使用 `chart.exportAsync()`,并安装可选 `canvas`。一次性制图不应额外引入 Python、MCP、HTTP 服务或 CLI。
|
|
110
|
+
|
|
111
|
+
## 场景五:项目和 Diagram
|
|
112
|
+
|
|
113
|
+
- `gantt`、`timeline`、`milestone`、`burndown`:项目进度和交付计划;
|
|
114
|
+
- 项目分析:capacity、velocity、release forecast、risk、issue aging;
|
|
115
|
+
- `flow`、`swimlane`:流程、责任、分组、Port 和受控编辑。
|
|
116
|
+
|
|
117
|
+
保留稳定的 record、node、edge、lane、group、port ID。业务编辑先返回 preview,确认后再 commit,并返回审计结果。
|
|
118
|
+
|
|
119
|
+
## 场景六:CI 和定时报表
|
|
120
|
+
|
|
121
|
+
在 Node ESM 脚本中生成:
|
|
122
|
+
|
|
123
|
+
```js
|
|
124
|
+
const svg = chart.export({ type: 'svg' });
|
|
125
|
+
const json = chart.export({ type: 'json' });
|
|
126
|
+
const png = await chart.exportAsync({ type: 'png' });
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
建议把 JSON 作为可复现快照,把 SVG/PNG 作为视觉交付物。导出失败时返回结构化错误,不要静默替换格式。
|
|
130
|
+
|
|
131
|
+
## 边界
|
|
132
|
+
|
|
133
|
+
- iChart.js 是 JavaScript UI/Runtime,不是数据服务。
|
|
134
|
+
- Skill 只提供 Agent 工作流指导;没有 JavaScript 宿主时不能直接渲染。
|
|
135
|
+
- 宿主项目负责认证、存储、分享和应用路由。
|
|
136
|
+
- 普通使用不需要核心 CLI、MCP、HTTP 服务或 Python API。
|
|
137
|
+
- 如果数据在 Python 中准备,输出 JSON/CSV,再由 JavaScript 进程渲染。
|
|
138
|
+
- 除非 `getCapabilities()` 明确声明,否则不要生成地图或 3D 图表。
|
|
139
|
+
|
|
140
|
+
## Agent 交付清单
|
|
141
|
+
|
|
142
|
+
每次 Agent 返回结果都应包含:
|
|
143
|
+
|
|
144
|
+
1. 选择的图表/视图及原因;
|
|
145
|
+
2. 输入字段、转换和稳定 ID;
|
|
146
|
+
3. 已校验 Spec 或结构化修复请求;
|
|
147
|
+
4. 假设、警告、不支持请求和无障碍选择;
|
|
148
|
+
5. `explain/state` 自检结果;
|
|
149
|
+
6. 准确预览地址、代码文件或产物路径;
|
|
150
|
+
7. 导出格式和可选依赖。
|
|
151
|
+
|
|
152
|
+
相关文档:[快速上手](quickstart.md)、[前端集成](frontend-integration.md)、[Coding Agent 集成](coding-agent-integration.md)、[Runtime 契约](runtime-contract.md)、[项目场景](project-scenario.md)、[Diagram 场景](diagram-scenario.md)。
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
"agentPlanning": "src/index.mjs#planChart",
|
|
7
7
|
"agentExplanation": "Chart#explain",
|
|
8
8
|
"agentQuickstart": "docs/agent/quickstart.md",
|
|
9
|
+
"usageScenarios": "docs/agent/usage-scenarios.md",
|
|
9
10
|
"agentWorkflowExample": "examples/agent-workflow.mjs",
|
|
10
11
|
"officialSkill": "skills/ichartjs/SKILL.md",
|
|
11
12
|
"scenarios": {
|
|
@@ -22,22 +23,22 @@
|
|
|
22
23
|
"preview": true,
|
|
23
24
|
"json": true,
|
|
24
25
|
"svg": true,
|
|
25
|
-
"png": "Node headless PNG requires the optional `canvas` npm package (
|
|
26
|
+
"png": "Node headless PNG requires the optional `canvas` npm package and chart.exportAsync({type:\"png\"}). Otherwise use chart.export({type:\"svg\"})."
|
|
26
27
|
},
|
|
27
28
|
"export": {
|
|
28
29
|
"types": ["png", "jpeg", "svg", "json"],
|
|
29
30
|
"mime": { "png": "image/png", "jpeg": "image/jpeg", "svg": "image/svg+xml", "json": "application/json" },
|
|
30
31
|
"browser": { "png": true, "jpeg": true, "svg": true, "json": true },
|
|
31
32
|
"headless": {
|
|
32
|
-
"png": "optional: install the `canvas` npm package for createCanvas",
|
|
33
|
+
"png": "optional: install the `canvas` npm package for createCanvas and use exportAsync",
|
|
33
34
|
"jpeg": "same as png",
|
|
34
35
|
"svg": true,
|
|
35
36
|
"json": true
|
|
36
37
|
},
|
|
37
38
|
"methods": {
|
|
38
|
-
"exportPNG": "chart.export({ type:\"png\" }) returns
|
|
39
|
+
"exportPNG": "chart.export({ type:\"png\" }) returns a browser data URL. In Node headless use chart.exportAsync({ type:\"png\" }) after installing the optional canvas package, or fall back to SVG.",
|
|
39
40
|
"exportSVG": "chart.export({ type:\"svg\" }) returns the SVG string in both browser and headless; as:\"dataurl\" for embeds, as:\"blob\" for a Blob (browser).",
|
|
40
|
-
"exportJSON": "chart.export({ type:\"json\" }) returns pretty JSON; use as:\"object\"
|
|
41
|
+
"exportJSON": "chart.export({ type:\"json\" }) returns pretty JSON; use as:\"object\", as:\"dataurl\", or as:\"blob\" for the corresponding representation.",
|
|
41
42
|
"downloadPNG": "chart.downloadPNG() triggers a browser save-as dialog (filename derived from title + timestamp).",
|
|
42
43
|
"downloadSVG": "chart.downloadSVG() same semantics as downloadPNG but for SVG.",
|
|
43
44
|
"downloadJSON": "chart.downloadJSON() saves {version,spec,state} as a .json document.",
|
package/package.json
CHANGED
package/skills/ichartjs/SKILL.md
CHANGED
|
@@ -7,6 +7,8 @@ description: Plan, validate, render, explain, and safely edit iChart.js visualiz
|
|
|
7
7
|
|
|
8
8
|
Use the public Agent contract as the source of truth. Do not infer capabilities from renderer internals or duplicate chart-selection logic in generated code.
|
|
9
9
|
|
|
10
|
+
Read [`docs/agent/usage-scenarios.md`](../../docs/agent/usage-scenarios.md) when the request is ambiguous about whether the output should be a live project component, a Coding Agent change, a Skill-generated artifact, or a scheduled report.
|
|
11
|
+
|
|
10
12
|
## Source and Runtime Setup
|
|
11
13
|
|
|
12
14
|
- Official repository: `https://github.com/wanghetommy/ichartjs`
|
|
@@ -35,16 +37,24 @@ Do not install the unscoped npm registry package named `ichartjs`; it is current
|
|
|
35
37
|
10. Provide an exact preview URL or artifact path and report assumptions, warnings, and deferred checks.
|
|
36
38
|
11. Prefer `theme: { mode: 'auto', preset, palette }`; preserve explicit user style choices and use `chart.setTheme()` for live switching.
|
|
37
39
|
|
|
38
|
-
Use
|
|
40
|
+
Use `@taylorwong/ichartjs` for package imports. Use `examples/agent-workflow.mjs` as the executable baseline when working in the repository.
|
|
39
41
|
|
|
40
42
|
## Task Routing
|
|
41
43
|
|
|
44
|
+
Route by requested output:
|
|
45
|
+
|
|
46
|
+
- **Live product component**: modify the host JavaScript project and mount `createChart()`; return changed files and the host preview URL.
|
|
47
|
+
- **Coding Agent change**: inspect the repository, use the Runtime, run focused checks, and return the validated Spec plus changed files.
|
|
48
|
+
- **One-off artifact**: generate SVG/JSON directly; use browser PNG export or `exportAsync()` with optional `canvas` for Node PNG/JPEG.
|
|
49
|
+
- **Project or Diagram workflow**: load the matching scenario guide and preserve all stable IDs.
|
|
50
|
+
- **CI/report output**: keep JSON as the reproducible checkpoint and SVG/PNG as presentation artifacts.
|
|
51
|
+
|
|
42
52
|
- For standard data analysis, read `references/chart-selection.md` and use foundational recipes.
|
|
43
53
|
- For Gantt, Timeline, Milestone, Burndown, capacity, release, risk, or aging, use project capabilities and `agent-recipes/project-management.json`.
|
|
44
54
|
- For Flow or Swimlane, preserve node, edge, lane, group, and port IDs; use diagram recipes and validated edit commands.
|
|
45
55
|
- For business edits, preview first, preserve the preview ID and revision, require confirmation when declared, then commit or reject atomically.
|
|
46
56
|
- For browser deliverables, start `npm run playground` and return the exact maintained Playground URL.
|
|
47
|
-
- Release workflow (npm publish + develop→master merge) is **AUTHOR ONLY**. Read the
|
|
57
|
+
- Release workflow (npm publish + develop→master merge) is **AUTHOR ONLY**. Read the release SOP in [`docs/agent/development/release-sop.md`](../../docs/agent/development/release-sop.md). Never initiate any release step unless the author explicitly instructs.
|
|
48
58
|
|
|
49
59
|
## Guardrails
|
|
50
60
|
|
package/src/capabilities.mjs
CHANGED
|
@@ -137,15 +137,15 @@ export function getCapabilities() {
|
|
|
137
137
|
mime: { png: 'image/png', jpeg: 'image/jpeg', svg: 'image/svg+xml', json: 'application/json' },
|
|
138
138
|
browser: { png: true, jpeg: true, svg: true, json: true },
|
|
139
139
|
headless: {
|
|
140
|
-
png: 'optional: install the `canvas` npm package for createCanvas',
|
|
140
|
+
png: 'optional: install the `canvas` npm package for createCanvas and use exportAsync',
|
|
141
141
|
jpeg: 'same as png',
|
|
142
142
|
svg: true,
|
|
143
143
|
json: true,
|
|
144
144
|
},
|
|
145
145
|
methods: {
|
|
146
|
-
exportPNG: 'chart.export({ type:"png" }) returns
|
|
146
|
+
exportPNG: 'chart.export({ type:"png" }) returns a browser data URL. In Node headless use chart.exportAsync({ type:"png" }) after installing the optional canvas package, or fall back to SVG.',
|
|
147
147
|
exportSVG: 'chart.export({ type:"svg" }) returns the SVG string in both browser and headless; as:"dataurl" for embeds, as:"blob" for a Blob (browser).',
|
|
148
|
-
exportJSON: 'chart.export({ type:"json" }) returns pretty JSON; use as:"object"
|
|
148
|
+
exportJSON: 'chart.export({ type:"json" }) returns pretty JSON; use as:"object", as:"dataurl", or as:"blob" for the corresponding representation.',
|
|
149
149
|
downloadPNG: 'chart.downloadPNG() triggers a browser save-as dialog (filename derived from title + timestamp).',
|
|
150
150
|
downloadSVG: 'chart.downloadSVG() same semantics as downloadPNG but for SVG.',
|
|
151
151
|
downloadJSON: 'chart.downloadJSON() saves {version,spec,state} as a .json document.',
|
package/src/index.mjs
CHANGED
|
@@ -54,6 +54,12 @@ function _svgStyleFontParts(raw = '') {
|
|
|
54
54
|
.trim();
|
|
55
55
|
return { size, weight, family };
|
|
56
56
|
}
|
|
57
|
+
const NO_PAINT_TOKENS = new Set(['none', 'transparent', '']);
|
|
58
|
+
function hasPaint(style, key) {
|
|
59
|
+
const value = style?.[key];
|
|
60
|
+
if (value == null) return false;
|
|
61
|
+
return typeof value !== 'string' || !NO_PAINT_TOKENS.has(value.trim().toLowerCase());
|
|
62
|
+
}
|
|
57
63
|
function sceneToSvgString(scene, spec = {}) {
|
|
58
64
|
const w = Number(scene?.width ?? spec?.width ?? 640);
|
|
59
65
|
const h = Number(scene?.height ?? spec?.height ?? 360);
|
|
@@ -114,9 +120,10 @@ function sceneToSvgString(scene, spec = {}) {
|
|
|
114
120
|
} else {
|
|
115
121
|
tag = 'g';
|
|
116
122
|
}
|
|
117
|
-
if (
|
|
118
|
-
else if (
|
|
119
|
-
if (s
|
|
123
|
+
if (['path', 'circle', 'rect', 'arc'].includes(node.type)) attrs.push(`fill="${hasPaint(s, 'fill') ? _svgEscape(s.fill, 'attr') : 'none'}"`);
|
|
124
|
+
else if (node.type === 'text') attrs.push(`fill="${hasPaint(s, 'fill') ? _svgEscape(s.fill, 'attr') : '#0f172a'}"`);
|
|
125
|
+
if (hasPaint(s, 'stroke')) attrs.push(`stroke="${_svgEscape(s.stroke, 'attr')}"`);
|
|
126
|
+
else if (s.stroke != null) attrs.push('stroke="none"');
|
|
120
127
|
if (s.strokeWidth) attrs.push(`stroke-width="${s.strokeWidth}"`);
|
|
121
128
|
if (s.opacity != null) {
|
|
122
129
|
const opacity = (node.highlighted || node.selected) ? 1 : s.opacity;
|
|
@@ -145,6 +152,29 @@ function sceneToSvgString(scene, spec = {}) {
|
|
|
145
152
|
lines.push(`</svg>`);
|
|
146
153
|
return lines.join('\n');
|
|
147
154
|
}
|
|
155
|
+
function exportError(code, message, suggestion, extra = {}) { return { valid: false, code, message, suggestion, ...extra }; }
|
|
156
|
+
function dataUrlToBlob(dataUrl, mime) {
|
|
157
|
+
if (typeof Blob === 'undefined') return exportError('BLOB_HEADLESS', 'Blob is not available in this runtime.', 'Use as=dataurl or as=string.');
|
|
158
|
+
const comma = dataUrl.indexOf(',');
|
|
159
|
+
if (comma < 0) return exportError('EXPORT_UNSUPPORTED', 'The renderer returned an invalid data URL.', 'Use chart.export({ type: "svg" }) and inspect the returned string.');
|
|
160
|
+
const header = dataUrl.slice(0, comma), payload = dataUrl.slice(comma + 1);
|
|
161
|
+
const bytes = /;base64$/i.test(header)
|
|
162
|
+
? Uint8Array.from(atob(payload), value => value.charCodeAt(0))
|
|
163
|
+
: new TextEncoder().encode(decodeURIComponent(payload));
|
|
164
|
+
return new Blob([bytes], { type: mime });
|
|
165
|
+
}
|
|
166
|
+
function canvasFactoryFromModule(module) { return module?.createCanvas || module?.default?.createCanvas || null; }
|
|
167
|
+
function renderRasterWithCanvas(createCanvas, spec, scene, rasterType, as) {
|
|
168
|
+
const canvas = createCanvas(spec.width, spec.height), renderer = new CanvasRenderer({ ...spec });
|
|
169
|
+
renderer.canvas = canvas;
|
|
170
|
+
renderer.ctx = canvas.getContext('2d');
|
|
171
|
+
if (!renderer.ctx) return exportError('HEADLESS_EXPORT_UNSUPPORTED', 'The optional canvas package did not provide a 2D context.', 'Install a supported canvas implementation or use SVG export.');
|
|
172
|
+
renderer.resize(spec.width, spec.height);
|
|
173
|
+
renderer.render(scene);
|
|
174
|
+
const dataUrl = renderer.exportImage(rasterType);
|
|
175
|
+
if (as === 'blob') return dataUrlToBlob(dataUrl, rasterType);
|
|
176
|
+
return dataUrl;
|
|
177
|
+
}
|
|
148
178
|
|
|
149
179
|
export class Chart {
|
|
150
180
|
constructor(input = {}) {
|
|
@@ -368,7 +398,11 @@ export class Chart {
|
|
|
368
398
|
if (this.renderer instanceof SVGRenderer) {
|
|
369
399
|
if (wantsSvg) {
|
|
370
400
|
const headlessEnv = typeof document === 'undefined' || typeof XMLSerializer === 'undefined';
|
|
371
|
-
const string = headlessEnv
|
|
401
|
+
const string = headlessEnv
|
|
402
|
+
? sceneToSvgString(this.model.scene, this.spec)
|
|
403
|
+
: this.renderer.container
|
|
404
|
+
? this.renderer.exportString()
|
|
405
|
+
: (() => { const renderer = new SVGRenderer(this.spec); renderer.resize(this.spec.width, this.spec.height); renderer.render(this.model.scene); return renderer.exportString(); })();
|
|
372
406
|
return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(string)}`;
|
|
373
407
|
}
|
|
374
408
|
if (typeof document !== 'undefined') {
|
|
@@ -382,19 +416,17 @@ export class Chart {
|
|
|
382
416
|
}
|
|
383
417
|
return { valid: false, code: 'HEADLESS_EXPORT_UNSUPPORTED', rasterCode: 'RASTER_EXPORT_UNSUPPORTED', message: `SVG renderer raster ('${type}') in headless requires npm i canvas, or fall back to chart.export({type:"svg"}).`, suggestion: 'Use chart.export({type:"svg"}) for headless, or render with { renderer: "canvas" }.' };
|
|
384
418
|
}
|
|
385
|
-
if (this.renderer instanceof CanvasRenderer && typeof this.renderer.exportImage === 'function') return this.renderer.exportImage(type);
|
|
386
|
-
return
|
|
419
|
+
if (this.renderer instanceof CanvasRenderer && typeof this.renderer.exportImage === 'function' && this.renderer.canvas) return this.renderer.exportImage(type);
|
|
420
|
+
return exportError(typeof document === 'undefined' ? 'HEADLESS_EXPORT_UNSUPPORTED' : 'EXPORT_UNSUPPORTED', `toDataURL('${type}') is not available because the renderer has no canvas surface.`, 'Mount the chart in a browser or use chart.export({ type: "svg" }).');
|
|
387
421
|
}
|
|
388
422
|
toBlob(type = 'image/png') {
|
|
389
423
|
if (typeof document === 'undefined') return { valid: false, code: 'BLOB_HEADLESS', message: 'toBlob() requires a browser document. Use toDataURL() or export({type:"svg"}) in headless.', suggestion: 'Use chart.export({type:"svg"}) or chart.toDataURL() which both return strings usable in headless.' };
|
|
390
|
-
|
|
424
|
+
let url;
|
|
425
|
+
try { url = this.toDataURL(type); } catch (error) { return exportError('EXPORT_UNSUPPORTED', String(error?.message || error), 'Use chart.export({ type: "svg" }) or mount a supported renderer.'); }
|
|
391
426
|
if (!url || typeof url !== 'string') return { valid: false, code: 'EXPORT_UNSUPPORTED', message: "Couldn't produce a data URL for export.", suggestion: (url && url.message) || url };
|
|
392
|
-
const
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
const payload = comma >= 0 ? url.slice(comma + 1) : url;
|
|
396
|
-
const bytes = base64 ? Uint8Array.from(atob(payload), c => c.charCodeAt(0)) : (new TextEncoder()).encode(decodeURIComponent(payload));
|
|
397
|
-
return new Blob([bytes], { type: mime });
|
|
427
|
+
const normalizedType = String(type).toLowerCase();
|
|
428
|
+
const mime = normalizedType.includes('svg') ? 'image/svg+xml' : /^image\//i.test(normalizedType) ? normalizedType : 'image/png';
|
|
429
|
+
return dataUrlToBlob(url, mime);
|
|
398
430
|
}
|
|
399
431
|
export(options = {}) {
|
|
400
432
|
const rawType = String(options.type || (this.renderer instanceof CanvasRenderer ? 'image/png' : 'image/svg+xml')).toLowerCase();
|
|
@@ -406,9 +438,15 @@ export class Chart {
|
|
|
406
438
|
return 'auto';
|
|
407
439
|
};
|
|
408
440
|
const kind = normalizeType(rawType);
|
|
441
|
+
if (kind === 'auto') return exportError('EXPORT_TYPE_UNSUPPORTED', `Unknown export type: ${rawType}`, 'Use png, svg, jpeg, or json.');
|
|
409
442
|
if (kind === 'json') {
|
|
410
443
|
const payload = { version: '2.0', spec: this.getSpec(), state: this.getState() };
|
|
411
|
-
|
|
444
|
+
const json = JSON.stringify(payload, null, 2);
|
|
445
|
+
if (!options.as || options.as === 'string') return json;
|
|
446
|
+
if (options.as === 'object') return payload;
|
|
447
|
+
if (options.as === 'dataurl') return `data:application/json;charset=utf-8,${encodeURIComponent(json)}`;
|
|
448
|
+
if (options.as === 'blob') return dataUrlToBlob(`data:application/json;charset=utf-8,${encodeURIComponent(json)}`, 'application/json');
|
|
449
|
+
return exportError('EXPORT_TYPE_UNSUPPORTED', `Unsupported JSON export format: ${options.as}`, 'Use string, dataurl, blob, or object.');
|
|
412
450
|
}
|
|
413
451
|
if (kind === 'svg') {
|
|
414
452
|
const svgRendererReady = this.renderer instanceof SVGRenderer && this.renderer.container;
|
|
@@ -457,34 +495,7 @@ export class Chart {
|
|
|
457
495
|
}
|
|
458
496
|
return dataUrl;
|
|
459
497
|
}
|
|
460
|
-
|
|
461
|
-
try {
|
|
462
|
-
const c = require('canvas');
|
|
463
|
-
if (c && c.createCanvas) {
|
|
464
|
-
const cv = c.createCanvas(this.spec.width, this.spec.height);
|
|
465
|
-
const r = new CanvasRenderer({ ...this.spec });
|
|
466
|
-
r.canvas = cv; r.ctx = cv.getContext('2d'); r.resize(this.spec.width, this.spec.height); r.render(this.model.scene);
|
|
467
|
-
const out = r.exportImage(rasterType);
|
|
468
|
-
return options.as === 'blob' ? (typeof Blob !== 'undefined' ? new Blob([Uint8Array.from(atob(out.slice(out.indexOf(',') + 1)), x => x.charCodeAt(0))], { type: rasterType }) : out) : out;
|
|
469
|
-
}
|
|
470
|
-
} catch (_) { /* ignore */ }
|
|
471
|
-
return { valid: false, code: 'HEADLESS_EXPORT_UNSUPPORTED', rasterCode: 'RASTER_EXPORT_UNSUPPORTED', message: `Raster export (${rasterType}) with SVG renderer in headless requires npm i canvas.`, suggestion: 'Install the `canvas` package, or fall back to chart.export({type:"svg"}) which works browser+headless with zero dependencies.' };
|
|
472
|
-
}
|
|
473
|
-
const canvasUnavailable = { valid: false, code: 'HEADLESS_EXPORT_UNSUPPORTED', rasterCode: 'RASTER_EXPORT_UNSUPPORTED', message: `Raster export (${rasterType}) requires a mounted CanvasRenderer with a real canvas element.`, suggestion: 'Mount the chart with { renderer: "canvas", container: "#id" } in a browser, or fall back to chart.export({type:"svg"}) which works browser+headless.' };
|
|
474
|
-
if (typeof document === 'undefined') {
|
|
475
|
-
try {
|
|
476
|
-
const c = require('canvas');
|
|
477
|
-
if (c && c.createCanvas) {
|
|
478
|
-
const cv = c.createCanvas(this.spec.width, this.spec.height);
|
|
479
|
-
const r = new CanvasRenderer({ ...this.spec });
|
|
480
|
-
r.canvas = cv; r.ctx = cv.getContext('2d'); r.resize(this.spec.width, this.spec.height); r.render(this.model.scene);
|
|
481
|
-
const out = r.exportImage(rasterType);
|
|
482
|
-
return options.as === 'blob' ? (typeof Blob !== 'undefined' ? new Blob([Uint8Array.from(atob(out.slice(out.indexOf(',') + 1)), x => x.charCodeAt(0))], { type: rasterType }) : out) : out;
|
|
483
|
-
}
|
|
484
|
-
} catch (_) { /* ignore: continue to structured error */ }
|
|
485
|
-
return { ...canvasUnavailable, headless: true, note: 'In Node headless, run `npm i canvas` and ensure createCanvas() is available, or use chart.export({type:"svg"}).' };
|
|
486
|
-
}
|
|
487
|
-
return canvasUnavailable;
|
|
498
|
+
return exportError('HEADLESS_EXPORT_UNSUPPORTED', `Raster export (${rasterType}) requires a mounted CanvasRenderer or the async optional canvas adapter.`, 'Use chart.exportAsync({ type: "png" }) with the optional canvas package, or fall back to chart.export({ type: "svg" }).', { rasterCode: 'RASTER_EXPORT_UNSUPPORTED' });
|
|
488
499
|
}
|
|
489
500
|
async exportAsync(options = {}) {
|
|
490
501
|
const rawType = String(options.type || (this.renderer instanceof CanvasRenderer ? 'image/png' : 'image/svg+xml')).toLowerCase();
|
|
@@ -496,12 +507,21 @@ export class Chart {
|
|
|
496
507
|
return 'auto';
|
|
497
508
|
};
|
|
498
509
|
const kind = normalizeType(rawType);
|
|
499
|
-
if (kind === '
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
510
|
+
if (kind === 'auto') return exportError('EXPORT_TYPE_UNSUPPORTED', `Unknown export type: ${rawType}`, 'Use png, svg, jpeg, or json.');
|
|
511
|
+
if (kind === 'png' || kind === 'jpeg') {
|
|
512
|
+
const direct = this.export(options);
|
|
513
|
+
if (!(direct && typeof direct === 'object' && direct.code === 'HEADLESS_EXPORT_UNSUPPORTED')) return direct;
|
|
514
|
+
try {
|
|
515
|
+
const moduleName = 'canvas';
|
|
516
|
+
const module = await import(moduleName);
|
|
517
|
+
const createCanvas = canvasFactoryFromModule(module);
|
|
518
|
+
if (!createCanvas) return direct;
|
|
519
|
+
return renderRasterWithCanvas(createCanvas, this.spec, this.model.scene, kind === 'jpeg' ? 'image/jpeg' : 'image/png', options.as);
|
|
520
|
+
} catch {
|
|
521
|
+
return direct;
|
|
522
|
+
}
|
|
503
523
|
}
|
|
504
|
-
return
|
|
524
|
+
return this.export(options);
|
|
505
525
|
}
|
|
506
526
|
_safeFilename(prefix = 'ichart') {
|
|
507
527
|
const sanitize = s => String(s == null ? '' : s).replace(/[\\/:*?"<>|\s]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40) || prefix;
|
|
@@ -559,4 +579,4 @@ export { normalizeProjectCalendar, applyWorkingCalendar, normalizeDependencies,
|
|
|
559
579
|
export { normalizeLinkedFilters, normalizeLinkedSelection, filterProjectRows, createLinkedProjectState, linkedRecordId };
|
|
560
580
|
|
|
561
581
|
export { contrastRatio, planStyle, resolveTheme, styleCapabilities, themeModes, themePalettes, themePresets, validateThemeContrast, annotationPlugin, dataZoomPlugin, dataLabelsPlugin, accessibilityPlugin };
|
|
562
|
-
export const iChart = { version: '2.0.
|
|
582
|
+
export const iChart = { version: '2.0.5', createChart, inspectData, normalizeData, binData, applyTransforms, normalizeSpec, validateSpec, data, getCapabilities, getChartCapability, planChart, recommend, explainChart, contrastRatio, planStyle, resolveTheme, styleCapabilities, themeModes, themePalettes, themePresets, validateThemeContrast, annotationPlugin, dataZoomPlugin, dataLabelsPlugin, accessibilityPlugin, getBusinessSchema, inspectDataSchema, validateData, getEditCapabilities, validateEdit, previewEdit, commitPreview, validateRecipe, normalizeProjectCalendar, applyWorkingCalendar, normalizeDependencies, analyzeSchedule, analyzeBurndownSeries, analyzeCapacity, buildCapacityView, buildCumulativeFlowSeries, buildVelocitySeries, buildReleaseForecast, buildRiskMatrixSeries, buildIssueAgingSeries, normalizeLinkedFilters, normalizeLinkedSelection, filterProjectRows, createLinkedProjectState, linkedRecordId };
|
package/src/renderer.mjs
CHANGED
|
@@ -29,9 +29,9 @@ export class BaseRenderer {
|
|
|
29
29
|
|
|
30
30
|
export class CanvasRenderer extends BaseRenderer {
|
|
31
31
|
mount(container) { super.mount(container); this.canvas = this.container.tagName === 'CANVAS' ? this.container : document.createElement('canvas'); if (this.canvas !== this.container) { this.container.innerHTML = ''; this.container.appendChild(this.canvas); } this.canvas.style.background = this.options.background || '#ffffff'; this.ctx = this.canvas.getContext('2d'); return this; }
|
|
32
|
-
resize(width, height, pixelRatio =
|
|
32
|
+
resize(width, height, pixelRatio = globalThis.devicePixelRatio || 1) { this.width = width; this.height = height; this.pixelRatio = pixelRatio; this.canvas.width = Math.round(width * pixelRatio); this.canvas.height = Math.round(height * pixelRatio); if (this.canvas.style) { this.canvas.style.width = `${width}px`; this.canvas.style.height = `${height}px`; } this.ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0); }
|
|
33
33
|
clear() { if (this.ctx) this.ctx.clearRect(0, 0, this.width, this.height); }
|
|
34
|
-
render(scene) { this.clear(); const ctx = this.ctx; const draw = node => { if (!node.visible) return; const g = node.geometry || {}; const s = node.style || {}; ctx.save(); ctx.globalAlpha = s.opacity == null ? 1 : s.opacity; ctx.fillStyle = hasFill(s) ? color(s, 'fill', node.highlighted || node.selected ? '#1d4ed8' : '#2563eb') : 'rgba(0,0,0,0)'; ctx.strokeStyle = hasStroke(s) ? color(s, 'stroke', '#0f172a') : 'rgba(0,0,0,0)'; ctx.lineWidth = s.strokeWidth || 1; if (node.type === 'rect') { if (hasFill(s)) ctx.fillRect(g.x, g.y, g.width, g.height); if (hasStroke(s)) ctx.strokeRect(g.x, g.y, g.width, g.height); } else if (node.type === 'line') { ctx.beginPath(); ctx.moveTo(g.x1, g.y1); ctx.lineTo(g.x2, g.y2); if (hasStroke(s)) ctx.stroke(); } else if (node.type === 'path') { ctx.beginPath(); g.points.forEach((point, index) => index ? ctx.lineTo(point.x, point.y) : ctx.moveTo(point.x, point.y)); if (g.closed) ctx.closePath(); if (hasFill(s)) ctx.fill(); if (hasStroke(s)) ctx.stroke(); } else if (node.type === 'circle') { ctx.beginPath(); ctx.arc(g.cx, g.cy, g.r, 0, Math.PI * 2); if (hasFill(s)) ctx.fill(); if (hasStroke(s)) ctx.stroke(); } else if (node.type === 'arc') { ctx.beginPath(); ctx.arc(g.cx, g.cy, g.r, g.start, g.end); if (g.innerR > 0) { ctx.arc(g.cx, g.cy, g.innerR, g.end, g.start, true); } else ctx.lineTo(g.cx, g.cy); ctx.closePath(); if (hasFill(s)) ctx.fill(); if (hasStroke(s)) ctx.stroke(); } else if (node.type === 'text') {
|
|
34
|
+
render(scene) { this.clear(); const ctx = this.ctx; if (hasFill({ fill: this.options?.background })) { ctx.save(); ctx.fillStyle = this.options.background; ctx.fillRect(0, 0, this.width, this.height); ctx.restore(); } const draw = node => { if (!node.visible) return; const g = node.geometry || {}; const s = node.style || {}; ctx.save(); ctx.globalAlpha = s.opacity == null ? 1 : s.opacity; ctx.fillStyle = hasFill(s) ? color(s, 'fill', node.highlighted || node.selected ? '#1d4ed8' : '#2563eb') : 'rgba(0,0,0,0)'; ctx.strokeStyle = hasStroke(s) ? color(s, 'stroke', '#0f172a') : 'rgba(0,0,0,0)'; ctx.lineWidth = s.strokeWidth || 1; if (node.type === 'rect') { if (hasFill(s)) ctx.fillRect(g.x, g.y, g.width, g.height); if (hasStroke(s)) ctx.strokeRect(g.x, g.y, g.width, g.height); } else if (node.type === 'line') { ctx.beginPath(); ctx.moveTo(g.x1, g.y1); ctx.lineTo(g.x2, g.y2); if (hasStroke(s)) ctx.stroke(); } else if (node.type === 'path') { ctx.beginPath(); g.points.forEach((point, index) => index ? ctx.lineTo(point.x, point.y) : ctx.moveTo(point.x, point.y)); if (g.closed) ctx.closePath(); if (hasFill(s)) ctx.fill(); if (hasStroke(s)) ctx.stroke(); } else if (node.type === 'circle') { ctx.beginPath(); ctx.arc(g.cx, g.cy, g.r, 0, Math.PI * 2); if (hasFill(s)) ctx.fill(); if (hasStroke(s)) ctx.stroke(); } else if (node.type === 'arc') { ctx.beginPath(); ctx.arc(g.cx, g.cy, g.r, g.start, g.end); if (g.innerR > 0) { ctx.arc(g.cx, g.cy, g.innerR, g.end, g.start, true); } else ctx.lineTo(g.cx, g.cy); ctx.closePath(); if (hasFill(s)) ctx.fill(); if (hasStroke(s)) ctx.stroke(); } else if (node.type === 'text') { ctx.fillStyle = hasFill(s) ? s.fill : '#0f172a'; ctx.font = s.font || '12px system-ui'; const align = s.textAlign || (s.textAnchor === 'end' ? 'end' : s.textAnchor === 'middle' || s.textAnchor === 'center' ? 'center' : s.textAnchor === 'start' ? 'start' : 'left'); ctx.textAlign = align; ctx.textBaseline = s.textBaseline || s.baseline || 'alphabetic'; ctx.fillText(g.text, g.x, g.y); } node.children.slice().sort((a, b) => a.zIndex - b.zIndex).forEach(draw); ctx.restore(); }; draw(scene.root); }
|
|
35
35
|
exportImage(type = 'image/png') { return this.canvas.toDataURL(type); }
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -86,6 +86,6 @@ export class SVGRenderer extends BaseRenderer {
|
|
|
86
86
|
this.svg.setAttribute('viewBox', `0 0 ${w} ${h}`);
|
|
87
87
|
this.width = w; this.height = h;
|
|
88
88
|
}
|
|
89
|
-
this.clear(); const renderNode = node => { if (!node.visible || node.type === 'root') { node.children.slice().sort((a, b) => a.zIndex - b.zIndex).forEach(renderNode); return; } const g = node.geometry || {}, s = node.style || {}; const element = document.createElementNS('http://www.w3.org/2000/svg', node.type === 'path' || node.type === 'arc' ? 'path' : node.type === 'text' ? 'text' : node.type); element.setAttribute('id', node.id); element.setAttribute('data-node-id', node.id); if (node.dataRef) element.dataset.dataRef = JSON.stringify(node.dataRef); if (node.type === 'rect') { element.setAttribute('x', g.x); element.setAttribute('y', g.y); element.setAttribute('width', g.width); element.setAttribute('height', g.height); } else if (node.type === 'line') { ['x1', 'y1', 'x2', 'y2'].forEach(key => element.setAttribute(key, g[key])); } else if (node.type === 'circle') { element.setAttribute('cx', g.cx); element.setAttribute('cy', g.cy); element.setAttribute('r', g.r); } else if (node.type === 'arc') { const large = g.end - g.start > Math.PI ? 1 : 0, outerStart = `${g.cx + g.r * Math.cos(g.start)} ${g.cy + g.r * Math.sin(g.start)}`, outerEnd = `${g.cx + g.r * Math.cos(g.end)} ${g.cy + g.r * Math.sin(g.end)}`; const p = g.innerR > 0 ? [`M ${outerStart}`, `A ${g.r} ${g.r} 0 ${large} 1 ${outerEnd}`, `L ${g.cx + g.innerR * Math.cos(g.end)} ${g.cy + g.innerR * Math.sin(g.end)}`, `A ${g.innerR} ${g.innerR} 0 ${large} 0 ${g.cx + g.innerR * Math.cos(g.start)} ${g.cy + g.innerR * Math.sin(g.start)}`, 'Z'] : [`M ${g.cx} ${g.cy}`, `L ${outerStart}`, `A ${g.r} ${g.r} 0 ${large} 1 ${outerEnd}`, 'Z']; element.setAttribute('d', p.join(' ')); } else if (node.type === 'path') element.setAttribute('d', `${g.points.map((p, i) => `${i ? 'L' : 'M'} ${p.x} ${p.y}`).join(' ')}${g.closed ? ' Z' : ''}`); else if (node.type === 'text') { element.setAttribute('x', g.x); element.setAttribute('y', g.y); element.textContent = g.text; const anchor = s.textAnchor || (s.textAlign === 'end' ? 'end' : s.textAlign === 'center' ? 'middle' : s.textAlign === 'right' ? 'end' : s.textAlign === 'left' ? 'start' : 'start'); if (anchor) element.setAttribute('text-anchor', anchor); const baseline = s.textBaseline || s.baseline || 'alphabetic'; if (baseline === 'top') { element.setAttribute('dominant-baseline', 'text-before-edge'); element.setAttribute('alignment-baseline', 'before-edge'); } else if (baseline === 'middle' || baseline === 'central') { element.setAttribute('dominant-baseline', 'middle'); element.setAttribute('alignment-baseline', 'middle'); } else if (baseline === 'bottom' || baseline === 'hanging') { element.setAttribute('dominant-baseline', 'text-after-edge'); element.setAttribute('alignment-baseline', 'after-edge'); } else { element.setAttribute('dominant-baseline', 'alphabetic'); element.setAttribute('alignment-baseline', 'alphabetic'); } if (s.font) { const raw = String(s.font).trim(); element.setAttribute('font', raw); const sizeMatch = raw.match(/\b(\d+(?:\.\d+)?)\s*(px|em|rem|pt|%)/i); const size = sizeMatch ? `${sizeMatch[1]}${sizeMatch[2].toLowerCase()}` : ''; if (size) element.style.fontSize = size; const weightMatch = raw.match(/^\s*(\d{3}|normal|bold|lighter|bolder)\b/i); if (weightMatch) element.style.fontWeight = weightMatch[1]; const family = raw.replace(/^\s*(?:(?:normal|italic|oblique)(?:\s+[^0-9\s]+)?\s+)?(?:\d{3}|normal|bold|lighter|bolder)\s+/i, '').replace(/\b\d+(?:\.\d+)?\s*(?:px|em|rem|pt|%)(?:\s*\/\s*\S+)?\s*/i, '').trim(); if (family) element.style.fontFamily = family; } } if (node.type === 'path' || node.type === 'circle' || node.type === 'rect' || node.type === 'arc') {
|
|
89
|
+
this.clear(); const renderNode = node => { if (!node.visible || node.type === 'root') { node.children.slice().sort((a, b) => a.zIndex - b.zIndex).forEach(renderNode); return; } const g = node.geometry || {}, s = node.style || {}; const element = document.createElementNS('http://www.w3.org/2000/svg', node.type === 'path' || node.type === 'arc' ? 'path' : node.type === 'text' ? 'text' : node.type); element.setAttribute('id', node.id); element.setAttribute('data-node-id', node.id); if (node.dataRef) element.dataset.dataRef = JSON.stringify(node.dataRef); if (node.type === 'rect') { element.setAttribute('x', g.x); element.setAttribute('y', g.y); element.setAttribute('width', g.width); element.setAttribute('height', g.height); } else if (node.type === 'line') { ['x1', 'y1', 'x2', 'y2'].forEach(key => element.setAttribute(key, g[key])); } else if (node.type === 'circle') { element.setAttribute('cx', g.cx); element.setAttribute('cy', g.cy); element.setAttribute('r', g.r); } else if (node.type === 'arc') { const large = g.end - g.start > Math.PI ? 1 : 0, outerStart = `${g.cx + g.r * Math.cos(g.start)} ${g.cy + g.r * Math.sin(g.start)}`, outerEnd = `${g.cx + g.r * Math.cos(g.end)} ${g.cy + g.r * Math.sin(g.end)}`; const p = g.innerR > 0 ? [`M ${outerStart}`, `A ${g.r} ${g.r} 0 ${large} 1 ${outerEnd}`, `L ${g.cx + g.innerR * Math.cos(g.end)} ${g.cy + g.innerR * Math.sin(g.end)}`, `A ${g.innerR} ${g.innerR} 0 ${large} 0 ${g.cx + g.innerR * Math.cos(g.start)} ${g.cy + g.innerR * Math.sin(g.start)}`, 'Z'] : [`M ${g.cx} ${g.cy}`, `L ${outerStart}`, `A ${g.r} ${g.r} 0 ${large} 1 ${outerEnd}`, 'Z']; element.setAttribute('d', p.join(' ')); } else if (node.type === 'path') element.setAttribute('d', `${g.points.map((p, i) => `${i ? 'L' : 'M'} ${p.x} ${p.y}`).join(' ')}${g.closed ? ' Z' : ''}`); else if (node.type === 'text') { element.setAttribute('x', g.x); element.setAttribute('y', g.y); element.textContent = g.text; const anchor = s.textAnchor || (s.textAlign === 'end' ? 'end' : s.textAlign === 'center' ? 'middle' : s.textAlign === 'right' ? 'end' : s.textAlign === 'left' ? 'start' : 'start'); if (anchor) element.setAttribute('text-anchor', anchor); const baseline = s.textBaseline || s.baseline || 'alphabetic'; if (baseline === 'top') { element.setAttribute('dominant-baseline', 'text-before-edge'); element.setAttribute('alignment-baseline', 'before-edge'); } else if (baseline === 'middle' || baseline === 'central') { element.setAttribute('dominant-baseline', 'middle'); element.setAttribute('alignment-baseline', 'middle'); } else if (baseline === 'bottom' || baseline === 'hanging') { element.setAttribute('dominant-baseline', 'text-after-edge'); element.setAttribute('alignment-baseline', 'after-edge'); } else { element.setAttribute('dominant-baseline', 'alphabetic'); element.setAttribute('alignment-baseline', 'alphabetic'); } if (s.font) { const raw = String(s.font).trim(); element.setAttribute('font', raw); const sizeMatch = raw.match(/\b(\d+(?:\.\d+)?)\s*(px|em|rem|pt|%)/i); const size = sizeMatch ? `${sizeMatch[1]}${sizeMatch[2].toLowerCase()}` : ''; if (size) element.style.fontSize = size; const weightMatch = raw.match(/^\s*(\d{3}|normal|bold|lighter|bolder)\b/i); if (weightMatch) element.style.fontWeight = weightMatch[1]; const family = raw.replace(/^\s*(?:(?:normal|italic|oblique)(?:\s+[^0-9\s]+)?\s+)?(?:\d{3}|normal|bold|lighter|bolder)\s+/i, '').replace(/\b\d+(?:\.\d+)?\s*(?:px|em|rem|pt|%)(?:\s*\/\s*\S+)?\s*/i, '').trim(); if (family) element.style.fontFamily = family; } } if (node.type === 'path' || node.type === 'circle' || node.type === 'rect' || node.type === 'arc') { element.setAttribute('fill', hasFill(s) ? s.fill : 'none'); } else if (node.type === 'text') { element.setAttribute('fill', hasFill(s) ? s.fill : '#0f172a'); } if (hasStroke(s)) element.setAttribute('stroke', s.stroke); else if (s.stroke != null && typeof s.stroke === 'string' && NO_FILL_TOKENS.has(s.stroke.trim().toLowerCase())) element.setAttribute('stroke', 'none'); if (s.strokeWidth != null) element.setAttribute('stroke-width', s.strokeWidth); if (s.opacity != null) element.setAttribute('opacity', node.highlighted || node.selected ? 1 : s.opacity); if (node.highlighted || node.selected) element.setAttribute('filter', 'brightness(1.2)'); if (node.interactive) element.style.cursor = 'pointer'; if (s.pointerEvents === 'none') element.style.pointerEvents = 'none'; if (s.ariaHidden === 'true') element.setAttribute('aria-hidden', 'true'); if (s.role === 'presentation') element.setAttribute('role', 'presentation'); if (node.decorative) { element.setAttribute('aria-hidden', 'true'); element.setAttribute('role', 'presentation'); element.style.pointerEvents = 'none'; element.style.userSelect = 'none'; } this.svg.appendChild(element); node.children.slice().sort((a, b) => a.zIndex - b.zIndex).forEach(renderNode); }; scene.root.children.slice().sort((a, b) => a.zIndex - b.zIndex).forEach(renderNode); }
|
|
90
90
|
exportString() { if (!this.svg) return ''; return new XMLSerializer().serializeToString(this.svg); }
|
|
91
91
|
}
|
package/src/spec.mjs
CHANGED
|
@@ -60,6 +60,7 @@ function dependencyErrors(rows) {
|
|
|
60
60
|
|
|
61
61
|
export function normalizeSpec(input = {}) {
|
|
62
62
|
const spec = merge(defaults, input);
|
|
63
|
+
if (input.branding === undefined && input.theme && typeof input.theme === 'object' && input.theme.branding !== undefined) spec.branding = clone(input.theme.branding);
|
|
63
64
|
if (spec.branding === false) spec.branding = { enabled: false };
|
|
64
65
|
else if (spec.branding === true) spec.branding = { enabled: true };
|
|
65
66
|
else if (spec.branding == null) spec.branding = { enabled: true };
|
|
@@ -92,6 +93,8 @@ export function validateSpec(input) {
|
|
|
92
93
|
if (spec.theme.mode && !themeModes.includes(spec.theme.mode)) errors.push({ code: 'INVALID_THEME_MODE', path: 'theme.mode', message: `Unsupported theme mode: ${spec.theme.mode}`, suggestion: `Use ${themeModes.join(', ')}.` });
|
|
93
94
|
if (spec.theme.preset && !themePresets.includes(spec.theme.preset)) errors.push({ code: 'INVALID_THEME_PRESET', path: 'theme.preset', message: `Unsupported theme preset: ${spec.theme.preset}`, suggestion: `Use ${themePresets.join(', ')}.` });
|
|
94
95
|
if (spec.theme.palette && !themePalettes.includes(spec.theme.palette)) errors.push({ code: 'INVALID_THEME_PALETTE', path: 'theme.palette', message: `Unsupported theme palette: ${spec.theme.palette}`, suggestion: `Use ${themePalettes.join(', ')}.` });
|
|
96
|
+
if (spec.theme.branding != null && typeof spec.theme.branding !== 'boolean' && !(spec.theme.branding && typeof spec.theme.branding === 'object')) errors.push({ code: 'INVALID_BRANDING', path: 'theme.branding', message: 'theme.branding must be a boolean or a { enabled: boolean } object.', suggestion: 'Use theme: { branding: false } or theme: { branding: { enabled: false } }.' });
|
|
97
|
+
else if (spec.theme.branding && typeof spec.theme.branding === 'object' && (Object.keys(spec.theme.branding).some(key => key !== 'enabled') || typeof spec.theme.branding.enabled !== 'boolean')) errors.push({ code: 'INVALID_BRANDING', path: 'theme.branding', message: 'theme.branding objects only support a boolean enabled property.', suggestion: 'Use theme: { branding: { enabled: false } }.' });
|
|
95
98
|
}
|
|
96
99
|
if (Array.isArray(spec.encoding.y) && spec.encoding.y.length > 2) errors.push({ code: 'TOO_MANY_AXES', path: 'encoding.y', message: 'Only two quantitative axes are supported in this iteration.', suggestion: 'Use at most two y encodings.' });
|
|
97
100
|
if (spec.stack && !['bar', 'column', 'area'].includes(spec.type)) errors.push({ code: 'INVALID_STACK', path: 'stack', message: 'Stacking is supported by bar, column, and area charts.', suggestion: 'Remove stack or use a supported chart type.' });
|
package/types/index.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export interface ChartCapability { type: ChartType; family: string; intents: str
|
|
|
16
16
|
export interface RuntimeCapabilities { version: '2.0'; contractVersion: '1.0'; chartTypes: ChartType[]; charts: Record<ChartType, ChartCapability>; intents: string[]; renderers: Array<'canvas' | 'svg'>; interactions: string[]; exports: Array<'png' | 'svg' | 'json' | 'jpeg'>; styleSystem: { modes: ThemeMode[]; presets: ThemePreset[]; palettes: ThemePalette[]; switchable: boolean; automatic: boolean; [key: string]: unknown }; headless: { preview?: boolean; json: boolean; svg: boolean; png: boolean | string }; export: { types: string[]; mime: Record<string, string>; browser: Record<string, boolean>; headless: Record<string, string | boolean>; methods: string[]; options: Record<string, unknown>; branding: Record<string, unknown> }; branding: { defaultEnabled: boolean; signature: string; options: Record<string, unknown> }; [key: string]: unknown; }
|
|
17
17
|
export interface ChartPlan { version: '1.0'; intent: string; primary: ChartType; alternatives: ChartType[]; confidence: number; reasons: string[]; requiredFields: string[]; suggestedEncodings: { dimension: string | null; measure: string | null; secondaryMeasure: string | null }; assumptions: string[]; warnings: Diagnostic[]; unsupportedRequests: string[]; nextActions: string[]; capability: ChartCapability; styleRecommendation: StyleRecommendation; data: DataInspection; }
|
|
18
18
|
export interface ChartExplanation { version: '1.0'; type: ChartType; family: string; purpose: string; renderer: Renderer; dataCount: number; encodings: Record<string, string | string[]>; transforms: string[]; interactions: string[]; assumptions: string[]; warnings: Diagnostic[]; style: Partial<StyleRecommendation> & { name?: string }; lineage: { recordIds: string[]; sourcePreserved: boolean }; accessibility: { enabled: boolean; summary: string }; }
|
|
19
|
-
export interface ChartSpec { type: ChartType; renderer?: Renderer; container?: string | Element; width?: number; height?: number; data?: Array<Record<string, unknown>> | { values?: Array<Record<string, unknown>>; [key: string]: unknown }; encoding?: Record<string, unknown>; title?: { text?: string; subtitle?: string }; legend?: { visible?: boolean; position?: string }; grid?: { visible?: boolean; color?: string }; labels?: { enabled?: boolean; format?: string | Record<string, unknown>; color?: string; font?: string }; interaction?: Record<string, boolean>; accessibility?: { enabled?: boolean; description?: string }; theme?: ThemeMode | ThemePreset | ThemeConfig | ResolvedTheme; [key: string]: unknown; }
|
|
19
|
+
export interface ChartSpec { type: ChartType; renderer?: Renderer; container?: string | Element; width?: number; height?: number; data?: Array<Record<string, unknown>> | { values?: Array<Record<string, unknown>>; [key: string]: unknown }; encoding?: Record<string, unknown>; title?: { text?: string; subtitle?: string }; legend?: { visible?: boolean; position?: string }; grid?: { visible?: boolean; color?: string }; labels?: { enabled?: boolean; format?: string | Record<string, unknown>; color?: string; font?: string }; interaction?: Record<string, boolean>; accessibility?: { enabled?: boolean; description?: string }; branding?: boolean | { enabled?: boolean }; theme?: ThemeMode | ThemePreset | ThemeConfig | ResolvedTheme; [key: string]: unknown; }
|
|
20
20
|
export interface BinTransform { type: 'bin'; field: string; output?: string; thresholds?: number; step?: number; extent?: [number, number]; }
|
|
21
21
|
export interface RadarIndicator { name: string; field: string; min?: number; max?: number; }
|
|
22
22
|
export type BusinessFieldType = 'string' | 'number' | 'boolean' | 'date' | 'enum' | 'array' | 'object';
|
|
@@ -54,7 +54,7 @@ export interface Chart {
|
|
|
54
54
|
toDataURL(type?: 'image/png' | 'image/jpeg' | 'image/svg+xml'): string | ExportError;
|
|
55
55
|
toBlob(type?: 'image/png' | 'image/jpeg' | 'image/svg+xml'): Blob | ExportError;
|
|
56
56
|
export(options?: { type?: ExportKind; as?: ExportAs }): string | { version: string; spec: unknown; state: unknown } | Blob | ExportError;
|
|
57
|
-
exportAsync(options?: { type?: ExportKind; as?: ExportAs }): Promise<string | { version: string; spec: unknown; state: unknown } | Blob>;
|
|
57
|
+
exportAsync(options?: { type?: ExportKind; as?: ExportAs }): Promise<string | { version: string; spec: unknown; state: unknown } | Blob | ExportError>;
|
|
58
58
|
download(options?: { type?: ExportKind | 'json' }): { valid: boolean; filename?: string; size?: number; type?: string; code?: string; message?: string } | ExportError | string;
|
|
59
59
|
downloadPNG(): ReturnType<Chart['download']>;
|
|
60
60
|
downloadSVG(): ReturnType<Chart['download']>;
|