@taylorwong/ichartjs 2.0.19 → 2.0.20
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 +6 -0
- package/README.md +3 -2
- package/docs/agent/coding-agent-integration.md +1 -1
- package/docs/agent/conversational-workflow.md +134 -0
- package/docs/agent/development/2.0-release-readiness.md +1 -1
- package/docs/agent/development/iteration-16.md +70 -0
- package/docs/agent/development/roadmap.md +19 -2
- package/docs/agent/diagram-scenario.md +2 -0
- package/docs/agent/frontend-integration.md +1 -1
- package/docs/agent/runtime-contract.md +2 -0
- package/docs/agent/usage-scenarios.md +5 -1
- package/docs/agent/zh-CN/coding-agent-integration.md +1 -1
- package/docs/agent/zh-CN/conversational-workflow.md +128 -0
- package/docs/agent/zh-CN/diagram-scenario.md +2 -0
- package/docs/agent/zh-CN/frontend-integration.md +1 -1
- package/docs/agent/zh-CN/runtime-contract.md +2 -0
- package/docs/agent/zh-CN/usage-scenarios.md +5 -1
- package/docs/manifests/capabilities.json +35 -5
- package/package.json +1 -1
- package/skills/ichartjs/SKILL.md +3 -1
- package/src/capabilities.mjs +23 -2
- package/src/index.mjs +10 -6
- package/src/preferences.mjs +24 -1
- package/src/project.mjs +2 -2
- package/src/renderer.mjs +2 -2
- package/src/scene.mjs +5 -0
- package/types/index.d.ts +5 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.0.20 - 2026-09-30
|
|
4
|
+
|
|
5
|
+
- Completed Iteration 16 preference precedence, Agent source resolution, conversational workflow guidance, capability discovery, and Gallery acceptance coverage.
|
|
6
|
+
- Replaced Flow start/end polygon approximations with native ellipse geometry across Scene Graph, SVG, Canvas, export, scaling, and hit testing.
|
|
7
|
+
- Synchronized runtime, package, Playground, Skill, documentation, and release-pinned installation references.
|
|
8
|
+
|
|
3
9
|
## 2.0.19 - 2026-09-29
|
|
4
10
|
|
|
5
11
|
- Added Iteration 15 adaptive text fitting for Flow, Architecture, Mindmap, and Swimlane nodes, with inline-first edge labels, diagram-only contrast plates, unified diagnostics, and shared SVG/Canvas behavior.
|
package/README.md
CHANGED
|
@@ -36,6 +36,7 @@ getCapabilities
|
|
|
36
36
|
| Frontend integration | [`docs/agent/frontend-integration.md`](docs/agent/frontend-integration.md) |
|
|
37
37
|
| Visual style and themes | [`docs/agent/theme-guide.md`](docs/agent/theme-guide.md) |
|
|
38
38
|
| Chart and page preferences | [`docs/agent/theme-guide.md`](docs/agent/theme-guide.md#chart-and-page-preferences) |
|
|
39
|
+
| Natural-language chart changes | [`docs/agent/conversational-workflow.md`](docs/agent/conversational-workflow.md) |
|
|
39
40
|
| Official Agent Skill for Codex and WorkBuddy | [`skills/ichartjs/SKILL.md`](skills/ichartjs/SKILL.md) |
|
|
40
41
|
|
|
41
42
|
### Install
|
|
@@ -44,7 +45,7 @@ getCapabilities
|
|
|
44
45
|
npm install @taylorwong/ichartjs@^2
|
|
45
46
|
```
|
|
46
47
|
|
|
47
|
-
As a fallback for environments without npm access, install directly from GitHub: `npm install github:wanghetommy/ichartjs#v2.0.
|
|
48
|
+
As a fallback for environments without npm access, install directly from GitHub: `npm install github:wanghetommy/ichartjs#v2.0.20`.
|
|
48
49
|
|
|
49
50
|
### Optional Agent Skill
|
|
50
51
|
|
|
@@ -66,7 +67,7 @@ For a non-interactive global Codex installation:
|
|
|
66
67
|
npx skills add wanghetommy/ichartjs --skill ichartjs --agent codex --global --yes
|
|
67
68
|
```
|
|
68
69
|
|
|
69
|
-
For a release-pinned installation, use `npx skills add https://github.com/wanghetommy/ichartjs/tree/v2.0.
|
|
70
|
+
For a release-pinned installation, use `npx skills add https://github.com/wanghetommy/ichartjs/tree/v2.0.20/skills/ichartjs --agent codex --global --yes`. WorkBuddy users can import the same tagged `skills/ichartjs` URL through the host's Skill interface; do not assume a `--agent workbuddy` adapter unless the installed CLI declares it. Package consumers can still copy `node_modules/@taylorwong/ichartjs/skills/ichartjs` as a manual fallback. After installation, invoke `$ichartjs` when named Skill invocation is supported, or select `ichartjs` in the host UI.
|
|
70
71
|
|
|
71
72
|
### Agent workflow
|
|
72
73
|
|
|
@@ -33,7 +33,7 @@ Install the official Skill with the standard Agent Skills CLI:
|
|
|
33
33
|
npx skills add wanghetommy/ichartjs --skill ichartjs
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
For global non-interactive Codex setup, append `--agent codex --global --yes`. To pin the released workflow, install `https://github.com/wanghetommy/ichartjs/tree/v2.0.
|
|
36
|
+
For global non-interactive Codex setup, append `--agent codex --global --yes`. To pin the released workflow, install `https://github.com/wanghetommy/ichartjs/tree/v2.0.20/skills/ichartjs`. WorkBuddy can import that tagged directory through its Skill interface; only use a host-specific `--agent` value when the installed CLI declares it.
|
|
37
37
|
|
|
38
38
|
Verify discovery with `npx skills add wanghetommy/ichartjs --list`; the result should include `ichartjs`.
|
|
39
39
|
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Conversational Workflow
|
|
2
|
+
|
|
3
|
+
iChart.js does not parse natural-language requests inside the runtime. The host Agent, such as Codex or WorkBuddy, maps the user's request to the public JavaScript contract. The runtime then validates, applies, renders, explains, and exports the result.
|
|
4
|
+
|
|
5
|
+
## Safe Request Loop
|
|
6
|
+
|
|
7
|
+
Use this loop for every conversational change:
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
read current state
|
|
11
|
+
-> classify the request
|
|
12
|
+
-> build a structured change
|
|
13
|
+
-> validate or preview
|
|
14
|
+
-> confirm destructive changes
|
|
15
|
+
-> commit
|
|
16
|
+
-> explain and inspect state
|
|
17
|
+
-> return preview or export artifacts
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Read `getCapabilities()`, `chart.getSpec()`, `chart.getState()`, and `chart.getPreferences()` before changing an existing chart. Preserve the user's original wording separately from the structured change. Do not pass natural-language prose directly as `planChart().intent`; map it to a registered intent token first.
|
|
21
|
+
|
|
22
|
+
## Request Routing
|
|
23
|
+
|
|
24
|
+
| User request | Runtime operation | Confirmation |
|
|
25
|
+
| --- | --- | --- |
|
|
26
|
+
| Change theme, palette, font size, legend, labels, or grid | `setTheme()` or `setPreferences()` | No, unless the host policy requires it |
|
|
27
|
+
| Replace all rows | `setData()` | Usually no; host decides |
|
|
28
|
+
| Change a task, milestone, node, edge, or project record | `previewEdit()` then `applyEdit()` | Yes for bulk, delete, or structural changes |
|
|
29
|
+
| Change chart type, field mapping, axes, title, labels, or layout | `update()` | No, unless it changes the product contract |
|
|
30
|
+
| Change one exact JSON path | `applyPatch()` | Prefer `update()` first |
|
|
31
|
+
| Export a result | `export()`, `downloadSVG()`, or `downloadPNG()` | No |
|
|
32
|
+
|
|
33
|
+
## Visual Change
|
|
34
|
+
|
|
35
|
+
Use the allowlisted preference contract instead of inventing CSS or theme tokens:
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
const capabilities = getPreferenceCapabilities(chart.getSpec().type, { locale: 'en' });
|
|
39
|
+
const patch = {
|
|
40
|
+
theme: { mode: 'dark', palette: 'status' },
|
|
41
|
+
typography: { scale: 1.15 },
|
|
42
|
+
components: { grid: false }
|
|
43
|
+
};
|
|
44
|
+
const checked = validatePreferences(patch, { partial: true });
|
|
45
|
+
if (!checked.valid) throw new Error(JSON.stringify(checked.errors));
|
|
46
|
+
chart.setPreferences(checked.value, { source: 'agent' });
|
|
47
|
+
|
|
48
|
+
const state = chart.getState();
|
|
49
|
+
console.log(state.preferences, state.preferenceResolution, state.style);
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Use `scope: 'global'` when the request applies to every chart sharing a `PreferencesStore`. Use the default chart scope for one chart.
|
|
53
|
+
|
|
54
|
+
## Data and Business Change
|
|
55
|
+
|
|
56
|
+
Use `setData()` only when replacing the chart's complete row set:
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
chart.setData(nextRows);
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
For a semantic business change, preview a typed edit and commit the exact preview:
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
const preview = chart.previewEdit({
|
|
66
|
+
type: 'data-edit',
|
|
67
|
+
operations: [{ op: 'updateProgress', taskId: 'task-2', progress: 80 }]
|
|
68
|
+
});
|
|
69
|
+
if (!preview.valid) return preview.errors;
|
|
70
|
+
|
|
71
|
+
const result = chart.applyEdit(preview.command, {
|
|
72
|
+
preview,
|
|
73
|
+
confirmed: true,
|
|
74
|
+
source: 'agent'
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
This preserves validation, revision checks, ChangeSet information, history, and undo/redo. Never mutate business rows directly when a typed edit operation exists.
|
|
79
|
+
|
|
80
|
+
## Spec Change
|
|
81
|
+
|
|
82
|
+
Use `update()` for normal configuration changes:
|
|
83
|
+
|
|
84
|
+
```js
|
|
85
|
+
chart.update({
|
|
86
|
+
title: { text: 'Monthly revenue' },
|
|
87
|
+
yAxis: { title: 'Revenue', nice: true },
|
|
88
|
+
labels: { enabled: true }
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Use `applyPatch()` only when the Agent must address a known JSON path. Validate the result and inspect diagnostics after either operation.
|
|
93
|
+
|
|
94
|
+
## Required Self-check
|
|
95
|
+
|
|
96
|
+
After a committed change, inspect:
|
|
97
|
+
|
|
98
|
+
```js
|
|
99
|
+
const explanation = chart.explain();
|
|
100
|
+
const state = chart.getState();
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
revision: state.revision,
|
|
104
|
+
health: state.health,
|
|
105
|
+
warnings: state.warnings,
|
|
106
|
+
assumptions: state.assumptions,
|
|
107
|
+
style: state.style,
|
|
108
|
+
preferenceResolution: state.preferenceResolution,
|
|
109
|
+
lineage: explanation.lineage
|
|
110
|
+
};
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Report `UNKNOWN_INTENT`, `LABELS_SUPPRESSED`, `LABEL_TRUNCATED`, `FUNNEL_LABEL_TRUNCATED`, `VALUE_CLAMPED`, `NEGATIVE_VALUE_DROPPED`, `ZERO_TOTAL`, and unsupported-request diagnostics rather than hiding them.
|
|
114
|
+
|
|
115
|
+
## Prompt Template
|
|
116
|
+
|
|
117
|
+
The following prompt is suitable for Codex or WorkBuddy:
|
|
118
|
+
|
|
119
|
+
```text
|
|
120
|
+
Use the iChart.js Skill. Read the current Spec, state, preferences, and capabilities first.
|
|
121
|
+
Classify my request as visual style, complete data replacement, business record edit,
|
|
122
|
+
Spec configuration, diagram edit, or export.
|
|
123
|
+
|
|
124
|
+
Use setPreferences/setTheme for visual changes, setData for complete replacement,
|
|
125
|
+
previewEdit/applyEdit for business or diagram edits, update for normal Spec changes,
|
|
126
|
+
and applyPatch only for a precise JSON path. Validate or preview before committing.
|
|
127
|
+
Ask for confirmation before deletion, bulk edits, or structural changes. After applying,
|
|
128
|
+
run chart.explain() and chart.getState(), then return the changed values, warnings,
|
|
129
|
+
assumptions, lineage, preview URL, and artifact paths.
|
|
130
|
+
|
|
131
|
+
Request: <describe the desired change>
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The host remains responsible for authentication, permissions, persistence, confirmation UI, and the natural-language model. iChart.js remains the validated JavaScript runtime and does not require a CLI, MCP server, or HTTP service for this workflow.
|
|
@@ -40,7 +40,7 @@ iChart.js 2.0 is a new Agent-first product line. Compatibility with 1.x and a 1.
|
|
|
40
40
|
|
|
41
41
|
## Iteration 13 Addendum — 2026-09-23
|
|
42
42
|
|
|
43
|
-
The historical 2.0.0 release decision remains unchanged. Release `v2.0.
|
|
43
|
+
The historical 2.0.0 release decision remains unchanged. Release `v2.0.20` includes Iteration 13 contract hardening, Iteration 14 lightweight Flow semantics, Iteration 15 text-readability hardening, and Iteration 16 Agent presentation and acceptance hardening without adding a chart type: 18 chart profiles, 25 commands, 11 schemas, contract version `1.1`, atomic mutation validation, Mindmap edge discovery, strict TypeScript consumer compilation, no active source cycles, Node tests, Chromium browser acceptance, package dry-run verification, the 13F axis-free layout strategy, Timeline/Milestone layout hardening, renderer-parity Flow semantic shapes, native Flow ellipse primitives, adaptive diagram labels, diagram-only edge-label plates, polar label layout diagnostics for Pie, Donut, and Gauge, preference source resolution, conversational workflow guidance, and Gallery browser acceptance.
|
|
44
44
|
|
|
45
45
|
## Deferred npm Publication
|
|
46
46
|
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Iteration 16 — Agent-ready Presentation and Acceptance Hardening
|
|
2
|
+
|
|
3
|
+
Iteration 16 consolidates the runtime after the chart, diagram, preference, and readability work. It adds no chart type. The goal is to make page-level configuration, Agent changes, browser rendering, lifecycle behavior, and conversational workflows explicit and testable.
|
|
4
|
+
|
|
5
|
+
## 16A — Gallery and Preference Precedence
|
|
6
|
+
|
|
7
|
+
- Make page, chart, Spec, and automatic style sources observable.
|
|
8
|
+
- Keep page-level Gallery controls authoritative for the charts they target.
|
|
9
|
+
- Keep chart-menu changes isolated to one chart.
|
|
10
|
+
- Allow `auto` to clear an override.
|
|
11
|
+
- Version persisted preferences and keep storage failures non-fatal.
|
|
12
|
+
- Test `project-gallery.html` and `preferences-lab.html` in a real browser.
|
|
13
|
+
|
|
14
|
+
The runtime precedence is:
|
|
15
|
+
|
|
16
|
+
```text
|
|
17
|
+
defaults → chart Spec → global PreferencesStore → chart PreferencesStore
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`chart.getState().preferenceResolution` reports the precedence, storage mode, and source of each active scope.
|
|
21
|
+
|
|
22
|
+
## 16B — Agent State and Discoverability
|
|
23
|
+
|
|
24
|
+
- Expose preference resolution beside effective preferences.
|
|
25
|
+
- Publish the conversational routing contract through `getCapabilities()` and `capabilities.json`.
|
|
26
|
+
- Keep diagnostics for hidden, truncated, clamped, or unsupported content machine-readable.
|
|
27
|
+
- Document minimal executable examples for visual, data, Spec, project, and Diagram changes.
|
|
28
|
+
- Keep the Skill and English/Chinese Agent guides aligned.
|
|
29
|
+
|
|
30
|
+
## 16C — Renderer, Responsive, and Accessibility Acceptance
|
|
31
|
+
|
|
32
|
+
- Check SVG/Canvas parity for themes, labels, diagrams, and edge-label plates.
|
|
33
|
+
- Test narrow containers, long CJK labels, multiple legends, Funnel stages, and polar labels.
|
|
34
|
+
- Test settings-menu focus, keyboard operation, Escape closing, and ARIA labels.
|
|
35
|
+
- Use browser screenshots for visual acceptance in addition to SVG text assertions.
|
|
36
|
+
|
|
37
|
+
## 16D — Lifecycle and Performance Stability
|
|
38
|
+
|
|
39
|
+
- Repeated theme, preference, update, resize, and destroy operations must not duplicate listeners or menus.
|
|
40
|
+
- Recreate Gallery charts without stale observers or subscriptions.
|
|
41
|
+
- Record a baseline for the 18-chart Gallery and representative larger datasets.
|
|
42
|
+
- Keep package, export, Skill, docs, and manifest checks in the release gate.
|
|
43
|
+
|
|
44
|
+
## 16E — Conversational Workflow
|
|
45
|
+
|
|
46
|
+
- Define natural-language request classification without adding an NLP parser to the library.
|
|
47
|
+
- Route visual changes to `setPreferences()`/`setTheme()`.
|
|
48
|
+
- Route complete data replacement to `setData()`.
|
|
49
|
+
- Route business and Diagram edits through `previewEdit()`/`applyEdit()`.
|
|
50
|
+
- Route normal Spec changes to `update()` and precise JSON changes to `applyPatch()`.
|
|
51
|
+
- Require preview, validation, confirmation for destructive changes, and post-commit `explain()`/`getState()` checks.
|
|
52
|
+
- Add `docs/agent/conversational-workflow.md` and its Chinese companion.
|
|
53
|
+
|
|
54
|
+
## Acceptance
|
|
55
|
+
|
|
56
|
+
- Gallery top-level theme controls change the targeted charts and remain visible in `getState()`.
|
|
57
|
+
- A preference change reports its source and effective precedence.
|
|
58
|
+
- An Agent can follow one documented workflow to modify style, data, Spec, or Diagram content.
|
|
59
|
+
- Invalid changes remain atomic and produce structured diagnostics.
|
|
60
|
+
- SVG and Canvas remain behaviorally aligned for supported features.
|
|
61
|
+
- Repeated create/update/destroy cycles leave no stale runtime resources.
|
|
62
|
+
- `npm test`, browser tests, `npm run agent:check`, and `git diff --check` pass.
|
|
63
|
+
|
|
64
|
+
## Out of Scope
|
|
65
|
+
|
|
66
|
+
- New chart types.
|
|
67
|
+
- CLI, MCP, or HTTP services.
|
|
68
|
+
- A built-in natural-language model or parser.
|
|
69
|
+
- Collaborative editing.
|
|
70
|
+
- Arbitrary CSS injection or a large theme designer.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# iChart.js 2.0 Roadmap
|
|
2
2
|
|
|
3
|
-
> Roadmap baseline: 2026-09-14. Current release status: `v2.0.
|
|
3
|
+
> Roadmap baseline: 2026-09-14. Current release status: `v2.0.20` includes the completed Iteration 12 structured-diagram work, Iteration 13 contract hardening, Iteration 14 lightweight Flow semantics, Iteration 15 text-readability hardening, Iteration 16 Agent presentation and acceptance hardening, native Flow ellipse primitives, sector-aware Pie/Donut labels, radius-aware Gauge metrics, and the preceding layout and diagnostic improvements. Geographic charts and 3D rendering remain out of scope until explicitly reintroduced.
|
|
4
4
|
|
|
5
5
|
## Current Status
|
|
6
6
|
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
- Iteration 13A–13F is complete and included in `v2.0.18`: canonical contract registry and generated capability projection, atomic Spec/data/style mutations, row-complete project validation, explicit Mindmap edge metadata, complete public TypeScript declarations and consumer fixture, module-cycle and documentation/example gates, browser acceptance, package dry-run evidence, an axis-free layout strategy for Pie, Funnel, Gauge, and Radar, Timeline/Milestone layout hardening, and runtime diagnostics for constrained Funnel labels and Swimlane label aliases. No chart type was added.
|
|
19
19
|
- Iteration 14A–14D is complete and included in `v2.0.18`: lightweight Flow semantics for start/end, process, decision, input/output, and connector nodes, explicit decision branches and loops, shared SVG/Canvas geometry, capability discovery, validation, and Gallery coverage. No new chart type was added.
|
|
20
20
|
- Iteration 15A–15D is complete and included in `v2.0.19`: adaptive diagram node labels, semantic inline-first edge labels, diagram edge-label background plates, plate-free generic chart labels, shared SVG/Canvas fitting, unified label diagnostics, Agent/diagram documentation, and polar label layout hardening for Pie, Donut, and Gauge.
|
|
21
|
+
- Iteration 16A–16E is complete and included in `v2.0.20`: Gallery preference precedence, preference source resolution, conversational Agent routing, bilingual workflow guidance, manifest discovery, lifecycle regression coverage, and browser acceptance for Gallery theme controls.
|
|
22
|
+
- Flow start/end nodes now use native ellipse geometry across Scene Graph, SVG, Canvas, export, scaling, and hit testing.
|
|
21
23
|
- 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`.
|
|
22
24
|
|
|
23
25
|
## Iteration 4 — Agent Data Contract and Business Editing
|
|
@@ -276,6 +278,20 @@ Allow users and Agents to adjust a chart's visual presentation after creation th
|
|
|
276
278
|
- Settings UI is excluded from SVG, PNG, and JSON chart exports.
|
|
277
279
|
- `npm run agent:check`, `git diff --check`, and `http://localhost:3000/playground/preferences-lab.html` acceptance pass.
|
|
278
280
|
|
|
281
|
+
## Iteration 16 — Agent-ready Presentation and Acceptance Hardening
|
|
282
|
+
|
|
283
|
+
### Goal
|
|
284
|
+
|
|
285
|
+
Make page-level configuration, Agent changes, browser rendering, lifecycle behavior, and natural-language workflows explicit and testable without adding chart types or a separate service. The complete plan and acceptance contract are in `docs/agent/development/iteration-16.md`.
|
|
286
|
+
|
|
287
|
+
### Deliverables
|
|
288
|
+
|
|
289
|
+
- `chart.getState().preferenceResolution` and `PreferencesStore.getResolution()`.
|
|
290
|
+
- Capability and manifest entries for conversational routing and preference precedence.
|
|
291
|
+
- Bilingual `conversational-workflow.md` guidance for style, data, Spec, Diagram, and export requests.
|
|
292
|
+
- Gallery theme-control browser regression and Iteration 16 runtime tests.
|
|
293
|
+
- Updated Skill, README, runtime contract, usage scenarios, TypeScript declarations, and generated manifests.
|
|
294
|
+
|
|
279
295
|
## Recommended Execution Order
|
|
280
296
|
|
|
281
297
|
1. Finish Iteration 3 browser acceptance.
|
|
@@ -286,4 +302,5 @@ Allow users and Agents to adjust a chart's visual presentation after creation th
|
|
|
286
302
|
6. Execute Iteration 8 to complete existing chart behavior, Agent adaptation, and the 2.0 release gates.
|
|
287
303
|
7. Execute Iteration 9 to standardize adaptive visual styling without expanding chart count.
|
|
288
304
|
8. Execute Iteration 11 to add post-creation chart and page preferences without expanding chart count.
|
|
289
|
-
9.
|
|
305
|
+
9. Complete Iteration 16 acceptance hardening before introducing another chart family or service integration.
|
|
306
|
+
10. Re-evaluate geographic and 3D scope only after usage data confirms demand.
|
|
@@ -71,6 +71,8 @@ For a mindmap, prefer the compact parent contract:
|
|
|
71
71
|
|
|
72
72
|
Supported kinds are `start`, `end`, `process`, `decision`, `io`, and `connector`. A decision should have at least two outgoing labeled edges. Loops are ordinary explicit `from`/`to` edges and are allowed. Connectors are circular hand-off points; they do not use implicit matching, so every connection remains visible in `edges`.
|
|
73
73
|
|
|
74
|
+
Flow `start` and `end` nodes render as true ellipse shapes; `process` nodes are rectangles, `decision` nodes are diamonds, and `io` nodes are parallelograms.
|
|
75
|
+
|
|
74
76
|
Node labels are centered on the node and use adaptive fitting: they wrap to at most two lines, reduce to a bounded minimum font size, and truncate only as a last resort. Compact nodes such as small connectors suppress their internal label rather than rendering unreadable text; the full label remains in the node data and accessibility surface. Edge labels stay inline when space permits, use a high-opacity background plate when the line would reduce contrast, and move aside only when they collide with nodes or other labels. `getState().layout.labels` and `explain().warnings` expose any wrapping, scaling, truncation, or suppression.
|
|
75
77
|
|
|
76
78
|
## Current Capabilities
|
|
@@ -10,7 +10,7 @@ This is the production component path. For one-off files or Agent-led repository
|
|
|
10
10
|
npm install @taylorwong/ichartjs@^2
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
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.20`.
|
|
14
14
|
|
|
15
15
|
Use the package through a bundler or another environment that resolves npm ESM imports:
|
|
16
16
|
|
|
@@ -12,6 +12,8 @@ Agents should use `getCapabilities()` first instead of hard-coding undeclared ty
|
|
|
12
12
|
|
|
13
13
|
For post-creation visual settings, use `getPreferenceCapabilities(chartType, { locale }) → chart.getPreferences() → validatePreferences(patch) → chart.setPreferences(patch, { source: 'agent' }) → chart.getState().preferences`. This keeps Agent changes on the same allowlisted contract as the built-in settings menu.
|
|
14
14
|
|
|
15
|
+
The effective preference precedence is `defaults → chart Spec → global PreferencesStore → chart PreferencesStore`. Use `chart.getState().preferenceResolution` to inspect the active scopes, storage mode, and source metadata. The runtime does not parse natural-language prose; use the [Conversational Workflow](conversational-workflow.md) to route prose to the correct mutation API.
|
|
16
|
+
|
|
15
17
|
Iteration 8 adds per-chart profiles through `getChartCapability(type)`. Each profile declares required data roles, supported interactions, renderers, feature status, exports, and practical limits. Unsupported behavior must be handled from this profile or from validation diagnostics rather than guessed.
|
|
16
18
|
|
|
17
19
|
`planChart(data, { intent, renderer })` returns a versioned planning result with a primary chart, alternatives, confidence, reasons, required fields, suggested encodings, assumptions, warnings, unsupported requests, safe next actions, and the selected capability profile. `intent` must be one exact token from `getCapabilities().intents`; natural-language prose must be mapped before planning. An unknown token returns `UNKNOWN_INTENT` and a fallback plan, so Agents must inspect warnings before accepting `primary`. Planning never invents business meaning, units, dates, or missing fields.
|
|
@@ -8,6 +8,8 @@ iChart.js has three layers:
|
|
|
8
8
|
|
|
9
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
10
|
|
|
11
|
+
For natural-language changes to an existing chart, use the [Conversational Workflow](conversational-workflow.md). The Runtime does not parse prose itself; the host Agent maps prose to validated Runtime calls.
|
|
12
|
+
|
|
11
13
|
## Choose a Scenario
|
|
12
14
|
|
|
13
15
|
| Need | Use | Runtime location | Typical output |
|
|
@@ -79,6 +81,8 @@ const chart = createChart({
|
|
|
79
81
|
|
|
80
82
|
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.
|
|
81
83
|
|
|
84
|
+
For a request such as “use a dark theme, hide the grid, and change the March value,” route the style part through `setPreferences()` and the business-record part through `previewEdit()`/`applyEdit()` rather than mutating rows directly. Return the resulting `explain()` and `getState()` diagnostics.
|
|
85
|
+
|
|
82
86
|
Use this scenario for dashboards, admin pages, project management products, editors, and embedded analytics.
|
|
83
87
|
|
|
84
88
|
## Scenario 2: Ask a Coding Agent to Modify a Project
|
|
@@ -121,7 +125,7 @@ npx skills add wanghetommy/ichartjs --skill ichartjs --agent codex --global --ye
|
|
|
121
125
|
For reproducible installation, pin the released Skill directory:
|
|
122
126
|
|
|
123
127
|
```bash
|
|
124
|
-
npx skills add https://github.com/wanghetommy/ichartjs/tree/v2.0.
|
|
128
|
+
npx skills add https://github.com/wanghetommy/ichartjs/tree/v2.0.20/skills/ichartjs \
|
|
125
129
|
--agent codex --global --yes
|
|
126
130
|
```
|
|
127
131
|
|
|
@@ -22,7 +22,7 @@ npm Registry 中无作用域的 `ichartjs` 是安全占位包,并非本项目
|
|
|
22
22
|
npx skills add wanghetommy/ichartjs --skill ichartjs
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
Codex 全局无交互安装可追加 `--agent codex --global --yes`。需要固定发布版本时,安装 `https://github.com/wanghetommy/ichartjs/tree/v2.0.
|
|
25
|
+
Codex 全局无交互安装可追加 `--agent codex --global --yes`。需要固定发布版本时,安装 `https://github.com/wanghetommy/ichartjs/tree/v2.0.20/skills/ichartjs`。WorkBuddy 可通过自身 Skill 界面导入该带 Tag 的目录;只有当前 CLI 明确声明对应适配器时才使用宿主专用 `--agent` 参数。
|
|
26
26
|
|
|
27
27
|
使用 `npx skills add wanghetommy/ichartjs --list` 验证发现结果,其中应包含 `ichartjs`。
|
|
28
28
|
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# 自然语言修改工作流
|
|
2
|
+
|
|
3
|
+
iChart.js Runtime 不在内部解析自然语言。Codex、WorkBuddy 等宿主 Agent 负责理解用户请求,并将请求转换成公开的 JavaScript API;Runtime 负责校验、提交、渲染、解释和导出。
|
|
4
|
+
|
|
5
|
+
## 安全执行流程
|
|
6
|
+
|
|
7
|
+
所有对话式修改都遵循:
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
读取当前状态
|
|
11
|
+
→ 判断请求类型
|
|
12
|
+
→ 生成结构化变更
|
|
13
|
+
→ 校验或预览
|
|
14
|
+
→ 破坏性操作请求确认
|
|
15
|
+
→ 提交
|
|
16
|
+
→ explain 和 getState 自检
|
|
17
|
+
→ 返回预览或导出结果
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
修改已有图表前,先读取 `getCapabilities()`、`chart.getSpec()`、`chart.getState()` 和 `chart.getPreferences()`。原始自然语言应单独保留,不能直接把自然语言句子传给 `planChart().intent`,应先映射为已注册的 intent token。
|
|
21
|
+
|
|
22
|
+
## 请求分流
|
|
23
|
+
|
|
24
|
+
| 用户请求 | Runtime 操作 | 是否确认 |
|
|
25
|
+
| --- | --- | --- |
|
|
26
|
+
| 修改主题、配色、字号、图例、标签、网格 | `setTheme()` 或 `setPreferences()` | 通常不需要 |
|
|
27
|
+
| 替换全部数据行 | `setData()` | 由宿主策略决定 |
|
|
28
|
+
| 修改任务、里程碑、节点、连线或项目记录 | `previewEdit()` → `applyEdit()` | 批量、删除、结构调整需要 |
|
|
29
|
+
| 修改图表类型、字段、坐标轴、标题、标签或布局 | `update()` | 通常不需要 |
|
|
30
|
+
| 修改一个明确的 JSON 路径 | `applyPatch()` | 优先使用 `update()` |
|
|
31
|
+
| 导出结果 | `export()`、`downloadSVG()` 或 `downloadPNG()` | 不需要 |
|
|
32
|
+
|
|
33
|
+
## 修改样式
|
|
34
|
+
|
|
35
|
+
使用白名单偏好接口,不要让 Agent 自行发明 CSS 或颜色 Token:
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
const capabilities = getPreferenceCapabilities(chart.getSpec().type, { locale: 'zh-CN' });
|
|
39
|
+
const patch = {
|
|
40
|
+
theme: { mode: 'dark', palette: 'status' },
|
|
41
|
+
typography: { scale: 1.15 },
|
|
42
|
+
components: { grid: false }
|
|
43
|
+
};
|
|
44
|
+
const checked = validatePreferences(patch, { partial: true });
|
|
45
|
+
if (!checked.valid) throw new Error(JSON.stringify(checked.errors));
|
|
46
|
+
chart.setPreferences(checked.value, { source: 'agent' });
|
|
47
|
+
|
|
48
|
+
const state = chart.getState();
|
|
49
|
+
console.log(state.preferences, state.preferenceResolution, state.style);
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
使用共享 `PreferencesStore` 时,`scope: 'global'` 表示修改页面全部图表;默认作用于当前图表。
|
|
53
|
+
|
|
54
|
+
## 修改数据和业务记录
|
|
55
|
+
|
|
56
|
+
只有在替换整批数据时使用 `setData()`:
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
chart.setData(nextRows);
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
修改任务、里程碑、节点、边等业务记录时,使用带预览的编辑命令:
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
const preview = chart.previewEdit({
|
|
66
|
+
type: 'data-edit',
|
|
67
|
+
operations: [{ op: 'updateProgress', taskId: 'task-2', progress: 80 }]
|
|
68
|
+
});
|
|
69
|
+
if (!preview.valid) return preview.errors;
|
|
70
|
+
|
|
71
|
+
const result = chart.applyEdit(preview.command, {
|
|
72
|
+
preview,
|
|
73
|
+
confirmed: true,
|
|
74
|
+
source: 'agent'
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
这样可以保留校验、revision、ChangeSet、历史记录和撤销/重做。存在对应编辑命令时,不要直接修改业务数据对象。
|
|
79
|
+
|
|
80
|
+
## 修改 Spec 配置
|
|
81
|
+
|
|
82
|
+
普通配置修改使用 `update()`:
|
|
83
|
+
|
|
84
|
+
```js
|
|
85
|
+
chart.update({
|
|
86
|
+
title: { text: '月度收入' },
|
|
87
|
+
yAxis: { title: '收入', nice: true },
|
|
88
|
+
labels: { enabled: true }
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
只有在必须修改明确 JSON 路径时才使用 `applyPatch()`。两种修改后都要检查校验结果和诊断信息。
|
|
93
|
+
|
|
94
|
+
## 提交后的自检
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
const explanation = chart.explain();
|
|
98
|
+
const state = chart.getState();
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
revision: state.revision,
|
|
102
|
+
health: state.health,
|
|
103
|
+
warnings: state.warnings,
|
|
104
|
+
assumptions: state.assumptions,
|
|
105
|
+
style: state.style,
|
|
106
|
+
preferenceResolution: state.preferenceResolution,
|
|
107
|
+
lineage: explanation.lineage
|
|
108
|
+
};
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`UNKNOWN_INTENT`、`LABELS_SUPPRESSED`、`LABEL_TRUNCATED`、`FUNNEL_LABEL_TRUNCATED`、`VALUE_CLAMPED`、`NEGATIVE_VALUE_DROPPED`、`ZERO_TOTAL` 和不支持的配置都必须报告,不能静默忽略。
|
|
112
|
+
|
|
113
|
+
## 推荐提示词
|
|
114
|
+
|
|
115
|
+
```text
|
|
116
|
+
使用 iChart.js Skill。先读取当前 Spec、状态、偏好和能力清单。
|
|
117
|
+
判断我的请求属于视觉样式、整批数据替换、业务记录编辑、Spec 配置、Diagram 编辑还是导出。
|
|
118
|
+
|
|
119
|
+
视觉调整使用 setPreferences/setTheme;整批替换使用 setData;
|
|
120
|
+
业务和 Diagram 编辑使用 previewEdit/applyEdit;普通 Spec 配置使用 update;
|
|
121
|
+
只有精确 JSON 路径修改才使用 applyPatch。提交前先校验或预览。
|
|
122
|
+
删除、批量修改和结构调整先请求确认。提交后调用 chart.explain() 和 chart.getState(),
|
|
123
|
+
返回变更内容、警告、假设、lineage、预览地址和文件路径。
|
|
124
|
+
|
|
125
|
+
请求:<描述需要的修改>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
宿主负责权限、确认界面、持久化和自然语言模型;iChart.js 负责可校验的 JavaScript Runtime,不需要为了这个工作流增加 CLI、MCP 或 HTTP 服务。
|
|
@@ -70,6 +70,8 @@ Architecture 的 `layers` 是从上到下排列的水平分层。同层自动布
|
|
|
70
70
|
|
|
71
71
|
支持的 `kind` 为 `start`、`end`、`process`、`decision`、`io` 和 `connector`。判断节点建议至少有两条带标签的出边;循环就是普通的显式 `from` / `to` 回边。Connector 是圆形的断开连接点,不使用隐式同名匹配,所有连接都必须明确写在 `edges` 中。
|
|
72
72
|
|
|
73
|
+
Flow 的 `start` 和 `end` 节点使用真正的椭圆图元;`process` 使用矩形,`decision` 使用菱形,`io` 使用平行四边形。
|
|
74
|
+
|
|
73
75
|
节点文字默认在节点内居中,并按自适应规则处理:优先保持字号,必要时最多换成两行,再缩小到有限的最小字号,最后才截断。尺寸过小的节点(例如小型 Connector)会隐藏内部文字,避免显示不全;完整文字仍保留在节点数据和无障碍语义中。连线文字在空间足够时优先贴近连线显示;线条影响对比度时使用高不透明度背景底板,只有与节点或其他文字冲突时才移到连线旁边。`getState().layout.labels` 和 `explain().warnings` 会暴露换行、缩放、截断和隐藏结果。
|
|
74
76
|
|
|
75
77
|
## 当前能力
|
|
@@ -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.20`。
|
|
10
10
|
|
|
11
11
|
```js
|
|
12
12
|
import { createChart } from '@taylorwong/ichartjs';
|
|
@@ -12,6 +12,8 @@ Agent 应优先使用 `getCapabilities()`,不要硬编码未声明的图表类
|
|
|
12
12
|
|
|
13
13
|
图表创建后的视觉设置使用 `getPreferenceCapabilities(chartType, { locale }) → chart.getPreferences() → validatePreferences(patch) → chart.setPreferences(patch, { source: 'agent' }) → chart.getState().preferences`。这样 Agent 与内置设置菜单始终使用同一份白名单契约。
|
|
14
14
|
|
|
15
|
+
有效偏好优先级为 `defaults → chart Spec → global PreferencesStore → chart PreferencesStore`。使用 `chart.getState().preferenceResolution` 查看当前作用域、存储方式和来源。Runtime 不解析自然语言,请使用[自然语言修改工作流](conversational-workflow.md)把自然语言分流到正确的修改 API。
|
|
16
|
+
|
|
15
17
|
Iteration 8 通过 `getChartCapability(type)` 提供逐图表能力档案,包括必需数据角色、支持的交互、Renderer、功能状态、导出和建议限制。Agent 不应猜测未声明能力。
|
|
16
18
|
|
|
17
19
|
`planChart(data, { intent, renderer })` 返回版本化规划结果:主选图表、备选项、置信度、原因、缺失字段、建议编码、假设、警告、不支持请求和安全下一步。规划不会虚构业务含义、单位、日期或缺失字段。
|
|
@@ -8,6 +8,8 @@ iChart.js 可以分成三层:
|
|
|
8
8
|
|
|
9
9
|
Skill 不是第二套渲染器,也不是服务端。使用 Skill 的 Agent 仍然需要 JavaScript 宿主,才能生成交互页面或文件。
|
|
10
10
|
|
|
11
|
+
修改已有图表的自然语言请求请阅读[自然语言修改工作流](conversational-workflow.md)。Runtime 本身不解析自然语言;宿主 Agent 负责把自然语言映射为经过校验的 Runtime 调用。
|
|
12
|
+
|
|
11
13
|
## 场景选择
|
|
12
14
|
|
|
13
15
|
| 需求 | 使用方式 | 运行位置 | 常见输出 |
|
|
@@ -77,6 +79,8 @@ const chart = createChart({
|
|
|
77
79
|
|
|
78
80
|
宿主项目负责数据加载、认证、路由、持久化和状态管理。数据变化使用 `setData()`,Spec 变化使用 `update()`,替换组件前调用 `destroy()`。
|
|
79
81
|
|
|
82
|
+
例如“使用深色主题、隐藏网格,并修改三月数据”应拆分为 `setPreferences()` 和 `previewEdit()`/`applyEdit()`,不要直接修改业务数据对象。提交后返回 `explain()` 和 `getState()` 的诊断结果。
|
|
83
|
+
|
|
80
84
|
适用于管理后台、数据看板、项目管理、流程编辑和嵌入式分析页面。
|
|
81
85
|
|
|
82
86
|
## 场景二:让 Coding Agent 修改项目
|
|
@@ -116,7 +120,7 @@ npx skills add wanghetommy/ichartjs --skill ichartjs --agent codex --global --ye
|
|
|
116
120
|
需要固定发布版本时,直接安装已发布的 Skill 目录:
|
|
117
121
|
|
|
118
122
|
```bash
|
|
119
|
-
npx skills add https://github.com/wanghetommy/ichartjs/tree/v2.0.
|
|
123
|
+
npx skills add https://github.com/wanghetommy/ichartjs/tree/v2.0.20/skills/ichartjs \
|
|
120
124
|
--agent codex --global --yes
|
|
121
125
|
```
|
|
122
126
|
|
|
@@ -230,9 +230,9 @@
|
|
|
230
230
|
"interactiveSettingsUI": true,
|
|
231
231
|
"precedence": [
|
|
232
232
|
"defaults",
|
|
233
|
+
"spec",
|
|
233
234
|
"global",
|
|
234
|
-
"chart"
|
|
235
|
-
"temporary-agent-patch"
|
|
235
|
+
"chart"
|
|
236
236
|
],
|
|
237
237
|
"discovery": {
|
|
238
238
|
"capabilities": "getPreferenceCapabilities(chartType?, { locale? })",
|
|
@@ -282,8 +282,8 @@
|
|
|
282
282
|
"Short form `branding: false` and object form `branding: { enabled: false }` are both supported. Custom text, link, position, or font are intentionally NOT exposed in the current iteration."
|
|
283
283
|
]
|
|
284
284
|
},
|
|
285
|
-
"packageVersion": "2.0.
|
|
286
|
-
"runtimeVersion": "2.0.
|
|
285
|
+
"packageVersion": "2.0.20",
|
|
286
|
+
"runtimeVersion": "2.0.20",
|
|
287
287
|
"chartTypes": [
|
|
288
288
|
"line",
|
|
289
289
|
"area",
|
|
@@ -1408,5 +1408,35 @@
|
|
|
1408
1408
|
"toggleGroupCollapse",
|
|
1409
1409
|
"duplicateSelection",
|
|
1410
1410
|
"pasteSelection"
|
|
1411
|
-
]
|
|
1411
|
+
],
|
|
1412
|
+
"conversationalWorkflow": {
|
|
1413
|
+
"version": "1.0",
|
|
1414
|
+
"steps": [
|
|
1415
|
+
"read-state",
|
|
1416
|
+
"classify-request",
|
|
1417
|
+
"build-validated-change",
|
|
1418
|
+
"preview-or-validate",
|
|
1419
|
+
"confirm-when-required",
|
|
1420
|
+
"commit",
|
|
1421
|
+
"explain-and-report"
|
|
1422
|
+
],
|
|
1423
|
+
"routing": {
|
|
1424
|
+
"visual": "setPreferences or setTheme",
|
|
1425
|
+
"replaceData": "setData",
|
|
1426
|
+
"businessData": "previewEdit then applyEdit",
|
|
1427
|
+
"spec": "update",
|
|
1428
|
+
"jsonPointer": "applyPatch",
|
|
1429
|
+
"diagram": "previewEdit then applyEdit"
|
|
1430
|
+
},
|
|
1431
|
+
"confirmation": [
|
|
1432
|
+
"deletion",
|
|
1433
|
+
"structural-change",
|
|
1434
|
+
"bulk-edit"
|
|
1435
|
+
],
|
|
1436
|
+
"selfCheck": [
|
|
1437
|
+
"chart.explain()",
|
|
1438
|
+
"chart.getState()",
|
|
1439
|
+
"chart.getState().health"
|
|
1440
|
+
]
|
|
1441
|
+
}
|
|
1412
1442
|
}
|
package/package.json
CHANGED
package/skills/ichartjs/SKILL.md
CHANGED
|
@@ -8,6 +8,7 @@ description: Plan, validate, render, explain, and safely edit iChart.js visualiz
|
|
|
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
10
|
Read the [usage scenarios](https://github.com/wanghetommy/ichartjs/blob/master/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
|
+
Read the [conversational workflow](https://github.com/wanghetommy/ichartjs/blob/master/docs/agent/conversational-workflow.md) when the user asks to change an existing chart in natural language.
|
|
11
12
|
|
|
12
13
|
## Source and Runtime Setup
|
|
13
14
|
|
|
@@ -21,7 +22,7 @@ Recommended installation:
|
|
|
21
22
|
npx skills add wanghetommy/ichartjs --skill ichartjs
|
|
22
23
|
```
|
|
23
24
|
|
|
24
|
-
Use `--agent codex --global --yes` for global non-interactive Codex installation. Use the tagged directory `https://github.com/wanghetommy/ichartjs/tree/v2.0.
|
|
25
|
+
Use `--agent codex --global --yes` for global non-interactive Codex installation. Use the tagged directory `https://github.com/wanghetommy/ichartjs/tree/v2.0.20/skills/ichartjs` when reproducibility matters. WorkBuddy can import the same directory through its Skill interface; do not assume a `--agent workbuddy` adapter unless the installed CLI declares it.
|
|
25
26
|
|
|
26
27
|
The Skill is a workflow adapter, not the chart runtime. If the current JavaScript or TypeScript project does not already depend on iChart.js, install the matching runtime from npm:
|
|
27
28
|
|
|
@@ -48,6 +49,7 @@ Do not install the unscoped npm registry package named `ichartjs`; it is current
|
|
|
48
49
|
13. Provide an exact preview URL or artifact path and report assumptions, warnings, and deferred checks.
|
|
49
50
|
14. Prefer `theme: { mode: 'auto', preset, palette }`; preserve explicit user style choices and use `chart.setTheme()` for live switching.
|
|
50
51
|
15. For post-creation visual changes, call `getPreferenceCapabilities(chartType, { locale })`, validate the patch with `validatePreferences()`, apply it with `chart.setPreferences(..., { source: 'agent' })`, and verify `chart.getState().preferences`.
|
|
52
|
+
16. For natural-language changes, classify the request before mutating: visual → `setPreferences()`/`setTheme()`, complete data replacement → `setData()`, business or Diagram edit → `previewEdit()`/`applyEdit()`, normal Spec change → `update()`, exact JSON path → `applyPatch()`. Verify `chart.getState().preferenceResolution` and `chart.explain()` after commit.
|
|
51
53
|
|
|
52
54
|
Use `@taylorwong/ichartjs` for package imports. Use `examples/agent-workflow.mjs` as the executable baseline when working in the repository.
|
|
53
55
|
|
package/src/capabilities.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { inspectData } from './data.mjs';
|
|
5
5
|
import { planStyle, styleCapabilities } from './theme.mjs';
|
|
6
|
-
import { defaultPreferences, preferenceDensities, preferenceMotions, preferenceTriStates } from './preferences.mjs';
|
|
6
|
+
import { defaultPreferences, preferenceDensities, preferenceMotions, preferencePrecedence, preferenceTriStates } from './preferences.mjs';
|
|
7
7
|
import { businessModels, chartProfiles, chartTypes, commandTypes, contractVersion, diagramEdgeModels, diagramOperations, exportTypes, flowNodeKinds, interactionDefaults, renderers } from './contract-registry.mjs';
|
|
8
8
|
|
|
9
9
|
export { chartProfiles, chartTypes };
|
|
@@ -81,7 +81,7 @@ export function getPreferenceCapabilities(chartType = null, { locale = 'en' } =
|
|
|
81
81
|
field({ path: 'branding.enabled', group: 'components', key: 'branding', type: 'tri-state', values: preferenceTriStates, defaultValue: defaultPreferences.branding.enabled }),
|
|
82
82
|
field({ path: 'motion', group: 'behavior', key: 'motion', type: 'enum', values: preferenceMotions, defaultValue: defaultPreferences.motion })
|
|
83
83
|
];
|
|
84
|
-
return { version: '1.0', locale: resolvedLocale, chartType: profile?.type || null, scopes, persistence: ['memory', 'localStorage', 'adapter'], precedence: [
|
|
84
|
+
return { version: '1.0', locale: resolvedLocale, chartType: profile?.type || null, scopes, persistence: ['memory', 'localStorage', 'adapter'], precedence: [...preferencePrecedence], fields };
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
export function planChart(input, options = {}) {
|
|
@@ -223,6 +223,20 @@ export function getCapabilities() {
|
|
|
223
223
|
data: ['normalize', 'inspect', 'filter', 'sort', 'groupBy', 'sum', 'average', 'topN', 'percentage', 'bin'],
|
|
224
224
|
themes: [...styleCapabilities.modes],
|
|
225
225
|
styleSystem: JSON.parse(JSON.stringify(styleCapabilities)),
|
|
226
|
+
conversationalWorkflow: {
|
|
227
|
+
version: '1.0',
|
|
228
|
+
steps: ['read-state', 'classify-request', 'build-validated-change', 'preview-or-validate', 'confirm-when-required', 'commit', 'explain-and-report'],
|
|
229
|
+
routing: {
|
|
230
|
+
visual: 'setPreferences or setTheme',
|
|
231
|
+
replaceData: 'setData',
|
|
232
|
+
businessData: 'previewEdit then applyEdit',
|
|
233
|
+
spec: 'update',
|
|
234
|
+
jsonPointer: 'applyPatch',
|
|
235
|
+
diagram: 'previewEdit then applyEdit'
|
|
236
|
+
},
|
|
237
|
+
confirmation: ['deletion', 'structural-change', 'bulk-edit'],
|
|
238
|
+
selfCheck: ['chart.explain()', 'chart.getState()', 'chart.getState().health']
|
|
239
|
+
},
|
|
226
240
|
preferences: {
|
|
227
241
|
version: '1.0',
|
|
228
242
|
scopes: preferenceCapabilities.scopes,
|
|
@@ -231,6 +245,13 @@ export function getCapabilities() {
|
|
|
231
245
|
agentAdjustable: true,
|
|
232
246
|
interactiveSettingsUI: true,
|
|
233
247
|
precedence: preferenceCapabilities.precedence,
|
|
248
|
+
discovery: {
|
|
249
|
+
capabilities: 'getPreferenceCapabilities(chartType?, { locale? })',
|
|
250
|
+
current: 'chart.getPreferences()',
|
|
251
|
+
validate: 'validatePreferences(patch, { partial: true })',
|
|
252
|
+
apply: "chart.setPreferences(patch, { scope, source: 'agent' })",
|
|
253
|
+
verify: 'chart.getState().preferences'
|
|
254
|
+
},
|
|
234
255
|
schema: preferenceCapabilities
|
|
235
256
|
},
|
|
236
257
|
commands: [...commandTypes],
|
package/src/index.mjs
CHANGED
|
@@ -20,7 +20,7 @@ import { diagramLayoutModes, diagramModes, edgeRoutingModes, normalizeDiagramSpe
|
|
|
20
20
|
import { normalizeProjectCalendar, applyWorkingCalendar, normalizeDependencies, analyzeSchedule, analyzeBurndownSeries, analyzeCapacity, buildCapacityView, buildCumulativeFlowSeries, buildVelocitySeries, buildReleaseForecast, buildRiskMatrixSeries, buildIssueAgingSeries } from './project-analytics.mjs';
|
|
21
21
|
import { normalizeLinkedFilters, normalizeLinkedSelection, filterProjectRows, createLinkedProjectState, linkedRecordId } from './project-linking.mjs';
|
|
22
22
|
import { explainChart, getCapabilities as discoverCapabilities, getChartCapability, getPreferenceCapabilities, planChart } from './capabilities.mjs';
|
|
23
|
-
import { applyPreferencesToSpec, createPreferencesStore, defaultPreferences, mergePreferences, mergeThemePreference, normalizePreferences, validatePreferences } from './preferences.mjs';
|
|
23
|
+
import { applyPreferencesToSpec, createPreferencesStore, defaultPreferences, mergePreferences, mergeThemePreference, normalizePreferences, preferencePrecedence, validatePreferences } from './preferences.mjs';
|
|
24
24
|
import { mountChartSettings } from './preferences-ui.mjs';
|
|
25
25
|
import { ChartValidationError } from './errors.mjs';
|
|
26
26
|
import { diagramOperations, flowNodeKinds } from './contract-registry.mjs';
|
|
@@ -91,6 +91,8 @@ function sceneToSvgString(scene, spec = {}) {
|
|
|
91
91
|
attrs.push(`x1="${g.x1}" y1="${g.y1}" x2="${g.x2}" y2="${g.y2}"`);
|
|
92
92
|
} else if (node.type === 'circle') {
|
|
93
93
|
attrs.push(`cx="${g.cx}" cy="${g.cy}" r="${g.r}"`);
|
|
94
|
+
} else if (node.type === 'ellipse') {
|
|
95
|
+
attrs.push(`cx="${g.cx}" cy="${g.cy}" rx="${g.rx}" ry="${g.ry}"`);
|
|
94
96
|
} else if (node.type === 'arc') {
|
|
95
97
|
tag = 'path';
|
|
96
98
|
const large = g.end - g.start > Math.PI ? 1 : 0;
|
|
@@ -129,7 +131,7 @@ function sceneToSvgString(scene, spec = {}) {
|
|
|
129
131
|
} else {
|
|
130
132
|
tag = 'g';
|
|
131
133
|
}
|
|
132
|
-
if (['path', 'circle', 'rect', 'arc'].includes(node.type)) attrs.push(`fill="${hasPaint(s, 'fill') ? _svgEscape(s.fill, 'attr') : 'none'}"`);
|
|
134
|
+
if (['path', 'circle', 'ellipse', 'rect', 'arc'].includes(node.type)) attrs.push(`fill="${hasPaint(s, 'fill') ? _svgEscape(s.fill, 'attr') : 'none'}"`);
|
|
133
135
|
else if (node.type === 'text') attrs.push(`fill="${hasPaint(s, 'fill') ? _svgEscape(s.fill, 'attr') : '#0f172a'}"`);
|
|
134
136
|
if (hasPaint(s, 'stroke')) attrs.push(`stroke="${_svgEscape(s.stroke, 'attr')}"`);
|
|
135
137
|
else if (s.stroke != null) attrs.push('stroke="none"');
|
|
@@ -197,6 +199,7 @@ export class Chart {
|
|
|
197
199
|
this.chartId = input.chartId ?? input.id ?? null;
|
|
198
200
|
this._preferencesStore = isPreferencesStore(input.preferencesStore) ? input.preferencesStore : isPreferencesStore(input.preferences) ? input.preferences : null;
|
|
199
201
|
this._localPreferences = this._preferencesStore ? null : normalizePreferences(input.preferences || {});
|
|
202
|
+
this._localPreferenceSource = input.preferences ? 'host' : null;
|
|
200
203
|
delete result.spec.preferences;
|
|
201
204
|
delete result.spec.preferencesStore;
|
|
202
205
|
delete result.spec.chartId;
|
|
@@ -442,8 +445,9 @@ export class Chart {
|
|
|
442
445
|
}
|
|
443
446
|
getTheme() { return clone(this.spec.theme); }
|
|
444
447
|
getPreferences() { return this._preferencesStore ? this._preferencesStore.getEffective(this.chartId) : clone(this._localPreferences || defaultPreferences); }
|
|
445
|
-
|
|
446
|
-
|
|
448
|
+
getPreferenceResolution() { if (this._preferencesStore?.getResolution) return this._preferencesStore.getResolution(this.chartId); return { version: '1.0', precedence: [...preferencePrecedence], storage: this._preferencesStore?.storage || 'memory', scopes: { defaults: 'runtime', spec: 'chart-spec', global: this._preferencesStore ? 'host' : null, chart: this._preferencesStore ? 'host' : this._localPreferenceSource } }; }
|
|
449
|
+
setPreferences(patch = {}, options = {}) { const checked = validatePreferences(patch, { partial: true }); if (!checked.valid) throw new ChartValidationError('setPreferences', checked.errors); const source = options.source || 'user'; if (this._preferencesStore) { const scope = options.scope === 'global' ? 'global' : 'chart'; if (scope === 'global') this._preferencesStore.setGlobal(checked.value, { ...options, source }); else this._preferencesStore.setChart(this.chartId || 'default', checked.value, { ...options, source }); return this; } const snapshot = this._snapshotMutation(); try { this._localPreferences = mergePreferences(this._localPreferences, checked.value); this._localPreferenceSource = source; this._preferencesSnapshot = JSON.stringify(this._localPreferences); this._resolveStyle(); this._commitMutation(); this.emit('preferenceschange', { chart: this, preferences: this.getPreferences(), source, scope: 'chart', persisted: false }); return this; } catch (error) { this._restoreMutation(snapshot); throw error; } }
|
|
450
|
+
resetPreferences(options = {}) { if (this._preferencesStore) { this._preferencesStore.reset({ ...options, scope: options.scope || 'chart', chartId: this.chartId || 'default' }); return this; } const snapshot = this._snapshotMutation(); try { this._localPreferences = normalizePreferences({}); this._localPreferenceSource = options.source || 'user'; this._preferencesSnapshot = JSON.stringify(this._localPreferences); this._resolveStyle(); this._commitMutation(); this.emit('preferenceschange', { chart: this, preferences: this.getPreferences(), source: options.source || 'user', scope: 'chart', persisted: false }); return this; } catch (error) { this._restoreMutation(snapshot); throw error; } }
|
|
447
451
|
resize(width = this.spec.width, height = this.spec.height) { this.spec.width = width; this.spec.height = height; this.render(); this.emit('resize', { chart: this, width, height }); return this; }
|
|
448
452
|
getSpec() { return JSON.parse(JSON.stringify(this.spec)); }
|
|
449
453
|
_getDiagnostics() { return [...new Map([...(this._specDiagnostics?.warnings || []), ...(this.model.data?.warnings || []), ...(this.spec.theme?.warnings || [])].map(item => [`${item.code}:${item.path || ''}`, item])).values()]; }
|
|
@@ -455,7 +459,7 @@ export class Chart {
|
|
|
455
459
|
const clampedValues = warnings.filter(item => item.code === 'VALUE_CLAMPED').reduce((sum, item) => sum + Number(item.count || 1), 0);
|
|
456
460
|
return { version: '1.0', status: isEmpty ? 'empty' : warnings.length ? 'degraded' : 'ready', renderable: !isEmpty, issues: [...new Set(warnings.map(item => item.code))], metrics: { warnings: warnings.length, suppressedLabels, clampedValues, renderedMarks: marks.length } };
|
|
457
461
|
}
|
|
458
|
-
getState() { const brandingSignature = discoverCapabilities().branding.signature, warnings = this._getDiagnostics(), layoutState = this.model.state?.plot ? { family: this.model.state.layoutFamily || null, plot: clone(this.model.state.plot), chrome: clone(this.model.state.chrome || null), labels: clone(this.model.state.labelLayout || null) } : null; return { renderer: this.renderer.constructor.name, width: this.spec.width, height: this.spec.height, dataCount: this.model.data.rows.length, selected: [...this._selected.values()], revision: this._revision, history: this._history.state(), view: clone(this.spec.view || null), style: clone({ name: this.spec.theme.name, mode: this.spec.theme.mode, resolvedMode: this.spec.theme.resolvedMode, preset: this.spec.theme.preset, palette: this.spec.theme.palette, reasons: this.spec.theme.reasons }), layout: layoutState, timeAxis: clone(this.model.state?.timeAxis || null), axes: clone(this.model.state?.axes || null), health: this._getHealth(warnings), preferences: this.getPreferences(), branding: { enabled: Boolean(this.spec.branding?.enabled), signature: brandingSignature, text: this.spec.branding?.enabled === true ? brandingSignature : null }, warnings: clone(warnings), assumptions: clone(this.model.data?.assumptions || []), normalizations: clone(this._specDiagnostics?.normalizations || []), collapsedGroups: this.getCollapsedGroupIds(), clipboard: { nodes: this._clipboard?.nodes?.length || 0, edges: this._clipboard?.edges?.length || 0 }, projectAnalytics: clone(this.model.state?.projectAnalytics || null), linked: clone(this.model.state?.linked || null) }; }
|
|
462
|
+
getState() { const brandingSignature = discoverCapabilities().branding.signature, warnings = this._getDiagnostics(), layoutState = this.model.state?.plot ? { family: this.model.state.layoutFamily || null, plot: clone(this.model.state.plot), chrome: clone(this.model.state.chrome || null), labels: clone(this.model.state.labelLayout || null) } : null; return { renderer: this.renderer.constructor.name, width: this.spec.width, height: this.spec.height, dataCount: this.model.data.rows.length, selected: [...this._selected.values()], revision: this._revision, history: this._history.state(), view: clone(this.spec.view || null), style: clone({ name: this.spec.theme.name, mode: this.spec.theme.mode, resolvedMode: this.spec.theme.resolvedMode, preset: this.spec.theme.preset, palette: this.spec.theme.palette, reasons: this.spec.theme.reasons }), layout: layoutState, timeAxis: clone(this.model.state?.timeAxis || null), axes: clone(this.model.state?.axes || null), health: this._getHealth(warnings), preferences: this.getPreferences(), preferenceResolution: this.getPreferenceResolution(), branding: { enabled: Boolean(this.spec.branding?.enabled), signature: brandingSignature, text: this.spec.branding?.enabled === true ? brandingSignature : null }, warnings: clone(warnings), assumptions: clone(this.model.data?.assumptions || []), normalizations: clone(this._specDiagnostics?.normalizations || []), collapsedGroups: this.getCollapsedGroupIds(), clipboard: { nodes: this._clipboard?.nodes?.length || 0, edges: this._clipboard?.edges?.length || 0 }, projectAnalytics: clone(this.model.state?.projectAnalytics || null), linked: clone(this.model.state?.linked || null) }; }
|
|
459
463
|
getProjectAnalytics() { return clone(this.model.state?.projectAnalytics || null); }
|
|
460
464
|
getLinkedState() { return clone(this.model.state?.linked || null); }
|
|
461
465
|
setLinkedFilters(filters = {}) { this.spec.project = { ...(this.spec.project || {}), linked: { ...(this.spec.project?.linked || {}), filters: normalizeLinkedFilters(filters) } }; this.emit('linkedstatechange', { chart: this, linked: this.spec.project.linked }); return this.render(); }
|
|
@@ -771,4 +775,4 @@ export { normalizeLinkedFilters, normalizeLinkedSelection, filterProjectRows, cr
|
|
|
771
775
|
|
|
772
776
|
export { contrastRatio, planStyle, resolveTheme, styleCapabilities, themeModes, themePalettes, themePresets, validateThemeContrast, annotationPlugin, dataZoomPlugin, dataLabelsPlugin, accessibilityPlugin };
|
|
773
777
|
export { applyPreferencesToSpec, createPreferencesStore, defaultPreferences, mergePreferences, mergeThemePreference, mountChartSettings, normalizePreferences, validatePreferences };
|
|
774
|
-
export const iChart = { version: '2.0.
|
|
778
|
+
export const iChart = { version: '2.0.20', createChart, ChartValidationError, inspectData, normalizeData, binData, applyTransforms, data, getCapabilities, getChartCapability, getPreferenceCapabilities, planChart, recommend, explainChart, contrastRatio, planStyle, resolveTheme, styleCapabilities, themeModes, themePalettes, themePresets, validateThemeContrast, createPreferencesStore, defaultPreferences, normalizePreferences, mergePreferences, validatePreferences, applyPreferencesToSpec, mountChartSettings, 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/preferences.mjs
CHANGED
|
@@ -8,6 +8,7 @@ export const preferenceVersion = '1.0';
|
|
|
8
8
|
export const preferenceDensities = ['compact', 'comfortable', 'spacious'];
|
|
9
9
|
export const preferenceTriStates = ['auto', true, false];
|
|
10
10
|
export const preferenceMotions = ['auto', 'full', 'reduced', 'off'];
|
|
11
|
+
export const preferencePrecedence = ['defaults', 'spec', 'global', 'chart'];
|
|
11
12
|
|
|
12
13
|
export const defaultPreferences = Object.freeze({
|
|
13
14
|
version: preferenceVersion,
|
|
@@ -101,6 +102,7 @@ function readState(storage, key) {
|
|
|
101
102
|
|
|
102
103
|
export function createPreferencesStore(options = {}) {
|
|
103
104
|
const storage = resolveStorage(options.storage);
|
|
105
|
+
const storageKind = options.storage === 'localStorage' ? 'localStorage' : storage ? 'adapter' : 'memory';
|
|
104
106
|
const storageKey = options.storageKey || 'ichartjs:preferences:v1';
|
|
105
107
|
const loaded = readState(storage, storageKey);
|
|
106
108
|
let state = loaded || {
|
|
@@ -108,6 +110,10 @@ export function createPreferencesStore(options = {}) {
|
|
|
108
110
|
global: normalizePreferences(options.global || {}),
|
|
109
111
|
charts: Object.fromEntries(Object.entries(options.charts || {}).map(([id, value]) => [id, normalizePreferences(value, { partial: true })]))
|
|
110
112
|
};
|
|
113
|
+
const sources = {
|
|
114
|
+
global: loaded ? 'persisted' : options.global ? 'host' : null,
|
|
115
|
+
charts: loaded ? Object.fromEntries(Object.keys(loaded.charts || {}).map(id => [id, 'persisted'])) : Object.fromEntries(Object.keys(options.charts || {}).map(id => [id, 'host']))
|
|
116
|
+
};
|
|
111
117
|
const listeners = new Set();
|
|
112
118
|
const persist = () => {
|
|
113
119
|
if (!storage) return false;
|
|
@@ -121,6 +127,17 @@ export function createPreferencesStore(options = {}) {
|
|
|
121
127
|
const getGlobal = () => clone(state.global);
|
|
122
128
|
const getChart = chartId => clone(state.charts[String(chartId)] || {});
|
|
123
129
|
const getEffective = chartId => mergePreferences(defaultPreferences, state.global, chartId == null ? {} : state.charts[String(chartId)] || {});
|
|
130
|
+
const getResolution = chartId => ({
|
|
131
|
+
version: '1.0',
|
|
132
|
+
precedence: [...preferencePrecedence],
|
|
133
|
+
storage: storageKind,
|
|
134
|
+
scopes: {
|
|
135
|
+
defaults: 'runtime',
|
|
136
|
+
spec: 'chart-spec',
|
|
137
|
+
global: sources.global,
|
|
138
|
+
chart: chartId == null ? null : sources.charts[String(chartId)] || null
|
|
139
|
+
}
|
|
140
|
+
});
|
|
124
141
|
const update = (scope, chartId, patch, { source = 'user', persist: shouldPersist = true } = {}) => {
|
|
125
142
|
const checked = validatePreferences(patch, { partial: true });
|
|
126
143
|
if (!checked.valid) { const error = new Error(checked.errors.map(item => item.message).join(' ')); error.code = 'INVALID_PREFERENCES'; error.details = checked.errors; throw error; }
|
|
@@ -128,7 +145,9 @@ export function createPreferencesStore(options = {}) {
|
|
|
128
145
|
else {
|
|
129
146
|
const key = String(chartId || 'default');
|
|
130
147
|
state.charts[key] = merge(state.charts[key] || {}, checked.value);
|
|
148
|
+
sources.charts[key] = source;
|
|
131
149
|
}
|
|
150
|
+
if (scope === 'global') sources.global = source;
|
|
132
151
|
const persisted = shouldPersist ? persist() : false;
|
|
133
152
|
notify(scope, chartId, source, persisted);
|
|
134
153
|
return scope === 'global' ? getGlobal() : getEffective(chartId);
|
|
@@ -140,19 +159,23 @@ export function createPreferencesStore(options = {}) {
|
|
|
140
159
|
getGlobal,
|
|
141
160
|
getChart,
|
|
142
161
|
getEffective,
|
|
162
|
+
getResolution,
|
|
143
163
|
setGlobal(patch, options = {}) { return update('global', null, patch, options); },
|
|
144
164
|
setChart(chartId, patch, options = {}) { return update('chart', chartId, patch, options); },
|
|
145
165
|
reset({ scope = 'all', chartId, persist: shouldPersist = true, source = 'user' } = {}) {
|
|
146
166
|
if (scope === 'all' || scope === 'global') state.global = normalizePreferences({});
|
|
147
167
|
if (scope === 'all') state.charts = {};
|
|
148
168
|
if (scope === 'chart' && chartId != null) delete state.charts[String(chartId)];
|
|
169
|
+
if (scope === 'all' || scope === 'global') sources.global = source;
|
|
170
|
+
if (scope === 'all') sources.charts = {};
|
|
171
|
+
if (scope === 'chart' && chartId != null) sources.charts[String(chartId)] = source;
|
|
149
172
|
const persisted = shouldPersist ? persist() : false;
|
|
150
173
|
notify(scope, chartId, source, persisted);
|
|
151
174
|
return chartId != null ? getEffective(chartId) : getGlobal();
|
|
152
175
|
},
|
|
153
176
|
subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener); },
|
|
154
177
|
persist,
|
|
155
|
-
storage:
|
|
178
|
+
storage: storageKind
|
|
156
179
|
};
|
|
157
180
|
}
|
|
158
181
|
|
package/src/project.mjs
CHANGED
|
@@ -68,7 +68,7 @@ function diagramNodeLabelLayout(row, kind, geometry, spec, font = null) {
|
|
|
68
68
|
|
|
69
69
|
function flowNodeVisual(kind, box) {
|
|
70
70
|
const cx = box.x + box.width / 2, cy = box.y + box.height / 2;
|
|
71
|
-
if (kind === 'start' || kind === 'end') return { type: '
|
|
71
|
+
if (kind === 'start' || kind === 'end') return { type: 'ellipse', geometry: { cx, cy, rx: box.width / 2, ry: box.height / 2 } };
|
|
72
72
|
if (kind === 'decision') return { type: 'path', geometry: { points: [{ x: cx, y: box.y }, { x: box.x + box.width, y: cy }, { x: cx, y: box.y + box.height }, { x: box.x, y: cy }], closed: true } };
|
|
73
73
|
if (kind === 'io') {
|
|
74
74
|
const skew = Math.min(18, box.width * 0.18);
|
|
@@ -557,7 +557,7 @@ export function buildProjectScene(spec) {
|
|
|
557
557
|
const geometry = node.geometry;
|
|
558
558
|
['x', 'x1', 'x2', 'cx'].forEach(key => { if (geometry[key] != null) geometry[key] = mapX(geometry[key]); });
|
|
559
559
|
['y', 'y1', 'y2', 'cy'].forEach(key => { if (geometry[key] != null) geometry[key] = mapY(geometry[key]); });
|
|
560
|
-
['width', 'height', 'r'].forEach(key => { if (geometry[key] != null) geometry[key] *= view.scale; });
|
|
560
|
+
['width', 'height', 'r', 'rx', 'ry'].forEach(key => { if (geometry[key] != null) geometry[key] *= view.scale; });
|
|
561
561
|
if (geometry.points) geometry.points = geometry.points.map(point => ({ x: mapX(point.x), y: mapY(point.y) }));
|
|
562
562
|
if (node.bounds) node.bounds = { x: mapX(node.bounds.x), y: mapY(node.bounds.y), width: node.bounds.width * view.scale, height: node.bounds.height * view.scale };
|
|
563
563
|
});
|
package/src/renderer.mjs
CHANGED
|
@@ -31,7 +31,7 @@ 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
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; 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(); if (g.curve === 'cubic' && g.points?.length === 4) { ctx.moveTo(g.points[0].x, g.points[0].y); ctx.bezierCurveTo(g.points[1].x, g.points[1].y, g.points[2].x, g.points[2].y, g.points[3].x, g.points[3].y); } else 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'; if (Number.isFinite(Number(s.rotation)) && Number(s.rotation) !== 0) { ctx.translate(g.x, g.y); ctx.rotate(Number(s.rotation) * Math.PI / 180); ctx.fillText(g.text, 0, 0); } else 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); }
|
|
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(); if (g.curve === 'cubic' && g.points?.length === 4) { ctx.moveTo(g.points[0].x, g.points[0].y); ctx.bezierCurveTo(g.points[1].x, g.points[1].y, g.points[2].x, g.points[2].y, g.points[3].x, g.points[3].y); } else 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 === 'ellipse') { ctx.beginPath(); if (typeof ctx.ellipse === 'function') ctx.ellipse(g.cx, g.cy, g.rx, g.ry, 0, 0, Math.PI * 2); else { ctx.save(); ctx.translate(g.cx, g.cy); ctx.scale(g.rx, g.ry); ctx.arc(0, 0, 1, 0, Math.PI * 2); ctx.restore(); } 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'; if (Number.isFinite(Number(s.rotation)) && Number(s.rotation) !== 0) { ctx.translate(g.x, g.y); ctx.rotate(Number(s.rotation) * Math.PI / 180); ctx.fillText(g.text, 0, 0); } else 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') { const d = g.curve === 'cubic' && g.points?.length === 4 ? `M ${g.points[0].x} ${g.points[0].y} C ${g.points[1].x} ${g.points[1].y} ${g.points[2].x} ${g.points[2].y} ${g.points[3].x} ${g.points[3].y}` : `${g.points.map((p, i) => `${i ? 'L' : 'M'} ${p.x} ${p.y}`).join(' ')}${g.closed ? ' Z' : ''}`; element.setAttribute('d', d); } else if (node.type === 'text') { element.setAttribute('x', g.x); element.setAttribute('y', g.y); if (Number.isFinite(Number(s.rotation)) && Number(s.rotation) !== 0) element.setAttribute('transform', `rotate(${Number(s.rotation)} ${g.x} ${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); }
|
|
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 === 'ellipse') { element.setAttribute('cx', g.cx); element.setAttribute('cy', g.cy); element.setAttribute('rx', g.rx); element.setAttribute('ry', g.ry); } 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') { const d = g.curve === 'cubic' && g.points?.length === 4 ? `M ${g.points[0].x} ${g.points[0].y} C ${g.points[1].x} ${g.points[1].y} ${g.points[2].x} ${g.points[2].y} ${g.points[3].x} ${g.points[3].y}` : `${g.points.map((p, i) => `${i ? 'L' : 'M'} ${p.x} ${p.y}`).join(' ')}${g.closed ? ' Z' : ''}`; element.setAttribute('d', d); } else if (node.type === 'text') { element.setAttribute('x', g.x); element.setAttribute('y', g.y); if (Number.isFinite(Number(s.rotation)) && Number(s.rotation) !== 0) element.setAttribute('transform', `rotate(${Number(s.rotation)} ${g.x} ${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 === 'ellipse' || 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/scene.mjs
CHANGED
|
@@ -37,6 +37,11 @@ function geometryHit(node, x, y) {
|
|
|
37
37
|
const points = geometry.curve === 'cubic' ? sampleCubicBezier(geometry.points) : geometry.points;
|
|
38
38
|
return points.some((point, index) => index > 0 && pointToSegmentDistance({ x, y }, points[index - 1], point) <= tolerance);
|
|
39
39
|
}
|
|
40
|
+
if (node.type === 'ellipse') {
|
|
41
|
+
const rx = Math.max(1, Number(geometry.rx) || 0) + tolerance;
|
|
42
|
+
const ry = Math.max(1, Number(geometry.ry) || 0) + tolerance;
|
|
43
|
+
return ((x - geometry.cx) / rx) ** 2 + ((y - geometry.cy) / ry) ** 2 <= 1;
|
|
44
|
+
}
|
|
40
45
|
return Boolean(node.bounds && x >= node.bounds.x && x <= node.bounds.x + node.bounds.width && y >= node.bounds.y && y <= node.bounds.y + node.bounds.height);
|
|
41
46
|
}
|
|
42
47
|
|
package/types/index.d.ts
CHANGED
|
@@ -16,8 +16,9 @@ export type ChartPreferencesPatch = Partial<Omit<ChartPreferences, 'version'>> &
|
|
|
16
16
|
export interface PreferenceCapabilityOption { value: string | number | boolean | null; label: string; }
|
|
17
17
|
export interface PreferenceFieldCapability { path: string; group: 'theme' | 'typography' | 'components' | 'behavior'; groupLabel: string; type: 'enum' | 'number' | 'tri-state'; label: string; description: string; default: string | number | boolean | null; options: PreferenceCapabilityOption[]; scopes: Array<'global' | 'chart'>; supported: boolean; applicability: string; menu: { visible: boolean; control: 'select' | 'toggle' }; minimum?: number; maximum?: number; }
|
|
18
18
|
export interface PreferenceCapabilities { version: '1.0'; locale: 'en' | 'zh-CN'; chartType: ChartType | null; scopes: Array<'global' | 'chart'>; persistence: Array<'memory' | 'localStorage' | 'adapter'>; precedence: string[]; fields: PreferenceFieldCapability[]; }
|
|
19
|
+
export interface PreferenceResolution { version: '1.0'; precedence: string[]; storage: 'localStorage' | 'adapter' | 'memory'; scopes: { defaults: string; spec: string; global: string | null; chart: string | null; }; }
|
|
19
20
|
export interface PreferencesStoreState { version: '1.0'; global: ChartPreferences; charts: Record<string, ChartPreferencesPatch>; }
|
|
20
|
-
export interface PreferencesStore { version: '1.0'; storageKey: string; storage: 'localStorage' | 'memory'; getState(): PreferencesStoreState; getGlobal(): ChartPreferences; getChart(chartId: string): ChartPreferencesPatch; getEffective(chartId?: string | null): ChartPreferences; setGlobal(patch: ChartPreferencesPatch, options?: { source?: string; persist?: boolean }): ChartPreferences; setChart(chartId: string, patch: ChartPreferencesPatch, options?: { source?: string; persist?: boolean }): ChartPreferences; reset(options?: { scope?: 'all' | 'global' | 'chart'; chartId?: string; source?: string; persist?: boolean }): ChartPreferences; subscribe(listener: (event: { type: 'change'; scope: string; chartId: string | null; source: string; persisted: boolean; preferences: ChartPreferences; state: PreferencesStoreState }) => void): () => void; persist(): boolean; }
|
|
21
|
+
export interface PreferencesStore { version: '1.0'; storageKey: string; storage: 'localStorage' | 'adapter' | 'memory'; getState(): PreferencesStoreState; getGlobal(): ChartPreferences; getChart(chartId: string): ChartPreferencesPatch; getEffective(chartId?: string | null): ChartPreferences; getResolution(chartId?: string | null): PreferenceResolution; setGlobal(patch: ChartPreferencesPatch, options?: { source?: string; persist?: boolean }): ChartPreferences; setChart(chartId: string, patch: ChartPreferencesPatch, options?: { source?: string; persist?: boolean }): ChartPreferences; reset(options?: { scope?: 'all' | 'global' | 'chart'; chartId?: string; source?: string; persist?: boolean }): ChartPreferences; subscribe(listener: (event: { type: 'change'; scope: string; chartId: string | null; source: string; persisted: boolean; preferences: ChartPreferences; state: PreferencesStoreState }) => void): () => void; persist(): boolean; }
|
|
21
22
|
export interface PreferencesStorage { getItem(key: string): string | null; setItem(key: string, value: string): void; removeItem?(key: string): void; }
|
|
22
23
|
export interface StyleRecommendation { version: '1.0'; preset: Exclude<ThemePreset, 'auto'>; mode: ThemeMode; resolvedMode: Exclude<ThemeMode, 'auto'>; palette: Exclude<ThemePalette, 'auto'>; reasons: string[]; warnings: Diagnostic[]; userOverride: boolean; }
|
|
23
24
|
export interface ResolvedTheme extends ThemeConfig { name: string; resolvedMode: Exclude<ThemeMode, 'auto'>; background: string; surface: string; text: string; muted: string; axis: string; grid: string; border: string; focus: string; selection: string; colors: string[]; palettes: Record<string, unknown>; status: Record<string, string>; typography: Record<string, { size: number; weight: number; lineHeight: number; font: string }>; layout: Record<string, unknown>; marks: Record<string, unknown>; reasons: string[]; warnings: Diagnostic[]; }
|
|
@@ -26,7 +27,7 @@ export class ChartValidationError extends Error { name: 'ChartValidationError';
|
|
|
26
27
|
export interface DataFieldInfo { name: string; type: 'quantitative' | 'temporal' | 'category' | 'unknown'; role: 'identifier' | 'measure' | 'temporal-dimension' | 'dimension'; unit: string | null; cardinality: number; validCount: number; nullCount: number; min?: number; max?: number; temporalMin?: string; temporalMax?: string; }
|
|
27
28
|
export interface DataInspection { version: '1.0'; rows: number; fields: DataFieldInfo[]; dimensions: string[]; measures: string[]; temporalFields: string[]; missingValueCount: number; warnings: Diagnostic[]; }
|
|
28
29
|
export interface ChartCapability { type: ChartType; family: string; intents: string[]; required: string[]; optional: string[]; dataShapes: string[]; interactions: string[]; features: Record<string, 'supported' | 'not-applicable' | 'degraded'>; renderers: Array<'canvas' | 'svg'>; exports: string[]; limits: Record<string, number>; }
|
|
29
|
-
export interface RuntimeCapabilities { version: '2.0'; contractVersion: '1.1'; chartTypes: ChartType[]; charts: Record<ChartType, ChartCapability>; intents: string[]; locale?: { default: string; recommended: string[]; appliesTo: string[]; inputDates: string }; renderers: Array<'canvas' | 'svg'>; interactions: string[]; exports: Array<'png' | 'svg' | 'json' | 'jpeg'>; chartModes?: Record<string, unknown>; diagram?: { flowNodeKinds?: FlowNodeKind[]; flowBranchEdges?: { label: string; minimumOutgoing: number }; flowLoops?: string; flowConnectors?: { kind: FlowNodeKind; linking: string }; [key: string]: unknown }; styleSystem: { modes: ThemeMode[]; presets: ThemePreset[]; palettes: ThemePalette[]; switchable: boolean; automatic: boolean; [key: string]: unknown }; preferences?: { version: '1.0'; scopes: string[]; persistence: string[]; fields: string[]; agentAdjustable: boolean; interactiveSettingsUI: boolean; precedence: string[]; schema: PreferenceCapabilities }; 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; }
|
|
30
|
+
export interface RuntimeCapabilities { version: '2.0'; contractVersion: '1.1'; chartTypes: ChartType[]; charts: Record<ChartType, ChartCapability>; intents: string[]; locale?: { default: string; recommended: string[]; appliesTo: string[]; inputDates: string }; renderers: Array<'canvas' | 'svg'>; interactions: string[]; exports: Array<'png' | 'svg' | 'json' | 'jpeg'>; chartModes?: Record<string, unknown>; conversationalWorkflow?: { version: '1.0'; steps: string[]; routing: Record<string, string>; confirmation: string[]; selfCheck: string[] }; diagram?: { flowNodeKinds?: FlowNodeKind[]; flowBranchEdges?: { label: string; minimumOutgoing: number }; flowLoops?: string; flowConnectors?: { kind: FlowNodeKind; linking: string }; [key: string]: unknown }; styleSystem: { modes: ThemeMode[]; presets: ThemePreset[]; palettes: ThemePalette[]; switchable: boolean; automatic: boolean; [key: string]: unknown }; preferences?: { version: '1.0'; scopes: string[]; persistence: string[]; fields: string[]; agentAdjustable: boolean; interactiveSettingsUI: boolean; precedence: string[]; schema: PreferenceCapabilities }; 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; }
|
|
30
31
|
export interface ChartPlan { version: '1.0'; intent: string; intentKnown?: boolean; intentSuggestions?: string[]; fallbackUsed?: boolean; 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; }
|
|
31
32
|
export interface AxisState { rawDomain: [number, number]; domain: [number, number]; ticks: number[]; step: number | null; policy: 'nice' | 'raw' | 'explicit'; }
|
|
32
33
|
export interface TimeAxisState { orientation: 'horizontal'; field: 'date'; coordinate: 'x'; rowCoordinate: 'y'; domain: [number, number]; domainISO: [string, string]; }
|
|
@@ -105,7 +106,7 @@ export interface ClipboardState { nodes: DiagramNode[]; edges: DiagramEdge[]; }
|
|
|
105
106
|
export interface ChartLayoutState { family: string | null; plot: Record<string, unknown>; chrome: Record<string, unknown> | null; labels: Record<string, unknown> | null; }
|
|
106
107
|
export interface ChartAxesState { y?: AxisState; right?: AxisState; [key: string]: unknown; }
|
|
107
108
|
export interface ChartBrandingState { enabled: boolean; signature: string; text: string | null; }
|
|
108
|
-
export interface ChartState { renderer: string; width: number; height: number; dataCount: number; selected: unknown[]; revision: number; history: { undo: number; redo: number }; view: Record<string, unknown> | null; style?: Partial<StyleRecommendation> & { name?: string; mode?: ThemeMode; resolvedMode?: string; palette?: ThemePalette }; layout?: ChartLayoutState | null; timeAxis?: TimeAxisState | null; axes?: ChartAxesState | null; health: ChartHealth; preferences: ChartPreferences; branding?: ChartBrandingState; warnings: Diagnostic[]; assumptions?: string[]; normalizations?: Diagnostic[]; collapsedGroups?: string[]; projectAnalytics?: ProjectAnalyticsState | null; linked?: LinkedState | null; clipboard: { nodes: number; edges: number }; [key: string]: unknown; }
|
|
109
|
+
export interface ChartState { renderer: string; width: number; height: number; dataCount: number; selected: unknown[]; revision: number; history: { undo: number; redo: number }; view: Record<string, unknown> | null; style?: Partial<StyleRecommendation> & { name?: string; mode?: ThemeMode; resolvedMode?: string; palette?: ThemePalette }; layout?: ChartLayoutState | null; timeAxis?: TimeAxisState | null; axes?: ChartAxesState | null; health: ChartHealth; preferences: ChartPreferences; preferenceResolution: PreferenceResolution; branding?: ChartBrandingState; warnings: Diagnostic[]; assumptions?: string[]; normalizations?: Diagnostic[]; collapsedGroups?: string[]; projectAnalytics?: ProjectAnalyticsState | null; linked?: LinkedState | null; clipboard: { nodes: number; edges: number }; [key: string]: unknown; }
|
|
109
110
|
export interface DiagramConfig { mode?: 'process' | 'architecture' | 'mindmap'; layout?: 'manual' | 'layered' | 'tree' | 'radial'; routing?: 'straight' | 'orthogonal' | 'curved'; curveTension?: number; grid?: number; snap?: boolean; }
|
|
110
111
|
export interface ArchitectureLayer { id: string; label?: string; }
|
|
111
112
|
export interface ArchitectureBoundary { id: string; label?: string; nodeIds?: string[]; padding?: number; color?: string; }
|
|
@@ -137,6 +138,7 @@ export interface Chart {
|
|
|
137
138
|
setTheme(theme?: ChartSpec['theme']): this;
|
|
138
139
|
getTheme(): ResolvedTheme;
|
|
139
140
|
getPreferences(): ChartPreferences;
|
|
141
|
+
getPreferenceResolution(): PreferenceResolution;
|
|
140
142
|
setPreferences(patch?: ChartPreferencesPatch, options?: { scope?: 'chart' | 'global'; source?: string; persist?: boolean }): this;
|
|
141
143
|
resetPreferences(options?: { scope?: 'all' | 'global' | 'chart'; source?: string; persist?: boolean }): this;
|
|
142
144
|
resize(width?: number, height?: number): this;
|