@taylorwong/ichartjs 2.0.5 → 2.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +11 -4
- package/docs/agent/coding-agent-integration.md +6 -2
- package/docs/agent/development/iteration-11.md +44 -0
- package/docs/agent/development/roadmap.md +28 -3
- package/docs/agent/frontend-integration.md +1 -1
- package/docs/agent/theme-guide.md +31 -0
- package/docs/agent/usage-scenarios.md +38 -1
- package/docs/agent/zh-CN/coding-agent-integration.md +6 -2
- package/docs/agent/zh-CN/frontend-integration.md +1 -1
- package/docs/agent/zh-CN/theme-guide.md +31 -0
- package/docs/agent/zh-CN/usage-scenarios.md +38 -1
- package/docs/manifests/capabilities.json +8 -0
- package/package.json +1 -1
- package/skills/ichartjs/SKILL.md +10 -2
- package/src/capabilities.mjs +9 -0
- package/src/index.mjs +38 -6
- package/src/preferences-ui.mjs +86 -0
- package/src/preferences.mjs +194 -0
- package/types/index.d.ts +20 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.0.6 - 2026-09-18
|
|
4
|
+
|
|
5
|
+
- Added compact per-chart visual settings with explicit default font sizing, capability-aware visibility toggles, bilingual labels, and theme-aware hamburger icon contrast.
|
|
6
|
+
- Added shared page-level preferences with localStorage persistence and Agent JSON patch support through the Preferences API and Playground settings page.
|
|
7
|
+
- Synchronized Runtime, Playground, package metadata, release-pinned installation guidance, and roadmap status for the `v2.0.6` release.
|
|
8
|
+
|
|
3
9
|
## 2.0.5 - 2026-09-18
|
|
4
10
|
|
|
5
11
|
- Hardened JSON, SVG, PNG, and JPEG export contracts with deterministic type errors, JSON output representations, and an explicit optional Node `canvas` path through `exportAsync()`.
|
package/README.md
CHANGED
|
@@ -31,6 +31,7 @@ getCapabilities
|
|
|
31
31
|
| Coding Agent integration | [`docs/agent/coding-agent-integration.md`](docs/agent/coding-agent-integration.md) |
|
|
32
32
|
| Frontend integration | [`docs/agent/frontend-integration.md`](docs/agent/frontend-integration.md) |
|
|
33
33
|
| Visual style and themes | [`docs/agent/theme-guide.md`](docs/agent/theme-guide.md) |
|
|
34
|
+
| Chart and page preferences | [`docs/agent/theme-guide.md`](docs/agent/theme-guide.md#chart-and-page-preferences) |
|
|
34
35
|
| Official Agent Skill for Codex and WorkBuddy | [`skills/ichartjs/SKILL.md`](skills/ichartjs/SKILL.md) |
|
|
35
36
|
|
|
36
37
|
### Install
|
|
@@ -39,7 +40,7 @@ getCapabilities
|
|
|
39
40
|
npm install @taylorwong/ichartjs@^2
|
|
40
41
|
```
|
|
41
42
|
|
|
42
|
-
As a fallback for environments without npm access, install directly from GitHub: `npm install github:wanghetommy/ichartjs#v2.0.
|
|
43
|
+
As a fallback for environments without npm access, install directly from GitHub: `npm install github:wanghetommy/ichartjs#v2.0.6`.
|
|
43
44
|
|
|
44
45
|
### Optional Agent Skill
|
|
45
46
|
|
|
@@ -49,13 +50,19 @@ The runtime API remains the source of truth; the Skill only teaches and orchestr
|
|
|
49
50
|
https://github.com/wanghetommy/ichartjs/tree/master/skills/ichartjs
|
|
50
51
|
```
|
|
51
52
|
|
|
52
|
-
|
|
53
|
+
Install the latest Skill with the standard Agent Skills CLI:
|
|
53
54
|
|
|
54
55
|
```bash
|
|
55
|
-
|
|
56
|
+
npx skills add wanghetommy/ichartjs --skill ichartjs
|
|
56
57
|
```
|
|
57
58
|
|
|
58
|
-
|
|
59
|
+
For a non-interactive global Codex installation:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npx skills add wanghetommy/ichartjs --skill ichartjs --agent codex --global --yes
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
For a release-pinned installation, use `npx skills add https://github.com/wanghetommy/ichartjs/tree/v2.0.6/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.
|
|
59
66
|
|
|
60
67
|
### Agent workflow
|
|
61
68
|
|
|
@@ -27,12 +27,16 @@ npm install
|
|
|
27
27
|
npm run playground
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
Install the official Skill with the standard Agent Skills CLI:
|
|
31
31
|
|
|
32
32
|
```bash
|
|
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.6/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
|
+
|
|
38
|
+
Verify discovery with `npx skills add wanghetommy/ichartjs --list`; the result should include `ichartjs`.
|
|
39
|
+
|
|
36
40
|
## Agent Request Pattern
|
|
37
41
|
|
|
38
42
|
A useful request is explicit about data, intent, output, and acceptance:
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Iteration 11 — Chart Preferences and Agent Adjustments
|
|
2
|
+
|
|
3
|
+
Iteration 11A–11D adds a small, allowlisted preference layer for visual configuration. It does not add chart types or change data encoding behavior. The same contract powers per-chart settings, page-wide defaults, browser persistence, and Agent conversational adjustments.
|
|
4
|
+
|
|
5
|
+
## 11A — Preferences Contract
|
|
6
|
+
|
|
7
|
+
- Add `createPreferencesStore()`, `normalizePreferences()`, `mergePreferences()`, and `validatePreferences()`.
|
|
8
|
+
- Keep the public fields limited to theme mode/preset/palette, typography scale, density, legend/labels/grid visibility, branding, and motion.
|
|
9
|
+
- Resolve precedence as defaults → global page preferences → chart preferences → temporary Agent patch.
|
|
10
|
+
- Keep preference state separate from business data and provide structured change events with `source`, `scope`, and persistence status.
|
|
11
|
+
- Fall back to memory in Node/SSR and support browser `localStorage` or a host storage adapter explicitly.
|
|
12
|
+
|
|
13
|
+
## 11B — Per-Chart Settings
|
|
14
|
+
|
|
15
|
+
- Add `Chart#getPreferences()`, `Chart#setPreferences()`, and `Chart#resetPreferences()`.
|
|
16
|
+
- Add the optional `mountChartSettings()` browser UI with an accessible settings button in the chart container's top-right corner.
|
|
17
|
+
- Limit the per-chart menu to high-frequency theme, palette, font-scale, and capability-supported visibility controls; keep advanced and global controls on a dedicated page outside the chart popover.
|
|
18
|
+
- Keep the settings DOM outside SVG/Canvas so it is not included in PNG/SVG exports.
|
|
19
|
+
- Apply changes without recreating the Chart instance and preserve explicit chart colors/background/padding overrides.
|
|
20
|
+
|
|
21
|
+
## 11C — Global Page Configuration
|
|
22
|
+
|
|
23
|
+
- Allow one `PreferencesStore` to be shared by all charts on a page.
|
|
24
|
+
- Support global and chart scopes with chart-specific inheritance and reset behavior.
|
|
25
|
+
- Persist only the versioned preference document, never business rows or credentials.
|
|
26
|
+
- Expose the effective preferences in `chart.getState()` and the capabilities manifest.
|
|
27
|
+
|
|
28
|
+
## 11D — Agent and Playground Integration
|
|
29
|
+
|
|
30
|
+
- Let an Agent apply a validated visual patch with `chart.setPreferences(patch, { source: 'agent' })`.
|
|
31
|
+
- Keep Agent updates and UI updates on the same `preferenceschange` event path.
|
|
32
|
+
- Add `preferences-lab.html` and settings controls to the Complete Gallery for browser acceptance.
|
|
33
|
+
- Document conversational examples such as “use dashboard style, status colors, larger labels, and hide the grid.”
|
|
34
|
+
|
|
35
|
+
## Acceptance
|
|
36
|
+
|
|
37
|
+
- `npm run agent:check` passes.
|
|
38
|
+
- Preferences work in headless memory mode and browser `localStorage` mode.
|
|
39
|
+
- Global updates reach all charts sharing a store; chart updates remain isolated.
|
|
40
|
+
- Invalid preference patches return structured validation details and do not mutate state.
|
|
41
|
+
- Preview pages:
|
|
42
|
+
- `http://localhost:3000/playground/preferences-lab.html`
|
|
43
|
+
- `http://localhost:3000/playground/project-gallery.html`
|
|
44
|
+
- No new chart type, data transform, or export format is introduced.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# iChart.js 2.0 Roadmap
|
|
2
2
|
|
|
3
|
-
> Roadmap baseline: 2026-09-14. Current release status: `v2.0.
|
|
3
|
+
> Roadmap baseline: 2026-09-14. Current release status: `v2.0.6` is prepared from the completed Iteration 11 visual preference work. Geographic charts and 3D rendering remain out of scope until explicitly reintroduced.
|
|
4
4
|
|
|
5
5
|
## Current Status
|
|
6
6
|
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
- Iteration 7 local runtime and browser acceptance are complete for foundational composition, Heatmap, and Radar; physical-device checks remain host integration evidence.
|
|
13
13
|
- Iteration 8A–8D is complete and accepted in automated tests, Chromium, Firefox 144, WebKit 26, native Safari 26.6.2, and a 390 px touch viewport. Physical iOS/Android and representative release-host measurements remain post-release host/device follow-up. No new public chart type was introduced.
|
|
14
14
|
- Iteration 9A–9D implemented the lightweight visual style system, adaptive theme planning, runtime switching, renderer integration, and bilingual Agent guidance without adding a chart type; these capabilities remain in the `2.0.x` line.
|
|
15
|
-
- Iteration 10A Export Contract Hardening
|
|
15
|
+
- Iteration 10A Export Contract Hardening was implemented and released in `v2.0.5`: export representations and type errors are deterministic, optional Node raster export uses `exportAsync()`, Canvas/SVG paint semantics are aligned, and the playground server has safer port/path handling. No chart behavior or public chart type was added.
|
|
16
|
+
- Iteration 11 visual preference controls are included in the `v2.0.6` release: compact per-chart settings, capability-aware visibility controls, theme-aware icon contrast, explicit font-size defaults, shared page preferences, and Agent-adjustable global settings.
|
|
16
17
|
- 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`.
|
|
17
18
|
|
|
18
19
|
## Iteration 4 — Agent Data Contract and Business Editing
|
|
@@ -248,6 +249,29 @@ The following remain deferred beyond this roadmap baseline:
|
|
|
248
249
|
|
|
249
250
|
They should only be scheduled after the core runtime, diagram model, and project analytics contracts are stable.
|
|
250
251
|
|
|
252
|
+
## Iteration 11 — Chart Preferences and Agent Adjustments
|
|
253
|
+
|
|
254
|
+
### Goal
|
|
255
|
+
|
|
256
|
+
Allow users and Agents to adjust a chart's visual presentation after creation through one small, auditable contract. The detailed 11A–11D plan is in `docs/agent/development/iteration-11.md`.
|
|
257
|
+
|
|
258
|
+
### Tasks
|
|
259
|
+
|
|
260
|
+
1. Add allowlisted preferences for theme, typography scale, density, components, branding, and motion.
|
|
261
|
+
2. Support chart-scoped and page-global inheritance through an explicit Preferences Store.
|
|
262
|
+
3. Persist only versioned preference state through browser `localStorage` or a host adapter.
|
|
263
|
+
4. Add an optional accessible settings button outside the SVG/Canvas export surface.
|
|
264
|
+
5. Let Agents apply natural-language intent as a validated preference patch with source and scope metadata.
|
|
265
|
+
6. Add Playground and bilingual guidance for UI and Agent adjustment workflows.
|
|
266
|
+
|
|
267
|
+
### Verification
|
|
268
|
+
|
|
269
|
+
- Global changes reach all charts sharing a store; chart overrides remain isolated.
|
|
270
|
+
- UI, Agent, and host code use the same `preferenceschange` event path.
|
|
271
|
+
- Invalid patches do not mutate preferences or business data.
|
|
272
|
+
- Settings UI is excluded from SVG, PNG, and JSON chart exports.
|
|
273
|
+
- `npm run agent:check`, `git diff --check`, and `http://localhost:3000/playground/preferences-lab.html` acceptance pass.
|
|
274
|
+
|
|
251
275
|
## Recommended Execution Order
|
|
252
276
|
|
|
253
277
|
1. Finish Iteration 3 browser acceptance.
|
|
@@ -257,4 +281,5 @@ They should only be scheduled after the core runtime, diagram model, and project
|
|
|
257
281
|
5. Execute Iteration 7 foundations before considering additional specialized chart types.
|
|
258
282
|
6. Execute Iteration 8 to complete existing chart behavior, Agent adaptation, and the 2.0 release gates.
|
|
259
283
|
7. Execute Iteration 9 to standardize adaptive visual styling without expanding chart count.
|
|
260
|
-
8.
|
|
284
|
+
8. Execute Iteration 11 to add post-creation chart and page preferences without expanding chart count.
|
|
285
|
+
9. Re-evaluate geographic and 3D scope only after usage data confirms demand.
|
|
@@ -10,7 +10,7 @@ This is the production component path. For one-off files or Agent-led repository
|
|
|
10
10
|
npm install @taylorwong/ichartjs@^2
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
For environments without npm registry access, install from GitHub as a fallback: `npm install github:wanghetommy/ichartjs#v2.0.
|
|
13
|
+
For environments without npm registry access, install from GitHub as a fallback: `npm install github:wanghetommy/ichartjs#v2.0.6`.
|
|
14
14
|
|
|
15
15
|
Use the package through a bundler or another environment that resolves npm ESM imports:
|
|
16
16
|
|
|
@@ -57,3 +57,34 @@ Built-in modes expose text, muted text, axis, grid, focus, selection, missing-va
|
|
|
57
57
|
|
|
58
58
|
Preview and acceptance: `http://localhost:3000/playground/theme-gallery.html`.
|
|
59
59
|
|
|
60
|
+
## Chart and Page Preferences
|
|
61
|
+
|
|
62
|
+
Use preferences for visual adjustments that a user or Agent may change after the chart has been created. Keep business data, encodings, and chart selection outside this surface.
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
import { createChart, createPreferencesStore, mountChartSettings } from '@taylorwong/ichartjs';
|
|
66
|
+
|
|
67
|
+
const pagePreferences = createPreferencesStore({
|
|
68
|
+
storage: 'localStorage',
|
|
69
|
+
storageKey: 'my-app:chart-preferences'
|
|
70
|
+
});
|
|
71
|
+
const chart = createChart({
|
|
72
|
+
chartId: 'revenue',
|
|
73
|
+
container: '#revenue',
|
|
74
|
+
type: 'line',
|
|
75
|
+
data: { values: rows },
|
|
76
|
+
preferences: pagePreferences
|
|
77
|
+
});
|
|
78
|
+
mountChartSettings(chart, { locale: 'en' });
|
|
79
|
+
|
|
80
|
+
// The same operation can come from an Agent conversation.
|
|
81
|
+
chart.setPreferences({
|
|
82
|
+
theme: { preset: 'dashboard', palette: 'status' },
|
|
83
|
+
typography: { scale: 1.15 },
|
|
84
|
+
components: { grid: false }
|
|
85
|
+
}, { source: 'agent' });
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The optional per-chart menu uses a compact hamburger icon and intentionally exposes only high-frequency controls: theme mode, palette, font scale, and supported legend/label/grid visibility. Capability checks hide controls that do not apply to the current chart. Changes apply immediately and the UI supports `en`, `zh-CN`, and automatic document-language detection.
|
|
89
|
+
|
|
90
|
+
Keep low-frequency and page-wide controls in a dedicated settings surface outside the chart popover. Share one store across the page, and call `store.setGlobal()` or `chart.setPreferences(patch, { scope: 'global' })`. The full surface may expose preset, density, branding, and other allowlisted preferences without overloading every chart. The store uses memory in Node/SSR unless `localStorage` or a host adapter is explicitly selected. Preview the quick menu in `http://localhost:3000/playground/project-gallery.html` and the full page at `http://localhost:3000/playground/preferences-lab.html`.
|
|
@@ -35,6 +35,22 @@ The Skill is not a second renderer or service. A Skill-enabled Agent still needs
|
|
|
35
35
|
|
|
36
36
|
JSON is the machine-readable source of truth. SVG and PNG/JPEG are presentation artifacts. Code is the integration artifact. An interactive page is the product artifact.
|
|
37
37
|
|
|
38
|
+
## Visual Preferences: UI and Agent
|
|
39
|
+
|
|
40
|
+
For post-creation visual adjustments, share a `createPreferencesStore()` with the page's charts. Use `storage: 'localStorage'` only in a browser when the host wants preferences to survive refreshes. `mountChartSettings(chart)` adds an optional accessible quick-settings button outside the export surface; keep the full page-level settings surface as a separate host route.
|
|
41
|
+
|
|
42
|
+
The host or Agent can use the same patch contract:
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
chart.setPreferences({
|
|
46
|
+
theme: { preset: 'dashboard', palette: 'status' },
|
|
47
|
+
typography: { scale: 1.15 },
|
|
48
|
+
components: { grid: false }
|
|
49
|
+
}, { source: 'agent' });
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Keep the quick menu limited to theme, palette, font scale, and capability-supported visibility toggles. Use `scope: 'global'` for a page-wide update, the default chart scope for a single chart, and `chart.getState().preferences` for an auditable effective result. Preview both layers at `http://localhost:3000/playground/project-gallery.html` and `http://localhost:3000/playground/preferences-lab.html`.
|
|
53
|
+
|
|
38
54
|
## Scenario 1: Integrate into a Web Project
|
|
39
55
|
|
|
40
56
|
Install the runtime in the host application:
|
|
@@ -90,7 +106,28 @@ For a consumer project, return that project's own development URL.
|
|
|
90
106
|
|
|
91
107
|
## Scenario 3: Use the Official Skill
|
|
92
108
|
|
|
93
|
-
Install
|
|
109
|
+
Install the latest Skill with the standard Agent Skills CLI:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
npx skills add wanghetommy/ichartjs --skill ichartjs
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Install globally for Codex without prompts:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
npx skills add wanghetommy/ichartjs --skill ichartjs --agent codex --global --yes
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
For reproducible installation, pin the released Skill directory:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
npx skills add https://github.com/wanghetommy/ichartjs/tree/v2.0.6/skills/ichartjs \
|
|
125
|
+
--agent codex --global --yes
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
WorkBuddy users can import the same tagged GitHub directory through the host's Skill interface. Do not assume that `--agent workbuddy` exists unless the installed Skills CLI lists that adapter. A manual copy from `node_modules/@taylorwong/ichartjs/skills/ichartjs` remains a fallback for hosts with custom Skill directories.
|
|
129
|
+
|
|
130
|
+
After installation or selection, ask:
|
|
94
131
|
|
|
95
132
|
```text
|
|
96
133
|
Use the iChart.js Skill. Read this dataset, create a project Burndown,
|
|
@@ -16,12 +16,16 @@ npm install @taylorwong/ichartjs@^2
|
|
|
16
16
|
|
|
17
17
|
npm Registry 中无作用域的 `ichartjs` 是安全占位包,并非本项目。正式包名是 `@taylorwong/ichartjs`,优先从 npm 安装。
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
使用标准 Agent Skills CLI 安装官方 Skill:
|
|
20
20
|
|
|
21
21
|
```bash
|
|
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.6/skills/ichartjs`。WorkBuddy 可通过自身 Skill 界面导入该带 Tag 的目录;只有当前 CLI 明确声明对应适配器时才使用宿主专用 `--agent` 参数。
|
|
26
|
+
|
|
27
|
+
使用 `npx skills add wanghetommy/ichartjs --list` 验证发现结果,其中应包含 `ichartjs`。
|
|
28
|
+
|
|
25
29
|
推荐请求:
|
|
26
30
|
|
|
27
31
|
```text
|
|
@@ -6,7 +6,7 @@ iChart.js 应作为普通 JavaScript UI 组件运行在浏览器应用中。数
|
|
|
6
6
|
npm install @taylorwong/ichartjs@^2
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
无法访问 npm Registry 的环境请用 GitHub 源作为后备:`npm install github:wanghetommy/ichartjs#v2.0.
|
|
9
|
+
无法访问 npm Registry 的环境请用 GitHub 源作为后备:`npm install github:wanghetommy/ichartjs#v2.0.6`。
|
|
10
10
|
|
|
11
11
|
```js
|
|
12
12
|
import { createChart } from '@taylorwong/ichartjs';
|
|
@@ -48,3 +48,34 @@ const resolved = chart.getTheme();
|
|
|
48
48
|
|
|
49
49
|
预览与验收:`http://localhost:3000/playground/theme-gallery.html`。
|
|
50
50
|
|
|
51
|
+
## 单图表与页面级偏好
|
|
52
|
+
|
|
53
|
+
偏好用于图表创建后的视觉调整,适合用户操作和 Agent 对话调整。业务数据、编码和图表类型不应放进这个配置面。
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
import { createChart, createPreferencesStore, mountChartSettings } from '@taylorwong/ichartjs';
|
|
57
|
+
|
|
58
|
+
const pagePreferences = createPreferencesStore({
|
|
59
|
+
storage: 'localStorage',
|
|
60
|
+
storageKey: 'my-app:chart-preferences'
|
|
61
|
+
});
|
|
62
|
+
const chart = createChart({
|
|
63
|
+
chartId: 'revenue',
|
|
64
|
+
container: '#revenue',
|
|
65
|
+
type: 'line',
|
|
66
|
+
data: { values: rows },
|
|
67
|
+
preferences: pagePreferences
|
|
68
|
+
});
|
|
69
|
+
mountChartSettings(chart, { locale: 'zh-CN' });
|
|
70
|
+
|
|
71
|
+
// Agent 对话也使用同一套 API。
|
|
72
|
+
chart.setPreferences({
|
|
73
|
+
theme: { preset: 'dashboard', palette: 'status' },
|
|
74
|
+
typography: { scale: 1.15 },
|
|
75
|
+
components: { grid: false }
|
|
76
|
+
}, { source: 'agent' });
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
单图表快捷菜单使用紧凑的汉堡图标,只保留高频操作:主题模式、配色、字号,以及当前图表真正支持的图例、数据标签和网格线。能力检测会自动隐藏无效设置;修改即时生效,并支持 `zh-CN`、`en` 和按文档语言自动识别。
|
|
80
|
+
|
|
81
|
+
低频选项和页面级配置应放在图表弹出菜单之外的独立设置页。页面内图表共享同一个 store,并使用 `store.setGlobal()` 或 `chart.setPreferences(patch, { scope: 'global' })` 更新全局。完整设置页可提供 preset、密度、署名等白名单能力,而不挤占每张图表。Node/SSR 默认使用内存,浏览器需要显式选择 `localStorage` 或宿主存储适配器。快捷菜单验收地址:`http://localhost:3000/playground/project-gallery.html`;完整页面设置验收地址:`http://localhost:3000/playground/preferences-lab.html`。
|
|
@@ -35,6 +35,22 @@ Skill 不是第二套渲染器,也不是服务端。使用 Skill 的 Agent 仍
|
|
|
35
35
|
|
|
36
36
|
JSON 是机器可读的事实来源;SVG 和 PNG/JPEG 是视觉交付物;代码是集成交付物;交互页面是产品交付物。
|
|
37
37
|
|
|
38
|
+
## 视觉偏好:界面与 Agent
|
|
39
|
+
|
|
40
|
+
图表创建后需要调整视觉样式时,可让页面图表共享 `createPreferencesStore()`。浏览器中只有在宿主希望刷新后保留配置时,才显式使用 `storage: 'localStorage'`。`mountChartSettings(chart)` 会在导出区域之外增加可访问的快捷设置按钮;完整的页面级设置中心应作为宿主的独立路由。
|
|
41
|
+
|
|
42
|
+
宿主交互和 Agent 对话使用同一个 patch 合约:
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
chart.setPreferences({
|
|
46
|
+
theme: { preset: 'dashboard', palette: 'status' },
|
|
47
|
+
typography: { scale: 1.15 },
|
|
48
|
+
components: { grid: false }
|
|
49
|
+
}, { source: 'agent' });
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
快捷菜单只保留主题、配色、字号和当前图表支持的显示开关。使用 `scope: 'global'` 修改页面全部图表;默认只修改当前图表;使用 `chart.getState().preferences` 获取可审计的最终配置。两层体验可在 `http://localhost:3000/playground/project-gallery.html` 和 `http://localhost:3000/playground/preferences-lab.html` 验收。
|
|
53
|
+
|
|
38
54
|
## 场景一:集成到 Web 项目
|
|
39
55
|
|
|
40
56
|
在宿主项目中安装 Runtime:
|
|
@@ -85,7 +101,28 @@ Agent 应返回:
|
|
|
85
101
|
|
|
86
102
|
## 场景三:作为官方 Skill 使用
|
|
87
103
|
|
|
88
|
-
|
|
104
|
+
使用标准 Agent Skills CLI 安装最新版:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
npx skills add wanghetommy/ichartjs --skill ichartjs
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
无交互地全局安装到 Codex:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
npx skills add wanghetommy/ichartjs --skill ichartjs --agent codex --global --yes
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
需要固定发布版本时,直接安装已发布的 Skill 目录:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
npx skills add https://github.com/wanghetommy/ichartjs/tree/v2.0.6/skills/ichartjs \
|
|
120
|
+
--agent codex --global --yes
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
WorkBuddy 用户可以通过宿主的 Skill 界面导入同一个带 Tag 的 GitHub 目录。在当前 Skills CLI 没有明确声明适配器时,不要假设存在 `--agent workbuddy`。具有自定义 Skill 目录的宿主仍可从 `node_modules/@taylorwong/ichartjs/skills/ichartjs` 手动复制。
|
|
124
|
+
|
|
125
|
+
安装或选择 `ichartjs` 后可以这样提问:
|
|
89
126
|
|
|
90
127
|
```text
|
|
91
128
|
使用 iChart.js Skill。读取这份数据,生成项目 Burndown,校验 Spec,
|
|
@@ -59,7 +59,15 @@
|
|
|
59
59
|
"projectIntelligenceDemo": "playground/project-intelligence.html",
|
|
60
60
|
"foundationalGallery": "playground/foundational-gallery.html",
|
|
61
61
|
"themeGallery": "playground/theme-gallery.html",
|
|
62
|
+
"preferencesLab": "playground/preferences-lab.html",
|
|
62
63
|
"themeGuide": "docs/agent/theme-guide.md",
|
|
64
|
+
"preferences": {
|
|
65
|
+
"version": "1.0",
|
|
66
|
+
"scopes": ["global", "chart"],
|
|
67
|
+
"persistence": ["memory", "localStorage", "adapter"],
|
|
68
|
+
"agentAdjustable": true,
|
|
69
|
+
"interactiveSettingsUI": true
|
|
70
|
+
},
|
|
63
71
|
"styleSystem": {
|
|
64
72
|
"modes": ["auto", "light", "dark", "contrast"],
|
|
65
73
|
"presets": ["auto", "analysis", "dashboard", "report", "presentation", "project", "diagram"],
|
package/package.json
CHANGED
package/skills/ichartjs/SKILL.md
CHANGED
|
@@ -7,7 +7,7 @@ description: Plan, validate, render, explain, and safely edit iChart.js visualiz
|
|
|
7
7
|
|
|
8
8
|
Use the public Agent contract as the source of truth. Do not infer capabilities from renderer internals or duplicate chart-selection logic in generated code.
|
|
9
9
|
|
|
10
|
-
Read [
|
|
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
11
|
|
|
12
12
|
## Source and Runtime Setup
|
|
13
13
|
|
|
@@ -15,6 +15,14 @@ Read [`docs/agent/usage-scenarios.md`](../../docs/agent/usage-scenarios.md) when
|
|
|
15
15
|
- Official Skill source: `https://github.com/wanghetommy/ichartjs/tree/master/skills/ichartjs`
|
|
16
16
|
- Supported Skill hosts include Codex, WorkBuddy, and other Agent Skills-compatible environments.
|
|
17
17
|
|
|
18
|
+
Recommended installation:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npx skills add wanghetommy/ichartjs --skill ichartjs
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Use `--agent codex --global --yes` for global non-interactive Codex installation. Use the tagged directory `https://github.com/wanghetommy/ichartjs/tree/v2.0.6/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
|
+
|
|
18
26
|
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 GitHub:
|
|
19
27
|
|
|
20
28
|
```bash
|
|
@@ -54,7 +62,7 @@ Route by requested output:
|
|
|
54
62
|
- For Flow or Swimlane, preserve node, edge, lane, group, and port IDs; use diagram recipes and validated edit commands.
|
|
55
63
|
- For business edits, preview first, preserve the preview ID and revision, require confirmation when declared, then commit or reject atomically.
|
|
56
64
|
- For browser deliverables, start `npm run playground` and return the exact maintained Playground URL.
|
|
57
|
-
- Release workflow (npm publish + develop→master merge) is **AUTHOR ONLY**. Read the release SOP
|
|
65
|
+
- Release workflow (npm publish + develop→master merge) is **AUTHOR ONLY**. Read the [release SOP](https://github.com/wanghetommy/ichartjs/blob/master/docs/agent/development/release-sop.md). Never initiate any release step unless the author explicitly instructs.
|
|
58
66
|
|
|
59
67
|
## Guardrails
|
|
60
68
|
|
package/src/capabilities.mjs
CHANGED
|
@@ -184,6 +184,15 @@ export function getCapabilities() {
|
|
|
184
184
|
data: ['normalize', 'inspect', 'filter', 'sort', 'groupBy', 'sum', 'average', 'topN', 'percentage', 'bin'],
|
|
185
185
|
themes: [...styleCapabilities.modes],
|
|
186
186
|
styleSystem: JSON.parse(JSON.stringify(styleCapabilities)),
|
|
187
|
+
preferences: {
|
|
188
|
+
version: '1.0',
|
|
189
|
+
scopes: ['global', 'chart'],
|
|
190
|
+
persistence: ['memory', 'localStorage', 'adapter'],
|
|
191
|
+
fields: ['theme', 'typography.scale', 'density', 'components.legend', 'components.labels', 'components.grid', 'branding.enabled', 'motion'],
|
|
192
|
+
agentAdjustable: true,
|
|
193
|
+
interactiveSettingsUI: true,
|
|
194
|
+
precedence: ['defaults', 'global', 'chart', 'temporary-agent-patch']
|
|
195
|
+
},
|
|
187
196
|
plugins: true,
|
|
188
197
|
branding: { defaultEnabled: true, signature: 'Powered by iChart.js', options: [{ name: 'enabled', type: 'boolean', default: true }] },
|
|
189
198
|
};
|
package/src/index.mjs
CHANGED
|
@@ -20,11 +20,14 @@ import { diagramLayoutModes, edgeRoutingModes, normalizeDiagramSpec, validateDia
|
|
|
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, planChart } from './capabilities.mjs';
|
|
23
|
+
import { applyPreferencesToSpec, createPreferencesStore, defaultPreferences, mergePreferences, mergeThemePreference, normalizePreferences } from './preferences.mjs';
|
|
24
|
+
import { mountChartSettings } from './preferences-ui.mjs';
|
|
23
25
|
|
|
24
26
|
import { isDiagram, paintSelection, diagramPointer, diagramKeyboard } from './diagram-interaction.mjs';
|
|
25
27
|
|
|
26
28
|
function resolveContainer(container) { return typeof container === 'string' ? document.querySelector(container) : container; }
|
|
27
29
|
function clone(value) { return value == null ? value : JSON.parse(JSON.stringify(value)); }
|
|
30
|
+
function isPreferencesStore(value) { return Boolean(value && typeof value.getEffective === 'function' && typeof value.setChart === 'function' && typeof value.subscribe === 'function'); }
|
|
28
31
|
export function resolveZoomWindow(count, current, factor) {
|
|
29
32
|
const total = Math.max(0, Math.round(Number(count) || 0));
|
|
30
33
|
if (total <= 1) return { start: 0, end: total };
|
|
@@ -178,12 +181,23 @@ function renderRasterWithCanvas(createCanvas, spec, scene, rasterType, as) {
|
|
|
178
181
|
|
|
179
182
|
export class Chart {
|
|
180
183
|
constructor(input = {}) {
|
|
181
|
-
const
|
|
184
|
+
const specInput = { ...input };
|
|
185
|
+
delete specInput.preferences;
|
|
186
|
+
delete specInput.preferencesStore;
|
|
187
|
+
delete specInput.chartId;
|
|
188
|
+
const result = validateSpec(specInput);
|
|
182
189
|
if (!result.valid) { const error = new Error(result.errors.map(item => item.message).join(' ')); error.details = result.errors; throw error; }
|
|
183
190
|
this._specDiagnostics = { warnings: result.warnings, normalizations: result.normalizations };
|
|
191
|
+
this.chartId = input.chartId ?? input.id ?? null;
|
|
192
|
+
this._preferencesStore = isPreferencesStore(input.preferencesStore) ? input.preferencesStore : isPreferencesStore(input.preferences) ? input.preferences : null;
|
|
193
|
+
this._localPreferences = this._preferencesStore ? null : normalizePreferences(input.preferences || {});
|
|
194
|
+
delete result.spec.preferences;
|
|
195
|
+
delete result.spec.preferencesStore;
|
|
196
|
+
delete result.spec.chartId;
|
|
184
197
|
this._themeInput = input.theme ?? 'auto';
|
|
185
198
|
this._styleOverrides = { colors: input.colors !== undefined, background: input.background !== undefined, padding: input.padding !== undefined };
|
|
186
199
|
this.spec = result.spec;
|
|
200
|
+
this._preferenceBase = Object.fromEntries(['legend', 'labels', 'grid', 'branding', 'padding'].map(key => [key, clone(this.spec[key])]));
|
|
187
201
|
this._resolveStyle();
|
|
188
202
|
this.container = typeof document === 'undefined' ? null : resolveContainer(this.spec.container);
|
|
189
203
|
this.listeners = new Map();
|
|
@@ -198,12 +212,26 @@ export class Chart {
|
|
|
198
212
|
this._observeResize();
|
|
199
213
|
this._observeColorScheme();
|
|
200
214
|
this.render();
|
|
215
|
+
this._preferencesSnapshot = JSON.stringify(this.getPreferences());
|
|
216
|
+
if (this._preferencesStore) this._preferencesUnsubscribe = this._preferencesStore.subscribe(event => {
|
|
217
|
+
const next = this._preferencesStore.getEffective(this.chartId);
|
|
218
|
+
const serialized = JSON.stringify(next);
|
|
219
|
+
if (serialized === this._preferencesSnapshot) return;
|
|
220
|
+
this._preferencesSnapshot = serialized;
|
|
221
|
+
this._resolveStyle();
|
|
222
|
+
this.render();
|
|
223
|
+
this.emit('preferenceschange', { chart: this, preferences: clone(next), source: event.source, scope: event.scope, persisted: event.persisted });
|
|
224
|
+
});
|
|
201
225
|
}
|
|
202
226
|
_resolveStyle() {
|
|
203
|
-
this.spec
|
|
227
|
+
['legend', 'labels', 'grid', 'branding'].forEach(key => { this.spec[key] = clone(this._preferenceBase[key]); });
|
|
228
|
+
if (this._styleOverrides.padding) this.spec.padding = clone(this._preferenceBase.padding);
|
|
229
|
+
const preferences = this.getPreferences();
|
|
230
|
+
this.spec.theme = resolveTheme(mergeThemePreference(this._themeInput, preferences.theme), this.spec);
|
|
204
231
|
if (!this._styleOverrides.colors) this.spec.colors = [...this.spec.theme.colors];
|
|
205
232
|
if (!this._styleOverrides.background) this.spec.background = this.spec.theme.background;
|
|
206
233
|
if (!this._styleOverrides.padding) this.spec.padding = clone(this.spec.theme.layout.padding);
|
|
234
|
+
this.spec = applyPreferencesToSpec(this.spec, preferences);
|
|
207
235
|
}
|
|
208
236
|
_observeColorScheme() {
|
|
209
237
|
if (typeof matchMedia !== 'function') return;
|
|
@@ -299,13 +327,16 @@ export class Chart {
|
|
|
299
327
|
on(type, listener) { if (!this.listeners.has(type)) this.listeners.set(type, new Set()); this.listeners.get(type).add(listener); return this; }
|
|
300
328
|
off(type, listener) { this.listeners.get(type)?.delete(listener); return this; }
|
|
301
329
|
emit(type, event) { this.listeners.get(type)?.forEach(listener => listener(event)); }
|
|
302
|
-
update(next = {}) { this._editor.invalidate(); if (
|
|
330
|
+
update(next = {}) { this._editor.invalidate(); const { preferences, preferencesStore, chartId, ...specUpdate } = next || {}; if (preferences !== undefined) this.setPreferences(preferences); if (specUpdate.theme !== undefined) this._themeInput = specUpdate.theme; ['colors', 'background', 'padding'].forEach(key => { if (specUpdate[key] !== undefined) this._styleOverrides[key] = true; }); this.spec = normalizeSpec({ ...this.spec, ...specUpdate, theme: this._themeInput, data: specUpdate.data === undefined ? this.spec.data : specUpdate.data }); ['legend', 'labels', 'grid', 'branding', 'padding'].forEach(key => { if (specUpdate[key] !== undefined) this._preferenceBase[key] = clone(this.spec[key]); }); this._resolveStyle(); return this.render(); }
|
|
303
331
|
setData(data) { this._editor.invalidate(); this.spec = normalizeSpec({ ...this.spec, theme: this._themeInput, data: { values: data } }); this._resolveStyle(); return this.render(); }
|
|
304
332
|
setTheme(theme = 'auto') { this._themeInput = theme; this._resolveStyle(); this.render(); this.emit('themechange', { chart: this, theme: this.getTheme(), source: 'user' }); return this; }
|
|
305
333
|
getTheme() { return clone(this.spec.theme); }
|
|
334
|
+
getPreferences() { return this._preferencesStore ? this._preferencesStore.getEffective(this.chartId) : clone(this._localPreferences || defaultPreferences); }
|
|
335
|
+
setPreferences(patch = {}, options = {}) { if (this._preferencesStore) { const scope = options.scope === 'global' ? 'global' : 'chart'; if (scope === 'global') this._preferencesStore.setGlobal(patch, { ...options, source: options.source || 'user' }); else this._preferencesStore.setChart(this.chartId || 'default', patch, { ...options, source: options.source || 'user' }); return this; } this._localPreferences = mergePreferences(this._localPreferences, patch); this._preferencesSnapshot = JSON.stringify(this._localPreferences); this._resolveStyle(); this.render(); this.emit('preferenceschange', { chart: this, preferences: this.getPreferences(), source: options.source || 'user', scope: 'chart', persisted: false }); return this; }
|
|
336
|
+
resetPreferences(options = {}) { if (this._preferencesStore) { this._preferencesStore.reset({ ...options, scope: options.scope || 'chart', chartId: this.chartId || 'default' }); return this; } this._localPreferences = normalizePreferences({}); this._preferencesSnapshot = JSON.stringify(this._localPreferences); this._resolveStyle(); this.render(); this.emit('preferenceschange', { chart: this, preferences: this.getPreferences(), source: options.source || 'user', scope: 'chart', persisted: false }); return this; }
|
|
306
337
|
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; }
|
|
307
338
|
getSpec() { return JSON.parse(JSON.stringify(this.spec)); }
|
|
308
|
-
getState() { const brandingSignature = discoverCapabilities().branding.signature; 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 }), branding: { enabled: Boolean(this.spec.branding?.enabled), signature: brandingSignature, text: this.spec.branding?.enabled === true ? brandingSignature : null }, warnings: clone([...(this._specDiagnostics?.warnings || []), ...(this.model.data?.warnings || []), ...(this.spec.theme?.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) }; }
|
|
339
|
+
getState() { const brandingSignature = discoverCapabilities().branding.signature; 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 }), preferences: this.getPreferences(), branding: { enabled: Boolean(this.spec.branding?.enabled), signature: brandingSignature, text: this.spec.branding?.enabled === true ? brandingSignature : null }, warnings: clone([...(this._specDiagnostics?.warnings || []), ...(this.model.data?.warnings || []), ...(this.spec.theme?.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) }; }
|
|
309
340
|
getProjectAnalytics() { return clone(this.model.state?.projectAnalytics || null); }
|
|
310
341
|
getLinkedState() { return clone(this.model.state?.linked || null); }
|
|
311
342
|
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(); }
|
|
@@ -567,7 +598,7 @@ export class Chart {
|
|
|
567
598
|
return { valid: false, code: 'DOWNLOAD_FAILED', message: String(err && err.message || err), suggestion: 'Try chart.export() and write the result manually.' };
|
|
568
599
|
}
|
|
569
600
|
}
|
|
570
|
-
destroy() { if (this._resizeObserver) this._resizeObserver.disconnect(); this._colorSchemeQuery?.removeEventListener?.('change', this._colorSchemeHandler); if (this._eventsBound) { const { target, handler, start, move, end, leave, touchStart, touchMove, touchEnd, wheel } = this._eventsBound; target.removeEventListener('mousemove', handler); target.removeEventListener('click', handler); target.removeEventListener('pointerdown', start); target.removeEventListener('pointermove', move); target.removeEventListener('pointerup', end); target.removeEventListener('pointercancel', end); target.removeEventListener('pointerleave', leave); target.removeEventListener('touchstart', touchStart); target.removeEventListener('touchmove', touchMove); target.removeEventListener('touchend', touchEnd); target.removeEventListener('wheel', wheel); } const target = this.renderer.svg || this.renderer.canvas; if (this._keyboardHandler) target?.removeEventListener('keydown', this._keyboardHandler); this._tooltip?.remove(); this.plugins.destroy(); this.renderer.destroy(); this.listeners.clear(); this._eventsBound = null; this._selected.clear(); this._clipboard = { nodes: [], edges: [] }; }
|
|
601
|
+
destroy() { this._preferencesUnsubscribe?.(); if (this._resizeObserver) this._resizeObserver.disconnect(); this._colorSchemeQuery?.removeEventListener?.('change', this._colorSchemeHandler); if (this._eventsBound) { const { target, handler, start, move, end, leave, touchStart, touchMove, touchEnd, wheel } = this._eventsBound; target.removeEventListener('mousemove', handler); target.removeEventListener('click', handler); target.removeEventListener('pointerdown', start); target.removeEventListener('pointermove', move); target.removeEventListener('pointerup', end); target.removeEventListener('pointercancel', end); target.removeEventListener('pointerleave', leave); target.removeEventListener('touchstart', touchStart); target.removeEventListener('touchmove', touchMove); target.removeEventListener('touchend', touchEnd); target.removeEventListener('wheel', wheel); } const target = this.renderer.svg || this.renderer.canvas; if (this._keyboardHandler) target?.removeEventListener('keydown', this._keyboardHandler); this._tooltip?.remove(); this.plugins.destroy(); this.renderer.destroy(); this.listeners.clear(); this._eventsBound = null; this._selected.clear(); this._clipboard = { nodes: [], edges: [] }; }
|
|
571
602
|
}
|
|
572
603
|
|
|
573
604
|
export function createChart(spec) { return new Chart(spec); }
|
|
@@ -579,4 +610,5 @@ export { normalizeProjectCalendar, applyWorkingCalendar, normalizeDependencies,
|
|
|
579
610
|
export { normalizeLinkedFilters, normalizeLinkedSelection, filterProjectRows, createLinkedProjectState, linkedRecordId };
|
|
580
611
|
|
|
581
612
|
export { contrastRatio, planStyle, resolveTheme, styleCapabilities, themeModes, themePalettes, themePresets, validateThemeContrast, annotationPlugin, dataZoomPlugin, dataLabelsPlugin, accessibilityPlugin };
|
|
582
|
-
export
|
|
613
|
+
export { applyPreferencesToSpec, createPreferencesStore, defaultPreferences, mergePreferences, mergeThemePreference, mountChartSettings, normalizePreferences };
|
|
614
|
+
export const iChart = { version: '2.0.6', createChart, inspectData, normalizeData, binData, applyTransforms, normalizeSpec, validateSpec, data, getCapabilities, getChartCapability, planChart, recommend, explainChart, contrastRatio, planStyle, resolveTheme, styleCapabilities, themeModes, themePalettes, themePresets, validateThemeContrast, createPreferencesStore, defaultPreferences, normalizePreferences, mergePreferences, 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 };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional, framework-neutral quick settings popover for browser hosts.
|
|
3
|
+
* It stays outside the SVG/Canvas surface and is never part of exports.
|
|
4
|
+
*/
|
|
5
|
+
import { getChartCapability } from './capabilities.mjs';
|
|
6
|
+
import { contrastRatio, themeModes, themePalettes } from './theme.mjs';
|
|
7
|
+
|
|
8
|
+
const styleId = 'ichart-preferences-ui-style';
|
|
9
|
+
const messages = {
|
|
10
|
+
'zh-CN': {
|
|
11
|
+
title: '图表快捷设置', mode: '主题', palette: '配色', scale: '字号', automatic: '自动', defaultSize: '默认', small: '小', large: '大', extraLarge: '特大', legend: '图例', labels: '数据标签', grid: '网格线'
|
|
12
|
+
},
|
|
13
|
+
en: {
|
|
14
|
+
title: 'Chart quick settings', mode: 'Theme', palette: 'Palette', scale: 'Font size', automatic: 'Auto', defaultSize: 'Default', small: 'Small', large: 'Large', extraLarge: 'Extra large', legend: 'Legend', labels: 'Data labels', grid: 'Grid lines'
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function option(label, value) { return `<option value="${value}">${label}</option>`; }
|
|
19
|
+
function safe(value) { return value == null ? '' : String(value); }
|
|
20
|
+
function localeKey(locale) {
|
|
21
|
+
const requested = locale && locale !== 'auto' ? locale : document.documentElement.lang || navigator.language || 'en';
|
|
22
|
+
const normalized = String(requested).toLowerCase();
|
|
23
|
+
if (normalized === 'zh' || normalized.startsWith('zh-')) return 'zh-CN';
|
|
24
|
+
if (normalized === 'en' || normalized.startsWith('en-')) return 'en';
|
|
25
|
+
return 'en';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function ensureStyle() {
|
|
29
|
+
if (typeof document === 'undefined' || document.getElementById(styleId)) return;
|
|
30
|
+
const style = document.createElement('style');
|
|
31
|
+
style.id = styleId;
|
|
32
|
+
style.textContent = `.ichart-preferences{position:absolute;top:8px;right:8px;left:8px;height:32px;z-index:20;pointer-events:none;font:12px/1.4 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color:#172033}.ichart-preferences__button{position:absolute;top:0;right:0;display:grid;place-items:center;width:32px;height:32px;min-width:32px;min-height:32px;border:0;background:transparent;color:var(--ichart-preferences-icon-color,#172033);border-radius:7px;padding:0;pointer-events:auto;cursor:pointer;font-size:22px;line-height:1;text-shadow:0 1px 2px #0004}.ichart-preferences__panel{display:none;position:absolute;top:38px;right:0;width:min(320px,100%);padding:12px;border:0;border-radius:10px;background:#fff;pointer-events:auto;box-shadow:0 12px 30px #1720332b}.ichart-preferences[data-open="true"] .ichart-preferences__panel{display:block}.ichart-preferences__grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}.ichart-preferences__field{display:grid;min-width:0;gap:3px;margin:0}.ichart-preferences__field>span{color:#475467}.ichart-preferences__field select{width:100%;min-width:0;height:32px;border:1px solid #c5cfdb;border-radius:6px;padding:4px 7px;background:#fff;color:inherit}.ichart-preferences__toggles{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:6px;margin-top:8px;padding-top:8px;border-top:1px solid #e2e8f0}.ichart-preferences__toggle{display:flex;align-items:center;justify-content:center;gap:5px;min-height:32px;padding:5px 6px;color:#344054;cursor:pointer}.ichart-preferences__toggle input{width:auto;min-height:auto;margin:0;accent-color:#2563eb}@media(max-width:420px){.ichart-preferences__panel{width:min(288px,100%)}.ichart-preferences__grid,.ichart-preferences__toggles{grid-template-columns:1fr}.ichart-preferences__toggle{justify-content:flex-start}}`;
|
|
33
|
+
document.head.appendChild(style);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function mountChartSettings(chart, { container = chart?.container, locale = 'auto', title } = {}) {
|
|
37
|
+
if (typeof document === 'undefined' || !chart || !container) return { valid: false, code: 'PREFERENCES_UI_UNAVAILABLE', message: 'Chart settings require a browser container.' };
|
|
38
|
+
ensureStyle();
|
|
39
|
+
const text = messages[localeKey(locale)], accessibleTitle = title || text.title, host = container;
|
|
40
|
+
const capability = getChartCapability(chart.getSpec().type), supported = name => capability?.features?.[name] === 'supported';
|
|
41
|
+
const visibleComponents = ['legend', 'labels', 'grid'].filter(supported);
|
|
42
|
+
if (getComputedStyle(host).position === 'static') host.style.position = 'relative';
|
|
43
|
+
const root = document.createElement('div');
|
|
44
|
+
root.className = 'ichart-preferences';
|
|
45
|
+
root.dataset.open = 'false';
|
|
46
|
+
const componentMarkup = visibleComponents.map(name => `<label class="ichart-preferences__toggle"><input type="checkbox" data-key="${name}"><span>${text[name]}</span></label>`).join('');
|
|
47
|
+
root.innerHTML = `<button class="ichart-preferences__button" type="button" aria-expanded="false"><span aria-hidden="true">≡</span></button><div class="ichart-preferences__panel" role="dialog"><div class="ichart-preferences__grid"><label class="ichart-preferences__field"><span>${text.mode}</span><select data-key="mode">${themeModes.map(value => option(value === 'auto' ? text.automatic : value, value)).join('')}</select></label><label class="ichart-preferences__field"><span>${text.palette}</span><select data-key="palette">${themePalettes.map(value => option(value === 'auto' ? text.automatic : value, value)).join('')}</select></label><label class="ichart-preferences__field"><span>${text.scale}</span><select data-key="scale">${option(text.defaultSize, '1')}${option(text.small, '0.85')}${option(text.large, '1.15')}${option(text.extraLarge, '1.3')}</select></label></div>${componentMarkup ? `<div class="ichart-preferences__toggles">${componentMarkup}</div>` : ''}</div>`;
|
|
48
|
+
const button = root.querySelector('.ichart-preferences__button'), panel = root.querySelector('.ichart-preferences__panel');
|
|
49
|
+
button.setAttribute('aria-label', accessibleTitle); button.title = accessibleTitle; panel.setAttribute('aria-label', accessibleTitle);
|
|
50
|
+
host.appendChild(root);
|
|
51
|
+
const values = key => root.querySelector(`[data-key="${key}"]`);
|
|
52
|
+
const readField = key => {
|
|
53
|
+
const control = values(key), value = control?.type === 'checkbox' ? control.checked : control?.value;
|
|
54
|
+
if (['mode', 'palette'].includes(key)) return { theme: { [key]: value === 'auto' ? null : value } };
|
|
55
|
+
if (key === 'scale') return { typography: { scale: Number(value) } };
|
|
56
|
+
if (visibleComponents.includes(key)) return { components: { [key]: Boolean(value) } };
|
|
57
|
+
return {};
|
|
58
|
+
};
|
|
59
|
+
const sync = () => {
|
|
60
|
+
const prefs = chart.getPreferences(), spec = chart.getSpec(), set = (key, value) => { if (values(key)) values(key).value = safe(value); };
|
|
61
|
+
set('mode', prefs.theme?.mode || 'auto'); set('palette', prefs.theme?.palette || 'auto');
|
|
62
|
+
const scaleControl = values('scale'), scale = Number(prefs.typography?.scale || 1), scaleValue = String(scale);
|
|
63
|
+
scaleControl.querySelector('option[data-current]')?.remove();
|
|
64
|
+
if (![...scaleControl.options].some(currentOption => currentOption.value === scaleValue)) { const currentOption = document.createElement('option'); currentOption.value = scaleValue; currentOption.textContent = `${Math.round(scale * 100)}%`; currentOption.dataset.current = 'true'; scaleControl.appendChild(currentOption); }
|
|
65
|
+
scaleControl.value = scaleValue;
|
|
66
|
+
if (values('legend')) values('legend').checked = spec.legend?.visible !== false;
|
|
67
|
+
if (values('labels')) values('labels').checked = Boolean(spec.labels?.enabled);
|
|
68
|
+
if (values('grid')) values('grid').checked = spec.grid?.visible !== false;
|
|
69
|
+
};
|
|
70
|
+
const close = () => { root.dataset.open = 'false'; button.setAttribute('aria-expanded', 'false'); };
|
|
71
|
+
const updateIconColor = () => { const theme = chart.getTheme(), background = chart.getSpec().background || theme.background, darkRatio = contrastRatio('#172033', background), lightRatio = contrastRatio('#ffffff', background), color = darkRatio == null || lightRatio == null ? theme.text || '#172033' : darkRatio >= lightRatio ? '#172033' : '#ffffff'; button.style.setProperty('--ichart-preferences-icon-color', color); };
|
|
72
|
+
const onPreferencesChange = () => { sync(); updateIconColor(); };
|
|
73
|
+
const applyField = key => { try { chart.setPreferences(readField(key), { source: 'ui' }); } catch {} };
|
|
74
|
+
const onToggle = () => { const open = root.dataset.open !== 'true'; root.dataset.open = String(open); button.setAttribute('aria-expanded', String(open)); if (open) sync(); };
|
|
75
|
+
const onDocumentPointer = event => { if (!root.contains(event.target)) close(); };
|
|
76
|
+
const onDocumentKey = event => { if (event.key === 'Escape') { close(); button.focus(); } };
|
|
77
|
+
button.addEventListener('click', onToggle);
|
|
78
|
+
root.querySelectorAll('select,input[data-key]').forEach(control => control.addEventListener('change', () => applyField(control.dataset.key)));
|
|
79
|
+
document.addEventListener('pointerdown', onDocumentPointer);
|
|
80
|
+
document.addEventListener('keydown', onDocumentKey);
|
|
81
|
+
chart.on('preferenceschange', onPreferencesChange);
|
|
82
|
+
chart.on('themechange', updateIconColor);
|
|
83
|
+
sync();
|
|
84
|
+
updateIconColor();
|
|
85
|
+
return { valid: true, element: root, locale: localeKey(locale), destroy() { chart.off('preferenceschange', onPreferencesChange); chart.off('themechange', updateIconColor); document.removeEventListener('pointerdown', onDocumentPointer); document.removeEventListener('keydown', onDocumentKey); root.remove(); } };
|
|
86
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small, renderer-neutral preference contract shared by the Chart API,
|
|
3
|
+
* interactive settings UI, and Agent integrations.
|
|
4
|
+
*/
|
|
5
|
+
import { themeModes, themePalettes, themePresets } from './theme.mjs';
|
|
6
|
+
|
|
7
|
+
export const preferenceVersion = '1.0';
|
|
8
|
+
export const preferenceDensities = ['compact', 'comfortable', 'spacious'];
|
|
9
|
+
export const preferenceTriStates = ['auto', true, false];
|
|
10
|
+
export const preferenceMotions = ['auto', 'full', 'reduced', 'off'];
|
|
11
|
+
|
|
12
|
+
export const defaultPreferences = Object.freeze({
|
|
13
|
+
version: preferenceVersion,
|
|
14
|
+
theme: Object.freeze({ mode: null, preset: null, palette: null }),
|
|
15
|
+
typography: Object.freeze({ scale: 1 }),
|
|
16
|
+
density: 'comfortable',
|
|
17
|
+
components: Object.freeze({ legend: 'auto', labels: 'auto', grid: 'auto' }),
|
|
18
|
+
branding: Object.freeze({ enabled: 'auto' }),
|
|
19
|
+
motion: 'auto'
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function clone(value) { return value == null ? value : JSON.parse(JSON.stringify(value)); }
|
|
23
|
+
|
|
24
|
+
function merge(base, extra) {
|
|
25
|
+
const result = clone(base) || {};
|
|
26
|
+
Object.entries(extra || {}).forEach(([key, value]) => {
|
|
27
|
+
if (value && typeof value === 'object' && !Array.isArray(value) && result[key] && typeof result[key] === 'object' && !Array.isArray(result[key])) result[key] = merge(result[key], value);
|
|
28
|
+
else if (value !== undefined) result[key] = clone(value);
|
|
29
|
+
});
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isPlainObject(value) { return Boolean(value) && typeof value === 'object' && !Array.isArray(value); }
|
|
34
|
+
|
|
35
|
+
function diagnostic(code, path, message, expected, suggestion) {
|
|
36
|
+
return { code, path, message, ...(expected === undefined ? {} : { expected }), ...(suggestion ? { suggestion } : {}) };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function validatePreferences(input = {}, { partial = true } = {}) {
|
|
40
|
+
const errors = [];
|
|
41
|
+
if (!isPlainObject(input)) return { valid: false, errors: [diagnostic('INVALID_PREFERENCES', '', 'Preferences must be an object.', 'an object', 'Pass a preferences object.')], value: clone(defaultPreferences) };
|
|
42
|
+
const theme = input.theme;
|
|
43
|
+
if (theme !== undefined) {
|
|
44
|
+
if (!isPlainObject(theme)) errors.push(diagnostic('INVALID_PREFERENCE_THEME', 'theme', 'Preferences theme must be an object.', 'an object'));
|
|
45
|
+
else {
|
|
46
|
+
if (theme.mode !== undefined && theme.mode !== null && !themeModes.includes(theme.mode)) errors.push(diagnostic('INVALID_PREFERENCE_THEME_MODE', 'theme.mode', `Unsupported preference theme mode: ${theme.mode}.`, themeModes));
|
|
47
|
+
if (theme.preset !== undefined && theme.preset !== null && !themePresets.includes(theme.preset)) errors.push(diagnostic('INVALID_PREFERENCE_THEME_PRESET', 'theme.preset', `Unsupported preference theme preset: ${theme.preset}.`, themePresets));
|
|
48
|
+
if (theme.palette !== undefined && theme.palette !== null && !themePalettes.includes(theme.palette)) errors.push(diagnostic('INVALID_PREFERENCE_THEME_PALETTE', 'theme.palette', `Unsupported preference theme palette: ${theme.palette}.`, themePalettes));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const scale = input.typography?.scale;
|
|
52
|
+
if (scale !== undefined && (!(Number.isFinite(Number(scale))) || Number(scale) < 0.75 || Number(scale) > 1.5)) errors.push(diagnostic('INVALID_PREFERENCE_SCALE', 'typography.scale', 'Typography scale must be between 0.75 and 1.5.', '0.75..1.5', 'Use 1 for the default size.'));
|
|
53
|
+
if (input.density !== undefined && !preferenceDensities.includes(input.density)) errors.push(diagnostic('INVALID_PREFERENCE_DENSITY', 'density', `Unsupported density: ${input.density}.`, preferenceDensities));
|
|
54
|
+
['legend', 'labels', 'grid'].forEach(name => {
|
|
55
|
+
const value = input.components?.[name];
|
|
56
|
+
if (value !== undefined && !preferenceTriStates.includes(value)) errors.push(diagnostic('INVALID_PREFERENCE_COMPONENT', `components.${name}`, `${name} must be auto, true, or false.`, preferenceTriStates));
|
|
57
|
+
});
|
|
58
|
+
if (input.branding?.enabled !== undefined && !preferenceTriStates.includes(input.branding.enabled)) errors.push(diagnostic('INVALID_PREFERENCE_BRANDING', 'branding.enabled', 'branding.enabled must be auto, true, or false.', preferenceTriStates));
|
|
59
|
+
if (input.motion !== undefined && !preferenceMotions.includes(input.motion)) errors.push(diagnostic('INVALID_PREFERENCE_MOTION', 'motion', `Unsupported motion preference: ${input.motion}.`, preferenceMotions));
|
|
60
|
+
if (!partial && input.version !== undefined && input.version !== preferenceVersion) errors.push(diagnostic('UNSUPPORTED_PREFERENCE_VERSION', 'version', `Unsupported preference version: ${input.version}.`, preferenceVersion));
|
|
61
|
+
return { valid: errors.length === 0, errors, value: normalizePreferences(input, { partial }) };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function normalizePreferences(input = {}, { partial = false } = {}) {
|
|
65
|
+
const source = isPlainObject(input) ? input : {};
|
|
66
|
+
const normalized = partial ? {} : clone(defaultPreferences);
|
|
67
|
+
if (source.theme && isPlainObject(source.theme)) {
|
|
68
|
+
normalized.theme = merge(normalized.theme || {}, {
|
|
69
|
+
mode: source.theme.mode === undefined ? (partial ? undefined : null) : source.theme.mode,
|
|
70
|
+
preset: source.theme.preset === undefined ? (partial ? undefined : null) : source.theme.preset,
|
|
71
|
+
palette: source.theme.palette === undefined ? (partial ? undefined : null) : source.theme.palette
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
if (source.typography && isPlainObject(source.typography) && source.typography.scale !== undefined) normalized.typography = merge(normalized.typography || {}, { scale: Number(source.typography.scale) });
|
|
75
|
+
if (source.density !== undefined) normalized.density = source.density;
|
|
76
|
+
if (source.components && isPlainObject(source.components)) normalized.components = merge(normalized.components || {}, Object.fromEntries(['legend', 'labels', 'grid'].filter(name => source.components[name] !== undefined).map(name => [name, source.components[name]])));
|
|
77
|
+
if (source.branding && isPlainObject(source.branding) && source.branding.enabled !== undefined) normalized.branding = merge(normalized.branding || {}, { enabled: source.branding.enabled });
|
|
78
|
+
if (source.motion !== undefined) normalized.motion = source.motion;
|
|
79
|
+
normalized.version = preferenceVersion;
|
|
80
|
+
return normalized;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function mergePreferences(...values) {
|
|
84
|
+
return normalizePreferences(values.reduce((result, value) => merge(result, normalizePreferences(value, { partial: true })), {}));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function resolveStorage(storage) {
|
|
88
|
+
if (storage && typeof storage.getItem === 'function' && typeof storage.setItem === 'function') return storage;
|
|
89
|
+
if (storage === 'localStorage' && typeof globalThis !== 'undefined' && globalThis.localStorage) return globalThis.localStorage;
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function readState(storage, key) {
|
|
94
|
+
if (!storage) return null;
|
|
95
|
+
try {
|
|
96
|
+
const parsed = JSON.parse(storage.getItem(key) || 'null');
|
|
97
|
+
if (!isPlainObject(parsed)) return null;
|
|
98
|
+
return { version: preferenceVersion, global: normalizePreferences(parsed.global), charts: isPlainObject(parsed.charts) ? Object.fromEntries(Object.entries(parsed.charts).map(([id, value]) => [id, normalizePreferences(value, { partial: true })])) : {} };
|
|
99
|
+
} catch { return null; }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function createPreferencesStore(options = {}) {
|
|
103
|
+
const storage = resolveStorage(options.storage);
|
|
104
|
+
const storageKey = options.storageKey || 'ichartjs:preferences:v1';
|
|
105
|
+
const loaded = readState(storage, storageKey);
|
|
106
|
+
let state = loaded || {
|
|
107
|
+
version: preferenceVersion,
|
|
108
|
+
global: normalizePreferences(options.global || {}),
|
|
109
|
+
charts: Object.fromEntries(Object.entries(options.charts || {}).map(([id, value]) => [id, normalizePreferences(value, { partial: true })]))
|
|
110
|
+
};
|
|
111
|
+
const listeners = new Set();
|
|
112
|
+
const persist = () => {
|
|
113
|
+
if (!storage) return false;
|
|
114
|
+
try { storage.setItem(storageKey, JSON.stringify(state)); return true; } catch { return false; }
|
|
115
|
+
};
|
|
116
|
+
const snapshot = () => clone(state);
|
|
117
|
+
const notify = (scope, chartId, source, persisted) => {
|
|
118
|
+
const payload = { type: 'change', scope, chartId: chartId || null, source: source || 'user', persisted, preferences: chartId ? getEffective(chartId) : clone(state.global), state: snapshot() };
|
|
119
|
+
listeners.forEach(listener => listener(payload));
|
|
120
|
+
};
|
|
121
|
+
const getGlobal = () => clone(state.global);
|
|
122
|
+
const getChart = chartId => clone(state.charts[String(chartId)] || {});
|
|
123
|
+
const getEffective = chartId => mergePreferences(defaultPreferences, state.global, chartId == null ? {} : state.charts[String(chartId)] || {});
|
|
124
|
+
const update = (scope, chartId, patch, { source = 'user', persist: shouldPersist = true } = {}) => {
|
|
125
|
+
const checked = validatePreferences(patch, { partial: true });
|
|
126
|
+
if (!checked.valid) { const error = new Error(checked.errors.map(item => item.message).join(' ')); error.code = 'INVALID_PREFERENCES'; error.details = checked.errors; throw error; }
|
|
127
|
+
if (scope === 'global') state.global = mergePreferences(state.global, patch);
|
|
128
|
+
else {
|
|
129
|
+
const key = String(chartId || 'default');
|
|
130
|
+
state.charts[key] = merge(state.charts[key] || {}, checked.value);
|
|
131
|
+
}
|
|
132
|
+
const persisted = shouldPersist ? persist() : false;
|
|
133
|
+
notify(scope, chartId, source, persisted);
|
|
134
|
+
return scope === 'global' ? getGlobal() : getEffective(chartId);
|
|
135
|
+
};
|
|
136
|
+
return {
|
|
137
|
+
version: preferenceVersion,
|
|
138
|
+
storageKey,
|
|
139
|
+
getState: snapshot,
|
|
140
|
+
getGlobal,
|
|
141
|
+
getChart,
|
|
142
|
+
getEffective,
|
|
143
|
+
setGlobal(patch, options = {}) { return update('global', null, patch, options); },
|
|
144
|
+
setChart(chartId, patch, options = {}) { return update('chart', chartId, patch, options); },
|
|
145
|
+
reset({ scope = 'all', chartId, persist: shouldPersist = true, source = 'user' } = {}) {
|
|
146
|
+
if (scope === 'all' || scope === 'global') state.global = normalizePreferences({});
|
|
147
|
+
if (scope === 'all') state.charts = {};
|
|
148
|
+
if (scope === 'chart' && chartId != null) delete state.charts[String(chartId)];
|
|
149
|
+
const persisted = shouldPersist ? persist() : false;
|
|
150
|
+
notify(scope, chartId, source, persisted);
|
|
151
|
+
return chartId != null ? getEffective(chartId) : getGlobal();
|
|
152
|
+
},
|
|
153
|
+
subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener); },
|
|
154
|
+
persist,
|
|
155
|
+
storage: storage ? 'localStorage' : 'memory'
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function mergeThemePreference(theme, preferenceTheme = {}) {
|
|
160
|
+
const patch = Object.fromEntries(['mode', 'preset', 'palette'].filter(key => preferenceTheme?.[key] != null).map(key => [key, preferenceTheme[key]]));
|
|
161
|
+
if (!Object.keys(patch).length) return theme;
|
|
162
|
+
if (typeof theme === 'string') {
|
|
163
|
+
const base = themeModes.includes(theme) ? { mode: theme } : themePresets.includes(theme) ? { preset: theme } : {};
|
|
164
|
+
return { ...base, ...patch };
|
|
165
|
+
}
|
|
166
|
+
return { ...(isPlainObject(theme) ? theme : {}), ...patch };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function scaledFont(font, scale) {
|
|
170
|
+
return String(font || '').replace(/(\d+(?:\.\d+)?)px/, (_, value) => `${Number((Number(value) * scale).toFixed(2))}px`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function applyPreferencesToSpec(spec, preferences = {}) {
|
|
174
|
+
const result = clone(spec) || {};
|
|
175
|
+
const prefs = mergePreferences(preferences);
|
|
176
|
+
result.theme = clone(result.theme) || {};
|
|
177
|
+
const scale = prefs.typography.scale;
|
|
178
|
+
if (scale !== 1 && result.theme.typography) {
|
|
179
|
+
result.theme.typography = Object.fromEntries(Object.entries(result.theme.typography).map(([key, value]) => [key, { ...value, size: Number((Number(value.size) * scale).toFixed(2)), font: scaledFont(value.font, scale) }]));
|
|
180
|
+
}
|
|
181
|
+
if (prefs.density !== 'comfortable') {
|
|
182
|
+
const factor = prefs.density === 'compact' ? 0.82 : 1.2;
|
|
183
|
+
const padding = result.theme.layout?.padding || result.padding;
|
|
184
|
+
if (padding) result.padding = Object.fromEntries(Object.entries(padding).map(([key, value]) => [key, Math.max(0, Math.round(Number(value) * factor))]));
|
|
185
|
+
if (result.theme.layout) result.theme.layout = { ...result.theme.layout, density: prefs.density, spacing: Number((Number(result.theme.layout.spacing || 8) * factor).toFixed(2)) };
|
|
186
|
+
}
|
|
187
|
+
const component = (path, value) => value === 'auto' ? undefined : value;
|
|
188
|
+
const legend = component('legend', prefs.components.legend), labels = component('labels', prefs.components.labels), grid = component('grid', prefs.components.grid);
|
|
189
|
+
if (legend !== undefined) result.legend = { ...(result.legend || {}), visible: legend };
|
|
190
|
+
if (labels !== undefined) result.labels = { ...(result.labels || {}), enabled: labels };
|
|
191
|
+
if (grid !== undefined) result.grid = { ...(result.grid || {}), visible: grid };
|
|
192
|
+
if (prefs.branding.enabled !== 'auto') result.branding = { ...(result.branding || {}), enabled: prefs.branding.enabled };
|
|
193
|
+
return result;
|
|
194
|
+
}
|
package/types/index.d.ts
CHANGED
|
@@ -3,20 +3,28 @@ export type ChartType = 'line' | 'area' | 'bar' | 'column' | 'pie' | 'scatter' |
|
|
|
3
3
|
export type ThemeMode = 'auto' | 'light' | 'dark' | 'contrast';
|
|
4
4
|
export type ThemePreset = 'auto' | 'analysis' | 'dashboard' | 'report' | 'presentation' | 'project' | 'diagram';
|
|
5
5
|
export type ThemePalette = 'auto' | 'categorical' | 'sequential' | 'diverging' | 'status';
|
|
6
|
+
export type PreferenceTriState = 'auto' | boolean;
|
|
7
|
+
export type PreferenceDensity = 'compact' | 'comfortable' | 'spacious';
|
|
8
|
+
export type PreferenceMotion = 'auto' | 'full' | 'reduced' | 'off';
|
|
6
9
|
export type ExportKind = 'json' | 'svg' | 'png' | 'jpeg' | 'jpg' | 'image/png' | 'image/jpeg' | 'image/svg+xml' | 'application/json';
|
|
7
10
|
export type ExportAs = 'string' | 'dataurl' | 'blob' | 'object';
|
|
8
11
|
export interface ExportError { valid: false; code: string; rasterCode?: string; message?: string; suggestion?: string; [key: string]: unknown }
|
|
9
12
|
export interface ThemeConfig { mode?: ThemeMode; preset?: ThemePreset; palette?: ThemePalette; tokens?: Record<string, unknown>; branding?: boolean | { enabled?: boolean; [key: string]: unknown }; [key: string]: unknown; }
|
|
13
|
+
export interface ChartPreferences { version: '1.0'; theme: { mode: ThemeMode | null; preset: ThemePreset | null; palette: ThemePalette | null }; typography: { scale: number }; density: PreferenceDensity; components: { legend: PreferenceTriState; labels: PreferenceTriState; grid: PreferenceTriState }; branding: { enabled: PreferenceTriState }; motion: PreferenceMotion; }
|
|
14
|
+
export type ChartPreferencesPatch = Partial<Omit<ChartPreferences, 'version'>> & { version?: '1.0'; theme?: Partial<ChartPreferences['theme']>; typography?: Partial<ChartPreferences['typography']>; components?: Partial<ChartPreferences['components']>; branding?: Partial<ChartPreferences['branding']> };
|
|
15
|
+
export interface PreferencesStoreState { version: '1.0'; global: ChartPreferences; charts: Record<string, ChartPreferencesPatch>; }
|
|
16
|
+
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; }
|
|
17
|
+
export interface PreferencesStorage { getItem(key: string): string | null; setItem(key: string, value: string): void; removeItem?(key: string): void; }
|
|
10
18
|
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; }
|
|
11
19
|
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[]; }
|
|
12
20
|
export interface Diagnostic { code: string; path?: string; message: string; expected?: unknown; suggestion?: string; [key: string]: unknown; }
|
|
13
21
|
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; }
|
|
14
22
|
export interface DataInspection { version: '1.0'; rows: number; fields: DataFieldInfo[]; dimensions: string[]; measures: string[]; temporalFields: string[]; missingValueCount: number; warnings: Diagnostic[]; }
|
|
15
23
|
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>; }
|
|
16
|
-
export interface RuntimeCapabilities { version: '2.0'; contractVersion: '1.0'; chartTypes: ChartType[]; charts: Record<ChartType, ChartCapability>; intents: string[]; renderers: Array<'canvas' | 'svg'>; interactions: string[]; exports: Array<'png' | 'svg' | 'json' | 'jpeg'>; styleSystem: { modes: ThemeMode[]; presets: ThemePreset[]; palettes: ThemePalette[]; switchable: boolean; automatic: boolean; [key: string]: unknown }; headless: { preview?: boolean; json: boolean; svg: boolean; png: boolean | string }; export: { types: string[]; mime: Record<string, string>; browser: Record<string, boolean>; headless: Record<string, string | boolean>; methods: string[]; options: Record<string, unknown>; branding: Record<string, unknown> }; branding: { defaultEnabled: boolean; signature: string; options: Record<string, unknown> }; [key: string]: unknown; }
|
|
24
|
+
export interface RuntimeCapabilities { version: '2.0'; contractVersion: '1.0'; chartTypes: ChartType[]; charts: Record<ChartType, ChartCapability>; intents: string[]; renderers: Array<'canvas' | 'svg'>; interactions: string[]; exports: Array<'png' | 'svg' | 'json' | 'jpeg'>; styleSystem: { modes: ThemeMode[]; presets: ThemePreset[]; palettes: ThemePalette[]; switchable: boolean; automatic: boolean; [key: string]: unknown }; preferences?: { version: '1.0'; scopes: string[]; persistence: string[]; fields: string[]; agentAdjustable: boolean; interactiveSettingsUI: boolean; precedence: string[] }; headless: { preview?: boolean; json: boolean; svg: boolean; png: boolean | string }; export: { types: string[]; mime: Record<string, string>; browser: Record<string, boolean>; headless: Record<string, string | boolean>; methods: string[]; options: Record<string, unknown>; branding: Record<string, unknown> }; branding: { defaultEnabled: boolean; signature: string; options: Record<string, unknown> }; [key: string]: unknown; }
|
|
17
25
|
export interface ChartPlan { version: '1.0'; intent: string; primary: ChartType; alternatives: ChartType[]; confidence: number; reasons: string[]; requiredFields: string[]; suggestedEncodings: { dimension: string | null; measure: string | null; secondaryMeasure: string | null }; assumptions: string[]; warnings: Diagnostic[]; unsupportedRequests: string[]; nextActions: string[]; capability: ChartCapability; styleRecommendation: StyleRecommendation; data: DataInspection; }
|
|
18
26
|
export interface ChartExplanation { version: '1.0'; type: ChartType; family: string; purpose: string; renderer: Renderer; dataCount: number; encodings: Record<string, string | string[]>; transforms: string[]; interactions: string[]; assumptions: string[]; warnings: Diagnostic[]; style: Partial<StyleRecommendation> & { name?: string }; lineage: { recordIds: string[]; sourcePreserved: boolean }; accessibility: { enabled: boolean; summary: string }; }
|
|
19
|
-
export interface ChartSpec { type: ChartType; renderer?: Renderer; container?: string | Element; width?: number; height?: number; data?: Array<Record<string, unknown>> | { values?: Array<Record<string, unknown>>; [key: string]: unknown }; encoding?: Record<string, unknown>; title?: { text?: string; subtitle?: string }; legend?: { visible?: boolean; position?: string }; grid?: { visible?: boolean; color?: string }; labels?: { enabled?: boolean; format?: string | Record<string, unknown>; color?: string; font?: string }; interaction?: Record<string, boolean>; accessibility?: { enabled?: boolean; description?: string }; branding?: boolean | { enabled?: boolean }; theme?: ThemeMode | ThemePreset | ThemeConfig | ResolvedTheme; [key: string]: unknown; }
|
|
27
|
+
export interface ChartSpec { type: ChartType; renderer?: Renderer; container?: string | Element; chartId?: string; width?: number; height?: number; data?: Array<Record<string, unknown>> | { values?: Array<Record<string, unknown>>; [key: string]: unknown }; encoding?: Record<string, unknown>; title?: { text?: string; subtitle?: string }; legend?: { visible?: boolean; position?: string }; grid?: { visible?: boolean; color?: string }; labels?: { enabled?: boolean; format?: string | Record<string, unknown>; color?: string; font?: string }; interaction?: Record<string, boolean>; accessibility?: { enabled?: boolean; description?: string }; branding?: boolean | { enabled?: boolean }; theme?: ThemeMode | ThemePreset | ThemeConfig | ResolvedTheme; preferences?: ChartPreferencesPatch | PreferencesStore; preferencesStore?: PreferencesStore; [key: string]: unknown; }
|
|
20
28
|
export interface BinTransform { type: 'bin'; field: string; output?: string; thresholds?: number; step?: number; extent?: [number, number]; }
|
|
21
29
|
export interface RadarIndicator { name: string; field: string; min?: number; max?: number; }
|
|
22
30
|
export type BusinessFieldType = 'string' | 'number' | 'boolean' | 'date' | 'enum' | 'array' | 'object';
|
|
@@ -46,6 +54,9 @@ export interface Chart {
|
|
|
46
54
|
setData(data: Array<Record<string, unknown>>): this;
|
|
47
55
|
setTheme(theme?: ChartSpec['theme']): this;
|
|
48
56
|
getTheme(): ResolvedTheme;
|
|
57
|
+
getPreferences(): ChartPreferences;
|
|
58
|
+
setPreferences(patch?: ChartPreferencesPatch, options?: { scope?: 'chart' | 'global'; source?: string; persist?: boolean }): this;
|
|
59
|
+
resetPreferences(options?: { scope?: 'all' | 'global' | 'chart'; source?: string; persist?: boolean }): this;
|
|
49
60
|
resize(width?: number, height?: number): this;
|
|
50
61
|
resetZoom(): this;
|
|
51
62
|
zoomTo(view: Record<string, number>): this;
|
|
@@ -87,6 +98,13 @@ export interface Chart {
|
|
|
87
98
|
}
|
|
88
99
|
|
|
89
100
|
export function createChart(spec: ChartSpec): Chart;
|
|
101
|
+
export const defaultPreferences: ChartPreferences;
|
|
102
|
+
export function createPreferencesStore(options?: { storage?: 'localStorage' | 'memory' | PreferencesStorage; storageKey?: string; global?: ChartPreferencesPatch; charts?: Record<string, ChartPreferencesPatch> }): PreferencesStore;
|
|
103
|
+
export function normalizePreferences(input?: ChartPreferencesPatch, options?: { partial?: boolean }): ChartPreferences;
|
|
104
|
+
export function mergePreferences(...values: ChartPreferencesPatch[]): ChartPreferences;
|
|
105
|
+
export function mergeThemePreference(theme: ChartSpec['theme'], preferenceTheme?: Partial<ChartPreferences['theme']>): ChartSpec['theme'];
|
|
106
|
+
export function applyPreferencesToSpec(spec: ChartSpec, preferences?: ChartPreferencesPatch): ChartSpec;
|
|
107
|
+
export function mountChartSettings(chart: Chart, options?: { container?: Element; locale?: 'auto' | 'zh-CN' | 'en'; title?: string }): { valid: true; element: Element; locale: 'zh-CN' | 'en'; destroy(): void } | { valid: false; code: string; message: string };
|
|
90
108
|
export function normalizeSpec(spec: Partial<ChartSpec>): ChartSpec;
|
|
91
109
|
export function validateSpec(spec: Partial<ChartSpec>): { valid: boolean; errors: Diagnostic[]; warnings: Diagnostic[]; normalizations: Diagnostic[]; spec: ChartSpec };
|
|
92
110
|
export function normalizeData(input: unknown): { rows: Array<Record<string, unknown>>; fields: DataFieldInfo[]; warnings: Diagnostic[] };
|