@taylorwong/ichartjs 2.0.13 → 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.
@@ -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": { "key": "id", "required": ["id", "name", "start", "end"], "editable": ["name", "start", "end", "progress", "status", "dependencies"] },
6
- "timeline-event": { "key": "id", "required": ["id", "title", "date"], "editable": ["title", "date", "description", "status"] },
7
- "milestone": { "key": "id", "required": ["id", "title", "date"], "editable": ["title", "date", "status"] },
8
- "burndown-sample": { "key": "id", "required": ["id", "date", "remaining"], "editable": ["date", "remaining", "scopeChange"] },
9
- "flow-node": { "key": "id", "required": ["id", "label"], "editable": ["label", "position", "size", "laneId", "description", "status"] },
10
- "flow-edge": { "key": "id", "required": ["id", "from", "to"], "editable": ["from", "to", "fromPort", "toPort", "label", "status", "routing", "curveTension", "waypoints"] },
11
- "swimlane": { "key": "id", "required": ["id", "label"], "editable": ["label"] },
12
- "architecture-node": { "key": "id", "required": ["id", "label"], "editable": ["label", "layerId", "boundaryId", "position", "size", "description", "status"] },
13
- "architecture-edge": { "key": "id", "required": ["id", "from", "to"], "editable": ["from", "to", "label", "relation", "routing", "curveTension", "waypoints", "status"] },
14
- "mindmap-node": { "key": "id", "required": ["id", "label"], "editable": ["label", "parentId", "position", "size", "branch", "description", "status"] }
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.13",
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",
@@ -37,14 +37,20 @@
37
37
  "test": "node --test tests/*.test.mjs",
38
38
  "test:browser": "node --test tests/browser/*.test.mjs",
39
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",
40
45
  "docs:check": "node scripts/check-agent-docs.mjs",
41
46
  "example:agent": "node examples/agent-workflow.mjs",
42
47
  "playground": "node scripts/serve-playground.mjs",
43
- "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",
44
49
  "rc:check": "npm test && npm run check && git diff --check"
45
50
  },
46
51
  "devDependencies": {
47
- "playwright-core": "^1.63.0"
52
+ "playwright-core": "^1.63.0",
53
+ "typescript": "^5.9.3"
48
54
  },
49
55
  "repository": {
50
56
  "type": "git",
@@ -52,8 +58,18 @@
52
58
  },
53
59
  "homepage": "https://github.com/wanghetommy/ichartjs#readme",
54
60
  "bugs": "https://github.com/wanghetommy/ichartjs/issues",
55
- "keywords": ["chart", "visualization", "agent", "svg", "canvas", "gantt", "diagram"],
56
- "engines": { "node": ">=18" },
61
+ "keywords": [
62
+ "chart",
63
+ "visualization",
64
+ "agent",
65
+ "svg",
66
+ "canvas",
67
+ "gantt",
68
+ "diagram"
69
+ ],
70
+ "engines": {
71
+ "node": ">=18"
72
+ },
57
73
  "author": "taylorwong <wanghetommy@gmail.com>",
58
74
  "license": "Apache-2.0"
59
75
  }
@@ -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.13/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.
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
 
@@ -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 const chartTypes = ['line', 'area', 'bar', 'column', 'pie', 'scatter', 'funnel', 'gauge', 'heatmap', 'radar', 'gantt', 'timeline', 'milestone', 'burndown', 'flow', 'swimlane', 'architecture', 'mindmap'];
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: '1.0',
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: ['canvas', 'svg'],
212
+ renderers: [...renderers],
213
+ interactionDefaults: { ...interactionDefaults },
244
214
  interactions: [...new Set(Object.values(chartProfiles).flatMap(profile => profile.interactions))],
245
- exports: ['png', 'svg', 'json'],
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
@@ -142,27 +142,35 @@ function layout(spec, data, legendEntries = []) {
142
142
  const leftLabelWidth = maxWidth(leftTexts), rightLabelWidth = yEncodings.length > 1 && !stackMode ? maxWidth(rightTicks.map(value => formatValue(value, spec.yAxis?.right?.format, spec.locale))) : 0;
143
143
  const centeredPlot = ['pie', 'funnel', 'gauge', 'radar'].includes(spec.type);
144
144
  const sideLabelWidth = Math.max(spec.type === 'bar' ? leftLabelWidth : 0, spec.type === 'heatmap' ? maxWidth(matrixTexts) : 0);
145
- const leftTitleBand = spec.yAxis?.title && spec.type !== 'bar' ? axisSize + 16 : 0;
145
+ const leftTitleBand = spec.yAxis?.title ? axisSize + 16 : 0;
146
146
  const rightTitleBand = spec.yAxis?.right?.title && yEncodings.length > 1 && !stackMode ? axisSize + 16 : 0;
147
147
  const cartesianLeft = 8 + leftTitleBand + leftLabelWidth + 10;
148
148
  const plotLeft = centeredPlot
149
149
  ? Math.max(p.left, p.right)
150
- : ['bar', 'heatmap'].includes(spec.type)
151
- ? Math.min(width * 0.35, Math.max(p.left, sideLabelWidth + 24))
152
- : Math.max(p.left, cartesianLeft);
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);
153
155
  const plotRight = centeredPlot ? plotLeft : Math.max(p.right, rightLabelWidth ? rightLabelWidth + rightTitleBand + 18 : 0);
154
156
  const labelTopReserve = spec.type === 'column' && spec.labels?.enabled ? Math.ceil(fontSize(spec, 'label', 12) * 1.5 + 4) : 0;
155
- const barTitleReserve = spec.type === 'bar' && spec.yAxis?.title ? axisSize + 8 : 0;
156
- const plotTop = Math.max(p.top, chrome.bottom + (chrome.bottom ? 12 : 0)) + labelTopReserve + barTitleReserve;
157
+ const plotTop = Math.max(p.top, chrome.bottom + (chrome.bottom ? 12 : 0)) + labelTopReserve;
157
158
  const plotWidth = Math.max(1, width - plotLeft - plotRight);
158
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;
159
160
  const xLabels = axisLabelLayout(spec, spec.type === 'bar' ? [] : axisLabelTexts, plotWidth);
160
161
  const bottomReserve = Math.max(p.bottom, Math.ceil(16 + xLabels.projectedHeight + (spec.xAxis?.title ? 20 : 0)));
161
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 });
162
170
  const rightTransform = value => rightType === 'log' ? Math.log10(Math.max(0.000001, Number(value))) : Number(value);
163
171
  const yRight = value => plot.y + plot.height - ((rightTransform(value) - rightTransform(rightMin)) / (rightTransform(rightMax) - rightTransform(rightMin) || 1)) * plot.height;
164
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' });
165
- return { plot, chrome, xLabels, 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 };
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 };
166
174
  }
167
175
 
168
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 })); }
@@ -178,7 +186,7 @@ function addAxes(scene, spec, state) {
178
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' }));
179
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' }));
180
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' });
181
- if (spec.yAxis?.title) addText(scene, 'axis-y-title', truncateText(spec.yAxis.title, Math.max(axisSize, plot.x - 16), axisSize), 8, plot.y - 8, { fill: spec.theme.text, font: font(spec, 'axis'), textAnchor: 'start' });
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 });
182
190
  return;
183
191
  }
184
192
  const labels = state.temporal || state.quantitativeX ? timeTicks(state.xMin, state.xMax, 5) : categories;
@@ -191,8 +199,8 @@ function addAxes(scene, spec, state) {
191
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' }));
192
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') }));
193
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' });
194
- if (spec.yAxis?.title) addText(scene, 'axis-y-title', truncateText(spec.yAxis.title, plot.height, axisSize), 8 + axisSize / 2, plot.y + plot.height / 2, { fill: spec.theme.text, font: font(spec, 'axis'), textAnchor: 'middle', textBaseline: 'middle', baseline: 'middle', rotation: -90 });
195
- if (state.yEncodings.length > 1 && !spec.stack && spec.yAxis?.right?.title) addText(scene, 'axis-y-right-title', truncateText(spec.yAxis.right.title, plot.height, axisSize), 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 });
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 });
196
204
  }
197
205
 
198
206
  function addTitles(scene, spec, positions) {
@@ -375,6 +383,7 @@ export function buildScene(spec) {
375
383
  const entries = legendEntries(spec, data, series, colors);
376
384
  const state = layout(spec, data, entries);
377
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.' }));
378
387
  scene._labelArea = state.plot;
379
388
  addTitles(scene, spec, state.chrome.title);
380
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 = ['updateField', 'updateRecord', 'updateTask', 'shiftTask', 'updateProgress', 'addDependency', 'removeDependency', 'updateMilestone', 'moveNode', 'moveNodes', 'moveNodeToLane', 'resizeNode', 'alignNodes', 'snapNodes', 'moveGroup', 'resizeGroup', 'assignNodesToGroup', 'duplicateGroup', 'deleteGroup', 'updateEdge', 'removeEdge', 'toggleGroupCollapse', 'addEdge', 'duplicateSelection', 'pasteSelection'];
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,
@@ -0,0 +1,59 @@
1
+ /**
2
+ * JSON-safe public contract metadata shared by runtime discovery and release checks.
3
+ * Keep this module dependency-free so browser consumers can import the registry safely.
4
+ */
5
+ export const contractVersion = '1.1';
6
+ export const chartTypes = ['line', 'area', 'bar', 'column', 'pie', 'scatter', 'funnel', 'gauge', 'heatmap', 'radar', 'gantt', 'timeline', 'milestone', 'burndown', 'flow', 'swimlane', 'architecture', 'mindmap'];
7
+ export const renderers = ['canvas', 'svg'];
8
+ export const exportTypes = ['png', 'jpeg', 'svg', 'json'];
9
+ export const interactionDefaults = { zoom: false, pan: false, brush: false, drag: false, edgeDrag: false, portConnect: false, editing: false };
10
+ export const commandTypes = ['updateField', 'updateRecord', 'updateTask', 'shiftTask', 'updateProgress', 'addDependency', 'removeDependency', 'updateMilestone', 'moveNode', 'moveNodes', 'moveNodeToLane', 'resizeNode', 'alignNodes', 'snapNodes', 'moveGroup', 'resizeGroup', 'assignNodesToGroup', 'duplicateGroup', 'deleteGroup', 'updateEdge', 'removeEdge', 'addEdge', 'toggleGroupCollapse', 'duplicateSelection', 'pasteSelection'];
11
+ export const diagramOperations = ['moveNode', 'moveNodes', 'resizeNode', 'alignNodes', 'snapNodes', 'moveNodeToLane', 'moveGroup', 'resizeGroup', 'assignNodesToGroup', 'duplicateGroup', 'deleteGroup', 'updateEdge', 'removeEdge', 'addEdge', 'toggleGroupCollapse', 'duplicateSelection', 'pasteSelection'];
12
+ export const businessModels = ['project-task', 'timeline-event', 'milestone', 'burndown-sample', 'flow-node', 'flow-edge', 'swimlane', 'architecture-node', 'architecture-edge', 'mindmap-edge', 'mindmap-node'];
13
+ export const diagramEdgeModels = ['flow-edge', 'architecture-edge', 'mindmap-edge'];
14
+
15
+ const status = (supported, notApplicable = []) => Object.fromEntries(supported.map(name => [name, 'supported']).concat(notApplicable.map(name => [name, 'not-applicable'])));
16
+ const commonPresentation = ['title', 'subtitle', 'theme', 'responsive', 'empty-state', 'invalid-data-state', 'export', 'branding'];
17
+
18
+ export const chartDefinitions = {
19
+ 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']) },
20
+ 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']) },
21
+ 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']) },
22
+ 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']) },
23
+ 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']) },
24
+ 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']) },
25
+ funnel: { family: 'stage', intents: ['funnel', 'conversion'], required: ['dimension', 'measure'], optional: [], interactions: ['hover', 'tooltip', 'selection', 'keyboard'], features: status([...commonPresentation, 'labels', 'formatting'], ['axes', 'grid', 'zoom', 'pan']) },
26
+ 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']) },
27
+ 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']) },
28
+ 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']) },
29
+ 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']) },
30
+ timeline: { family: 'project', intents: ['timeline', 'milestone'], required: ['date', 'label'], optional: ['status'], interactions: ['hover', 'tooltip', 'selection', 'zoom', 'pan', 'keyboard'], features: status([...commonPresentation, 'labels', 'formatting']) },
31
+ 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']) },
32
+ 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']) },
33
+ 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']) },
34
+ 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']) },
35
+ 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']) },
36
+ 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']) }
37
+ };
38
+
39
+ export const chartProfiles = Object.fromEntries(chartTypes.map(type => [type, {
40
+ type,
41
+ ...chartDefinitions[type],
42
+ dataShapes: chartDefinitions[type].family === 'diagram' ? ['diagram-spec'] : chartDefinitions[type].family === 'project' ? ['records'] : ['records', 'data-table'],
43
+ renderers: [...renderers],
44
+ exports: [...exportTypes],
45
+ limits: type === 'pie' ? { recommendedCategories: 8 } : type === 'radar' ? { minimumIndicators: 3, recommendedIndicators: 8 } : type === 'heatmap' ? { recommendedCells: 2500 } : {}
46
+ }]));
47
+
48
+ export const contractRegistry = Object.freeze({
49
+ contractVersion,
50
+ chartTypes: [...chartTypes],
51
+ chartProfiles: JSON.parse(JSON.stringify(chartProfiles)),
52
+ renderers: [...renderers],
53
+ exportTypes: [...exportTypes],
54
+ interactionDefaults: { ...interactionDefaults },
55
+ commandTypes: [...commandTypes],
56
+ diagramOperations: [...diagramOperations],
57
+ businessModels: [...businessModels],
58
+ diagramEdgeModels: [...diagramEdgeModels]
59
+ });
@@ -60,7 +60,7 @@ export class EditController {
60
60
  edgeSchema: spec.data.edgeSchema,
61
61
  type: spec.type,
62
62
  nodeModel: spec.type === 'architecture' ? 'architecture-node' : spec.type === 'mindmap' ? 'mindmap-node' : 'flow-node',
63
- edgeModel: spec.type === 'architecture' ? 'architecture-edge' : 'flow-edge',
63
+ edgeModel: spec.type === 'architecture' ? 'architecture-edge' : spec.type === 'mindmap' ? 'mindmap-edge' : 'flow-edge',
64
64
  validationOptions: { ...spec.validationOptions, references: { ...spec.validationOptions?.references, ...(diagram ? { 'flow-node': nodes.map(row => row.id), swimlane: lanes.map(row => row.id) } : {}) } },
65
65
  requireConfirmation: spec.editing?.requireConfirmation,
66
66
  grid: spec.diagram?.grid || 8,