@taylorwong/ichartjs 2.0.12 → 2.0.14
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 +17 -0
- package/README.md +2 -2
- package/docs/agent/charting-scenario.md +3 -1
- package/docs/agent/coding-agent-integration.md +1 -1
- package/docs/agent/development/2.0-release-readiness.md +4 -0
- package/docs/agent/development/iteration-13.md +278 -0
- package/docs/agent/development/release-sop.md +1 -1
- package/docs/agent/development/roadmap.md +2 -1
- package/docs/agent/diagram-scenario.md +4 -0
- package/docs/agent/editing-contract.md +3 -0
- package/docs/agent/frontend-integration.md +1 -1
- package/docs/agent/project-scenario.md +3 -0
- package/docs/agent/runtime-contract.md +4 -0
- package/docs/agent/usage-scenarios.md +1 -1
- package/docs/agent/zh-CN/charting-scenario.md +3 -1
- package/docs/agent/zh-CN/coding-agent-integration.md +1 -1
- package/docs/agent/zh-CN/diagram-scenario.md +17 -1
- package/docs/agent/zh-CN/editing-contract.md +2 -0
- package/docs/agent/zh-CN/frontend-integration.md +1 -1
- package/docs/agent/zh-CN/project-scenario.md +4 -2
- package/docs/agent/zh-CN/runtime-contract.md +3 -1
- package/docs/agent/zh-CN/usage-scenarios.md +1 -1
- package/docs/manifests/capabilities.json +1310 -32
- package/docs/manifests/commands.json +7 -7
- package/docs/manifests/schemas.json +182 -10
- package/package.json +24 -4
- package/skills/ichartjs/SKILL.md +1 -1
- package/src/capabilities.mjs +10 -36
- package/src/chart-lifecycle.mjs +31 -0
- package/src/charts.mjs +49 -19
- package/src/command.mjs +3 -2
- package/src/contract-registry.mjs +59 -0
- package/src/diagram.mjs +30 -0
- package/src/edit-controller.mjs +1 -1
- package/src/errors.mjs +31 -0
- package/src/index.mjs +142 -10
- package/src/layout.mjs +26 -1
- package/src/project.mjs +45 -16
- package/src/schema.mjs +1 -0
- package/src/spec.mjs +45 -3
- package/types/consumer-fixture.ts +35 -0
- package/types/index.d.ts +33 -3
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"version": "1.0",
|
|
3
3
|
"source": "src/command.mjs#operationTypes",
|
|
4
4
|
"commands": {
|
|
5
|
-
"updateField": { "models": ["project-task", "timeline-event", "milestone", "burndown-sample", "flow-node", "flow-edge", "swimlane", "architecture-node", "architecture-edge", "mindmap-node"], "previewRequired": true, "undoable": true },
|
|
6
|
-
"updateRecord": { "models": ["project-task", "timeline-event", "milestone", "burndown-sample", "flow-node", "flow-edge", "swimlane", "architecture-node", "architecture-edge", "mindmap-node"], "previewRequired": true, "undoable": true },
|
|
5
|
+
"updateField": { "models": ["project-task", "timeline-event", "milestone", "burndown-sample", "flow-node", "flow-edge", "swimlane", "architecture-node", "architecture-edge", "mindmap-node", "mindmap-edge"], "previewRequired": true, "undoable": true },
|
|
6
|
+
"updateRecord": { "models": ["project-task", "timeline-event", "milestone", "burndown-sample", "flow-node", "flow-edge", "swimlane", "architecture-node", "architecture-edge", "mindmap-node", "mindmap-edge"], "previewRequired": true, "undoable": true },
|
|
7
7
|
"updateTask": { "models": ["project-task"], "previewRequired": true, "undoable": true },
|
|
8
8
|
"shiftTask": { "models": ["project-task"], "required": ["taskId", "days"], "previewRequired": true, "undoable": true },
|
|
9
9
|
"updateProgress": { "models": ["project-task"], "required": ["taskId", "progress"], "previewRequired": true, "undoable": true },
|
|
@@ -21,11 +21,11 @@
|
|
|
21
21
|
"assignNodesToGroup": { "models": ["flow-node"], "required": ["nodeIds", "groupId"], "previewRequired": true, "confirmationRequired": true, "undoable": true },
|
|
22
22
|
"duplicateGroup": { "models": ["flow-node", "flow-edge"], "required": ["groupId"], "previewRequired": true, "confirmationRequired": true, "undoable": true },
|
|
23
23
|
"deleteGroup": { "models": ["flow-node", "flow-edge"], "required": ["groupId", "policy"], "previewRequired": true, "confirmationRequired": true, "undoable": true },
|
|
24
|
-
"updateEdge": { "models": ["flow-edge", "architecture-edge"], "required": ["edgeId", "changes"], "previewRequired": true, "undoable": true },
|
|
25
|
-
"removeEdge": { "models": ["flow-edge", "architecture-edge"], "required": ["edgeId"], "previewRequired": true, "confirmationRequired": true, "undoable": true },
|
|
26
|
-
"addEdge": { "models": ["flow-edge", "architecture-edge"], "required": ["from", "to"], "previewRequired": true, "confirmationRequired": true, "undoable": true },
|
|
24
|
+
"updateEdge": { "models": ["flow-edge", "architecture-edge", "mindmap-edge"], "required": ["edgeId", "changes"], "previewRequired": true, "undoable": true },
|
|
25
|
+
"removeEdge": { "models": ["flow-edge", "architecture-edge", "mindmap-edge"], "required": ["edgeId"], "previewRequired": true, "confirmationRequired": true, "undoable": true },
|
|
26
|
+
"addEdge": { "models": ["flow-edge", "architecture-edge", "mindmap-edge"], "required": ["from", "to"], "previewRequired": true, "confirmationRequired": true, "undoable": true },
|
|
27
27
|
"toggleGroupCollapse": { "models": ["flow-node"], "required": ["groupId"], "previewRequired": true, "confirmationRequired": true, "undoable": true },
|
|
28
|
-
"duplicateSelection": { "models": ["flow-node", "flow-edge"], "required": ["nodeIds"], "previewRequired": true, "confirmationRequired": true, "undoable": true },
|
|
29
|
-
"pasteSelection": { "models": ["flow-node", "flow-edge"], "required": ["nodes"], "previewRequired": true, "confirmationRequired": true, "undoable": true }
|
|
28
|
+
"duplicateSelection": { "models": ["flow-node", "flow-edge", "mindmap-edge"], "required": ["nodeIds"], "previewRequired": true, "confirmationRequired": true, "undoable": true },
|
|
29
|
+
"pasteSelection": { "models": ["flow-node", "flow-edge", "mindmap-edge"], "required": ["nodes"], "previewRequired": true, "confirmationRequired": true, "undoable": true }
|
|
30
30
|
}
|
|
31
31
|
}
|
|
@@ -2,15 +2,187 @@
|
|
|
2
2
|
"version": "1.0",
|
|
3
3
|
"source": "src/schema.mjs#getBusinessSchema",
|
|
4
4
|
"models": {
|
|
5
|
-
"project-task": {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
5
|
+
"project-task": {
|
|
6
|
+
"key": "id",
|
|
7
|
+
"required": [
|
|
8
|
+
"id",
|
|
9
|
+
"name",
|
|
10
|
+
"start",
|
|
11
|
+
"end"
|
|
12
|
+
],
|
|
13
|
+
"editable": [
|
|
14
|
+
"name",
|
|
15
|
+
"start",
|
|
16
|
+
"end",
|
|
17
|
+
"baselineStart",
|
|
18
|
+
"baselineEnd",
|
|
19
|
+
"actualStart",
|
|
20
|
+
"actualEnd",
|
|
21
|
+
"baselineDate",
|
|
22
|
+
"actualDate",
|
|
23
|
+
"progress",
|
|
24
|
+
"status",
|
|
25
|
+
"owner",
|
|
26
|
+
"resource",
|
|
27
|
+
"capacity",
|
|
28
|
+
"load",
|
|
29
|
+
"points",
|
|
30
|
+
"sprint",
|
|
31
|
+
"priority",
|
|
32
|
+
"labels",
|
|
33
|
+
"dependencies"
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
"timeline-event": {
|
|
37
|
+
"key": "id",
|
|
38
|
+
"required": [
|
|
39
|
+
"id",
|
|
40
|
+
"title",
|
|
41
|
+
"date"
|
|
42
|
+
],
|
|
43
|
+
"editable": [
|
|
44
|
+
"title",
|
|
45
|
+
"date",
|
|
46
|
+
"description",
|
|
47
|
+
"status"
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
"milestone": {
|
|
51
|
+
"key": "id",
|
|
52
|
+
"required": [
|
|
53
|
+
"id",
|
|
54
|
+
"title",
|
|
55
|
+
"date"
|
|
56
|
+
],
|
|
57
|
+
"editable": [
|
|
58
|
+
"title",
|
|
59
|
+
"date",
|
|
60
|
+
"status"
|
|
61
|
+
]
|
|
62
|
+
},
|
|
63
|
+
"burndown-sample": {
|
|
64
|
+
"key": "id",
|
|
65
|
+
"required": [
|
|
66
|
+
"id",
|
|
67
|
+
"date",
|
|
68
|
+
"remaining"
|
|
69
|
+
],
|
|
70
|
+
"editable": [
|
|
71
|
+
"date",
|
|
72
|
+
"remaining",
|
|
73
|
+
"scopeChange"
|
|
74
|
+
]
|
|
75
|
+
},
|
|
76
|
+
"flow-node": {
|
|
77
|
+
"key": "id",
|
|
78
|
+
"required": [
|
|
79
|
+
"id",
|
|
80
|
+
"label"
|
|
81
|
+
],
|
|
82
|
+
"editable": [
|
|
83
|
+
"label",
|
|
84
|
+
"position",
|
|
85
|
+
"size",
|
|
86
|
+
"laneId",
|
|
87
|
+
"description",
|
|
88
|
+
"status"
|
|
89
|
+
]
|
|
90
|
+
},
|
|
91
|
+
"flow-edge": {
|
|
92
|
+
"key": "id",
|
|
93
|
+
"required": [
|
|
94
|
+
"id",
|
|
95
|
+
"from",
|
|
96
|
+
"to"
|
|
97
|
+
],
|
|
98
|
+
"editable": [
|
|
99
|
+
"from",
|
|
100
|
+
"to",
|
|
101
|
+
"fromPort",
|
|
102
|
+
"toPort",
|
|
103
|
+
"label",
|
|
104
|
+
"status",
|
|
105
|
+
"routing",
|
|
106
|
+
"curveTension",
|
|
107
|
+
"waypoints"
|
|
108
|
+
]
|
|
109
|
+
},
|
|
110
|
+
"swimlane": {
|
|
111
|
+
"key": "id",
|
|
112
|
+
"required": [
|
|
113
|
+
"id",
|
|
114
|
+
"label"
|
|
115
|
+
],
|
|
116
|
+
"editable": [
|
|
117
|
+
"label"
|
|
118
|
+
]
|
|
119
|
+
},
|
|
120
|
+
"architecture-node": {
|
|
121
|
+
"key": "id",
|
|
122
|
+
"required": [
|
|
123
|
+
"id",
|
|
124
|
+
"label"
|
|
125
|
+
],
|
|
126
|
+
"editable": [
|
|
127
|
+
"label",
|
|
128
|
+
"layerId",
|
|
129
|
+
"boundaryId",
|
|
130
|
+
"position",
|
|
131
|
+
"size",
|
|
132
|
+
"description",
|
|
133
|
+
"status"
|
|
134
|
+
]
|
|
135
|
+
},
|
|
136
|
+
"architecture-edge": {
|
|
137
|
+
"key": "id",
|
|
138
|
+
"required": [
|
|
139
|
+
"id",
|
|
140
|
+
"from",
|
|
141
|
+
"to"
|
|
142
|
+
],
|
|
143
|
+
"editable": [
|
|
144
|
+
"from",
|
|
145
|
+
"to",
|
|
146
|
+
"label",
|
|
147
|
+
"relation",
|
|
148
|
+
"routing",
|
|
149
|
+
"curveTension",
|
|
150
|
+
"waypoints",
|
|
151
|
+
"status"
|
|
152
|
+
]
|
|
153
|
+
},
|
|
154
|
+
"mindmap-edge": {
|
|
155
|
+
"key": "id",
|
|
156
|
+
"required": [
|
|
157
|
+
"id",
|
|
158
|
+
"from",
|
|
159
|
+
"to"
|
|
160
|
+
],
|
|
161
|
+
"editable": [
|
|
162
|
+
"from",
|
|
163
|
+
"to",
|
|
164
|
+
"label",
|
|
165
|
+
"routing",
|
|
166
|
+
"curveTension",
|
|
167
|
+
"waypoints",
|
|
168
|
+
"status"
|
|
169
|
+
]
|
|
170
|
+
},
|
|
171
|
+
"mindmap-node": {
|
|
172
|
+
"key": "id",
|
|
173
|
+
"required": [
|
|
174
|
+
"id",
|
|
175
|
+
"label"
|
|
176
|
+
],
|
|
177
|
+
"editable": [
|
|
178
|
+
"label",
|
|
179
|
+
"parentId",
|
|
180
|
+
"position",
|
|
181
|
+
"size",
|
|
182
|
+
"branch",
|
|
183
|
+
"description",
|
|
184
|
+
"status"
|
|
185
|
+
]
|
|
186
|
+
}
|
|
15
187
|
}
|
|
16
188
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taylorwong/ichartjs",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.14",
|
|
4
4
|
"description": "Agent-first, renderer-independent charting and project visualization runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.mjs",
|
|
@@ -35,21 +35,41 @@
|
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
37
|
"test": "node --test tests/*.test.mjs",
|
|
38
|
+
"test:browser": "node --test tests/browser/*.test.mjs",
|
|
38
39
|
"check": "for file in src/*.mjs; do node --check \"$file\" || exit 1; done",
|
|
40
|
+
"contracts:generate": "node scripts/generate-contract-manifests.mjs",
|
|
41
|
+
"contracts:check": "node scripts/check-contracts.mjs",
|
|
42
|
+
"types:check": "node scripts/check-types.mjs",
|
|
43
|
+
"deps:check": "node scripts/check-cycles.mjs",
|
|
44
|
+
"docs:examples": "node examples/agent-workflow.mjs",
|
|
39
45
|
"docs:check": "node scripts/check-agent-docs.mjs",
|
|
40
46
|
"example:agent": "node examples/agent-workflow.mjs",
|
|
41
47
|
"playground": "node scripts/serve-playground.mjs",
|
|
42
|
-
"agent:check": "npm run docs:check && npm run check && npm test",
|
|
48
|
+
"agent:check": "npm run contracts:check && npm run types:check && npm run deps:check && npm run docs:check && npm run docs:examples && npm run check && npm test",
|
|
43
49
|
"rc:check": "npm test && npm run check && git diff --check"
|
|
44
50
|
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"playwright-core": "^1.63.0",
|
|
53
|
+
"typescript": "^5.9.3"
|
|
54
|
+
},
|
|
45
55
|
"repository": {
|
|
46
56
|
"type": "git",
|
|
47
57
|
"url": "git+https://github.com/wanghetommy/ichartjs.git"
|
|
48
58
|
},
|
|
49
59
|
"homepage": "https://github.com/wanghetommy/ichartjs#readme",
|
|
50
60
|
"bugs": "https://github.com/wanghetommy/ichartjs/issues",
|
|
51
|
-
"keywords": [
|
|
52
|
-
|
|
61
|
+
"keywords": [
|
|
62
|
+
"chart",
|
|
63
|
+
"visualization",
|
|
64
|
+
"agent",
|
|
65
|
+
"svg",
|
|
66
|
+
"canvas",
|
|
67
|
+
"gantt",
|
|
68
|
+
"diagram"
|
|
69
|
+
],
|
|
70
|
+
"engines": {
|
|
71
|
+
"node": ">=18"
|
|
72
|
+
},
|
|
53
73
|
"author": "taylorwong <wanghetommy@gmail.com>",
|
|
54
74
|
"license": "Apache-2.0"
|
|
55
75
|
}
|
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.14/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
|
|
package/src/capabilities.mjs
CHANGED
|
@@ -4,44 +4,13 @@
|
|
|
4
4
|
import { inspectData } from './data.mjs';
|
|
5
5
|
import { planStyle, styleCapabilities } from './theme.mjs';
|
|
6
6
|
import { defaultPreferences, preferenceDensities, preferenceMotions, preferenceTriStates } from './preferences.mjs';
|
|
7
|
+
import { businessModels, chartProfiles, chartTypes, commandTypes, contractVersion, diagramEdgeModels, diagramOperations, exportTypes, interactionDefaults, renderers } from './contract-registry.mjs';
|
|
7
8
|
|
|
8
|
-
export
|
|
9
|
+
export { chartProfiles, chartTypes };
|
|
9
10
|
|
|
10
11
|
const cartesian = ['line', 'area', 'bar', 'column', 'scatter'];
|
|
11
12
|
const project = ['gantt', 'timeline', 'milestone', 'burndown'];
|
|
12
13
|
const diagrams = ['flow', 'swimlane', 'architecture', 'mindmap'];
|
|
13
|
-
const status = (supported, notApplicable = []) => Object.fromEntries(supported.map(name => [name, 'supported']).concat(notApplicable.map(name => [name, 'not-applicable'])));
|
|
14
|
-
const commonPresentation = ['title', 'subtitle', 'theme', 'responsive', 'empty-state', 'invalid-data-state', 'export', 'branding'];
|
|
15
|
-
|
|
16
|
-
const definitions = {
|
|
17
|
-
line: { family: 'cartesian', intents: ['trend', 'time-series'], required: ['dimension', 'measure'], optional: ['series'], interactions: ['hover', 'tooltip', 'selection', 'crosshair', 'zoom', 'pan', 'keyboard'], features: status([...commonPresentation, 'axes', 'grid', 'legend', 'labels', 'formatting', 'multi-series', 'dual-axis']) },
|
|
18
|
-
area: { family: 'cartesian', intents: ['trend', 'composition'], required: ['dimension', 'measure'], optional: ['series'], interactions: ['hover', 'tooltip', 'selection', 'crosshair', 'zoom', 'pan', 'keyboard'], features: status([...commonPresentation, 'axes', 'grid', 'legend', 'labels', 'formatting', 'multi-series', 'stacked', 'percent-stacked']) },
|
|
19
|
-
bar: { family: 'cartesian', intents: ['comparison', 'ranking'], required: ['dimension', 'measure'], optional: ['series'], interactions: ['hover', 'tooltip', 'selection', 'zoom', 'pan', 'keyboard'], features: status([...commonPresentation, 'axes', 'grid', 'legend', 'labels', 'formatting', 'multi-series', 'stacked', 'percent-stacked']) },
|
|
20
|
-
column: { family: 'cartesian', intents: ['comparison', 'distribution', 'composition'], required: ['dimension', 'measure'], optional: ['series'], interactions: ['hover', 'tooltip', 'selection', 'zoom', 'pan', 'keyboard'], features: status([...commonPresentation, 'axes', 'grid', 'legend', 'labels', 'formatting', 'multi-series', 'stacked', 'percent-stacked', 'mixed-line-column', 'dual-axis', 'bin']) },
|
|
21
|
-
pie: { family: 'part-to-whole', intents: ['part-to-whole'], required: ['dimension', 'measure'], optional: [], interactions: ['hover', 'tooltip', 'selection', 'keyboard'], features: status([...commonPresentation, 'legend', 'labels', 'formatting', 'donut', 'zero-total'], ['axes', 'grid', 'zoom', 'pan']) },
|
|
22
|
-
scatter: { family: 'cartesian', intents: ['relationship', 'correlation'], required: ['measure', 'measure'], optional: ['dimension'], interactions: ['hover', 'tooltip', 'selection', 'brush', 'zoom', 'pan', 'keyboard'], features: status([...commonPresentation, 'axes', 'grid', 'labels', 'formatting'], ['stacked']) },
|
|
23
|
-
funnel: { family: 'stage', intents: ['funnel', 'conversion'], required: ['dimension', 'measure'], optional: [], interactions: ['hover', 'tooltip', 'selection', 'keyboard'], features: status([...commonPresentation, 'labels', 'formatting'], ['axes', 'grid', 'zoom', 'pan']) },
|
|
24
|
-
gauge: { family: 'indicator', intents: ['progress', 'single-value'], required: ['measure'], optional: [], interactions: ['hover', 'tooltip', 'selection', 'keyboard'], features: status([...commonPresentation, 'labels', 'formatting', 'domain'], ['axes', 'grid', 'legend', 'zoom', 'pan']) },
|
|
25
|
-
heatmap: { family: 'matrix', intents: ['matrix', 'correlation-grid'], required: ['dimension', 'dimension', 'measure'], optional: [], interactions: ['hover', 'tooltip', 'selection', 'keyboard'], features: status([...commonPresentation, 'axes', 'labels', 'formatting', 'color-scale', 'missing-values'], ['zoom', 'pan']) },
|
|
26
|
-
radar: { family: 'radial', intents: ['multidimensional', 'profile'], required: ['three-or-more-measures'], optional: ['series'], interactions: ['hover', 'tooltip', 'selection', 'keyboard'], features: status([...commonPresentation, 'legend', 'labels', 'formatting', 'multi-series', 'indicator-domains', 'mixed-unit-warning'], ['axes', 'grid', 'zoom', 'pan']) },
|
|
27
|
-
gantt: { family: 'project', intents: ['schedule', 'variance'], required: ['id', 'start', 'end'], optional: ['dependencies', 'progress', 'baseline', 'actual'], interactions: ['hover', 'tooltip', 'selection', 'zoom', 'pan', 'keyboard', 'linked-filter', 'linked-selection'], features: status([...commonPresentation, 'labels', 'formatting', 'critical-path', 'slack', 'baseline-actual-variance', 'working-calendar', 'dependencies']) },
|
|
28
|
-
timeline: { family: 'project', intents: ['timeline', 'milestone'], required: ['date', 'label'], optional: ['status'], interactions: ['hover', 'tooltip', 'selection', 'zoom', 'pan', 'keyboard'], features: status([...commonPresentation, 'labels', 'formatting']) },
|
|
29
|
-
milestone: { family: 'project', intents: ['milestone'], required: ['date', 'label'], optional: ['baseline', 'actual'], interactions: ['hover', 'tooltip', 'selection', 'zoom', 'pan', 'keyboard'], features: status([...commonPresentation, 'labels', 'formatting', 'baseline-actual-variance']) },
|
|
30
|
-
burndown: { family: 'project', intents: ['progress', 'release'], required: ['date', 'remaining'], optional: ['ideal', 'scopeChange'], interactions: ['hover', 'tooltip', 'selection', 'zoom', 'pan', 'keyboard'], features: status([...commonPresentation, 'axes', 'labels', 'formatting', 'scope-change', 'release-forecast']) },
|
|
31
|
-
flow: { family: 'diagram', intents: ['workflow'], required: ['nodes'], optional: ['edges', 'groups', 'ports'], interactions: ['hover', 'tooltip', 'selection', 'edge-selection', 'drag', 'edgeDrag', 'zoom', 'pan', 'keyboard', 'keyboard-edit', 'keyboard-port-connect', 'portConnect', 'copy-paste'], features: status([...commonPresentation, 'labels', 'routing', 'manual-routing', 'layout', 'groups', 'ports', 'history'], ['axes', 'grid', 'legend']) },
|
|
32
|
-
swimlane: { family: 'diagram', intents: ['responsibility'], required: ['nodes', 'lanes'], optional: ['edges', 'groups', 'ports'], interactions: ['hover', 'tooltip', 'selection', 'edge-selection', 'drag', 'edgeDrag', 'zoom', 'pan', 'keyboard', 'keyboard-edit', 'keyboard-port-connect', 'portConnect', 'copy-paste'], features: status([...commonPresentation, 'labels', 'routing', 'manual-routing', 'layout', 'groups', 'ports', 'lanes', 'history'], ['axes', 'grid', 'legend']) },
|
|
33
|
-
architecture: { family: 'diagram', intents: ['architecture', 'business-architecture', 'data-architecture', 'technical-architecture'], required: ['nodes'], optional: ['edges', 'layers', 'boundaries', 'groups', 'ports'], interactions: ['hover', 'tooltip', 'selection', 'edge-selection', 'drag', 'edgeDrag', 'zoom', 'pan', 'keyboard', 'keyboard-edit', 'keyboard-port-connect', 'portConnect', 'copy-paste'], features: status([...commonPresentation, 'labels', 'routing', 'manual-routing', 'layout', 'layers', 'boundaries', 'groups', 'ports', 'history'], ['axes', 'grid', 'legend']) },
|
|
34
|
-
mindmap: { family: 'diagram', intents: ['mindmap', 'hierarchy', 'brainstorm'], required: ['nodes'], optional: ['parentId', 'edges', 'groups'], interactions: ['hover', 'tooltip', 'selection', 'edge-selection', 'drag', 'edgeDrag', 'zoom', 'pan', 'keyboard', 'keyboard-edit', 'keyboard-port-connect', 'portConnect', 'copy-paste'], features: status([...commonPresentation, 'labels', 'routing', 'cubic-bezier', 'curve-tension', 'obstacle-fallback', 'manual-routing', 'layout', 'tree-layout', 'radial-layout', 'history'], ['axes', 'grid', 'legend']) }
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
export const chartProfiles = Object.fromEntries(chartTypes.map(type => [type, {
|
|
38
|
-
type,
|
|
39
|
-
...definitions[type],
|
|
40
|
-
dataShapes: definitions[type].family === 'diagram' ? ['diagram-spec'] : definitions[type].family === 'project' ? ['records'] : ['records', 'data-table'],
|
|
41
|
-
renderers: ['canvas', 'svg'],
|
|
42
|
-
exports: ['png', 'svg', 'json'],
|
|
43
|
-
limits: type === 'pie' ? { recommendedCategories: 8 } : type === 'radar' ? { minimumIndicators: 3, recommendedIndicators: 8 } : type === 'heatmap' ? { recommendedCells: 2500 } : {}
|
|
44
|
-
}]));
|
|
45
14
|
|
|
46
15
|
const intentMap = {
|
|
47
16
|
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'
|
|
@@ -225,7 +194,7 @@ export function getCapabilities() {
|
|
|
225
194
|
};
|
|
226
195
|
return {
|
|
227
196
|
version: '2.0',
|
|
228
|
-
contractVersion
|
|
197
|
+
contractVersion,
|
|
229
198
|
chartTypes,
|
|
230
199
|
charts: JSON.parse(JSON.stringify(chartProfiles)),
|
|
231
200
|
intents,
|
|
@@ -240,9 +209,10 @@ export function getCapabilities() {
|
|
|
240
209
|
},
|
|
241
210
|
diagrams,
|
|
242
211
|
excluded: ['map', '3d'],
|
|
243
|
-
renderers: [
|
|
212
|
+
renderers: [...renderers],
|
|
213
|
+
interactionDefaults: { ...interactionDefaults },
|
|
244
214
|
interactions: [...new Set(Object.values(chartProfiles).flatMap(profile => profile.interactions))],
|
|
245
|
-
exports: [
|
|
215
|
+
exports: [...exportTypes],
|
|
246
216
|
headless: {
|
|
247
217
|
preview: true,
|
|
248
218
|
json: true,
|
|
@@ -263,6 +233,10 @@ export function getCapabilities() {
|
|
|
263
233
|
precedence: preferenceCapabilities.precedence,
|
|
264
234
|
schema: preferenceCapabilities
|
|
265
235
|
},
|
|
236
|
+
commands: [...commandTypes],
|
|
237
|
+
diagramOperations: [...diagramOperations],
|
|
238
|
+
businessModels: [...businessModels],
|
|
239
|
+
diagramEdgeModels: [...diagramEdgeModels],
|
|
266
240
|
plugins: true,
|
|
267
241
|
branding: { defaultEnabled: true, signature: 'Powered by iChart.js', options: [{ name: 'enabled', type: 'boolean', default: true }] },
|
|
268
242
|
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Renderer-independent chart lifecycle cleanup. */
|
|
2
|
+
export function destroyChart(chart) {
|
|
3
|
+
if (chart._destroyed) return;
|
|
4
|
+
chart._destroyed = true;
|
|
5
|
+
chart._preferencesUnsubscribe?.();
|
|
6
|
+
chart._resizeObserver?.disconnect();
|
|
7
|
+
chart._colorSchemeQuery?.removeEventListener?.('change', chart._colorSchemeHandler);
|
|
8
|
+
if (chart._eventsBound) {
|
|
9
|
+
const { target, handler, start, move, end, leave, touchStart, touchMove, touchEnd, wheel } = chart._eventsBound;
|
|
10
|
+
target.removeEventListener('mousemove', handler);
|
|
11
|
+
target.removeEventListener('click', handler);
|
|
12
|
+
target.removeEventListener('pointerdown', start);
|
|
13
|
+
target.removeEventListener('pointermove', move);
|
|
14
|
+
target.removeEventListener('pointerup', end);
|
|
15
|
+
target.removeEventListener('pointercancel', end);
|
|
16
|
+
target.removeEventListener('pointerleave', leave);
|
|
17
|
+
target.removeEventListener('touchstart', touchStart);
|
|
18
|
+
target.removeEventListener('touchmove', touchMove);
|
|
19
|
+
target.removeEventListener('touchend', touchEnd);
|
|
20
|
+
target.removeEventListener('wheel', wheel);
|
|
21
|
+
}
|
|
22
|
+
const target = chart.renderer.svg || chart.renderer.canvas;
|
|
23
|
+
if (chart._keyboardHandler) target?.removeEventListener('keydown', chart._keyboardHandler);
|
|
24
|
+
chart._tooltip?.remove();
|
|
25
|
+
chart.plugins.destroy();
|
|
26
|
+
chart.renderer.destroy();
|
|
27
|
+
chart.listeners.clear();
|
|
28
|
+
chart._eventsBound = null;
|
|
29
|
+
chart._selected.clear();
|
|
30
|
+
chart._clipboard = { nodes: [], edges: [] };
|
|
31
|
+
}
|
package/src/charts.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { applyTransforms } from './transforms.mjs';
|
|
|
8
8
|
import { buildProjectScene } from './project.mjs';
|
|
9
9
|
import { formatValue } from './format.mjs';
|
|
10
10
|
import { contrastRatio, resolveTheme } from './theme.mjs';
|
|
11
|
-
import { axisLabelLayout, boxesOverlap, estimateTextWidth, fontSize, titleLayout } from './layout.mjs';
|
|
11
|
+
import { axisLabelLayout, boxesOverlap, estimateTextWidth, fontSize, titleLayout, truncateText } from './layout.mjs';
|
|
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;
|
|
@@ -72,7 +72,11 @@ function chromeLayout(spec, entries = []) {
|
|
|
72
72
|
const size = fontSize(spec, 'legend', 12), lineHeight = Math.max(18, size * Number(spec.theme?.typography?.legend?.lineHeight || 1.4));
|
|
73
73
|
const swatchSize = Math.max(8, Math.round(size * 0.75)), gap = Math.max(12, Math.round(size));
|
|
74
74
|
const availableWidth = Math.max(1, spec.width - spec.padding.left - spec.padding.right);
|
|
75
|
-
const
|
|
75
|
+
const maxLabelWidth = Math.max(size, availableWidth - swatchSize - 6 - gap);
|
|
76
|
+
const measured = entries.map((entry, index) => {
|
|
77
|
+
const rawName = String(entry.name), name = truncateText(rawName, maxLabelWidth, size);
|
|
78
|
+
return { ...entry, name, rawName, truncated: name !== rawName, index, width: swatchSize + 6 + estimateTextWidth(name, size) + gap };
|
|
79
|
+
});
|
|
76
80
|
const rows = [];
|
|
77
81
|
measured.forEach(item => {
|
|
78
82
|
let row = rows.at(-1);
|
|
@@ -88,7 +92,7 @@ function chromeLayout(spec, entries = []) {
|
|
|
88
92
|
row.items.forEach(item => { items.push({ ...item, x, y, swatchSize }); x += item.width; });
|
|
89
93
|
});
|
|
90
94
|
const bottom = top + rows.length * lineHeight;
|
|
91
|
-
return { title, legend: { visible: true, items, bottom }, bottom: Math.max(title.bottom, bottom) };
|
|
95
|
+
return { title, legend: { visible: true, items, bottom, truncatedCount: measured.filter(item => item.truncated).length }, bottom: Math.max(title.bottom, bottom) };
|
|
92
96
|
}
|
|
93
97
|
|
|
94
98
|
function styledSpec(spec) {
|
|
@@ -107,18 +111,6 @@ function layout(spec, data, legendEntries = []) {
|
|
|
107
111
|
const dates = temporal ? data.rows.map(row => new Date(row[xField]).getTime()).filter(Number.isFinite) : [];
|
|
108
112
|
const xNumbers = quantitativeX ? data.rows.map(row => Number(row[xField])).filter(Number.isFinite) : [];
|
|
109
113
|
const xMin = dates.length ? Math.min(...dates) : xNumbers.length ? Math.min(...xNumbers) : 0, xMax = dates.length ? Math.max(...dates) : xNumbers.length ? Math.max(...xNumbers) : 1;
|
|
110
|
-
const categoryField = spec.encoding.x?.field || 'name';
|
|
111
|
-
const categoryLabelLength = spec.type === 'bar' ? Math.max(0, ...data.rows.map(row => String(row[categoryField] ?? '').length)) : 0;
|
|
112
|
-
const matrixYField = spec.encoding.y?.field || 'y', matrixLabelLength = spec.type === 'heatmap' ? Math.max(0, ...data.rows.map(row => String(row[matrixYField] ?? '').length)) : 0;
|
|
113
|
-
const sideLabelLength = Math.max(categoryLabelLength, matrixLabelLength);
|
|
114
|
-
const centeredPlot = ['pie', 'funnel', 'gauge', 'radar'].includes(spec.type), plotLeft = centeredPlot ? Math.max(p.left, p.right) : ['bar', 'heatmap'].includes(spec.type) ? Math.min(width * 0.35, Math.max(p.left, sideLabelLength * fontSize(spec, 'axis', 12) * 0.62 + 24)) : p.left, plotRight = centeredPlot ? plotLeft : p.right;
|
|
115
|
-
const labelTopReserve = spec.type === 'column' && spec.labels?.enabled ? Math.ceil(fontSize(spec, 'label', 12) * 1.5 + 4) : 0;
|
|
116
|
-
const plotTop = Math.max(p.top, chrome.bottom + (chrome.bottom ? 12 : 0)) + labelTopReserve;
|
|
117
|
-
const plotWidth = Math.max(1, width - plotLeft - plotRight);
|
|
118
|
-
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;
|
|
119
|
-
const xLabels = axisLabelLayout(spec, spec.type === 'bar' ? [] : axisLabelTexts, plotWidth);
|
|
120
|
-
const bottomReserve = Math.max(p.bottom, Math.ceil(16 + xLabels.projectedHeight + (spec.xAxis?.title ? 20 : 0)));
|
|
121
|
-
const plot = { x: plotLeft, y: plotTop, width: plotWidth, height: Math.max(1, height - plotTop - bottomReserve) };
|
|
122
114
|
const yEncodings = Array.isArray(spec.encoding.y) ? spec.encoding.y : [spec.encoding.y || { field: 'value', type: 'quantitative' }];
|
|
123
115
|
const yField = yEncodings[0]?.field || 'value';
|
|
124
116
|
const stackMode = typeof spec.stack === 'string' ? spec.stack : spec.stack?.mode;
|
|
@@ -140,17 +132,52 @@ function layout(spec, data, legendEntries = []) {
|
|
|
140
132
|
const rawRightMin = Math.min(0, ...(yRightNumbers.length ? yRightNumbers : [0])), rawRightMax = Math.max(1, ...(yRightNumbers.length ? yRightNumbers : [1]));
|
|
141
133
|
const rightType = spec.yAxis?.right?.type || yEncodings[1]?.type || 'linear';
|
|
142
134
|
const [rightMin, rightMax] = resolveAxisDomain(rawRightMin, rawRightMax, spec.yAxis?.right || {}, rightType);
|
|
135
|
+
const yTicks = axisTicks(min, max, spec.yAxis || {}, axisType), rightTicks = axisTicks(rightMin, rightMax, spec.yAxis?.right || {}, rightType);
|
|
136
|
+
const axisSize = fontSize(spec, 'axis', 12);
|
|
137
|
+
const maxWidth = values => Math.max(0, ...values.map(value => estimateTextWidth(value, axisSize)));
|
|
138
|
+
const categoryField = spec.encoding.x?.field || 'name';
|
|
139
|
+
const categoryTexts = data.rows.map(row => String(row[categoryField] ?? ''));
|
|
140
|
+
const matrixYField = spec.encoding.y?.field || 'y', matrixTexts = spec.type === 'heatmap' ? data.rows.map(row => String(row[matrixYField] ?? '')) : [];
|
|
141
|
+
const leftTexts = spec.type === 'bar' ? categoryTexts : yTicks.map(value => formatValue(value, spec.yAxis?.format, spec.locale));
|
|
142
|
+
const leftLabelWidth = maxWidth(leftTexts), rightLabelWidth = yEncodings.length > 1 && !stackMode ? maxWidth(rightTicks.map(value => formatValue(value, spec.yAxis?.right?.format, spec.locale))) : 0;
|
|
143
|
+
const centeredPlot = ['pie', 'funnel', 'gauge', 'radar'].includes(spec.type);
|
|
144
|
+
const sideLabelWidth = Math.max(spec.type === 'bar' ? leftLabelWidth : 0, spec.type === 'heatmap' ? maxWidth(matrixTexts) : 0);
|
|
145
|
+
const leftTitleBand = spec.yAxis?.title ? axisSize + 16 : 0;
|
|
146
|
+
const rightTitleBand = spec.yAxis?.right?.title && yEncodings.length > 1 && !stackMode ? axisSize + 16 : 0;
|
|
147
|
+
const cartesianLeft = 8 + leftTitleBand + leftLabelWidth + 10;
|
|
148
|
+
const plotLeft = centeredPlot
|
|
149
|
+
? Math.max(p.left, p.right)
|
|
150
|
+
: spec.type === 'bar'
|
|
151
|
+
? Math.max(p.left, spec.yAxis?.title ? cartesianLeft : sideLabelWidth + 24)
|
|
152
|
+
: spec.type === 'heatmap'
|
|
153
|
+
? Math.min(width * 0.35, Math.max(p.left, sideLabelWidth + 24))
|
|
154
|
+
: Math.max(p.left, cartesianLeft);
|
|
155
|
+
const plotRight = centeredPlot ? plotLeft : Math.max(p.right, rightLabelWidth ? rightLabelWidth + rightTitleBand + 18 : 0);
|
|
156
|
+
const labelTopReserve = spec.type === 'column' && spec.labels?.enabled ? Math.ceil(fontSize(spec, 'label', 12) * 1.5 + 4) : 0;
|
|
157
|
+
const plotTop = Math.max(p.top, chrome.bottom + (chrome.bottom ? 12 : 0)) + labelTopReserve;
|
|
158
|
+
const plotWidth = Math.max(1, width - plotLeft - plotRight);
|
|
159
|
+
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;
|
|
160
|
+
const xLabels = axisLabelLayout(spec, spec.type === 'bar' ? [] : axisLabelTexts, plotWidth);
|
|
161
|
+
const bottomReserve = Math.max(p.bottom, Math.ceil(16 + xLabels.projectedHeight + (spec.xAxis?.title ? 20 : 0)));
|
|
162
|
+
const plot = { x: plotLeft, y: plotTop, width: plotWidth, height: Math.max(1, height - plotTop - bottomReserve) };
|
|
163
|
+
const axisTitles = {
|
|
164
|
+
y: spec.yAxis?.title ? truncateText(spec.yAxis.title, plot.height, axisSize) : null,
|
|
165
|
+
right: spec.yAxis?.right?.title && yEncodings.length > 1 && !stackMode ? truncateText(spec.yAxis.right.title, plot.height, axisSize) : null
|
|
166
|
+
};
|
|
167
|
+
const axisTitleWarnings = [];
|
|
168
|
+
if (spec.yAxis?.title && axisTitles.y !== String(spec.yAxis.title)) axisTitleWarnings.push({ path: 'yAxis.title', title: String(spec.yAxis.title), availableWidth: plot.height });
|
|
169
|
+
if (spec.yAxis?.right?.title && axisTitles.right !== String(spec.yAxis.right.title) && yEncodings.length > 1 && !stackMode) axisTitleWarnings.push({ path: 'yAxis.right.title', title: String(spec.yAxis.right.title), availableWidth: plot.height });
|
|
143
170
|
const rightTransform = value => rightType === 'log' ? Math.log10(Math.max(0.000001, Number(value))) : Number(value);
|
|
144
171
|
const yRight = value => plot.y + plot.height - ((rightTransform(value) - rightTransform(rightMin)) / (rightTransform(rightMax) - rightTransform(rightMin) || 1)) * plot.height;
|
|
145
|
-
const yTicks = axisTicks(min, max, spec.yAxis || {}, axisType), rightTicks = axisTicks(rightMin, rightMax, spec.yAxis?.right || {}, rightType);
|
|
146
172
|
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' });
|
|
147
|
-
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 };
|
|
173
|
+
return { plot, chrome, xLabels, axisTitles, axisTitleWarnings, axisLayout: { size: axisSize, leftLabelWidth, rightLabelWidth }, 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 };
|
|
148
174
|
}
|
|
149
175
|
|
|
150
176
|
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 })); }
|
|
151
177
|
|
|
152
178
|
function addAxes(scene, spec, state) {
|
|
153
179
|
const { plot, categories, min, max, x, y, yRight } = state;
|
|
180
|
+
const axisSize = state.axisLayout.size;
|
|
154
181
|
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 } }));
|
|
155
182
|
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 } }));
|
|
156
183
|
if (spec.type === 'bar') {
|
|
@@ -159,7 +186,7 @@ function addAxes(scene, spec, state) {
|
|
|
159
186
|
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' }));
|
|
160
187
|
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' }));
|
|
161
188
|
if (spec.xAxis?.title) addText(scene, 'axis-x-title', spec.xAxis.title, plot.x + plot.width / 2, plot.y + plot.height + 42, { fill: spec.theme.text, font: font(spec, 'axis'), textAnchor: 'middle' });
|
|
162
|
-
if (
|
|
189
|
+
if (state.axisTitles.y) addText(scene, 'axis-y-title', state.axisTitles.y, 8 + axisSize / 2, plot.y + plot.height / 2, { fill: spec.theme.text, font: font(spec, 'axis'), textAnchor: 'middle', textBaseline: 'middle', baseline: 'middle', rotation: -90 });
|
|
163
190
|
return;
|
|
164
191
|
}
|
|
165
192
|
const labels = state.temporal || state.quantitativeX ? timeTicks(state.xMin, state.xMax, 5) : categories;
|
|
@@ -172,7 +199,8 @@ function addAxes(scene, spec, state) {
|
|
|
172
199
|
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' }));
|
|
173
200
|
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') }));
|
|
174
201
|
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' });
|
|
175
|
-
if (
|
|
202
|
+
if (state.axisTitles.y) addText(scene, 'axis-y-title', state.axisTitles.y, 8 + axisSize / 2, plot.y + plot.height / 2, { fill: spec.theme.text, font: font(spec, 'axis'), textAnchor: 'middle', textBaseline: 'middle', baseline: 'middle', rotation: -90 });
|
|
203
|
+
if (state.axisTitles.right) addText(scene, 'axis-y-right-title', state.axisTitles.right, spec.width - 8 - axisSize / 2, plot.y + plot.height / 2, { fill: spec.theme.text, font: font(spec, 'axis'), textAnchor: 'middle', textBaseline: 'middle', baseline: 'middle', rotation: 90 });
|
|
176
204
|
}
|
|
177
205
|
|
|
178
206
|
function addTitles(scene, spec, positions) {
|
|
@@ -354,6 +382,8 @@ export function buildScene(spec) {
|
|
|
354
382
|
const series = Array.isArray(spec.encoding.y) ? spec.encoding.y : [{ ...spec.encoding.y, name: spec.encoding.y?.name || spec.encoding.y?.field }];
|
|
355
383
|
const entries = legendEntries(spec, data, series, colors);
|
|
356
384
|
const state = layout(spec, data, entries);
|
|
385
|
+
if (state.chrome.legend.truncatedCount) data.warnings.push({ code: 'LEGEND_OVERFLOW', path: 'legend', count: state.chrome.legend.truncatedCount, message: `${state.chrome.legend.truncatedCount} legend label${state.chrome.legend.truncatedCount === 1 ? '' : 's'} exceeded the available width and was truncated.`, suggestion: 'Use shorter category names or a wider chart.' });
|
|
386
|
+
state.axisTitleWarnings.forEach(({ path, title, availableWidth }) => data.warnings.push({ code: 'TITLE_TRUNCATED', path, availableWidth, message: `Axis title "${title}" exceeded the available vertical plot space and was truncated.`, suggestion: 'Use a taller chart or a shorter axis title.' }));
|
|
357
387
|
scene._labelArea = state.plot;
|
|
358
388
|
addTitles(scene, spec, state.chrome.title);
|
|
359
389
|
if (!['pie', 'funnel', 'gauge', 'heatmap', 'radar'].includes(spec.type)) addAxes(scene, spec, state);
|
package/src/command.mjs
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
* Commands are JSON-safe, stable-ID based, and do not mutate input data.
|
|
4
4
|
*/
|
|
5
5
|
import { copyJSON, issue } from './schema.mjs';
|
|
6
|
+
import { commandTypes } from './contract-registry.mjs';
|
|
6
7
|
|
|
7
8
|
export const commandVersion = '1.0';
|
|
8
|
-
export const operationTypes = [
|
|
9
|
+
export const operationTypes = [...commandTypes];
|
|
9
10
|
|
|
10
11
|
export function normalizeCommand(input) {
|
|
11
12
|
const command = copyJSON(input);
|
|
@@ -63,7 +64,7 @@ export function commandCapabilities(schema) {
|
|
|
63
64
|
const fields = schema?.fields || {};
|
|
64
65
|
const can = name => fields[name]?.editable !== false && fields[name]?.agentEditable !== false;
|
|
65
66
|
const flowNode = ['flow-node', 'architecture-node', 'mindmap-node'].includes(schema?.name);
|
|
66
|
-
const flowEdge = ['flow-edge', 'architecture-edge'].includes(schema?.name);
|
|
67
|
+
const flowEdge = ['flow-edge', 'architecture-edge', 'mindmap-edge'].includes(schema?.name);
|
|
67
68
|
return {
|
|
68
69
|
updateField: Object.keys(fields).filter(can),
|
|
69
70
|
updateRecord: true,
|