@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 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.19`.
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.19/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
+ 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.19/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.
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.19` includes Iteration 13 contract hardening, Iteration 14 lightweight Flow semantics, and Iteration 15 text-readability 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, adaptive diagram labels, diagram-only edge-label plates, and polar label layout diagnostics for Pie, Donut, and Gauge.
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.19` includes the completed Iteration 12 structured-diagram work, Iteration 13 contract hardening, Iteration 14 lightweight Flow semantics, Iteration 15 text-readability hardening, 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.
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. Re-evaluate geographic and 3D scope only after usage data confirms demand.
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.19`.
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.19/skills/ichartjs \
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.19/skills/ichartjs`。WorkBuddy 可通过自身 Skill 界面导入该带 Tag 的目录;只有当前 CLI 明确声明对应适配器时才使用宿主专用 `--agent` 参数。
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.19`。
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.19/skills/ichartjs \
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.19",
286
- "runtimeVersion": "2.0.19",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taylorwong/ichartjs",
3
- "version": "2.0.19",
3
+ "version": "2.0.20",
4
4
  "description": "Agent-first, renderer-independent charting and project visualization runtime",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
@@ -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.19/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
+ 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
 
@@ -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: ['defaults', 'global', 'chart', 'temporary-agent-patch'], fields };
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
- setPreferences(patch = {}, options = {}) { const checked = validatePreferences(patch, { partial: true }); if (!checked.valid) throw new ChartValidationError('setPreferences', checked.errors); if (this._preferencesStore) { const scope = options.scope === 'global' ? 'global' : 'chart'; if (scope === 'global') this._preferencesStore.setGlobal(checked.value, { ...options, source: options.source || 'user' }); else this._preferencesStore.setChart(this.chartId || 'default', checked.value, { ...options, source: options.source || 'user' }); return this; } const snapshot = this._snapshotMutation(); try { this._localPreferences = mergePreferences(this._localPreferences, checked.value); 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; } }
446
- 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._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; } }
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.19', 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 };
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 };
@@ -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: storage ? 'localStorage' : 'memory'
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: 'path', geometry: { points: Array.from({ length: 16 }, (_, index) => { const angle = index / 16 * Math.PI * 2; return { x: cx + box.width / 2 * Math.cos(angle), y: cy + box.height / 2 * Math.sin(angle) }; }), closed: true } };
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;