@taylorwong/ichartjs 2.0.7 → 2.0.8
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 +2 -2
- package/agent-recipes/minimal-specs.json +25 -0
- package/docs/agent/charting-scenario.md +46 -1
- package/docs/agent/coding-agent-integration.md +1 -1
- package/docs/agent/development/roadmap.md +1 -1
- package/docs/agent/frontend-integration.md +1 -1
- package/docs/agent/project-scenario.md +1 -0
- package/docs/agent/quickstart.md +30 -0
- package/docs/agent/runtime-contract.md +8 -1
- package/docs/agent/usage-scenarios.md +1 -1
- package/docs/agent/zh-CN/charting-scenario.md +31 -1
- package/docs/agent/zh-CN/coding-agent-integration.md +1 -1
- package/docs/agent/zh-CN/frontend-integration.md +1 -1
- package/docs/agent/zh-CN/quickstart.md +30 -0
- package/docs/agent/zh-CN/runtime-contract.md +4 -0
- package/docs/agent/zh-CN/usage-scenarios.md +1 -1
- package/docs/manifests/capabilities.json +12 -0
- package/examples/agent-workflow.mjs +2 -1
- package/package.json +1 -1
- package/skills/ichartjs/SKILL.md +19 -10
- package/skills/ichartjs/references/agent-contract.md +5 -2
- package/skills/ichartjs/references/chart-selection.md +2 -0
- package/src/capabilities.mjs +23 -1
- package/src/charts.mjs +82 -21
- package/src/index.mjs +11 -3
- package/src/spec.mjs +78 -2
- package/types/index.d.ts +8 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.0.8 - 2026-09-20
|
|
4
|
+
|
|
5
|
+
- Hardened chart-specific encoding and field validation, including explicit Swimlane requirements and actionable diagnostics for Agents.
|
|
6
|
+
- Added Gauge domain contracts and clamping diagnostics, Heatmap label rendering, locale-aware temporal formatting, semantic Pie labels, and runtime health checkpoints.
|
|
7
|
+
- Added intent fallback suggestions, locale capability metadata, a complete minimal Spec catalog, and synchronized Agent guidance and capability manifests.
|
|
8
|
+
|
|
3
9
|
## 2.0.7 - 2026-09-20
|
|
4
10
|
|
|
5
11
|
- Added Architecture and Mindmap chart types with shared diagram contracts, layers, boundaries, parent-child validation, deterministic tree/radial layouts, and Agent-readable schemas and capabilities.
|
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ getCapabilities
|
|
|
40
40
|
npm install @taylorwong/ichartjs@^2
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
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.8`.
|
|
44
44
|
|
|
45
45
|
### Optional Agent Skill
|
|
46
46
|
|
|
@@ -62,7 +62,7 @@ For a non-interactive global Codex installation:
|
|
|
62
62
|
npx skills add wanghetommy/ichartjs --skill ichartjs --agent codex --global --yes
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
-
For a release-pinned installation, use `npx skills add https://github.com/wanghetommy/ichartjs/tree/v2.0.
|
|
65
|
+
For a release-pinned installation, use `npx skills add https://github.com/wanghetommy/ichartjs/tree/v2.0.8/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.
|
|
66
66
|
|
|
67
67
|
### Agent workflow
|
|
68
68
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "2.0",
|
|
3
|
+
"name": "minimal-specs",
|
|
4
|
+
"description": "Small validated starting Specs for every public iChart.js chart type.",
|
|
5
|
+
"examples": {
|
|
6
|
+
"line": { "type": "line", "renderer": "svg", "data": [{ "id": "jan", "month": "Jan", "value": 12 }], "encoding": { "x": { "field": "month" }, "y": { "field": "value" } } },
|
|
7
|
+
"area": { "type": "area", "renderer": "svg", "data": [{ "id": "jan", "month": "Jan", "value": 12 }], "encoding": { "x": { "field": "month" }, "y": { "field": "value" } } },
|
|
8
|
+
"bar": { "type": "bar", "renderer": "svg", "data": [{ "id": "a", "name": "A", "value": 12 }], "encoding": { "x": { "field": "name" }, "y": { "field": "value" } } },
|
|
9
|
+
"column": { "type": "column", "renderer": "svg", "data": [{ "id": "a", "name": "A", "value": 12 }], "encoding": { "x": { "field": "name" }, "y": { "field": "value" } } },
|
|
10
|
+
"pie": { "type": "pie", "renderer": "svg", "data": [{ "id": "a", "name": "A", "value": 12 }] },
|
|
11
|
+
"scatter": { "type": "scatter", "renderer": "svg", "data": [{ "id": "a", "x": 1, "y": 12 }], "encoding": { "x": { "field": "x" }, "y": { "field": "y" } } },
|
|
12
|
+
"funnel": { "type": "funnel", "renderer": "svg", "data": [{ "id": "visit", "name": "Visit", "value": 100 }] },
|
|
13
|
+
"gauge": { "type": "gauge", "renderer": "svg", "domain": [0, 100], "data": [{ "id": "completion", "name": "Completion", "value": 72 }] },
|
|
14
|
+
"heatmap": { "type": "heatmap", "renderer": "svg", "data": [{ "id": "a", "x": "Mon", "y": "AM", "value": 12 }], "encoding": { "x": { "field": "x" }, "y": { "field": "y" }, "color": { "field": "value" } } },
|
|
15
|
+
"radar": { "type": "radar", "renderer": "svg", "indicators": [{ "name": "Quality", "field": "quality", "min": 0, "max": 100 }, { "name": "Speed", "field": "speed", "min": 0, "max": 100 }, { "name": "Coverage", "field": "coverage", "min": 0, "max": 100 }], "data": [{ "id": "team-a", "quality": 80, "speed": 70, "coverage": 90 }] },
|
|
16
|
+
"gantt": { "type": "gantt", "renderer": "svg", "data": [{ "id": "task-a", "name": "Task A", "start": "2026-09-01", "end": "2026-09-03" }] },
|
|
17
|
+
"timeline": { "type": "timeline", "renderer": "svg", "data": [{ "id": "event-a", "date": "2026-09-01", "title": "Kickoff" }] },
|
|
18
|
+
"milestone": { "type": "milestone", "renderer": "svg", "data": [{ "id": "release", "date": "2026-09-19", "title": "Release" }] },
|
|
19
|
+
"burndown": { "type": "burndown", "renderer": "svg", "data": [{ "id": "day-1", "date": "2026-09-01", "remaining": 10 }, { "id": "day-2", "date": "2026-09-02", "remaining": 0 }] },
|
|
20
|
+
"flow": { "type": "flow", "renderer": "svg", "nodes": [{ "id": "start", "label": "Start" }, { "id": "done", "label": "Done" }], "edges": [{ "id": "start-done", "from": "start", "to": "done" }] },
|
|
21
|
+
"swimlane": { "type": "swimlane", "renderer": "svg", "lanes": [{ "id": "team", "label": "Team" }], "nodes": [{ "id": "task", "label": "Task", "laneId": "team" }], "edges": [] },
|
|
22
|
+
"architecture": { "type": "architecture", "renderer": "svg", "nodes": [{ "id": "api", "label": "API" }, { "id": "db", "label": "Database" }], "edges": [{ "id": "api-db", "from": "api", "to": "db" }] },
|
|
23
|
+
"mindmap": { "type": "mindmap", "renderer": "svg", "nodes": [{ "id": "root", "label": "Goal" }, { "id": "child", "label": "Next step", "parentId": "root" }], "edges": [] }
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -26,6 +26,47 @@ Agent usage and development guide for generic data analysis and metric visualiza
|
|
|
26
26
|
- Heatmap treats missing values separately from numeric zero through `colorScale.missing`.
|
|
27
27
|
- Radar should declare `min` and `max` for every indicator; omitted or mixed-unit domains produce warnings.
|
|
28
28
|
|
|
29
|
+
## Configuration Placement
|
|
30
|
+
|
|
31
|
+
`encoding` describes field roles and series semantics. Keep these options at the Spec level:
|
|
32
|
+
|
|
33
|
+
| Concern | Correct location | Applies to |
|
|
34
|
+
| --- | --- | --- |
|
|
35
|
+
| Axis title and format | `xAxis.title/format`, `yAxis.title/format` | Line, Area, Bar, Column, Scatter |
|
|
36
|
+
| Readable numeric domain | `yAxis.nice`, `yAxis.ticks`, `yAxis.domain` | Line, Area, Bar, Column, Scatter |
|
|
37
|
+
| Data labels | `labels.enabled/format` | Charts that declare `labels` in capabilities |
|
|
38
|
+
| Legend | `legend.visible/position` | Multi-series Cartesian, Pie, and Radar |
|
|
39
|
+
| Gauge domain | `domain: [min, max]` | Gauge |
|
|
40
|
+
| Heatmap color domain | `colorScale.domain` | Heatmap |
|
|
41
|
+
| Radar indicator domain | `indicators[].min/max` | Radar |
|
|
42
|
+
|
|
43
|
+
Do not put `title`, `format`, `labels`, or `legend` under `encoding`; `validateSpec()` reports those placements as warnings. Numeric y-axes use readable domains by default (`nice: true`, `ticks: "auto"`). Use `yAxis.domain: [min, max]` for an explicit range, or `yAxis.nice: false` to retain the raw boundary. `xAxis.min/max` and `xAxis.domain` are unsupported for categorical/time layouts and produce a structured warning.
|
|
44
|
+
|
|
45
|
+
## Encoding Contracts
|
|
46
|
+
|
|
47
|
+
Use only the channels declared for the selected chart: Cartesian charts use `encoding.x` and `encoding.y`; Pie, Funnel, and Gauge use `encoding.category` and `encoding.value`; Heatmap uses `encoding.x`, `encoding.y`, and `encoding.color`; Radar uses `indicators[].field`. `validateSpec()` reports `UNSUPPORTED_ENCODING_CHANNEL` for an unused channel and `MISSING_ENCODING_FIELD` when a referenced field is absent. Gauge additionally requires `domain: [min, max]`; values outside the domain are clamped for the rendered arc and report `VALUE_CLAMPED`.
|
|
48
|
+
|
|
49
|
+
The complete set of small starting Specs is available at `@taylorwong/ichartjs/recipes/minimal-specs`.
|
|
50
|
+
|
|
51
|
+
## Intent Vocabulary
|
|
52
|
+
|
|
53
|
+
Pass an exact value from `getCapabilities().intents` to `planChart()`. Common mappings are:
|
|
54
|
+
|
|
55
|
+
| User need | Registered intent | Primary chart |
|
|
56
|
+
| --- | --- | --- |
|
|
57
|
+
| Trend over time | `trend` or `time-series` | Line |
|
|
58
|
+
| Compare categories | `comparison` | Bar |
|
|
59
|
+
| Rank categories | `ranking` | Bar |
|
|
60
|
+
| Distribution or histogram | `distribution` | Column with `bin` transform |
|
|
61
|
+
| Relationship or correlation | `relationship` or `correlation` | Scatter |
|
|
62
|
+
| Composition | `composition` | Column or Area |
|
|
63
|
+
| Matrix intensity | `matrix` or `correlation-grid` | Heatmap |
|
|
64
|
+
| Profile across measures | `multidimensional` or `profile` | Radar |
|
|
65
|
+
|
|
66
|
+
Natural-language prose such as `trend over time` is not an intent token. Map it to `trend` first. An unknown token returns `UNKNOWN_INTENT` plus a fallback plan; never ignore that warning.
|
|
67
|
+
|
|
68
|
+
Unknown intent plans expose `intentKnown: false`, `fallbackUsed: true`, and deterministic `intentSuggestions`. Use those fields to remap the request or ask for confirmation instead of silently accepting the fallback chart.
|
|
69
|
+
|
|
29
70
|
## Agent Workflow
|
|
30
71
|
|
|
31
72
|
1. Call `inspectData(data)` to identify fields and missing values.
|
|
@@ -33,6 +74,10 @@ Agent usage and development guide for generic data analysis and metric visualiza
|
|
|
33
74
|
3. Create a JSON-serializable Chart Spec.
|
|
34
75
|
4. Call `validateSpec(spec)` before `createChart(spec)`.
|
|
35
76
|
5. Inspect the result with `chart.describe()` and `chart.getState()`.
|
|
77
|
+
6. Add stable string `id` values to rows when lineage checks, linked selection, or later updates matter.
|
|
78
|
+
7. Check `chart.getState().health.renderable`, `health.status`, and `warnings` before presenting the result. `ready` means no material diagnostic is active; `degraded` means the chart rendered with a material warning; `empty` means it has no meaningful result.
|
|
79
|
+
|
|
80
|
+
`locale` defaults to `en-US` and controls axis, label, tooltip, and export formatting. Set `locale: "zh-CN"` for Chinese output. Input dates should remain ISO-8601 strings; natural-language date parsing is not part of the runtime contract.
|
|
36
81
|
|
|
37
82
|
## Minimal Spec
|
|
38
83
|
|
|
@@ -41,7 +86,7 @@ Agent usage and development guide for generic data analysis and metric visualiza
|
|
|
41
86
|
type: 'line',
|
|
42
87
|
renderer: 'svg',
|
|
43
88
|
container: '#chart',
|
|
44
|
-
data: { values: [{ month: 'Jan', sales: 120 }] },
|
|
89
|
+
data: { values: [{ id: 'jan', month: 'Jan', sales: 120 }] },
|
|
45
90
|
encoding: {
|
|
46
91
|
x: { field: 'month', type: 'category' },
|
|
47
92
|
y: { field: 'sales', type: 'quantitative' }
|
|
@@ -33,7 +33,7 @@ Install the official Skill with the standard Agent Skills CLI:
|
|
|
33
33
|
npx skills add wanghetommy/ichartjs --skill ichartjs
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
For global non-interactive Codex setup, append `--agent codex --global --yes`. To pin the released workflow, install `https://github.com/wanghetommy/ichartjs/tree/v2.0.
|
|
36
|
+
For global non-interactive Codex setup, append `--agent codex --global --yes`. To pin the released workflow, install `https://github.com/wanghetommy/ichartjs/tree/v2.0.8/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
|
|
|
@@ -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.8` includes the completed Iteration 12 structured-diagram work, follow-up chart/menu fixes, and Agent contract hardening. Geographic charts and 3D rendering remain out of scope until explicitly reintroduced.
|
|
4
4
|
|
|
5
5
|
## Current Status
|
|
6
6
|
|
|
@@ -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.8`.
|
|
14
14
|
|
|
15
15
|
Use the package through a bundler or another environment that resolves npm ESM imports:
|
|
16
16
|
|
|
@@ -24,6 +24,7 @@ Agent usage and development guide for project planning, delivery tracking, and p
|
|
|
24
24
|
- Keep derived values separate from source rows. State and tooltips may expose variance, float, warnings, and assumptions, but transforms must not mutate source data.
|
|
25
25
|
- Linked filters and linked selection must use stable record IDs, not array positions.
|
|
26
26
|
- Forecasts, risk scores, and aging buckets are inspectable heuristics. They are not commitments, causal claims, or hidden inference.
|
|
27
|
+
- Project chart date axes are derived from `date`, `start`, and `end` records in the current contract; generic `xAxis.title/format` and `xAxis.min/max` settings do not customize them.
|
|
27
28
|
|
|
28
29
|
## Agent Workflow
|
|
29
30
|
|
package/docs/agent/quickstart.md
CHANGED
|
@@ -67,6 +67,12 @@ Call `planChart(rows, { intent, renderer })`. Read the full result rather than o
|
|
|
67
67
|
|
|
68
68
|
Stop before rendering when `requiredFields` is not empty. Ask for the missing information or choose a supported alternative without fabricating data.
|
|
69
69
|
|
|
70
|
+
#### Use registered intents only
|
|
71
|
+
|
|
72
|
+
`intent` is an exact machine token, not a natural-language sentence. Discover the allowlist from `getCapabilities().intents`, then pass values such as `trend`, `time-series`, `comparison`, `ranking`, `distribution`, `relationship`, `matrix`, `multidimensional`, `schedule`, `architecture`, or `mindmap`. Do not pass `trend over time` or `show a sales trend` directly. If an unknown token is passed, `planChart()` returns an `UNKNOWN_INTENT` warning and a safe fallback, which may select the wrong chart if the warning is ignored.
|
|
73
|
+
|
|
74
|
+
If the user gives prose, map it to a registered token before calling `planChart()` and preserve the original prose separately as user intent.
|
|
75
|
+
|
|
70
76
|
### 4. Build a JSON-Friendly Spec
|
|
71
77
|
|
|
72
78
|
For a trend or comparison with one dimension and one or more measures:
|
|
@@ -97,6 +103,28 @@ const spec = {
|
|
|
97
103
|
|
|
98
104
|
Use the selected capability and recipes for Pie, Gauge, Heatmap, Radar, project views, and diagrams because their required encodings differ.
|
|
99
105
|
|
|
106
|
+
#### Put options at the contract level
|
|
107
|
+
|
|
108
|
+
Keep `encoding` for field roles and series semantics. Put presentation and axis options at the Spec level:
|
|
109
|
+
|
|
110
|
+
| Need | Correct location | Common mistake |
|
|
111
|
+
| --- | --- | --- |
|
|
112
|
+
| Axis title | `xAxis.title`, `yAxis.title` | `encoding.x.title`, `encoding.y.title` |
|
|
113
|
+
| Axis number/date format | `xAxis.format`, `yAxis.format` | `encoding.x.format`, `encoding.y.format` |
|
|
114
|
+
| Data labels | `labels.enabled`, `labels.format` | `encoding.labels` |
|
|
115
|
+
| Legend | `legend.visible` | `encoding.legend` |
|
|
116
|
+
| Theme and palette | `theme.mode`, `theme.preset`, `theme.palette` | series or encoding color guesses |
|
|
117
|
+
|
|
118
|
+
`validateSpec()` reports these misplaced options as structured warnings. Repair them before presenting the chart. A single-series Cartesian chart shows the encoded field name in the legend by default; set `legend: { visible: false }` when that label adds no value.
|
|
119
|
+
|
|
120
|
+
#### Know the domain contract
|
|
121
|
+
|
|
122
|
+
Numeric Cartesian charts use a readable y-axis domain by default: `yAxis.nice` is `true`, and `yAxis.ticks` is `"auto"`. For an explicit range, use `yAxis.domain: [min, max]`; for example, `{ domain: [0, 2000], ticks: 5 }` produces a stable five-label scale. Set `yAxis.nice: false` to retain the raw data boundary. `yAxis.format` only changes display formatting. `chart.getState().axes` and `chart.explain().axes` expose `rawDomain`, resolved `domain`, `ticks`, `step`, and `policy` for Agent self-checks. `yAxis.right` accepts the same controls for a secondary numeric axis. `xAxis.min/max` and `xAxis.domain` remain unsupported because categorical/time x-axis ranges are derived from records. The other supported domain controls are chart-specific: `gauge.domain`, `heatmap.colorScale.domain`, and `radar.indicators[].min/max`. Project chart date ranges are derived from their records in the current version.
|
|
123
|
+
|
|
124
|
+
Chart-specific encoding is strict: Cartesian charts use `x`/`y`, Pie/Funnel/Gauge use `category`/`value`, Heatmap uses `x`/`y`/`color`, and Radar uses `indicators[].field`. Missing or unsupported fields are validation errors, not silent fallbacks. Gauge Specs must declare `domain`; inspect `VALUE_CLAMPED` when a value falls outside it. Use the complete minimal catalog at `@taylorwong/ichartjs/recipes/minimal-specs` when starting a new chart.
|
|
125
|
+
|
|
126
|
+
For Agent self-checks, `chart.getState().health` and `chart.explain().health` expose `ready`, `degraded`, or `empty`, plus warning, suppressed-label, clamped-value, and rendered-mark metrics. `locale` defaults to `en-US`; use `locale: "zh-CN"` for localized number/date output while keeping input dates in ISO-8601 form.
|
|
127
|
+
|
|
100
128
|
### 5. Validate
|
|
101
129
|
|
|
102
130
|
```js
|
|
@@ -177,6 +205,8 @@ Before returning a result, verify:
|
|
|
177
205
|
- the branding on/off state is documented so live view and exports stay consistent;
|
|
178
206
|
- the preview URL or output artifact (JSON/SVG/PNG/JPEG) is provided to the user.
|
|
179
207
|
|
|
208
|
+
For deterministic lineage checks and linked updates, give every input row a stable string `id`. Without one, the runtime uses a positional fallback such as `record-0`; that is sufficient for a local render but should not be treated as a durable business identity.
|
|
209
|
+
|
|
180
210
|
## Branding (Signature) Defaults
|
|
181
211
|
|
|
182
212
|
- Default `branding: true`: a low-contrast `Powered by iChart.js` signature appears in the bottom-right corner, synchronized across live rendering, PNG/SVG raster export, and JSON state persistence.
|
|
@@ -14,14 +14,19 @@ For post-creation visual settings, use `getPreferenceCapabilities(chartType, { l
|
|
|
14
14
|
|
|
15
15
|
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
16
|
|
|
17
|
-
`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. Planning never invents business meaning, units, dates, or missing fields.
|
|
17
|
+
`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.
|
|
18
|
+
|
|
19
|
+
Unknown intent results also include `intentKnown`, `intentSuggestions`, and `fallbackUsed`. A chart Spec is chart-specific: Cartesian channels are `x`/`y`, Pie/Funnel/Gauge channels are `category`/`value`, Heatmap channels are `x`/`y`/`color`, and Radar fields live in `indicators`. `validateSpec()` rejects unsupported channels and missing fields. Gauge requires an explicit `domain` and reports `VALUE_CLAMPED` when the rendered value exceeds it.
|
|
18
20
|
|
|
19
21
|
## Spec Rules
|
|
20
22
|
|
|
21
23
|
- Specs must be JSON-serializable.
|
|
22
24
|
- Call `validateSpec()` before rendering.
|
|
23
25
|
- Chart layout and data semantics are renderer-independent.
|
|
26
|
+
- Keep axis titles/formats on `xAxis`/`yAxis`, labels on `labels`, and legend settings on `legend`; misplaced options return structured warnings.
|
|
27
|
+
- Numeric y-axes use readable domains by default (`yAxis.nice: true`, `yAxis.ticks: "auto"`). Use `yAxis.domain: [min, max]` for an explicit range, `yAxis.nice: false` to retain the raw boundary, and `yAxis.right` for a secondary numeric axis. `chart.getState().axes` and `chart.explain().axes` expose the raw domain, resolved domain, ticks, step, and policy for Agent verification. `xAxis.min/max` and `xAxis.domain` remain unsupported for categorical/time layouts. Chart-specific domains remain available through `gauge.domain`, `heatmap.colorScale.domain`, and `radar.indicators[].min/max`.
|
|
24
28
|
- `flow` and `swimlane` use `nodes/edges/lanes`; `architecture` uses `nodes/edges/layers/boundaries`; `mindmap` uses `nodes` with `parentId` and optional `edges`; generic charts use `data.values`.
|
|
29
|
+
- `chart.getState().health` and `chart.explain().health` expose `ready`, `degraded`, or `empty`, with renderability and warning, suppressed-label, clamped-value, and rendered-mark metrics. `locale` defaults to `en-US`; set `locale: "zh-CN"` for output formatting and keep input dates as ISO-8601 strings.
|
|
25
30
|
|
|
26
31
|
## Renderer
|
|
27
32
|
|
|
@@ -119,6 +124,8 @@ chart.downloadJSON()
|
|
|
119
124
|
|
|
120
125
|
Validation results contain separate `errors`, `warnings`, and `normalizations`. Diagnostics use stable codes, JSON-oriented paths, expected values where useful, and actionable suggestions. `chart.explain()` returns encodings, transforms, interactions, assumptions, warnings, stable record lineage, and an accessibility summary.
|
|
121
126
|
|
|
127
|
+
For lineage checks and linked updates, provide stable string `id` values on input rows. Without one, the runtime uses deterministic positional IDs such as `record-0`; these are suitable for a local self-check but not for durable business identity.
|
|
128
|
+
|
|
122
129
|
## Interaction
|
|
123
130
|
|
|
124
131
|
Common interactions include Tooltip, Hover, Click, Selection, Zoom, Pan, Drag, Touch, and Keyboard; each chart's enabled interactions are controlled by its Spec and capability declaration.
|
|
@@ -121,7 +121,7 @@ npx skills add wanghetommy/ichartjs --skill ichartjs --agent codex --global --ye
|
|
|
121
121
|
For reproducible installation, pin the released Skill directory:
|
|
122
122
|
|
|
123
123
|
```bash
|
|
124
|
-
npx skills add https://github.com/wanghetommy/ichartjs/tree/v2.0.
|
|
124
|
+
npx skills add https://github.com/wanghetommy/ichartjs/tree/v2.0.8/skills/ichartjs \
|
|
125
125
|
--agent codex --global --yes
|
|
126
126
|
```
|
|
127
127
|
|
|
@@ -26,6 +26,32 @@
|
|
|
26
26
|
- Heatmap 通过 `colorScale.missing` 区分缺失值和数值零。
|
|
27
27
|
- Radar 应为每个指标声明 `min` 和 `max`;缺失域或混合单位会产生告警。
|
|
28
28
|
|
|
29
|
+
## 配置项位置
|
|
30
|
+
|
|
31
|
+
`encoding` 只描述字段角色和 Series 语义。以下配置应放在 Spec 顶层:
|
|
32
|
+
|
|
33
|
+
| 配置 | 正确位置 | 适用范围 |
|
|
34
|
+
| --- | --- | --- |
|
|
35
|
+
| 坐标轴标题和格式 | `xAxis.title/format`、`yAxis.title/format` | Line、Area、Bar、Column、Scatter |
|
|
36
|
+
| 易读数值域 | `yAxis.nice`、`yAxis.ticks`、`yAxis.domain` | Line、Area、Bar、Column、Scatter |
|
|
37
|
+
| 数据标签 | `labels.enabled/format` | 能力清单声明支持 labels 的图表 |
|
|
38
|
+
| 图例 | `legend.visible/position` | 多系列笛卡尔图、Pie、Radar |
|
|
39
|
+
| Gauge 坐标域 | `domain: [min, max]` | Gauge |
|
|
40
|
+
| Heatmap 颜色域 | `colorScale.domain` | Heatmap |
|
|
41
|
+
| Radar 指标域 | `indicators[].min/max` | Radar |
|
|
42
|
+
|
|
43
|
+
不要把 `title`、`format`、`labels` 或 `legend` 放在 `encoding` 中;`validateSpec()` 会报告结构化警告。数值纵轴默认使用易读域(`nice: true`、`ticks: "auto"`);需要固定范围时使用 `yAxis.domain: [min, max]`,需要保留原始边界时使用 `yAxis.nice: false`。分类/时间横轴的 `min/max` 和 `domain` 不支持。
|
|
44
|
+
|
|
45
|
+
## Encoding 契约
|
|
46
|
+
|
|
47
|
+
不同图表只接受对应的数据通道:笛卡尔图表使用 `encoding.x`/`encoding.y`;Pie、Funnel、Gauge 使用 `encoding.category`/`encoding.value`;Heatmap 使用 `encoding.x`/`encoding.y`/`encoding.color`;Radar 使用 `indicators[].field`。字段不存在或通道不支持会成为校验错误,不应静默改名。Gauge 必须提供 `domain: [min, max]`;超出范围时弧形会限制在范围内,并产生 `VALUE_CLAMPED`。全部图表的最小可执行 Spec 见 `@taylorwong/ichartjs/recipes/minimal-specs`。
|
|
48
|
+
|
|
49
|
+
## 意图注册词
|
|
50
|
+
|
|
51
|
+
传给 `planChart()` 的 `intent` 必须是 `getCapabilities().intents` 中的精确值,而不是用户原句。比如把“展示销售随时间变化”映射为 `trend`;未知词会返回 `UNKNOWN_INTENT` 和降级方案,不能忽略该警告。
|
|
52
|
+
|
|
53
|
+
未知词的规划结果还会给出 `intentKnown: false`、`fallbackUsed: true` 和 `intentSuggestions`,应据此重新映射或请求确认。
|
|
54
|
+
|
|
29
55
|
## Agent 流程
|
|
30
56
|
|
|
31
57
|
1. 使用 `inspectData(data)` 检查字段和缺失值。
|
|
@@ -33,6 +59,10 @@
|
|
|
33
59
|
3. 生成 JSON-serializable Chart Spec。
|
|
34
60
|
4. 先调用 `validateSpec(spec)`,再调用 `createChart(spec)`。
|
|
35
61
|
5. 使用 `chart.describe()` 和 `chart.getState()` 检查结果。
|
|
62
|
+
6. 如果需要 lineage、联动筛选或后续更新,为每条记录提供稳定字符串 `id`;没有 `id` 时 Runtime 会使用 `record-0` 这类确定性后备值。
|
|
63
|
+
7. 展示前检查 `chart.getState().health.renderable`、`health.status` 和 `warnings`。`ready` 表示没有阻止渲染的问题,`degraded` 表示带诊断继续渲染,`empty` 表示结果没有可用内容。
|
|
64
|
+
|
|
65
|
+
`locale` 默认是 `en-US`,需要中文数字/日期输出时设置 `locale: "zh-CN"`。输入日期保持 ISO-8601 字符串;运行时不负责自然语言日期解析。
|
|
36
66
|
|
|
37
67
|
## 最小 Spec
|
|
38
68
|
|
|
@@ -40,7 +70,7 @@
|
|
|
40
70
|
{
|
|
41
71
|
type: 'line',
|
|
42
72
|
renderer: 'svg',
|
|
43
|
-
data: { values: [{ month: 'Jan', sales: 120 }] },
|
|
73
|
+
data: { values: [{ id: 'jan', month: 'Jan', sales: 120 }] },
|
|
44
74
|
encoding: {
|
|
45
75
|
x: { field: 'month', type: 'category' },
|
|
46
76
|
y: { field: 'sales', type: 'quantitative' }
|
|
@@ -22,7 +22,7 @@ npm Registry 中无作用域的 `ichartjs` 是安全占位包,并非本项目
|
|
|
22
22
|
npx skills add wanghetommy/ichartjs --skill ichartjs
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
Codex 全局无交互安装可追加 `--agent codex --global --yes`。需要固定发布版本时,安装 `https://github.com/wanghetommy/ichartjs/tree/v2.0.
|
|
25
|
+
Codex 全局无交互安装可追加 `--agent codex --global --yes`。需要固定发布版本时,安装 `https://github.com/wanghetommy/ichartjs/tree/v2.0.8/skills/ichartjs`。WorkBuddy 可通过自身 Skill 界面导入该带 Tag 的目录;只有当前 CLI 明确声明对应适配器时才使用宿主专用 `--agent` 参数。
|
|
26
26
|
|
|
27
27
|
使用 `npx skills add wanghetommy/ichartjs --list` 验证发现结果,其中应包含 `ichartjs`。
|
|
28
28
|
|
|
@@ -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.8`。
|
|
10
10
|
|
|
11
11
|
```js
|
|
12
12
|
import { createChart } from '@taylorwong/ichartjs';
|
|
@@ -36,6 +36,34 @@ Agent 与开发者使用同一个 ESM 入口。编码 Agent 的完整方式见 [
|
|
|
36
36
|
6. 仅在 `validation.valid` 为 `true` 时调用 `createChart()`。
|
|
37
37
|
7. 用 `chart.explain()`、`chart.getState()`、JSON/SVG/PNG export 完成自检。需要持久化或附件生成时使用 `chart.export({type:'json'|'svg'|'png'})`,在浏览器环境可调用 `chart.downloadPNG()` / `chart.downloadSVG()` / `chart.downloadJSON()` 触发保存,最后调用 `chart.destroy()`。
|
|
38
38
|
|
|
39
|
+
### 意图必须使用注册词
|
|
40
|
+
|
|
41
|
+
`intent` 是精确的机器词,不是自然语言句子。先从 `getCapabilities().intents` 获取允许值,再传入 `trend`、`time-series`、`comparison`、`ranking`、`distribution`、`relationship`、`matrix`、`multidimensional`、`schedule`、`architecture` 或 `mindmap` 等值。不要直接传入 `trend over time` 或 `展示销售趋势`。未知词会返回 `UNKNOWN_INTENT` 警告并安全降级;如果忽略警告,可能选错图表。
|
|
42
|
+
|
|
43
|
+
如果用户输入的是自然语言,先映射为注册词,再调用 `planChart()`,同时保留原始用户意图用于展示。
|
|
44
|
+
|
|
45
|
+
### 配置项放在 Spec 正确层级
|
|
46
|
+
|
|
47
|
+
`encoding` 只描述字段角色和 Series 语义,标题、格式和显示组件放在 Spec 顶层:
|
|
48
|
+
|
|
49
|
+
| 需求 | 正确位置 | 常见错误 |
|
|
50
|
+
| --- | --- | --- |
|
|
51
|
+
| 坐标轴标题 | `xAxis.title`、`yAxis.title` | `encoding.x.title`、`encoding.y.title` |
|
|
52
|
+
| 坐标轴格式 | `xAxis.format`、`yAxis.format` | `encoding.x.format`、`encoding.y.format` |
|
|
53
|
+
| 数据标签 | `labels.enabled`、`labels.format` | `encoding.labels` |
|
|
54
|
+
| 图例 | `legend.visible` | `encoding.legend` |
|
|
55
|
+
| 主题和配色 | `theme.mode`、`theme.preset`、`theme.palette` | 随意猜测 Series 颜色 |
|
|
56
|
+
|
|
57
|
+
`validateSpec()` 会把这些错误位置报告为结构化警告,应先修复再展示。单系列笛卡尔图默认会把字段名作为图例;不需要时使用 `legend: { visible: false }`。
|
|
58
|
+
|
|
59
|
+
### 当前坐标域限制
|
|
60
|
+
|
|
61
|
+
数值型笛卡尔图表默认使用易读的纵轴域:`yAxis.nice` 默认为 `true`,`yAxis.ticks` 默认为 `"auto"`。需要固定范围时使用 `yAxis.domain: [min, max]`,例如 `{ domain: [0, 2000], ticks: 5 }` 可稳定生成五个标签;使用 `yAxis.nice: false` 可保留原始数据边界。`yAxis.format` 只负责格式化刻度显示,`yAxis.right` 支持同样的配置。Agent 可通过 `chart.getState().axes` 或 `chart.explain().axes` 获取原始域、计算域、刻度、步长和策略进行自检。分类/时间横轴的范围仍由数据记录推导,因此 `xAxis.min/max` 和 `xAxis.domain` 不支持。图表专用域配置仍包括:`gauge.domain`、`heatmap.colorScale.domain`、`radar.indicators[].min/max`;项目图表的日期范围当前由数据记录自动计算。
|
|
62
|
+
|
|
63
|
+
图表通道是严格按类型定义的:笛卡尔图表使用 `x`/`y`,Pie/Funnel/Gauge 使用 `category`/`value`,Heatmap 使用 `x`/`y`/`color`,Radar 使用 `indicators[].field`。缺失字段或不支持的通道会校验失败;Gauge 必须声明 `domain`。新建图表可直接使用 `@taylorwong/ichartjs/recipes/minimal-specs` 中的最小目录。
|
|
64
|
+
|
|
65
|
+
Agent 自检使用 `chart.getState().health` 和 `chart.explain().health`,其中包含 `ready`、`degraded`、`empty`、警告数、隐藏标签数、限制值数和已渲染标记数。`locale` 默认 `en-US`,需要中文输出时设置 `locale: "zh-CN"`,输入日期仍使用 ISO-8601。
|
|
66
|
+
|
|
39
67
|
## 品牌署名(Branding)默认行为
|
|
40
68
|
|
|
41
69
|
- 默认 `branding: true`:在画面与所有导出产物(PNG/SVG/JSON)右下角同步出现 `Powered by iChart.js` 低对比度署名。
|
|
@@ -73,6 +101,8 @@ const headlessPng = await chart.exportAsync({ type: 'png' });
|
|
|
73
101
|
|
|
74
102
|
不要虚构字段、单位、日期、依赖关系、日历规则或预测置信度。Radar 使用混合单位时必须提供显式 domain;Heatmap 必须区分缺失值和零;高基数占比数据优先使用 Bar 而不是 Pie。
|
|
75
103
|
|
|
104
|
+
为了让 lineage 自检和联动更新稳定,建议每条输入记录提供稳定字符串 `id`。没有 `id` 时 Runtime 会使用 `record-0` 这类位置后备值,只适合本地展示,不应当视为持久业务身份。
|
|
105
|
+
|
|
76
106
|
## 完整示例
|
|
77
107
|
|
|
78
108
|
```bash
|
|
@@ -16,6 +16,8 @@ Iteration 8 通过 `getChartCapability(type)` 提供逐图表能力档案,包
|
|
|
16
16
|
|
|
17
17
|
`planChart(data, { intent, renderer })` 返回版本化规划结果:主选图表、备选项、置信度、原因、缺失字段、建议编码、假设、警告、不支持请求和安全下一步。规划不会虚构业务含义、单位、日期或缺失字段。
|
|
18
18
|
|
|
19
|
+
未知 intent 还会返回 `intentKnown`、`intentSuggestions` 和 `fallbackUsed`。Spec 的通道按图表类型约束:笛卡尔图表是 `x`/`y`,Pie/Funnel/Gauge 是 `category`/`value`,Heatmap 是 `x`/`y`/`color`,Radar 字段位于 `indicators`。`validateSpec()` 会拒绝不支持的通道和缺失字段。Gauge 必须显式声明 `domain`,超出范围时会报告 `VALUE_CLAMPED`。
|
|
20
|
+
|
|
19
21
|
`validateSpec()` 分开返回 `errors`、`warnings` 和 `normalizations`;诊断包含稳定代码、JSON 路径、期望值和修复建议。`chart.explain()` 返回编码、转换、交互、假设、警告、稳定记录血缘和无障碍摘要。
|
|
20
22
|
|
|
21
23
|
## 关键规则
|
|
@@ -25,6 +27,8 @@ Iteration 8 通过 `getChartCapability(type)` 提供逐图表能力档案,包
|
|
|
25
27
|
- 布局和数据语义不依赖 Renderer。
|
|
26
28
|
- 通用图表使用 `data.values`;Flow/Swimlane 使用 `nodes/edges/lanes`。
|
|
27
29
|
- `svg` 适合 DOM 交互和可访问性;`canvas` 适合大量图元和绘制性能。
|
|
30
|
+
- 数值纵轴默认使用易读域(`yAxis.nice: true`、`yAxis.ticks: "auto"`);使用 `yAxis.domain: [min, max]` 固定范围,或使用 `yAxis.nice: false` 保留原始边界。Agent 可通过 `chart.getState().axes` 或 `chart.explain().axes` 自检原始域、计算域、刻度、步长和策略。分类/时间横轴的 `min/max` 和 `domain` 不支持,范围由数据确定。
|
|
31
|
+
- `chart.getState().health` 和 `chart.explain().health` 提供 `ready`、`degraded` 或 `empty`,以及可渲染性、问题代码、警告数、隐藏标签数、限制值数和已渲染标记数。`locale` 默认 `en-US`,可设置 `zh-CN` 影响输出格式;输入日期应使用 ISO-8601 字符串。
|
|
28
32
|
|
|
29
33
|
## 品牌署名(Branding)
|
|
30
34
|
|
|
@@ -116,7 +116,7 @@ npx skills add wanghetommy/ichartjs --skill ichartjs --agent codex --global --ye
|
|
|
116
116
|
需要固定发布版本时,直接安装已发布的 Skill 目录:
|
|
117
117
|
|
|
118
118
|
```bash
|
|
119
|
-
npx skills add https://github.com/wanghetommy/ichartjs/tree/v2.0.
|
|
119
|
+
npx skills add https://github.com/wanghetommy/ichartjs/tree/v2.0.8/skills/ichartjs \
|
|
120
120
|
--agent codex --global --yes
|
|
121
121
|
```
|
|
122
122
|
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"agentQuickstart": "docs/agent/quickstart.md",
|
|
9
9
|
"usageScenarios": "docs/agent/usage-scenarios.md",
|
|
10
10
|
"agentWorkflowExample": "examples/agent-workflow.mjs",
|
|
11
|
+
"minimalSpecs": "@taylorwong/ichartjs/recipes/minimal-specs",
|
|
11
12
|
"officialSkill": "skills/ichartjs/SKILL.md",
|
|
12
13
|
"scenarios": {
|
|
13
14
|
"charting": ["line", "area", "bar", "column", "pie", "scatter", "funnel", "gauge", "heatmap", "radar"],
|
|
@@ -17,6 +18,17 @@
|
|
|
17
18
|
"projectIntelligence": ["variance", "capacity", "release", "risk", "aging"],
|
|
18
19
|
"chartModes": ["stacked", "percent-stacked", "donut", "mixed-line-column", "bin", "architecture-layers", "architecture-boundaries", "mindmap-tree", "mindmap-radial", "mindmap-curved-edges"],
|
|
19
20
|
"renderers": ["canvas", "svg"],
|
|
21
|
+
"locale": {
|
|
22
|
+
"default": "en-US",
|
|
23
|
+
"recommended": ["en-US", "zh-CN"],
|
|
24
|
+
"appliesTo": ["axis", "labels", "tooltip", "export"],
|
|
25
|
+
"inputDates": "ISO-8601 strings; natural-language date parsing is not supported."
|
|
26
|
+
},
|
|
27
|
+
"health": {
|
|
28
|
+
"state": ["ready", "degraded", "empty"],
|
|
29
|
+
"apis": ["chart.getState().health", "chart.explain().health"],
|
|
30
|
+
"metrics": ["warnings", "suppressedLabels", "clampedValues", "renderedMarks"]
|
|
31
|
+
},
|
|
20
32
|
"interactionDefaults": { "zoom": false, "pan": false, "brush": false, "drag": false, "edgeDrag": false, "portConnect": false, "editing": false },
|
|
21
33
|
"diagramEditing": {
|
|
22
34
|
"renderers": ["canvas", "svg"],
|
|
@@ -57,6 +57,7 @@ export function runAgentWorkflow(rows, options = {}) {
|
|
|
57
57
|
try {
|
|
58
58
|
const explanation = chart.explain();
|
|
59
59
|
const state = chart.getState();
|
|
60
|
+
const expectedRecordIds = rows.map((row, index) => String(row.id ?? row.key ?? `record-${index}`));
|
|
60
61
|
return {
|
|
61
62
|
ok: true,
|
|
62
63
|
stage: 'complete',
|
|
@@ -69,7 +70,7 @@ export function runAgentWorkflow(rows, options = {}) {
|
|
|
69
70
|
state,
|
|
70
71
|
selfCheck: {
|
|
71
72
|
chartDeclared: capabilities.chartTypes.includes(plan.primary),
|
|
72
|
-
recordIdsPreserved:
|
|
73
|
+
recordIdsPreserved: expectedRecordIds.every(recordId => explanation.lineage.recordIds.includes(recordId)),
|
|
73
74
|
warningsVisible: plan.warnings.every(warning => state.warnings.some(item => item.code === warning.code)),
|
|
74
75
|
styleExplained: explanation.style?.preset === plan.styleRecommendation.preset
|
|
75
76
|
},
|
package/package.json
CHANGED
package/skills/ichartjs/SKILL.md
CHANGED
|
@@ -21,7 +21,7 @@ Recommended installation:
|
|
|
21
21
|
npx skills add wanghetommy/ichartjs --skill ichartjs
|
|
22
22
|
```
|
|
23
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.
|
|
24
|
+
Use `--agent codex --global --yes` for global non-interactive Codex installation. Use the tagged directory `https://github.com/wanghetommy/ichartjs/tree/v2.0.8/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
25
|
|
|
26
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:
|
|
27
27
|
|
|
@@ -36,15 +36,18 @@ Do not install the unscoped npm registry package named `ichartjs`; it is current
|
|
|
36
36
|
1. Locate the package or repository root. Read `docs/agent/quickstart.md` when available.
|
|
37
37
|
2. Call `getCapabilities()` before selecting a chart or interaction.
|
|
38
38
|
3. Call `inspectData()` and preserve stable record IDs.
|
|
39
|
-
4.
|
|
40
|
-
5.
|
|
41
|
-
6.
|
|
42
|
-
7.
|
|
43
|
-
8.
|
|
44
|
-
9.
|
|
45
|
-
10.
|
|
46
|
-
11.
|
|
47
|
-
12.
|
|
39
|
+
4. Discover `getCapabilities().intents`; map natural-language requests to an exact registered token before calling `planChart(data, { intent, renderer, context })`.
|
|
40
|
+
5. Inspect the complete planning result, including `styleRecommendation`, warnings, and fallback status.
|
|
41
|
+
6. Stop when `requiredFields` is non-empty; request data or explain a supported alternative.
|
|
42
|
+
7. Build a JSON-serializable Spec using `suggestedEncodings`, the selected capability, and an applicable recipe.
|
|
43
|
+
8. Use chart-specific channels: Cartesian `x`/`y`, Pie/Funnel/Gauge `category`/`value`, Heatmap `x`/`y`/`color`, and Radar `indicators[].field`. Start from `@taylorwong/ichartjs/recipes/minimal-specs` when a type is unfamiliar.
|
|
44
|
+
9. Keep titles/formats under `xAxis`/`yAxis`, labels under `labels`, and legend under `legend`; do not place them inside `encoding`.
|
|
45
|
+
10. Call `validateSpec()` before rendering. Repair `UNSUPPORTED_ENCODING_CHANNEL`, `MISSING_ENCODING_FIELD`, `MISSING_GAUGE_DOMAIN`, `UNKNOWN_INTENT`, misplaced-option, and unsupported-axis warnings before presenting the chart.
|
|
46
|
+
11. Call `createChart()` only after validation succeeds. Gauge Specs must declare a meaningful `domain`.
|
|
47
|
+
12. Self-check with `chart.explain()`, `chart.getState()`, `health.renderable`, and JSON export. Treat `VALUE_CLAMPED`, `LABELS_SUPPRESSED`, and `ZERO_TOTAL` as material diagnostics to report.
|
|
48
|
+
12. Provide an exact preview URL or artifact path and report assumptions, warnings, and deferred checks.
|
|
49
|
+
13. Prefer `theme: { mode: 'auto', preset, palette }`; preserve explicit user style choices and use `chart.setTheme()` for live switching.
|
|
50
|
+
14. 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`.
|
|
48
51
|
|
|
49
52
|
Use `@taylorwong/ichartjs` for package imports. Use `examples/agent-workflow.mjs` as the executable baseline when working in the repository.
|
|
50
53
|
|
|
@@ -69,9 +72,15 @@ Route by requested output:
|
|
|
69
72
|
|
|
70
73
|
- Never invent fields, units, dates, dependencies, calendar rules, domains, or forecast confidence.
|
|
71
74
|
- Never silently drop validation errors, warnings, assumptions, normalizations, or unsupported requests.
|
|
75
|
+
- Treat `getCapabilities().intents` as an allowlist; never pass a natural-language sentence as `planChart().intent`.
|
|
76
|
+
- If planning returns `fallbackUsed: true`, use `intentSuggestions` to remap or ask for confirmation; never silently accept the fallback chart.
|
|
77
|
+
- Keep axis titles/formats under `xAxis`/`yAxis`, labels under `labels`, and legend settings under `legend`.
|
|
78
|
+
- Repair `UNKNOWN_INTENT`, misplaced-option, and unsupported-axis warnings before presenting a chart. For numeric y-axes, prefer the default readable domain; use `yAxis.domain: [min, max]` for an explicit range, `yAxis.nice: false` for raw boundaries, and `yAxis.ticks` for a stable label count.
|
|
79
|
+
- Add stable string `id` values to tabular rows when lineage or linked updates are part of the deliverable.
|
|
72
80
|
- Avoid Pie for high-cardinality categories; prefer Bar for comparison.
|
|
73
81
|
- Require explicit Radar domains when units differ.
|
|
74
82
|
- Distinguish missing Heatmap values from zero.
|
|
83
|
+
- Set `locale` explicitly when output needs localization; the default is `en-US`, and input dates must be ISO-8601 strings.
|
|
75
84
|
- Use categorical, sequential, diverging, or status palettes by data semantics; do not invent arbitrary color sets or rely on color alone.
|
|
76
85
|
- Surface theme contrast diagnostics and high-cardinality color warnings.
|
|
77
86
|
- Do not generate Map or 3D Specs unless capabilities explicitly add them.
|
|
@@ -11,11 +11,11 @@ getCapabilities → inspectData → planChart → build Spec → validateSpec
|
|
|
11
11
|
- `getCapabilities()`: global and per-chart discoverability.
|
|
12
12
|
- `getChartCapability(type)`: required roles, interactions, renderers, features, exports, and limits.
|
|
13
13
|
- `inspectData(input)`: field roles, identifiers, cardinality, missingness, temporal coverage, and warnings.
|
|
14
|
-
- `planChart(input, options)`: primary type, alternatives, confidence, reasons, required fields, suggested encodings, assumptions, warnings, unsupported requests, and
|
|
14
|
+
- `planChart(input, options)`: primary type, alternatives, confidence, reasons, required fields, suggested encodings, assumptions, warnings, unsupported requests, next actions, and `intentKnown`/`intentSuggestions`/`fallbackUsed` metadata.
|
|
15
15
|
- `validateSpec(spec)`: errors, warnings, normalizations, and normalized Spec.
|
|
16
16
|
- `createChart(spec)`: headless or mounted chart lifecycle.
|
|
17
17
|
- `chart.explain()`: semantics, encodings, interactions, lineage, warnings, and accessibility summary.
|
|
18
|
-
- `chart.getState()`: renderer, dimensions, warnings, assumptions, selection, view, project analytics,
|
|
18
|
+
- `chart.getState()`: renderer, dimensions, warnings, assumptions, selection, view, project analytics, linked state, and `health`.
|
|
19
19
|
- `chart.export({ type: 'json' })`: JSON-safe Spec and state.
|
|
20
20
|
|
|
21
21
|
## Stop conditions
|
|
@@ -24,6 +24,8 @@ Do not render when:
|
|
|
24
24
|
|
|
25
25
|
- `plan.requiredFields` is non-empty;
|
|
26
26
|
- `validateSpec().valid` is false;
|
|
27
|
+
- chart-specific encoding channels are unsupported or reference missing fields;
|
|
28
|
+
- a Gauge has no explicit `domain`;
|
|
27
29
|
- the requested chart, renderer, interaction, or export is not declared;
|
|
28
30
|
- required project dates, diagram endpoints, ports, lanes, or business schema rules are invalid.
|
|
29
31
|
|
|
@@ -35,5 +37,6 @@ Verify that:
|
|
|
35
37
|
- validation passed without ignored errors;
|
|
36
38
|
- explanation lineage preserves stable source IDs;
|
|
37
39
|
- warnings and assumptions are visible in the response;
|
|
40
|
+
- `health.renderable` is true and `health.status` is reported; `VALUE_CLAMPED`, `LABELS_SUPPRESSED`, and `ZERO_TOTAL` are not hidden;
|
|
38
41
|
- the preview uses a maintained URL;
|
|
39
42
|
- replaced charts are destroyed.
|
|
@@ -20,3 +20,5 @@
|
|
|
20
20
|
| Responsibility | Swimlane | Require valid lane membership. |
|
|
21
21
|
|
|
22
22
|
Always prefer `planChart()` over this table when runtime capability output is available. Treat alternatives as tradeoffs, not automatic fallbacks.
|
|
23
|
+
|
|
24
|
+
`planChart()` accepts the exact registered intent token, not the user's full sentence. For example, map “show the sales trend over time” to `trend`; if an unknown token is passed, inspect and surface the returned `UNKNOWN_INTENT` warning instead of accepting the fallback silently.
|
package/src/capabilities.mjs
CHANGED
|
@@ -47,12 +47,27 @@ const intentMap = {
|
|
|
47
47
|
schedule: 'gantt', variance: 'gantt', timeline: 'timeline', milestone: 'milestone', progress: 'burndown', release: 'burndown', capacity: 'column', risk: 'scatter', aging: 'bar', matrix: 'heatmap', 'correlation-grid': 'heatmap', multidimensional: 'radar', profile: 'radar', workflow: 'flow', responsibility: 'swimlane', architecture: 'architecture', 'business-architecture': 'architecture', 'data-architecture': 'architecture', 'technical-architecture': 'architecture', mindmap: 'mindmap', hierarchy: 'mindmap', brainstorm: 'mindmap', trend: 'line', 'time-series': 'line', 'part-to-whole': 'pie', distribution: 'column', relationship: 'scatter', correlation: 'scatter', funnel: 'funnel', conversion: 'funnel', 'single-value': 'gauge', ranking: 'bar', comparison: 'bar', composition: 'column'
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
+
const intentAliases = {
|
|
51
|
+
trend: ['trend', 'time-series'], time: ['trend', 'time-series'], compare: ['comparison', 'ranking'], rank: ['ranking', 'comparison'],
|
|
52
|
+
distribution: ['distribution', 'comparison'], relationship: ['relationship', 'correlation'], correlation: ['correlation', 'relationship'],
|
|
53
|
+
matrix: ['matrix', 'correlation-grid'], profile: ['multidimensional', 'profile'], architecture: ['architecture', 'business-architecture', 'data-architecture', 'technical-architecture'],
|
|
54
|
+
mind: ['mindmap', 'hierarchy', 'brainstorm']
|
|
55
|
+
};
|
|
56
|
+
|
|
50
57
|
const alternatives = {
|
|
51
58
|
heatmap: ['scatter', 'column'], radar: ['bar', 'line'], gantt: ['timeline', 'milestone'], timeline: ['milestone', 'gantt'], milestone: ['timeline', 'gantt'], burndown: ['line', 'gantt'], column: ['bar', 'line'], scatter: ['bar', 'line'], bar: ['column', 'line'], flow: ['swimlane', 'architecture'], swimlane: ['flow', 'architecture'], architecture: ['flow', 'mindmap'], mindmap: ['architecture', 'flow'], line: ['area', 'column'], pie: ['bar', 'column'], funnel: ['bar', 'column'], gauge: ['column', 'bar']
|
|
52
59
|
};
|
|
53
60
|
|
|
54
61
|
function warning(code, path, message, suggestion) { return { code, path, message, ...(suggestion ? { suggestion } : {}) }; }
|
|
55
62
|
|
|
63
|
+
function suggestIntents(requested) {
|
|
64
|
+
const value = String(requested || '').trim().toLowerCase();
|
|
65
|
+
if (!value) return ['comparison'];
|
|
66
|
+
if (intentAliases[value]) return intentAliases[value];
|
|
67
|
+
const tokens = value.split(/[^a-z0-9-]+/).filter(Boolean);
|
|
68
|
+
return [...new Set(tokens.flatMap(token => intentAliases[token] || (intentMap[token] ? [token] : [])))].slice(0, 5);
|
|
69
|
+
}
|
|
70
|
+
|
|
56
71
|
export function getChartCapability(type) {
|
|
57
72
|
return chartProfiles[type] ? JSON.parse(JSON.stringify(chartProfiles[type])) : null;
|
|
58
73
|
}
|
|
@@ -104,10 +119,12 @@ export function planChart(input, options = {}) {
|
|
|
104
119
|
const report = inspectData(input);
|
|
105
120
|
const requestedIntent = options.intent || 'comparison';
|
|
106
121
|
const warnings = [...report.warnings];
|
|
122
|
+
const intentKnown = Boolean(intentMap[requestedIntent]);
|
|
123
|
+
const intentSuggestions = intentKnown ? [] : suggestIntents(requestedIntent);
|
|
107
124
|
let primary = intentMap[requestedIntent];
|
|
108
125
|
if (!primary) {
|
|
109
126
|
primary = report.fields.some(field => field.type === 'temporal') ? 'line' : report.measures.length >= 2 && report.dimensions.length === 0 ? 'scatter' : 'bar';
|
|
110
|
-
warnings.push(warning('UNKNOWN_INTENT', 'intent', `Intent ${requestedIntent} is not registered.`, 'Use an intent returned by getCapabilities().intents.'));
|
|
127
|
+
warnings.push(warning('UNKNOWN_INTENT', 'intent', `Intent ${requestedIntent} is not registered.`, intentSuggestions.length ? `Use one of: ${intentSuggestions.join(', ')}.` : 'Use an intent returned by getCapabilities().intents.'));
|
|
111
128
|
}
|
|
112
129
|
const profile = chartProfiles[primary];
|
|
113
130
|
const requiredFields = [];
|
|
@@ -138,6 +155,9 @@ export function planChart(input, options = {}) {
|
|
|
138
155
|
return {
|
|
139
156
|
version: '1.0',
|
|
140
157
|
intent: requestedIntent,
|
|
158
|
+
intentKnown,
|
|
159
|
+
intentSuggestions,
|
|
160
|
+
fallbackUsed: !intentKnown,
|
|
141
161
|
primary,
|
|
142
162
|
alternatives: alternatives[primary] || ['column', 'line'],
|
|
143
163
|
confidence,
|
|
@@ -170,6 +190,7 @@ export function explainChart(spec, model = {}) {
|
|
|
170
190
|
interactions: Object.keys(spec.interaction || {}).filter(key => spec.interaction[key]),
|
|
171
191
|
assumptions: [...(model.data?.assumptions || []), ...(model.state?.projectAnalytics?.assumptions || [])],
|
|
172
192
|
warnings,
|
|
193
|
+
axes: model.state?.axes || null,
|
|
173
194
|
style: spec.theme && typeof spec.theme === 'object' ? { name: spec.theme.name, preset: spec.theme.preset, mode: spec.theme.mode, resolvedMode: spec.theme.resolvedMode, palette: spec.theme.palette, reasons: spec.theme.reasons || [], warnings: spec.theme.warnings || [] } : planStyle(spec),
|
|
174
195
|
lineage: { recordIds: (model.data?.rows || []).map((row, index) => String(row.id ?? row.key ?? `record-${index}`)), sourcePreserved: true },
|
|
175
196
|
accessibility: { enabled: Boolean(spec.accessibility?.enabled), summary: spec.accessibility?.description || spec.title?.text || `${spec.type} chart with ${model.data?.rows?.length || 0} data items.` }
|
|
@@ -208,6 +229,7 @@ export function getCapabilities() {
|
|
|
208
229
|
chartTypes,
|
|
209
230
|
charts: JSON.parse(JSON.stringify(chartProfiles)),
|
|
210
231
|
intents,
|
|
232
|
+
locale: { default: 'en-US', recommended: ['en-US', 'zh-CN'], appliesTo: ['axis', 'labels', 'tooltip', 'export'], inputDates: 'ISO-8601 strings; natural-language date parsing is not supported.' },
|
|
211
233
|
chartModes: { stack: ['stacked', 'percent'], pie: ['standard', 'donut'], composition: ['multi-series', 'mixed-line-column', 'dual-axis'], transforms: ['bin'], diagrams: ['process', 'architecture', 'mindmap'], mindmapLayouts: ['tree', 'radial'], mindmapEdges: ['curved', 'straight', 'orthogonal'] },
|
|
212
234
|
projectManagement: project,
|
|
213
235
|
projectIntelligence: {
|
package/src/charts.mjs
CHANGED
|
@@ -12,6 +12,58 @@ import { axisLabelLayout, boxesOverlap, estimateTextWidth, fontSize, titleLayout
|
|
|
12
12
|
|
|
13
13
|
const pick = (row, encoding, fallback) => row?.[encoding?.field || fallback];
|
|
14
14
|
const font = (spec, role, fallback = '12px system-ui') => spec.theme?.typography?.[role]?.font || fallback;
|
|
15
|
+
const niceMantissas = [1, 1.2, 1.5, 1.8, 2, 2.5, 3, 4, 5, 6, 7.5, 8, 10];
|
|
16
|
+
|
|
17
|
+
function nearestNiceMantissa(value) {
|
|
18
|
+
return niceMantissas.reduce((best, candidate) => Math.abs(Math.log(value / candidate)) < Math.abs(Math.log(value / best)) ? candidate : best, niceMantissas[0]);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function niceCeil(value) {
|
|
22
|
+
const positive = Math.abs(Number(value));
|
|
23
|
+
if (!(positive > 0) || !Number.isFinite(positive)) return 1;
|
|
24
|
+
const power = 10 ** Math.floor(Math.log10(positive));
|
|
25
|
+
const normalized = positive / power;
|
|
26
|
+
const mantissa = niceMantissas.find(candidate => candidate >= normalized) || 10;
|
|
27
|
+
return mantissa * power;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function validDomain(domain) {
|
|
31
|
+
return Array.isArray(domain) && domain.length === 2 && domain.every(value => Number.isFinite(Number(value))) && Number(domain[1]) > Number(domain[0]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function resolveAxisDomain(rawMin, rawMax, axis = {}, type = 'linear') {
|
|
35
|
+
if (validDomain(axis.domain)) return [Number(axis.domain[0]), Number(axis.domain[1])];
|
|
36
|
+
if (type === 'log') return [rawMin, rawMax];
|
|
37
|
+
let min = rawMin, max = rawMax;
|
|
38
|
+
if (axis.nice !== false) {
|
|
39
|
+
min = min < 0 ? -niceCeil(-min) : 0;
|
|
40
|
+
max = niceCeil(max);
|
|
41
|
+
}
|
|
42
|
+
if (!(max > min)) max = min + (niceCeil(Math.abs(min) || 1) || 1);
|
|
43
|
+
return [min, max];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function axisTicks(min, max, axis = {}, type = 'linear') {
|
|
47
|
+
if (!(max > min)) return [min, max];
|
|
48
|
+
if (type === 'log') {
|
|
49
|
+
const start = Math.ceil(Math.log10(Math.max(min, 0.000001)));
|
|
50
|
+
const end = Math.floor(Math.log10(Math.max(max, 0.000001)));
|
|
51
|
+
const powers = Array.from({ length: Math.max(0, end - start + 1) }, (_, index) => 10 ** (start + index));
|
|
52
|
+
return [...new Set([min, ...powers.filter(value => value > min && value < max), max])];
|
|
53
|
+
}
|
|
54
|
+
const explicitCount = Number.isInteger(axis.ticks) && axis.ticks >= 2 ? axis.ticks : null;
|
|
55
|
+
const targetIntervals = explicitCount ? explicitCount - 1 : 5;
|
|
56
|
+
let intervals = explicitCount ? targetIntervals : 4, bestScore = Infinity;
|
|
57
|
+
if (!explicitCount) {
|
|
58
|
+
for (let candidate = 4; candidate <= 7; candidate += 1) {
|
|
59
|
+
const step = (max - min) / candidate;
|
|
60
|
+
const power = 10 ** Math.floor(Math.log10(Math.abs(step)));
|
|
61
|
+
const score = Math.abs(Math.log((step / power) / nearestNiceMantissa(step / power))) + Math.abs(candidate - targetIntervals) * 0.08;
|
|
62
|
+
if (score < bestScore) { bestScore = score; intervals = candidate; }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return Array.from({ length: intervals + 1 }, (_, index) => min + (max - min) * index / intervals);
|
|
66
|
+
}
|
|
15
67
|
|
|
16
68
|
function chromeLayout(spec, entries = []) {
|
|
17
69
|
const title = titleLayout(spec);
|
|
@@ -62,7 +114,7 @@ function layout(spec, data, legendEntries = []) {
|
|
|
62
114
|
const plotLeft = ['bar', 'heatmap'].includes(spec.type) ? Math.min(width * 0.35, Math.max(p.left, sideLabelLength * fontSize(spec, 'axis', 12) * 0.62 + 24)) : p.left;
|
|
63
115
|
const plotTop = Math.max(p.top, chrome.bottom + (chrome.bottom ? 12 : 0));
|
|
64
116
|
const plotWidth = Math.max(1, width - plotLeft - p.right);
|
|
65
|
-
const axisLabelTexts = temporal ? timeTicks(xMin, xMax, 5).map(value => formatTime(value, xMax - xMin)) : quantitativeX ? timeTicks(xMin, xMax, 5).map(value => formatValue(value, spec.xAxis?.format)) : categories;
|
|
117
|
+
const axisLabelTexts = temporal ? timeTicks(xMin, xMax, 5).map(value => formatTime(value, xMax - xMin, spec.xAxis?.format, spec.locale)) : quantitativeX ? timeTicks(xMin, xMax, 5).map(value => formatValue(value, spec.xAxis?.format, spec.locale)) : categories;
|
|
66
118
|
const xLabels = axisLabelLayout(spec, spec.type === 'bar' ? [] : axisLabelTexts, plotWidth);
|
|
67
119
|
const bottomReserve = Math.max(p.bottom, Math.ceil(16 + xLabels.projectedHeight + (spec.xAxis?.title ? 20 : 0)));
|
|
68
120
|
const plot = { x: plotLeft, y: plotTop, width: plotWidth, height: Math.max(1, height - plotTop - bottomReserve) };
|
|
@@ -75,19 +127,23 @@ function layout(spec, data, legendEntries = []) {
|
|
|
75
127
|
return [values.filter(value => value >= 0).reduce((sum, value) => sum + value, 0), values.filter(value => value < 0).reduce((sum, value) => sum + value, 0)];
|
|
76
128
|
}) : [];
|
|
77
129
|
const numbers = (stackTotals.length ? stackTotals : data.rows.flatMap(row => yEncodings.map(encoding => Number(row[encoding.field])))).filter(Number.isFinite);
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
const x = category => temporal ? plot.x + ((new Date(category).getTime() - xMin) / (xMax - xMin || 1)) * plot.width : quantitativeX ? plot.x + ((Number(category) - xMin) / (xMax - xMin || 1)) * plot.width : plot.x + (categories.indexOf(String(category)) + 0.5) * plot.width / Math.max(1, categories.length);
|
|
130
|
+
const rawMin = Math.min(0, ...(numbers.length ? numbers : [0]));
|
|
131
|
+
const rawMax = Math.max(1, ...(numbers.length ? numbers : [1]));
|
|
81
132
|
const axisType = spec.yAxis?.type || yEncodings[0]?.type || 'linear';
|
|
133
|
+
const [min, max] = resolveAxisDomain(rawMin, rawMax, spec.yAxis || {}, axisType);
|
|
134
|
+
const x = category => temporal ? plot.x + ((new Date(category).getTime() - xMin) / (xMax - xMin || 1)) * plot.width : quantitativeX ? plot.x + ((Number(category) - xMin) / (xMax - xMin || 1)) * plot.width : plot.x + (categories.indexOf(String(category)) + 0.5) * plot.width / Math.max(1, categories.length);
|
|
82
135
|
const transform = value => axisType === 'log' ? Math.log10(Math.max(0.000001, Number(value))) : Number(value);
|
|
83
136
|
const transformedMin = transform(min), transformedMax = transform(max);
|
|
84
137
|
const y = value => plot.y + plot.height - ((transform(value) - transformedMin) / (transformedMax - transformedMin || 1)) * plot.height;
|
|
85
138
|
const yRightNumbers = data.rows.map(row => Number(row[yEncodings[1]?.field])).filter(Number.isFinite);
|
|
86
|
-
const
|
|
139
|
+
const rawRightMin = Math.min(0, ...(yRightNumbers.length ? yRightNumbers : [0])), rawRightMax = Math.max(1, ...(yRightNumbers.length ? yRightNumbers : [1]));
|
|
87
140
|
const rightType = spec.yAxis?.right?.type || yEncodings[1]?.type || 'linear';
|
|
141
|
+
const [rightMin, rightMax] = resolveAxisDomain(rawRightMin, rawRightMax, spec.yAxis?.right || {}, rightType);
|
|
88
142
|
const rightTransform = value => rightType === 'log' ? Math.log10(Math.max(0.000001, Number(value))) : Number(value);
|
|
89
143
|
const yRight = value => plot.y + plot.height - ((rightTransform(value) - rightTransform(rightMin)) / (rightTransform(rightMax) - rightTransform(rightMin) || 1)) * plot.height;
|
|
90
|
-
|
|
144
|
+
const yTicks = axisTicks(min, max, spec.yAxis || {}, axisType), rightTicks = axisTicks(rightMin, rightMax, spec.yAxis?.right || {}, rightType);
|
|
145
|
+
const axisInfo = (rawDomain, domain, ticks, axis) => ({ rawDomain, domain, ticks, step: ticks.length > 1 ? (ticks[1] - ticks[0]) : null, policy: validDomain(axis?.domain) ? 'explicit' : axis?.nice === false ? 'raw' : 'nice' });
|
|
146
|
+
return { plot, chrome, xLabels, compact: width < 360 || height < 240, recommendedSize: { minWidth: 280, minHeight: 220 }, xField, xEncoding, temporal, quantitativeX, xMin, xMax, yField, yEncodings, categories, rawMin, rawMax, min, max, rawRightMin, rawRightMax, rightMin, rightMax, yTicks, rightTicks, axes: { y: axisInfo([rawMin, rawMax], [min, max], yTicks, spec.yAxis || {}), right: axisInfo([rawRightMin, rawRightMax], [rightMin, rightMax], rightTicks, spec.yAxis?.right || {}) }, x, y, yRight, axisType, rightType };
|
|
91
147
|
}
|
|
92
148
|
|
|
93
149
|
function addText(scene, id, text, x, y, style = {}, dataRef = null) { scene.add(new SceneNode({ id, type: 'text', geometry: { text: String(text), x, y }, style, dataRef })); }
|
|
@@ -97,21 +153,21 @@ function addAxes(scene, spec, state) {
|
|
|
97
153
|
scene.add(new SceneNode({ id: 'axis-x', type: 'line', geometry: { x1: plot.x, y1: plot.y + plot.height, x2: plot.x + plot.width, y2: plot.y + plot.height }, style: { stroke: spec.theme.axis } }));
|
|
98
154
|
scene.add(new SceneNode({ id: 'axis-y', type: 'line', geometry: { x1: plot.x, y1: plot.y, x2: plot.x, y2: plot.y + plot.height }, style: { stroke: spec.theme.axis } }));
|
|
99
155
|
if (spec.type === 'bar') {
|
|
100
|
-
const ticks =
|
|
156
|
+
const ticks = state.yTicks, numericX = value => plot.x + ((value - min) / (max - min || 1)) * plot.width;
|
|
101
157
|
if (spec.grid?.visible !== false) ticks.forEach((value, index) => scene.add(new SceneNode({ id: `grid-x-${index}`, type: 'line', geometry: { x1: numericX(value), y1: plot.y, x2: numericX(value), y2: plot.y + plot.height }, style: { stroke: spec.grid?.color || spec.theme.grid, strokeWidth: spec.theme.marks.gridWidth }, zIndex: -2 })));
|
|
102
|
-
ticks.forEach(value => addText(scene, `label-x-value-${value}`, formatValue(value, spec.xAxis?.format || spec.yAxis?.format), numericX(value), plot.y + plot.height + 22, { fill: spec.theme.muted, font: font(spec, 'axis'), textAnchor: 'middle' }));
|
|
158
|
+
ticks.forEach(value => addText(scene, `label-x-value-${value}`, formatValue(value, spec.xAxis?.format || spec.yAxis?.format, spec.locale), numericX(value), plot.y + plot.height + 22, { fill: spec.theme.muted, font: font(spec, 'axis'), textAnchor: 'middle' }));
|
|
103
159
|
categories.forEach((category, index) => addText(scene, `label-y-category-${index}`, category, plot.x - 10, plot.y + (index + .5) * plot.height / Math.max(1, categories.length) + 4, { fill: spec.theme.muted, font: font(spec, 'axis'), textAnchor: 'end' }));
|
|
104
160
|
return;
|
|
105
161
|
}
|
|
106
162
|
const labels = state.temporal || state.quantitativeX ? timeTicks(state.xMin, state.xMax, 5) : categories;
|
|
107
|
-
const yTicks =
|
|
163
|
+
const yTicks = state.yTicks;
|
|
108
164
|
if (spec.grid?.visible !== false) yTicks.forEach((value, index) => scene.add(new SceneNode({ id: `grid-y-${index}`, type: 'line', geometry: { x1: plot.x, y1: y(value), x2: plot.x + plot.width, y2: y(value) }, style: { stroke: spec.grid?.color || spec.theme.grid, strokeWidth: spec.theme.marks.gridWidth }, zIndex: -2 })));
|
|
109
165
|
labels.forEach((category, index) => {
|
|
110
166
|
if (index % state.xLabels.step !== 0 && index !== labels.length - 1) return;
|
|
111
|
-
addText(scene, `label-x-${category}`, state.temporal ? formatTime(category, state.xMax - state.xMin) : state.quantitativeX ? formatValue(category, spec.xAxis?.format) : category, x(category), plot.y + plot.height + (state.xLabels.rotation ? 10 : 22), { fill: spec.theme.muted, font: font(spec, 'axis'), textAnchor: state.xLabels.rotation ? 'end' : 'middle', textBaseline: state.xLabels.rotation ? 'middle' : 'alphabetic', rotation: state.xLabels.rotation });
|
|
167
|
+
addText(scene, `label-x-${category}`, state.temporal ? formatTime(category, state.xMax - state.xMin, spec.xAxis?.format, spec.locale) : state.quantitativeX ? formatValue(category, spec.xAxis?.format, spec.locale) : category, x(category), plot.y + plot.height + (state.xLabels.rotation ? 10 : 22), { fill: spec.theme.muted, font: font(spec, 'axis'), textAnchor: state.xLabels.rotation ? 'end' : 'middle', textBaseline: state.xLabels.rotation ? 'middle' : 'alphabetic', rotation: state.xLabels.rotation });
|
|
112
168
|
});
|
|
113
|
-
yTicks.forEach(value => addText(scene, `label-y-${value}`, formatValue(value, spec.yAxis?.format), plot.x - 10, y(value) + 4, { fill: spec.theme.muted, font: font(spec, 'axis'), textAnchor: 'end' }));
|
|
114
|
-
if (state.yEncodings.length > 1 && !spec.stack)
|
|
169
|
+
yTicks.forEach(value => addText(scene, `label-y-${value}`, formatValue(value, spec.yAxis?.format, spec.locale), plot.x - 10, y(value) + 4, { fill: spec.theme.muted, font: font(spec, 'axis'), textAnchor: 'end' }));
|
|
170
|
+
if (state.yEncodings.length > 1 && !spec.stack) state.rightTicks.forEach(value => addText(scene, `label-y-right-${value}`, formatValue(value, spec.yAxis?.right?.format, spec.locale), plot.x + plot.width + 10, yRight(value) + 4, { fill: spec.theme.muted, font: font(spec, 'axis') }));
|
|
115
171
|
if (spec.xAxis?.title) addText(scene, 'axis-x-title', spec.xAxis.title, plot.x + plot.width / 2, plot.y + plot.height + (state.xLabels.rotation ? state.xLabels.projectedHeight + 20 : 42), { fill: spec.theme.text, font: font(spec, 'axis'), textAnchor: 'middle' });
|
|
116
172
|
if (spec.yAxis?.title) addText(scene, 'axis-y-title', spec.yAxis.title, 8, plot.y + 12, { fill: spec.theme.text, font: font(spec, 'axis') });
|
|
117
173
|
}
|
|
@@ -134,9 +190,9 @@ function legendEntries(spec, data, series, colors) {
|
|
|
134
190
|
return [];
|
|
135
191
|
}
|
|
136
192
|
|
|
137
|
-
function addMarkLabel(scene, spec, id, value, x, y, dataRef, bounds = null, anchor = null) {
|
|
193
|
+
function addMarkLabel(scene, spec, id, value, x, y, dataRef, bounds = null, anchor = null, formatOverride = null) {
|
|
138
194
|
if (!spec.labels?.enabled) return;
|
|
139
|
-
const text = formatValue(value, spec.labels.format), size = fontSize(spec, 'label', 12), width = estimateTextWidth(text, size), height = size * 1.25;
|
|
195
|
+
const text = formatValue(value, formatOverride || spec.labels.format, spec.locale), size = fontSize(spec, 'label', 12), width = estimateTextWidth(text, size), height = size * 1.25;
|
|
140
196
|
const area = bounds || scene._labelArea || { x: 0, y: 0, width: spec.width, height: spec.height };
|
|
141
197
|
const safeX = Math.max(area.x + width / 2, Math.min(area.x + area.width - width / 2, x));
|
|
142
198
|
const safeY = Math.max(area.y + height / 2, Math.min(area.y + area.height - height / 2, y));
|
|
@@ -166,7 +222,10 @@ function addHeatmapScene(scene, spec, data, state, colors) {
|
|
|
166
222
|
xValues.forEach((value, index) => { if (index % state.xLabels.step !== 0 && index !== xValues.length - 1) return; addText(scene, `heatmap-x-${index}`, value, state.plot.x + (index + .5) * width, state.plot.y + state.plot.height + (state.xLabels.rotation ? 10 : 20), { fill: spec.theme.muted, font: font(spec, 'axis'), textAnchor: state.xLabels.rotation ? 'end' : 'middle', textBaseline: state.xLabels.rotation ? 'middle' : 'alphabetic', rotation: state.xLabels.rotation }); });
|
|
167
223
|
const yStep = Math.max(1, Math.ceil(fontSize(spec, 'axis', 12) * 1.35 / Math.max(1, height)));
|
|
168
224
|
yValues.forEach((value, index) => { if (index % yStep !== 0 && index !== yValues.length - 1) return; addText(scene, `heatmap-y-${index}`, value, state.plot.x - 10, state.plot.y + (index + .5) * height, { fill: spec.theme.muted, font: font(spec, 'axis'), textAnchor: 'end', textBaseline: 'middle', baseline: 'middle' }); });
|
|
169
|
-
|
|
225
|
+
let hiddenLabels = 0, visibleLabels = 0;
|
|
226
|
+
data.rows.forEach((row, index) => { const xIndex = xValues.indexOf(String(row[xField])), yIndex = yValues.indexOf(String(row[yField])), value = numericValue(row), ratio = Number.isFinite(value) ? (value - min) / (max - min || 1) : 0, geometry = { x: state.plot.x + xIndex * width + gap / 2, y: state.plot.y + yIndex * height + gap / 2, width: Math.max(1, width - gap), height: Math.max(1, height - gap) }; scene.add(new SceneNode({ id: `heatmap-cell-${index}`, type: 'rect', geometry, bounds: geometry, style: { fill: Number.isFinite(value) ? colorMix(low, high, ratio) : missing, stroke: spec.theme.background, strokeWidth: 1 }, dataRef: { dataIndex: index, x: row[xField], y: row[yField], value: Number.isFinite(value) ? value : null, recordId: row.id || `record-${index}` }, interactive: true })); if (spec.labels?.enabled && Number.isFinite(value)) { const label = formatValue(value, spec.labels.format, spec.locale), size = fontSize(spec, 'label', 12); if (geometry.width >= estimateTextWidth(label, size) + 8 && geometry.height >= size * 1.35) { addText(scene, `heatmap-cell-label-${index}`, label, geometry.x + geometry.width / 2, geometry.y + geometry.height / 2, { fill: spec.labels.color || (ratio > 0.58 ? '#ffffff' : spec.theme.text), font: spec.labels.font || font(spec, 'label'), textAnchor: 'middle', textBaseline: 'middle', baseline: 'middle' }); visibleLabels += 1; } else hiddenLabels += 1; } });
|
|
227
|
+
state.labelLayout = { ...(state.labelLayout || {}), heatmap: { hidden: hiddenLabels, visible: visibleLabels } };
|
|
228
|
+
if (hiddenLabels) data.warnings.push({ code: 'LABELS_SUPPRESSED', path: 'labels', count: hiddenLabels, message: `${hiddenLabels} heatmap labels were suppressed because cells are too small.`, suggestion: 'Increase chart width or height, reduce the matrix, or disable labels.' });
|
|
170
229
|
state.matrix = { xValues, yValues, min, max };
|
|
171
230
|
}
|
|
172
231
|
|
|
@@ -206,16 +265,16 @@ function addRadarScene(scene, spec, data, state, colors) {
|
|
|
206
265
|
}
|
|
207
266
|
|
|
208
267
|
function timeTicks(min, max, count) { return Array.from({ length: count }, (_, index) => min + (max - min) * index / Math.max(1, count - 1)); }
|
|
209
|
-
function formatTime(value, span) { const date = new Date(value); if (
|
|
268
|
+
function formatTime(value, span, format, locale = 'en-US') { const date = new Date(value); if (format) { const options = typeof format === 'string' ? { style: format } : format; return formatValue(value, options?.style || options?.options ? { style: options.style || 'date', ...options } : { style: 'date', options }, locale); } const days = span / 86400000, nowYear = date.getUTCFullYear(), startYear = new Date(value - span).getUTCFullYear(); if (days > 365 || (nowYear !== startYear && days > 45)) return String(nowYear); if (days > 60) return `${nowYear}-${String(date.getUTCMonth() + 1).padStart(2, '0')}`; return `${date.getUTCMonth() + 1}/${date.getUTCDate()}${nowYear !== startYear ? `/${nowYear}` : ''}`; }
|
|
210
269
|
|
|
211
270
|
function projectRows(spec, data) { return data.rows.length ? data.rows : (spec.tasks || spec.events || []); }
|
|
212
271
|
function projectDate(value, min, max, plot) { const time = new Date(value).getTime(); return plot.x + ((time - min) / (max - min || 1)) * plot.width; }
|
|
213
|
-
function addProjectAxes(scene, plot, min, max) { scene.add(new SceneNode({ id: 'project-axis', type: 'line', geometry: { x1: plot.x, y1: plot.y + plot.height, x2: plot.x + plot.width, y2: plot.y + plot.height }, style: { stroke: '#94a3b8' } })); timeTicks(min, max, 5).forEach(value => addText(scene, `project-label-${value}`, formatTime(value, max - min), projectDate(value, min, max, plot), plot.y + plot.height + 22, { fill: '#475569', font: '12px system-ui', textAnchor: 'middle' })); }
|
|
272
|
+
function addProjectAxes(scene, plot, min, max, spec = {}) { scene.add(new SceneNode({ id: 'project-axis', type: 'line', geometry: { x1: plot.x, y1: plot.y + plot.height, x2: plot.x + plot.width, y2: plot.y + plot.height }, style: { stroke: '#94a3b8' } })); timeTicks(min, max, 5).forEach(value => addText(scene, `project-label-${value}`, formatTime(value, max - min, spec.xAxis?.format, spec.locale), projectDate(value, min, max, plot), plot.y + plot.height + 22, { fill: '#475569', font: '12px system-ui', textAnchor: 'middle' })); }
|
|
214
273
|
function addProjectScene(scene, spec, data, state, colors) {
|
|
215
274
|
const { plot } = state;
|
|
216
275
|
if (['gantt', 'timeline', 'milestone'].includes(spec.type)) {
|
|
217
276
|
const rows = projectRows(spec, data), dates = rows.flatMap(row => [new Date(row.start || row.date || row.end).getTime(), new Date(row.end || row.date || row.start).getTime()]).filter(Number.isFinite), min = Math.min(...(dates.length ? dates : [Date.now()])), max = Math.max(...(dates.length ? dates : [min + 86400000]));
|
|
218
|
-
addProjectAxes(scene, plot, min, max);
|
|
277
|
+
addProjectAxes(scene, plot, min, max, spec);
|
|
219
278
|
const positions = new Map(); rows.forEach((row, index) => { const y = plot.y + (index + 0.5) * plot.height / Math.max(1, rows.length), start = projectDate(row.start || row.date || row.end, min, max, plot), end = projectDate(row.end || row.date || row.start, min, max, plot), x = Math.min(start, end), width = Math.max(8, Math.abs(end - start)); positions.set(row.id, { x, y, end, row }); const milestone = spec.type === 'milestone' || row.milestone || start === end; const geometry = milestone ? { cx: start, cy: y, r: 7 } : { x, y: y - 10, width, height: 20 }; scene.add(new SceneNode({ id: `project-item-${index}`, type: milestone ? 'circle' : 'rect', geometry, bounds: milestone ? { x: start - 10, y: y - 10, width: 20, height: 20 } : geometry, style: { fill: colors[index % colors.length], stroke: '#ffffff', strokeWidth: 1 }, dataRef: { dataIndex: index, taskId: row.id, field: 'project' }, interactive: true })); addText(scene, `project-label-${index}`, row.name || row.title || row.label || row.id || `Item ${index + 1}`, plot.x - 8, y + 4, { fill: '#334155', font: '12px system-ui', textAnchor: 'end' }); if (row.progress != null && !milestone) scene.add(new SceneNode({ id: `project-progress-${index}`, type: 'rect', geometry: { x, y: y - 10, width: width * Math.max(0, Math.min(1, Number(row.progress) > 1 ? Number(row.progress) / 100 : Number(row.progress))), height: 20 }, style: { fill: '#0f172a', opacity: 0.25 }, interactive: false })); });
|
|
220
279
|
const criticalPath = new Set(spec.criticalPath || []);
|
|
221
280
|
rows.forEach((row, index) => (row.dependencies || []).forEach((dependency, dependencyIndex) => { const from = positions.get(dependency), to = positions.get(row.id); if (!from || !to) return; const critical = criticalPath.has(row.id) && criticalPath.has(dependency); const angle = Math.atan2(to.y - from.y, to.x - from.end), arrow = 7; scene.add(new SceneNode({ id: `dependency-${index}-${dependencyIndex}`, type: 'line', geometry: { x1: from.end, y1: from.y, x2: to.x, y2: to.y }, style: { stroke: critical ? '#dc2626' : '#64748b', strokeWidth: critical ? 2.5 : 1.5, opacity: 0.85 }, dataRef: { from: dependency, to: row.id, critical }, interactive: false, zIndex: -1 })); scene.add(new SceneNode({ id: `dependency-arrow-${index}-${dependencyIndex}`, type: 'path', geometry: { points: [{ x: to.x, y: to.y }, { x: to.x - arrow * Math.cos(angle - Math.PI / 6), y: to.y - arrow * Math.sin(angle - Math.PI / 6) }, { x: to.x - arrow * Math.cos(angle + Math.PI / 6), y: to.y - arrow * Math.sin(angle + Math.PI / 6) }] }, style: { stroke: critical ? '#dc2626' : '#64748b', fill: critical ? '#dc2626' : '#64748b', strokeWidth: 1 }, interactive: false, zIndex: -1 })); }));
|
|
@@ -303,13 +362,15 @@ export function buildScene(spec) {
|
|
|
303
362
|
} else if (spec.type === 'pie') {
|
|
304
363
|
const categoryField = spec.encoding.category?.field || 'name', valueField = spec.encoding.value?.field || 'value', total = data.rows.reduce((sum, row) => sum + Math.max(0, Number(row[valueField]) || 0), 0), cx = state.plot.x + state.plot.width / 2, cy = state.plot.y + state.plot.height / 2, radius = Math.min(state.plot.width, state.plot.height) * 0.38, innerR = radius * Number(spec.innerRadius || 0); let angle = -Math.PI / 2;
|
|
305
364
|
if (!(total > 0)) { data.warnings.push({ code: 'ZERO_TOTAL', path: `encoding.value.${valueField}`, message: 'Pie requires a positive total.' }); addText(scene, 'pie-zero-total', spec.emptyText || 'No positive values', cx, cy, { fill: spec.theme.muted, font: font(spec, 'subtitle'), textAnchor: 'middle' }); }
|
|
306
|
-
else data.rows.forEach((row, index) => { const value = Math.max(0, Number(row[valueField]) || 0), end = angle + value / total * Math.PI * 2, middle = angle + (end - angle) / 2, dataRef = { seriesIndex: 0, dataIndex: index, category: row[categoryField], value }; scene.add(new SceneNode({ id: `series-0-item-${index}`, type: 'arc', geometry: { cx, cy, r: radius, innerR, start: angle, end }, bounds: { x: cx - radius, y: cy - radius, width: radius * 2, height: radius * 2 }, style: { fill: colors[index % colors.length], stroke: spec.theme.background, strokeWidth: 1 }, dataRef, interactive: true })); addMarkLabel(scene, spec, `series-0-item-${index}`, value / total, cx + Math.cos(middle) * radius * .72, cy + Math.sin(middle) * radius * .72, dataRef, state.plot); angle = end; });
|
|
365
|
+
else data.rows.forEach((row, index) => { const value = Math.max(0, Number(row[valueField]) || 0), end = angle + value / total * Math.PI * 2, middle = angle + (end - angle) / 2, dataRef = { seriesIndex: 0, dataIndex: index, category: row[categoryField], value }; scene.add(new SceneNode({ id: `series-0-item-${index}`, type: 'arc', geometry: { cx, cy, r: radius, innerR, start: angle, end }, bounds: { x: cx - radius, y: cy - radius, width: radius * 2, height: radius * 2 }, style: { fill: colors[index % colors.length], stroke: spec.theme.background, strokeWidth: 1 }, dataRef, interactive: true })); addMarkLabel(scene, spec, `series-0-item-${index}`, value / total, cx + Math.cos(middle) * radius * .72, cy + Math.sin(middle) * radius * .72, dataRef, state.plot, null, spec.labels?.format || { style: 'percent' }); angle = end; });
|
|
307
366
|
} else if (spec.type === 'funnel') {
|
|
308
367
|
const valueField = spec.encoding.value?.field || 'value'; const maxValue = Math.max(...data.rows.map(row => Number(row[valueField]) || 0), 1); const segmentHeight = state.plot.height / data.rows.length;
|
|
309
368
|
data.rows.forEach((row, index) => { const value = Number(row[valueField]) || 0, ratio = Math.max(0.1, value / maxValue); const width = state.plot.width * ratio; const geometry = { x: state.plot.x + (state.plot.width - width) / 2, y: state.plot.y + index * segmentHeight, width, height: Math.max(2, segmentHeight - 3) }, dataRef = { seriesIndex: 0, dataIndex: index, field: valueField }; scene.add(new SceneNode({ id: `series-0-item-${index}`, type: 'rect', geometry, bounds: geometry, style: { fill: colors[index % colors.length] }, dataRef, interactive: true })); addMarkLabel(scene, spec, `series-0-item-${index}`, value, spec.width / 2, geometry.y + geometry.height / 2 + 4, dataRef, state.plot); });
|
|
310
369
|
} else if (spec.type === 'gauge') {
|
|
311
|
-
const valueField = spec.encoding.value?.field || 'value', domain = spec.domain
|
|
312
|
-
|
|
370
|
+
const valueField = spec.encoding.value?.field || 'value', domain = Array.isArray(spec.domain) ? spec.domain : [0, 100], rawValue = Number(data.rows[0]?.[valueField]), numericValue = Number.isFinite(rawValue) ? rawValue : 0, value = Math.max(Number(domain[0]), Math.min(Number(domain[1]), numericValue)), ratio = (value - Number(domain[0])) / (Number(domain[1]) - Number(domain[0]) || 1), cx = state.plot.x + state.plot.width / 2, cy = state.plot.y + state.plot.height * 0.62, radius = Math.min(state.plot.width, state.plot.height) * 0.36, start = Math.PI, end = start + Math.PI * ratio;
|
|
371
|
+
if (!Array.isArray(spec.domain)) data.warnings.push({ code: 'MISSING_GAUGE_DOMAIN', path: 'domain', message: 'Gauge rendered with a compatibility fallback; provide an explicit domain to make the value meaningful.' });
|
|
372
|
+
if (numericValue < Number(domain[0]) || numericValue > Number(domain[1])) data.warnings.push({ code: 'VALUE_CLAMPED', path: `data.values[0].${valueField}`, count: 1, rawValue: numericValue, domain: [...domain], message: `Gauge value ${numericValue} is outside domain [${domain[0]}, ${domain[1]}] and was clamped for the arc.`, suggestion: 'Choose a domain that covers the value or review the source unit.' });
|
|
373
|
+
scene.add(new SceneNode({ id: 'gauge-background', type: 'arc', geometry: { cx, cy, r: radius, start: Math.PI, end: Math.PI * 2 }, style: { fill: spec.theme.grid } })); scene.add(new SceneNode({ id: 'gauge-value', type: 'arc', geometry: { cx, cy, r: radius, start, end }, style: { fill: colors[0] }, dataRef: { seriesIndex: 0, dataIndex: 0, value, rawValue: numericValue }, interactive: true })); addText(scene, 'gauge-label', formatValue(numericValue, spec.labels?.format || spec.encoding.value?.format || { maximumFractionDigits: 2 }, spec.locale), cx, cy - 12, { fill: spec.theme.text, font: font(spec, 'metric'), textAnchor: 'middle' });
|
|
313
374
|
}
|
|
314
375
|
addLegend(scene, spec, state.chrome.legend);
|
|
315
376
|
addBranding(scene, spec);
|
package/src/index.mjs
CHANGED
|
@@ -344,13 +344,21 @@ export class Chart {
|
|
|
344
344
|
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; }
|
|
345
345
|
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; }
|
|
346
346
|
getSpec() { return JSON.parse(JSON.stringify(this.spec)); }
|
|
347
|
-
|
|
347
|
+
_getDiagnostics() { return [...(this._specDiagnostics?.warnings || []), ...(this.model.data?.warnings || []), ...(this.spec.theme?.warnings || [])]; }
|
|
348
|
+
_getHealth(warnings) {
|
|
349
|
+
const isEmpty = this.model.data.rows.length === 0 && !['flow', 'swimlane', 'architecture', 'mindmap'].includes(this.spec.type) || warnings.some(item => item.code === 'ZERO_TOTAL');
|
|
350
|
+
const marks = [];
|
|
351
|
+
this.model.scene?.walk?.(node => { if (node.id !== 'root' && !['text', 'line'].includes(node.type)) marks.push(node); });
|
|
352
|
+
const suppressedLabels = warnings.reduce((sum, item) => sum + Number(item.count || 0), 0);
|
|
353
|
+
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: warnings.filter(item => item.code === 'VALUE_CLAMPED').length, renderedMarks: marks.length } };
|
|
354
|
+
}
|
|
355
|
+
getState() { const brandingSignature = discoverCapabilities().branding.signature, warnings = this._getDiagnostics(); 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 }), 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) }; }
|
|
348
356
|
getProjectAnalytics() { return clone(this.model.state?.projectAnalytics || null); }
|
|
349
357
|
getLinkedState() { return clone(this.model.state?.linked || null); }
|
|
350
358
|
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(); }
|
|
351
359
|
setLinkedSelection(selection = []) { this.spec.project = { ...(this.spec.project || {}), linked: { ...(this.spec.project?.linked || {}), selection: normalizeLinkedSelection(selection) } }; this.emit('linkedstatechange', { chart: this, linked: this.spec.project.linked }); return this.render(); }
|
|
352
360
|
describe() { return { type: this.spec.type, renderer: this.renderer.constructor.name, dimensions: [this.spec.encoding.x?.field || this.spec.encoding.category?.field], measures: (Array.isArray(this.spec.encoding.y) ? this.spec.encoding.y : [this.spec.encoding.y || this.spec.encoding.value]).filter(Boolean).map(encoding => encoding.field), dataCount: this.model.data.rows.length, theme: this.spec.theme?.name || 'custom', interactions: Object.keys(this.spec.interaction || {}).filter(key => this.spec.interaction[key]) }; }
|
|
353
|
-
explain() {
|
|
361
|
+
explain() { const explanation = explainChart(this.spec, this.model), state = this.getState(); return { ...explanation, warnings: state.warnings, health: state.health }; }
|
|
354
362
|
getAccessibleDescription() { const description = this.spec.accessibility?.description || this.spec.title?.text || `${this.spec.type} chart`; return `${description}; ${this.model.data.rows.length} data items.`; }
|
|
355
363
|
inspectDataSchema() { return inspectDataSchema(this.spec.data.schema || this.spec.schema); }
|
|
356
364
|
validateData() { return validateData(this.toDataTable(), this.spec.data.schema || this.spec.schema, this.spec.validationOptions); }
|
|
@@ -630,4 +638,4 @@ export { normalizeLinkedFilters, normalizeLinkedSelection, filterProjectRows, cr
|
|
|
630
638
|
|
|
631
639
|
export { contrastRatio, planStyle, resolveTheme, styleCapabilities, themeModes, themePalettes, themePresets, validateThemeContrast, annotationPlugin, dataZoomPlugin, dataLabelsPlugin, accessibilityPlugin };
|
|
632
640
|
export { applyPreferencesToSpec, createPreferencesStore, defaultPreferences, mergePreferences, mergeThemePreference, mountChartSettings, normalizePreferences, validatePreferences };
|
|
633
|
-
export const iChart = { version: '2.0.
|
|
641
|
+
export const iChart = { version: '2.0.8', createChart, inspectData, normalizeData, binData, applyTransforms, normalizeSpec, validateSpec, data, getCapabilities, getChartCapability, getPreferenceCapabilities, planChart, recommend, explainChart, contrastRatio, planStyle, resolveTheme, styleCapabilities, themeModes, themePalettes, themePresets, validateThemeContrast, createPreferencesStore, defaultPreferences, normalizePreferences, mergePreferences, validatePreferences, applyPreferencesToSpec, mountChartSettings, annotationPlugin, dataZoomPlugin, dataLabelsPlugin, accessibilityPlugin, getBusinessSchema, inspectDataSchema, validateData, getEditCapabilities, validateEdit, previewEdit, commitPreview, validateRecipe, normalizeProjectCalendar, applyWorkingCalendar, normalizeDependencies, analyzeSchedule, analyzeBurndownSeries, analyzeCapacity, buildCapacityView, buildCumulativeFlowSeries, buildVelocitySeries, buildReleaseForecast, buildRiskMatrixSeries, buildIssueAgingSeries, normalizeLinkedFilters, normalizeLinkedSelection, filterProjectRows, createLinkedProjectState, linkedRecordId };
|
package/src/spec.mjs
CHANGED
|
@@ -23,7 +23,8 @@ const defaults = {
|
|
|
23
23
|
labels: { enabled: false },
|
|
24
24
|
branding: { enabled: true },
|
|
25
25
|
interaction: { tooltip: true, hover: true, click: true, crosshair: false, zoom: false, pan: false, brush: false, drag: false, edgeDrag: false, portConnect: false, keyboard: true }
|
|
26
|
-
,responsive: [], accessibility: { enabled: false }, editing: { enabled: false, mode: 'command', requireConfirmation: true, allowDelete: false, allowStructuralChanges: false }, xAxis: {}, yAxis: {}
|
|
26
|
+
,responsive: [], accessibility: { enabled: false }, editing: { enabled: false, mode: 'command', requireConfirmation: true, allowDelete: false, allowStructuralChanges: false }, xAxis: {}, yAxis: { nice: true, ticks: 'auto' }
|
|
27
|
+
,locale: 'en-US'
|
|
27
28
|
};
|
|
28
29
|
|
|
29
30
|
function clone(value) {
|
|
@@ -59,6 +60,41 @@ function dependencyErrors(rows) {
|
|
|
59
60
|
return errors;
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
const encodingChannels = {
|
|
64
|
+
line: ['x', 'y'], area: ['x', 'y'], bar: ['x', 'y'], column: ['x', 'y'], scatter: ['x', 'y'],
|
|
65
|
+
pie: ['category', 'value'], funnel: ['category', 'value'], gauge: ['category', 'value'], heatmap: ['x', 'y', 'color'], radar: [],
|
|
66
|
+
gantt: [], timeline: [], milestone: [], burndown: [], flow: [], swimlane: [], architecture: [], mindmap: []
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const presentationEncodingKeys = new Set(['title', 'format', 'labels', 'legend']);
|
|
70
|
+
|
|
71
|
+
function finiteDomain(domain) {
|
|
72
|
+
return Array.isArray(domain) && domain.length === 2 && domain.every(value => Number.isFinite(Number(value))) && Number(domain[1]) > Number(domain[0]);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function validateEncodingContract(input, spec, errors) {
|
|
76
|
+
const rows = Array.isArray(spec.data?.values) ? spec.data.values : [], fields = new Set(rows.flatMap(row => Object.keys(row || {})));
|
|
77
|
+
if (!rows.length) return;
|
|
78
|
+
const rawEncoding = input.encoding && typeof input.encoding === 'object' ? input.encoding : {};
|
|
79
|
+
const allowed = new Set(encodingChannels[spec.type] || []);
|
|
80
|
+
Object.keys(rawEncoding).forEach(channel => {
|
|
81
|
+
if (presentationEncodingKeys.has(channel) || allowed.has(channel)) return;
|
|
82
|
+
errors.push({ code: 'UNSUPPORTED_ENCODING_CHANNEL', path: `encoding.${channel}`, message: `${spec.type} does not use encoding.${channel}.`, expected: [...allowed], suggestion: allowed.size ? `Use encoding.${[...allowed].join(' or encoding.')} for ${spec.type}.` : 'Remove field encodings from this chart type.' });
|
|
83
|
+
});
|
|
84
|
+
const check = (path, encoding) => {
|
|
85
|
+
const items = Array.isArray(encoding) ? encoding : [encoding];
|
|
86
|
+
items.forEach((item, index) => {
|
|
87
|
+
if (!item) return;
|
|
88
|
+
const field = item.field, itemPath = Array.isArray(encoding) ? `${path}.${index}.field` : `${path}.field`;
|
|
89
|
+
if (typeof field !== 'string' || !field) errors.push({ code: 'INVALID_ENCODING_FIELD', path: itemPath, message: 'Encoding fields must be non-empty strings.', suggestion: 'Use a field name that exists in data.values.' });
|
|
90
|
+
else if (!fields.has(field)) errors.push({ code: 'MISSING_ENCODING_FIELD', path: itemPath, message: `Field ${field} is not present in data.values.`, expected: [...fields], suggestion: 'Inspect the data schema and use an existing field.' });
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
if (['pie', 'funnel', 'gauge'].includes(spec.type) && (rawEncoding.x !== undefined || rawEncoding.y !== undefined)) return;
|
|
94
|
+
(encodingChannels[spec.type] || []).forEach(channel => check(`encoding.${channel}`, spec.encoding?.[channel]));
|
|
95
|
+
if (spec.type === 'radar') (spec.indicators || []).forEach((indicator, index) => check(`indicators.${index}`, indicator));
|
|
96
|
+
}
|
|
97
|
+
|
|
62
98
|
export function normalizeSpec(input = {}) {
|
|
63
99
|
const spec = merge(defaults, input);
|
|
64
100
|
if (input.branding === undefined && input.theme && typeof input.theme === 'object' && input.theme.branding !== undefined) spec.branding = clone(input.theme.branding);
|
|
@@ -84,7 +120,7 @@ export function normalizeSpec(input = {}) {
|
|
|
84
120
|
return spec;
|
|
85
121
|
}
|
|
86
122
|
|
|
87
|
-
export function validateSpec(input) {
|
|
123
|
+
export function validateSpec(input = {}) {
|
|
88
124
|
const spec = normalizeSpec(input);
|
|
89
125
|
const errors = [], warnings = [], normalizations = [];
|
|
90
126
|
if (!chartTypes.has(spec.type)) errors.push({ code: 'INVALID_TYPE', path: 'type', message: `Unsupported chart type: ${spec.type}`, suggestion: 'Use a type returned by getCapabilities().' });
|
|
@@ -97,10 +133,44 @@ export function validateSpec(input) {
|
|
|
97
133
|
if (spec.theme.branding != null && typeof spec.theme.branding !== 'boolean' && !(spec.theme.branding && typeof spec.theme.branding === 'object')) errors.push({ code: 'INVALID_BRANDING', path: 'theme.branding', message: 'theme.branding must be a boolean or a { enabled: boolean } object.', suggestion: 'Use theme: { branding: false } or theme: { branding: { enabled: false } }.' });
|
|
98
134
|
else if (spec.theme.branding && typeof spec.theme.branding === 'object' && (Object.keys(spec.theme.branding).some(key => key !== 'enabled') || typeof spec.theme.branding.enabled !== 'boolean')) errors.push({ code: 'INVALID_BRANDING', path: 'theme.branding', message: 'theme.branding objects only support a boolean enabled property.', suggestion: 'Use theme: { branding: { enabled: false } }.' });
|
|
99
135
|
}
|
|
136
|
+
if (typeof spec.locale !== 'string' || !spec.locale.trim()) errors.push({ code: 'INVALID_LOCALE', path: 'locale', message: 'locale must be a non-empty BCP 47 locale string.', suggestion: 'Use for example locale: "en-US" or locale: "zh-CN".' });
|
|
137
|
+
validateEncodingContract(input, spec, errors);
|
|
138
|
+
const axisEncodings = [['x', 'xAxis'], ['y', 'yAxis']];
|
|
139
|
+
axisEncodings.forEach(([encodingName, axisName]) => {
|
|
140
|
+
const encoding = spec.encoding?.[encodingName];
|
|
141
|
+
const encodingItems = Array.isArray(encoding) ? encoding : [encoding];
|
|
142
|
+
encodingItems.forEach((item, index) => {
|
|
143
|
+
if (!item || typeof item !== 'object') return;
|
|
144
|
+
const pathPrefix = Array.isArray(encoding) ? `encoding.${encodingName}.${index}` : `encoding.${encodingName}`;
|
|
145
|
+
if (item.title !== undefined) warnings.push({ code: 'MISPLACED_AXIS_TITLE', path: `${pathPrefix}.title`, message: `Axis titles belong on ${axisName}.title, not on an encoding.`, suggestion: `Move it to ${axisName}: { title: ... }.` });
|
|
146
|
+
if (item.format !== undefined) warnings.push({ code: 'MISPLACED_AXIS_FORMAT', path: `${pathPrefix}.format`, message: `Axis formats belong on ${axisName}.format, not on an encoding.`, suggestion: `Move it to ${axisName}: { format: ... }.` });
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
if (spec.encoding?.labels !== undefined) warnings.push({ code: 'MISPLACED_LABELS', path: 'encoding.labels', message: 'Data labels are a chart-level option.', suggestion: 'Move it to labels: { enabled: true, ... }.' });
|
|
150
|
+
if (spec.encoding?.legend !== undefined) warnings.push({ code: 'MISPLACED_LEGEND', path: 'encoding.legend', message: 'Legend visibility is a chart-level option.', suggestion: 'Move it to legend: { visible: false }.' });
|
|
151
|
+
[['xAxis', spec.xAxis], ['yAxis', spec.yAxis]].forEach(([axisName, axis]) => {
|
|
152
|
+
['min', 'max'].forEach(bound => {
|
|
153
|
+
if (axis?.[bound] !== undefined || axis?.right?.[bound] !== undefined) warnings.push({ code: 'UNSUPPORTED_AXIS_DOMAIN', path: `${axisName}.${bound}`, message: 'Use axis.domain rather than axis.min/axis.max for an explicit numeric domain.', suggestion: 'Use yAxis: { domain: [0, 2000] } or yAxis: { nice: true }.' });
|
|
154
|
+
});
|
|
155
|
+
if (axisName === 'xAxis' && axis?.domain !== undefined) warnings.push({ code: 'UNSUPPORTED_AXIS_DOMAIN', path: 'xAxis.domain', message: 'Explicit x-axis domain is not supported by the current categorical/time layout.', suggestion: 'Use yAxis.domain for numeric value axes; x-axis ranges are derived from records.' });
|
|
156
|
+
if (axisName === 'xAxis' && (axis?.nice !== undefined || axis?.ticks !== undefined)) warnings.push({ code: 'UNSUPPORTED_AXIS_TICKS', path: 'xAxis', message: 'Nice domains and tick counts currently apply to yAxis, not categorical/time x-axis layouts.', suggestion: 'Move numeric scale controls to yAxis.' });
|
|
157
|
+
if (axisName === 'xAxis') return;
|
|
158
|
+
[axis, axis?.right].forEach((axisConfig, index) => {
|
|
159
|
+
if (!axisConfig) return;
|
|
160
|
+
const pathPrefix = index ? `${axisName}.right` : axisName;
|
|
161
|
+
if (axisConfig.domain !== undefined && !(Array.isArray(axisConfig.domain) && axisConfig.domain.length === 2 && axisConfig.domain.every(value => Number.isFinite(Number(value))) && Number(axisConfig.domain[1]) > Number(axisConfig.domain[0]))) errors.push({ code: 'INVALID_AXIS_DOMAIN', path: `${pathPrefix}.domain`, message: 'Axis domain must be [min, max] with two finite numbers and max greater than min.', suggestion: 'Use for example yAxis: { domain: [0, 2000] }.' });
|
|
162
|
+
if (axisConfig.nice !== undefined && typeof axisConfig.nice !== 'boolean') errors.push({ code: 'INVALID_AXIS_NICE', path: `${pathPrefix}.nice`, message: 'Axis nice must be a boolean.', suggestion: 'Use nice: true or nice: false.' });
|
|
163
|
+
if (axisConfig.ticks !== undefined && axisConfig.ticks !== 'auto' && !(Number.isInteger(axisConfig.ticks) && axisConfig.ticks >= 2)) errors.push({ code: 'INVALID_AXIS_TICKS', path: `${pathPrefix}.ticks`, message: 'Axis ticks must be auto or an integer of at least 2 labels.', suggestion: 'Use ticks: "auto" or ticks: 5.' });
|
|
164
|
+
});
|
|
165
|
+
});
|
|
100
166
|
if (Array.isArray(spec.encoding.y) && spec.encoding.y.length > 2) errors.push({ code: 'TOO_MANY_AXES', path: 'encoding.y', message: 'Only two quantitative axes are supported in this iteration.', suggestion: 'Use at most two y encodings.' });
|
|
101
167
|
if (spec.stack && !['bar', 'column', 'area'].includes(spec.type)) errors.push({ code: 'INVALID_STACK', path: 'stack', message: 'Stacking is supported by bar, column, and area charts.', suggestion: 'Remove stack or use a supported chart type.' });
|
|
102
168
|
if (spec.stack && !['stacked', 'percent'].includes(typeof spec.stack === 'string' ? spec.stack : spec.stack.mode)) errors.push({ code: 'INVALID_STACK_MODE', path: 'stack', message: 'Stack mode must be stacked or percent.', suggestion: 'Use stack: "stacked" or stack: "percent".' });
|
|
103
169
|
if (spec.type === 'pie' && spec.innerRadius != null && (!(Number(spec.innerRadius) >= 0) || Number(spec.innerRadius) >= 1)) errors.push({ code: 'INVALID_INNER_RADIUS', path: 'innerRadius', message: 'Pie innerRadius must be a ratio from 0 up to, but not including, 1.', suggestion: 'Use a value such as 0.55.' });
|
|
170
|
+
if (spec.type === 'gauge') {
|
|
171
|
+
if (spec.domain === undefined) errors.push({ code: 'MISSING_GAUGE_DOMAIN', path: 'domain', message: 'Gauge requires an explicit numeric domain to avoid silently clipping values.', suggestion: 'Use domain: [0, 100] for a percentage KPI or declare the business range.' });
|
|
172
|
+
else if (!finiteDomain(spec.domain)) errors.push({ code: 'INVALID_GAUGE_DOMAIN', path: 'domain', message: 'Gauge domain must be [min, max] with finite numbers and max greater than min.', suggestion: 'Use for example domain: [0, 100].' });
|
|
173
|
+
}
|
|
104
174
|
if (spec.type === 'radar' && (!Array.isArray(spec.indicators) || spec.indicators.length < 3)) errors.push({ code: 'INVALID_INDICATORS', path: 'indicators', message: 'Radar requires at least three indicators.', suggestion: 'Declare indicator name, field, min, and max.' });
|
|
105
175
|
if (!spec.data || !Array.isArray(spec.data.values)) errors.push({ code: 'INVALID_DATA', path: 'data.values', message: 'data.values must be an array.', suggestion: 'Pass an array of row objects.' });
|
|
106
176
|
if (['flow', 'swimlane', 'architecture', 'mindmap'].includes(spec.type) && !Array.isArray(spec.nodes) && !Array.isArray(spec.data?.nodes)) errors.push({ code: 'INVALID_NODES', path: 'nodes', message: `${spec.type} charts require nodes.`, suggestion: 'Pass nodes on the Spec or in data.nodes.' });
|
|
@@ -117,6 +187,12 @@ export function validateSpec(input) {
|
|
|
117
187
|
if (spec.type === 'gantt') errors.push(...dependencyErrors(spec.data.values));
|
|
118
188
|
if (spec.type === 'pie' && spec.data.values.length > 8) warnings.push({ code: 'HIGH_CARDINALITY_PIE', path: 'data.values', message: `Pie contains ${spec.data.values.length} categories.`, expected: '8 or fewer categories', suggestion: 'Use bar/column or group smaller categories.' });
|
|
119
189
|
if (spec.type === 'radar' && Array.isArray(spec.indicators) && spec.indicators.some(indicator => !Number.isFinite(Number(indicator.min)) || !Number.isFinite(Number(indicator.max)))) warnings.push({ code: 'AMBIGUOUS_RADAR_DOMAIN', path: 'indicators', message: 'Radar indicator domains are incomplete.', expected: 'finite min and max for every indicator', suggestion: 'Declare explicit domains, especially for mixed units.' });
|
|
190
|
+
const profile = chartProfiles[spec.type], inputOptions = input && typeof input === 'object' ? input : {};
|
|
191
|
+
[['legend', 'legend'], ['grid', 'grid'], ['labels', 'labels']].forEach(([option, feature]) => {
|
|
192
|
+
if (inputOptions[option] !== undefined && profile?.features?.[feature] !== 'supported') warnings.push({ code: `UNSUPPORTED_${option.toUpperCase()}`, path: option, message: `${spec.type} does not support ${option} configuration in the current contract.`, suggestion: 'Remove the option or use a chart type that declares this feature.' });
|
|
193
|
+
});
|
|
194
|
+
if ((inputOptions.xAxis !== undefined || inputOptions.yAxis !== undefined) && !['line', 'area', 'bar', 'column', 'scatter'].includes(spec.type)) warnings.push({ code: 'UNSUPPORTED_AXIS', path: inputOptions.xAxis !== undefined ? 'xAxis' : 'yAxis', message: `${spec.type} does not expose Cartesian axis configuration.`, suggestion: 'Use chart-specific options such as domain, colorScale, or indicators.' });
|
|
195
|
+
if (spec.type === 'swimlane' && !((Array.isArray(spec.lanes) && spec.lanes.length) || (Array.isArray(spec.data?.lanes) && spec.data.lanes.length))) errors.push({ code: 'MISSING_REQUIRED', path: 'lanes', message: 'Swimlane requires at least one lane.', suggestion: 'Provide lanes: [{ id, label }].' });
|
|
120
196
|
const supportedInteractions = chartProfiles[spec.type]?.interactions || [];
|
|
121
197
|
Object.entries(spec.interaction || {}).forEach(([name, enabled]) => { if (enabled && !supportedInteractions.includes(name) && !['hover', 'click'].includes(name)) warnings.push({ code: 'UNSUPPORTED_INTERACTION', path: `interaction.${name}`, message: `${name} is not declared for ${spec.type}.`, expected: supportedInteractions, suggestion: 'Disable the interaction or use a compatible chart type.' }); });
|
|
122
198
|
if (spec.branding != null && typeof spec.branding !== 'boolean' && !(spec.branding && typeof spec.branding === 'object')) {
|
package/types/index.d.ts
CHANGED
|
@@ -24,12 +24,15 @@ export interface Diagnostic { code: string; path?: string; message: string; expe
|
|
|
24
24
|
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; }
|
|
25
25
|
export interface DataInspection { version: '1.0'; rows: number; fields: DataFieldInfo[]; dimensions: string[]; measures: string[]; temporalFields: string[]; missingValueCount: number; warnings: Diagnostic[]; }
|
|
26
26
|
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>; }
|
|
27
|
-
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[]; 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; }
|
|
28
|
-
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; }
|
|
29
|
-
export interface
|
|
27
|
+
export interface RuntimeCapabilities { version: '2.0'; contractVersion: '1.0'; 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'>; 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; }
|
|
28
|
+
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; }
|
|
29
|
+
export interface AxisState { rawDomain: [number, number]; domain: [number, number]; ticks: number[]; step: number | null; policy: 'nice' | 'raw' | 'explicit'; }
|
|
30
|
+
export interface ChartHealth { version: '1.0'; status: 'ready' | 'degraded' | 'empty'; renderable: boolean; issues: string[]; metrics: { warnings: number; suppressedLabels: number; clampedValues: number; renderedMarks: number }; }
|
|
31
|
+
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[]; axes?: { y?: AxisState; right?: AxisState } | null; health?: ChartHealth; style: Partial<StyleRecommendation> & { name?: string }; lineage: { recordIds: string[]; sourcePreserved: boolean }; accessibility: { enabled: boolean; summary: string }; }
|
|
30
32
|
export interface ChartInteraction { tooltip?: boolean; hover?: boolean; click?: boolean; crosshair?: boolean; zoom?: boolean; pan?: boolean; brush?: boolean; drag?: boolean; edgeDrag?: boolean; portConnect?: boolean; keyboard?: boolean; [key: string]: boolean | undefined; }
|
|
31
33
|
export interface ChartEditing { enabled?: boolean; mode?: 'command' | string; requireConfirmation?: boolean; allowDelete?: boolean; allowStructuralChanges?: boolean; }
|
|
32
|
-
export interface
|
|
34
|
+
export interface AxisSpec { type?: 'linear' | 'log' | 'quantitative' | 'temporal' | 'time' | string; title?: string; format?: string | Record<string, unknown>; domain?: [number, number]; nice?: boolean; ticks?: 'auto' | number; right?: AxisSpec; [key: string]: unknown; }
|
|
35
|
+
export interface ChartSpec { type: ChartType; renderer?: Renderer; container?: string | Element; chartId?: string; width?: number; height?: number; locale?: string; 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 }; xAxis?: AxisSpec; yAxis?: AxisSpec; diagram?: DiagramConfig; interaction?: ChartInteraction; editing?: ChartEditing; accessibility?: { enabled?: boolean; description?: string }; branding?: boolean | { enabled?: boolean }; theme?: ThemeMode | ThemePreset | ThemeConfig | ResolvedTheme; preferences?: ChartPreferencesPatch | PreferencesStore; preferencesStore?: PreferencesStore; [key: string]: unknown; }
|
|
33
36
|
export interface BinTransform { type: 'bin'; field: string; output?: string; thresholds?: number; step?: number; extent?: [number, number]; }
|
|
34
37
|
export interface RadarIndicator { name: string; field: string; min?: number; max?: number; }
|
|
35
38
|
export interface DiagramNode { id: string; label: string; position?: { x: number; y: number }; size?: { width: number; height: number }; ports?: Array<{ id: string; side?: 'left' | 'right' | 'top' | 'bottom'; offset?: number }>; groupId?: string; }
|
|
@@ -90,7 +93,7 @@ export interface Chart {
|
|
|
90
93
|
previewEdit(command: EditCommand): EditPreview;
|
|
91
94
|
applyEdit(command: EditCommand, options?: { preview?: EditPreview; confirmed?: boolean; approval?: unknown; previewId?: string; expectedRevision?: number; actor?: string; source?: string; reason?: string }): Record<string, unknown>;
|
|
92
95
|
getChangeSet(): Record<string, unknown> | null;
|
|
93
|
-
getState(): Record<string, unknown
|
|
96
|
+
getState(): Record<string, unknown> & { axes?: { y?: AxisState; right?: AxisState } | null; health?: ChartHealth; warnings?: Diagnostic[] };
|
|
94
97
|
getProjectAnalytics(): ProjectAnalyticsState | null;
|
|
95
98
|
getLinkedState(): LinkedState | null;
|
|
96
99
|
setLinkedFilters(filters: LinkedFilters): this;
|