@taylorwong/ichartjs 2.0.16 → 2.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.0.18 - 2026-09-28
4
+
5
+ - Added lightweight Flow semantics for start/end, process, decision, input/output, and connector nodes, including explicit decision branches and loop edges with shared SVG/Canvas geometry.
6
+ - Exposed Flow semantic capabilities and validation diagnostics through runtime metadata, TypeScript declarations, Agent Skill guidance, and generated manifests.
7
+ - Updated the full Gallery Flow example to demonstrate all supported semantic node kinds, Yes/No branches, a retry loop, and connector usage.
8
+
9
+ ## 2.0.17 - 2026-09-25
10
+
11
+ - Added explicit `FUNNEL_LABEL_TRUNCATED` diagnostics when narrow stages cannot display their full labels.
12
+ - Added consistent `UNSUPPORTED_DIAGRAM_LABEL_FIELD` diagnostics for `swimlane.lanes[].name`.
13
+ - Exposed Timeline/Milestone horizontal time-axis metadata through runtime state and explanations, with Agent documentation and browser regression coverage.
14
+
3
15
  ## 2.0.16 - 2026-09-25
4
16
 
5
17
  - Hardened Timeline and Milestone date layouts with proportional positioning, date-aware ticks, edge padding, collision-aware marker staggering, and `TIMELINE_COLLISION` diagnostics for physically constrained charts.
package/README.md CHANGED
@@ -44,7 +44,7 @@ getCapabilities
44
44
  npm install @taylorwong/ichartjs@^2
45
45
  ```
46
46
 
47
- As a fallback for environments without npm access, install directly from GitHub: `npm install github:wanghetommy/ichartjs#v2.0.16`.
47
+ As a fallback for environments without npm access, install directly from GitHub: `npm install github:wanghetommy/ichartjs#v2.0.18`.
48
48
 
49
49
  ### Optional Agent Skill
50
50
 
@@ -66,7 +66,7 @@ For a non-interactive global Codex installation:
66
66
  npx skills add wanghetommy/ichartjs --skill ichartjs --agent codex --global --yes
67
67
  ```
68
68
 
69
- For a release-pinned installation, use `npx skills add https://github.com/wanghetommy/ichartjs/tree/v2.0.16/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.
69
+ For a release-pinned installation, use `npx skills add https://github.com/wanghetommy/ichartjs/tree/v2.0.18/skills/ichartjs --agent codex --global --yes`. WorkBuddy users can import the same tagged `skills/ichartjs` URL through the host's Skill interface; do not assume a `--agent workbuddy` adapter unless the installed CLI declares it. Package consumers can still copy `node_modules/@taylorwong/ichartjs/skills/ichartjs` as a manual fallback. After installation, invoke `$ichartjs` when named Skill invocation is supported, or select `ichartjs` in the host UI.
70
70
 
71
71
  ### Agent workflow
72
72
 
@@ -6,7 +6,7 @@ Agent usage and development guide for generic data analysis and metric visualiza
6
6
 
7
7
  - `scatter` is a single x/y measure pair; it does not provide series or legend encoding.
8
8
  - `heatmap` uses matrix row/column labels from `encoding.x` and `encoding.y`; Cartesian `xAxis`/`yAxis` configuration is not supported.
9
- - `funnel` renders stage names from `encoding.category` (default `name`) and optional values from `labels.enabled`; it has no legend.
9
+ - `funnel` renders stage names from `encoding.category` (default `name`) and optional values from `labels.enabled`; it has no legend. If a narrow stage cannot fit its name, the label is truncated and `FUNNEL_LABEL_TRUNCATED` is reported; increase width or shorten the stage name rather than treating the ellipsis as source data.
10
10
  - `area` and `column` accept at most two quantitative `encoding.y` measures.
11
11
 
12
12
  ## Supported Types
@@ -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.16/skills/ichartjs`. WorkBuddy can import that tagged directory through its Skill interface; only use a host-specific `--agent` value when the installed CLI declares it.
36
+ For global non-interactive Codex setup, append `--agent codex --global --yes`. To pin the released workflow, install `https://github.com/wanghetommy/ichartjs/tree/v2.0.18/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
 
@@ -40,7 +40,7 @@ iChart.js 2.0 is a new Agent-first product line. Compatibility with 1.x and a 1.
40
40
 
41
41
  ## Iteration 13 Addendum — 2026-09-23
42
42
 
43
- The historical 2.0.0 release decision remains unchanged. Release `v2.0.16` includes Iteration 13 contract hardening without adding a chart type: 18 chart profiles, 25 commands, 11 schemas, contract version `1.1`, atomic mutation validation, Mindmap edge discovery, strict TypeScript consumer compilation, no active source cycles, Node tests, Chromium browser acceptance, package dry-run verification, the 13F axis-free layout strategy, and Timeline/Milestone layout hardening. It also corrects Bar y-axis title layout and exposes `TITLE_TRUNCATED` when physical chart height forces truncation.
43
+ The historical 2.0.0 release decision remains unchanged. Release `v2.0.18` includes Iteration 13 contract hardening and Iteration 14 lightweight Flow semantics without adding a chart type: 18 chart profiles, 25 commands, 11 schemas, contract version `1.1`, atomic mutation validation, Mindmap edge discovery, strict TypeScript consumer compilation, no active source cycles, Node tests, Chromium browser acceptance, package dry-run verification, the 13F axis-free layout strategy, Timeline/Milestone layout hardening, runtime diagnostics for constrained Funnel labels and Swimlane label aliases, and renderer-parity Flow semantic shapes, branches, loops, and connectors. It also corrects Bar y-axis title layout and exposes `TITLE_TRUNCATED` when physical chart height forces truncation.
44
44
 
45
45
  ## Deferred npm Publication
46
46
 
@@ -0,0 +1,58 @@
1
+ # Iteration 14 — Lightweight Flow Semantics
2
+
3
+ Iteration 14 enhances the existing `flow` chart instead of adding a new chart type. The goal is a small, readable process notation that an Agent can generate from a compact JSON Spec without introducing BPMN-level complexity.
4
+
5
+ ## Scope
6
+
7
+ ### 14A — Semantic nodes
8
+
9
+ Flow nodes support the following `kind` values:
10
+
11
+ | Kind | Shape | Meaning |
12
+ | --- | --- | --- |
13
+ | `start` | ellipse | Process start |
14
+ | `end` | ellipse | Process end |
15
+ | `process` | rectangle | Operation or step |
16
+ | `decision` | diamond | Condition with branches |
17
+ | `io` | parallelogram | Input or output |
18
+ | `connector` | circle | Explicit hand-off point for a disconnected flow |
19
+
20
+ Nodes without `kind` use `process` as the minimal default. Non-Flow diagrams continue to use their existing node visual language.
21
+
22
+ ### 14B — Process structures
23
+
24
+ - Sequential execution uses ordinary `from` → `to` edges.
25
+ - Decision branches use `edge.label`, normally `yes` / `no` or localized equivalents.
26
+ - Loops are ordinary edges that point to an earlier node; cycles are allowed for Flow.
27
+ - Connectors never use implicit matching. Every connection is explicit in `edges`.
28
+
29
+ The capability contract exposes these rules through `getCapabilities().diagram.flowNodeKinds`, `flowBranchEdges`, `flowLoops`, and `flowConnectors`.
30
+
31
+ ### 14C — Layout and rendering
32
+
33
+ - Existing layered, manual, and orthogonal layout/routing remain the source of truth.
34
+ - Semantic shapes share the existing Scene Graph, hit testing, selection, edge routing, export, and accessibility surfaces.
35
+ - SVG and Canvas use the same node geometry and edge path data.
36
+ - Process-only Flow remains concise when `kind` is omitted; this is a convenience default, not a migration compatibility promise. Non-process semantics should use an explicit `kind`.
37
+
38
+ ### 14D — Validation and Agent guidance
39
+
40
+ - Unknown `nodes[].kind` values are validation errors.
41
+ - A semantic `decision` with fewer than two outgoing edges produces `FLOW_DECISION_BRANCHES`.
42
+ - Semantic Flows without `start` or `end` produce actionable warnings.
43
+ - Unlabeled connectors produce `FLOW_CONNECTOR_LABEL` as a readability warning.
44
+ - Flow schemas, TypeScript declarations, recipes, bilingual guides, manifests, and focused tests stay synchronized.
45
+
46
+ ## Acceptance
47
+
48
+ - All six node kinds render in both SVG and Canvas.
49
+ - Sequential, branch, and loop structures validate and render.
50
+ - Connector paths remain explicit and inspectable through `explain()` / JSON export.
51
+ - Old rectangle-only Flow examples remain renderable through the `process` default.
52
+ - `npm run contracts:check`, `npm run types:check`, `npm run docs:check`, `npm test`, and browser acceptance pass.
53
+
54
+ ## Non-goals
55
+
56
+ - No `note` node in this iteration.
57
+ - No BPMN events, gateways, subprocesses, or implicit connector name resolution.
58
+ - No new chart type, CLI, MCP, HTTP, or Python runtime.
@@ -1,6 +1,6 @@
1
1
  # iChart.js 2.0 Roadmap
2
2
 
3
- > Roadmap baseline: 2026-09-14. Current release status: `v2.0.16` includes the completed Iteration 12 structured-diagram work, Iteration 13 contract hardening, the Bar y-axis title layout fix, the 13F axis-free chart layout strategy, and Timeline/Milestone layout hardening. Geographic charts and 3D rendering remain out of scope until explicitly reintroduced.
3
+ > Roadmap baseline: 2026-09-14. Current release status: `v2.0.18` includes the completed Iteration 12 structured-diagram work, Iteration 13 contract hardening, Iteration 14 lightweight Flow semantics, the Bar y-axis title layout fix, the 13F axis-free chart layout strategy, Timeline/Milestone layout hardening, and chart diagnostic improvements. Geographic charts and 3D rendering remain out of scope until explicitly reintroduced.
4
4
 
5
5
  ## Current Status
6
6
 
@@ -15,7 +15,8 @@
15
15
  - Iteration 10A Export Contract Hardening was implemented and released in `v2.0.5`: export representations and type errors are deterministic, optional Node raster export uses `exportAsync()`, Canvas/SVG paint semantics are aligned, and the playground server has safer port/path handling. No chart behavior or public chart type was added.
16
16
  - Iteration 11 visual preference controls are included in the `v2.0.6` release: compact per-chart settings, capability-aware visibility controls, theme-aware icon contrast, explicit font-size defaults, shared page preferences, and Agent-adjustable global settings.
17
17
  - Iteration 12A–12G is included in `v2.0.7`: shared structured-diagram contracts, Architecture layers/boundaries, Mindmap parent-child tree/radial layouts with true cubic-Bezier edges, Agent schemas/capabilities, renderer-parity edge hit testing and selection, waypoint/segment handles, persistent manual routing, and Gallery/documentation coverage. Navigation and editing remain disabled by default and require explicit host activation.
18
- - Iteration 13A–13F is complete and included in `v2.0.16`: canonical contract registry and generated capability projection, atomic Spec/data/style mutations, row-complete project validation, explicit Mindmap edge metadata, complete public TypeScript declarations and consumer fixture, module-cycle and documentation/example gates, browser acceptance, package dry-run evidence, an axis-free layout strategy for Pie, Funnel, Gauge, and Radar, and Timeline/Milestone layout hardening. No chart type was added.
18
+ - Iteration 13A–13F is complete and included in `v2.0.18`: canonical contract registry and generated capability projection, atomic Spec/data/style mutations, row-complete project validation, explicit Mindmap edge metadata, complete public TypeScript declarations and consumer fixture, module-cycle and documentation/example gates, browser acceptance, package dry-run evidence, an axis-free layout strategy for Pie, Funnel, Gauge, and Radar, Timeline/Milestone layout hardening, and runtime diagnostics for constrained Funnel labels and Swimlane label aliases. No chart type was added.
19
+ - Iteration 14A–14D is complete and included in `v2.0.18`: lightweight Flow semantics for start/end, process, decision, input/output, and connector nodes, explicit decision branches and loops, shared SVG/Canvas geometry, capability discovery, validation, and Gallery coverage. No new chart type was added.
19
20
  - The original `2.0.0` readiness record is historical and superseded by the published `2.0.x` releases. Current release checks are defined by `docs/agent/development/release-sop.md`.
20
21
 
21
22
  ## Iteration 4 — Agent Data Contract and Business Editing
@@ -43,11 +43,40 @@ For a mindmap, prefer the compact parent contract:
43
43
  }
44
44
  ```
45
45
 
46
+ ### Lightweight Flow Semantics
47
+
48
+ `flow` supports a small, unambiguous set of semantic node kinds. Legacy nodes without `kind` remain process rectangles.
49
+
50
+ ```js
51
+ {
52
+ type: 'flow',
53
+ nodes: [
54
+ { id: 'start', kind: 'start', label: 'Start' },
55
+ { id: 'load', kind: 'process', label: 'Load data' },
56
+ { id: 'valid', kind: 'decision', label: 'Valid?' },
57
+ { id: 'output', kind: 'io', label: 'Output result' },
58
+ { id: 'loop', kind: 'connector', label: 'retry' },
59
+ { id: 'end', kind: 'end', label: 'End' }
60
+ ],
61
+ edges: [
62
+ { id: 'start-load', from: 'start', to: 'load' },
63
+ { id: 'load-valid', from: 'load', to: 'valid' },
64
+ { id: 'valid-yes', from: 'valid', to: 'output', label: 'yes' },
65
+ { id: 'valid-no', from: 'valid', to: 'loop', label: 'no' },
66
+ { id: 'loop-load', from: 'loop', to: 'load' },
67
+ { id: 'output-end', from: 'output', to: 'end' }
68
+ ]
69
+ }
70
+ ```
71
+
72
+ Supported kinds are `start`, `end`, `process`, `decision`, `io`, and `connector`. A decision should have at least two outgoing labeled edges. Loops are ordinary explicit `from`/`to` edges and are allowed. Connectors are circular hand-off points; they do not use implicit matching, so every connection remains visible in `edges`.
73
+
46
74
  ## Current Capabilities
47
75
 
48
76
  Supported:
49
77
 
50
78
  - Nodes, edges, lanes, groups, and ports.
79
+ - Flow semantic shapes: start/end, process, decision, input/output, and connector.
51
80
  - `manual`, `layered`, `tree`, and `radial` layouts.
52
81
  - Architecture layers and boundaries, plus mindmap parent-child derivation.
53
82
  - `straight`, `orthogonal`, and true cubic-Bezier `curved` routing, with adjustable `curveTension` and obstacle-aware orthogonal fallback.
@@ -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.16`.
13
+ For environments without npm registry access, install from GitHub as a fallback: `npm install github:wanghetommy/ichartjs#v2.0.18`.
14
14
 
15
15
  Use the package through a bundler or another environment that resolves npm ESM imports:
16
16
 
@@ -24,7 +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; timeline and milestone use date-proportional horizontal positions while rows remain evenly spaced vertically. Generic `xAxis.title/format` and `xAxis.min/max` settings do not customize them.
27
+ - Project chart date axes are derived from `date`, `start`, and `end` records in the current contract; Timeline and Milestone use a horizontal time axis: `date` maps to x/`cx`, while y/`cy` is only the event row used for layout. Read `chart.getState().timeAxis` or `chart.explain().timeAxis` when an Agent needs the orientation, field, or resolved domain. Generic `xAxis.title/format` and `xAxis.min/max` settings do not customize them.
28
28
 
29
29
  ## Agent Workflow
30
30
 
@@ -51,6 +51,7 @@ Agent usage and development guide for project planning, delivery tracking, and p
51
51
  - Capacity warnings, risk quadrants, and aging buckets should stay explainable from source fields.
52
52
  - Gantt, Timeline, and Milestone reserve their left label column from Unicode-aware text widths. Labels wider than the bounded column are truncated by rendered width rather than character count.
53
53
  - Timeline and Milestone place events on a date-proportional horizontal domain with a small visual edge padding. Nearby markers are automatically staggered when possible; if the available height is insufficient, the result includes `TIMELINE_COLLISION` and suggests increasing height or reducing events.
54
+ - For automated geometry checks, compare event `x`/`cx` values with the horizontal tick `x` positions; equal y/`cy` spacing is expected and does not mean dates were ignored.
54
55
  - `locale` also applies to project date ticks, burndown forecast text, and project tooltips. Use `locale: 'zh-CN'` or another BCP 47 locale when those runtime labels must be localized.
55
56
 
56
57
  ## Editing
@@ -103,6 +103,8 @@ const spec = {
103
103
 
104
104
  Use the selected capability and recipes for Pie, Gauge, Heatmap, Radar, project views, and diagrams because their required encodings differ.
105
105
 
106
+ Use `data: { values: rows }` as the canonical public shape. The runtime also accepts a raw row array for compatibility, and the minimal recipe catalog uses that shorter form; do not mix diagram collections into `data.values`. Diagram inputs such as `nodes`, `edges`, `lanes`, `layers`, and `boundaries` stay at the Spec top level.
107
+
106
108
  #### Put options at the contract level
107
109
 
108
110
  Keep `encoding` for field roles and series semantics. Put presentation and axis options at the Spec level:
@@ -147,6 +149,8 @@ if (!validation.valid) {
147
149
 
148
150
  Do not silently discard diagnostics. Preserve stable `code`, `path`, `message`, `expected`, and `suggestion` fields in Agent output and automated repair loops.
149
151
 
152
+ Diagnostics have two phases: `validateSpec()` reports preflight contract problems, while layout-dependent warnings can appear only after `createChart()` in `chart.getState().warnings` and `chart.explain().warnings`. Always inspect both before returning a chart, especially for narrow Funnel labels.
153
+
150
154
  ### 6. Render
151
155
 
152
156
  Browser rendering:
@@ -165,6 +169,7 @@ Headless environment capabilities:
165
169
  - JSON and SVG string export work with zero dependencies.
166
170
  - PNG/JPEG raster export requires the optional `canvas` npm package; otherwise a structured `HEADLESS_EXPORT_UNSUPPORTED` error is returned.
167
171
  - The on-screen renderer is decoupled from the export backend; any mounted renderer can export any supported format.
172
+ - Prefer `svg` for accessibility, DOM inspection, and diagram editing; prefer `canvas` for larger mark counts when lower DOM overhead matters.
168
173
 
169
174
  ### 7. Explain, Export, and Self-Check
170
175
 
@@ -25,6 +25,7 @@ Unknown intent results also include `intentKnown`, `intentSuggestions`, and `fal
25
25
  - Chart layout and data semantics are renderer-independent.
26
26
  - Keep axis titles/formats on `xAxis`/`yAxis`, labels on `labels`, and legend settings on `legend`; misplaced options return structured warnings.
27
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`.
28
+ - Timeline and Milestone expose `chart.getState().timeAxis` and `chart.explain().timeAxis` with `orientation: "horizontal"`, `field: "date"`, `coordinate: "x"`, `rowCoordinate: "y"`, numeric `domain`, and ISO `domainISO`. Validate event dates against `x`/`cx`; y/`cy` is the row layout coordinate.
28
29
  - Diagram structure is always top-level: `flow` and `swimlane` use `nodes/edges/lanes`; `architecture` uses `nodes/edges/layers/boundaries`; `mindmap` uses `nodes` with `parentId` and optional `edges`. Do not put these fields under `data`; generic charts use `data.values`.
29
30
  - `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.
30
31
 
@@ -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.16/skills/ichartjs \
124
+ npx skills add https://github.com/wanghetommy/ichartjs/tree/v2.0.18/skills/ichartjs \
125
125
  --agent codex --global --yes
126
126
  ```
127
127
 
@@ -6,7 +6,7 @@
6
6
 
7
7
  - `scatter` 只支持一组 x/y 数值字段,不提供 Series 或 Legend 编码。
8
8
  - `heatmap` 使用 `encoding.x` 和 `encoding.y` 的矩阵行列标签,不支持笛卡尔 `xAxis`/`yAxis` 配置。
9
- - `funnel` 从 `encoding.category`(默认 `name`)渲染阶段名称,`labels.enabled` 决定是否追加数值;不提供 Legend。
9
+ - `funnel` 从 `encoding.category`(默认 `name`)渲染阶段名称,`labels.enabled` 决定是否追加数值;不提供 Legend。阶段过窄放不下名称时会截断,并报告 `FUNNEL_LABEL_TRUNCATED`;应增大宽度或缩短阶段名称,不要把省略号当成源数据。
10
10
  - `area` 和 `column` 的 `encoding.y` 最多接受两个数值度量。
11
11
 
12
12
  ## 图表选择
@@ -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.16/skills/ichartjs`。WorkBuddy 可通过自身 Skill 界面导入该带 Tag 的目录;只有当前 CLI 明确声明对应适配器时才使用宿主专用 `--agent` 参数。
25
+ Codex 全局无交互安装可追加 `--agent codex --global --yes`。需要固定发布版本时,安装 `https://github.com/wanghetommy/ichartjs/tree/v2.0.18/skills/ichartjs`。WorkBuddy 可通过自身 Skill 界面导入该带 Tag 的目录;只有当前 CLI 明确声明对应适配器时才使用宿主专用 `--agent` 参数。
26
26
 
27
27
  使用 `npx skills add wanghetommy/ichartjs --list` 验证发现结果,其中应包含 `ichartjs`。
28
28
 
@@ -42,9 +42,37 @@ Architecture 的 `layers` 是从上到下排列的水平分层。同层自动布
42
42
  }
43
43
  ```
44
44
 
45
+ ## 轻量级流程语义
46
+
47
+ `flow` 支持一组小而明确的节点语义。没有设置 `kind` 的旧节点继续渲染为处理矩形,保持兼容。
48
+
49
+ ```js
50
+ {
51
+ type: 'flow',
52
+ nodes: [
53
+ { id: 'start', kind: 'start', label: '开始' },
54
+ { id: 'load', kind: 'process', label: '读取数据' },
55
+ { id: 'valid', kind: 'decision', label: '是否有效?' },
56
+ { id: 'output', kind: 'io', label: '输出结果' },
57
+ { id: 'loop', kind: 'connector', label: '重试' },
58
+ { id: 'end', kind: 'end', label: '结束' }
59
+ ],
60
+ edges: [
61
+ { id: 'start-load', from: 'start', to: 'load' },
62
+ { id: 'load-valid', from: 'load', to: 'valid' },
63
+ { id: 'valid-yes', from: 'valid', to: 'output', label: '是' },
64
+ { id: 'valid-no', from: 'valid', to: 'loop', label: '否' },
65
+ { id: 'loop-load', from: 'loop', to: 'load' },
66
+ { id: 'output-end', from: 'output', to: 'end' }
67
+ ]
68
+ }
69
+ ```
70
+
71
+ 支持的 `kind` 为 `start`、`end`、`process`、`decision`、`io` 和 `connector`。判断节点建议至少有两条带标签的出边;循环就是普通的显式 `from` / `to` 回边。Connector 是圆形的断开连接点,不使用隐式同名匹配,所有连接都必须明确写在 `edges` 中。
72
+
45
73
  ## 当前能力
46
74
 
47
- 已支持节点、边、泳道、Group、Port、四种布局、三种路由、Mindmap 三次贝塞尔曲线、节点拖动、多选、对齐、网格吸附、键盘移动、Copy/Paste、Group 折叠展开、Port 键盘连线、边选择、折点/正交线段手柄、持久化 `waypoints`、边删除和 Undo/Redo。
75
+ 已支持节点、边、泳道、Group、Port、Flow 语义图形、四种布局、三种路由、Mindmap 三次贝塞尔曲线、节点拖动、多选、对齐、网格吸附、键盘移动、Copy/Paste、Group 折叠展开、Port 键盘连线、边选择、折点/正交线段手柄、持久化 `waypoints`、边删除和 Undo/Redo。
48
76
 
49
77
  导航和编辑能力默认关闭。普通图表保持静态,缩放、平移、框选、节点拖动、线段拖动、Port 连线和结构编辑必须由宿主显式开启:
50
78
 
@@ -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.16`。
9
+ 无法访问 npm Registry 的环境请用 GitHub 源作为后备:`npm install github:wanghetommy/ichartjs#v2.0.18`。
10
10
 
11
11
  ```js
12
12
  import { createChart } from '@taylorwong/ichartjs';
@@ -21,8 +21,9 @@
21
21
  - `scopeChange` 表示范围变化,不等于已完成工作量。
22
22
  - Forecast 是估计结果,不能描述为承诺或事实。
23
23
  - Gantt、Timeline 和 Milestone 根据 Unicode-aware 文本宽度预留左侧标签列;超过列宽的标签按像素省略,不按字符数截断。
24
- - Timeline 和 Milestone 的横向位置按 `date` 比例计算,纵向行槽位保持均匀,并在时间域边缘保留少量视觉留白;Milestone 覆盖层使用 `baselineDate` 和 `actualDate`,显示文本正式字段为 `title`(`name` 和 `label` 仍由兼容回退逻辑接受)。相邻标记会在可能时自动错位;如果空间不足,结果会返回 `TIMELINE_COLLISION`,建议增大高度或减少事件数量。
24
+ - Timeline 和 Milestone 使用横向时间轴:`date` 映射到 x/`cx`,y/`cy` 只表示用于布局的事件行。Agent 需要确认方向、字段和计算后的日期域时,应读取 `chart.getState().timeAxis` 或 `chart.explain().timeAxis`。两者会在时间域边缘保留少量视觉留白;Milestone 覆盖层使用 `baselineDate` 和 `actualDate`,显示文本正式字段为 `title`(`name` 和 `label` 仍由兼容回退逻辑接受)。相邻标记会在可能时自动错位;如果空间不足,结果会返回 `TIMELINE_COLLISION`,建议增大高度或减少事件数量。
25
25
  - `locale` 同时作用于项目日期刻度、Burndown 预测文案和项目 Tooltip;需要中文运行时文案时使用 `locale: 'zh-CN'`。
26
+ - 自动验收时应比较事件的 `x`/`cx` 与横向刻度的 `x`;y/`cy` 等距是正常的事件行布局,不表示日期被忽略。
26
27
 
27
28
  ## 编辑流程
28
29
 
@@ -34,7 +34,7 @@ Agent 与开发者使用同一个 ESM 入口。编码 Agent 的完整方式见 [
34
34
  4. 当 `requiredFields` 非空时停止渲染,向用户请求数据或选择有依据的备选方案。
35
35
  5. 构建 JSON 可序列化的 Spec,并调用 `validateSpec()`。Spec 可通过 `branding: false` 显式关闭品牌署名;默认保留署名以提升项目可见性。
36
36
  6. 仅在 `validation.valid` 为 `true` 时调用 `createChart()`。
37
- 7. 用 `chart.explain()`、`chart.getState()`、JSON/SVG/PNG export 完成自检。需要持久化或附件生成时使用 `chart.export({type:'json'|'svg'|'png'})`,在浏览器环境可调用 `chart.downloadPNG()` / `chart.downloadSVG()` / `chart.downloadJSON()` 触发保存,最后调用 `chart.destroy()`。
37
+ 7. 用 `chart.explain()`、`chart.getState()`、JSON/SVG/PNG export 完成自检。JSON/SVG 可直接使用同步 `chart.export()`;浏览器 PNG 使用 `chart.toDataURL()` 或 `chart.downloadPNG()`;Node 无头 PNG/JPEG 使用 `chart.exportAsync()`,可选安装 `canvas`。最后调用 `chart.destroy()`。
38
38
 
39
39
  ### 意图必须使用注册词
40
40
 
@@ -70,6 +70,8 @@ const spec = structuredClone(catalog.examples.radar);
70
70
 
71
71
  Agent 自检使用 `chart.getState().health` 和 `chart.explain().health`,其中包含 `ready`、`degraded`、`empty`、警告数、隐藏标签数、限制值数和已渲染标记数。`locale` 默认 `en-US`,需要中文输出时设置 `locale: "zh-CN"`,输入日期仍使用 ISO-8601。
72
72
 
73
+ 推荐正式数据写法是 `data: { values: rows }`;Runtime 也兼容直接传入行数组,最小 Recipe 为保持紧凑使用了数组形式。Flow、Swimlane、Architecture、Mindmap 的 `nodes`、`edges`、`lanes`、`layers` 和 `boundaries` 必须放在 Spec 顶层。`validateSpec()` 负责渲染前的契约检查;窄 Funnel 标签等依赖布局的告警要在 `chart.getState().warnings` 或 `chart.explain().warnings` 中检查。需要 DOM 可访问性或 Diagram 编辑时优先 SVG,大量标记且希望降低 DOM 开销时选择 Canvas。
74
+
73
75
  ## 品牌署名(Branding)默认行为
74
76
 
75
77
  - 默认 `branding: true`:在画面与所有导出产物(PNG/SVG/JSON)右下角同步出现 `Powered by iChart.js` 低对比度署名。
@@ -28,6 +28,7 @@ Iteration 8 通过 `getChartCapability(type)` 提供逐图表能力档案,包
28
28
  - Diagram 结构字段必须位于 Spec 顶层:通用图表使用 `data.values`;Flow/Swimlane 使用 `nodes/edges/lanes`,Architecture 使用 `nodes/edges/layers/boundaries`,Mindmap 使用带 `parentId` 的 `nodes` 和可选 `edges`。不要将这些字段放在 `data` 内。
29
29
  - `svg` 适合 DOM 交互和可访问性;`canvas` 适合大量图元和绘制性能。
30
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
+ - Timeline 和 Milestone 的 `chart.getState().timeAxis` 与 `chart.explain().timeAxis` 会返回 `orientation: "horizontal"`、`field: "date"`、`coordinate: "x"`、`rowCoordinate: "y"`、数值 `domain` 和 ISO `domainISO`。验收事件日期时应比较 `x`/`cx`;y/`cy` 只是事件行布局坐标。
31
32
  - `chart.getState().health` 和 `chart.explain().health` 提供 `ready`、`degraded` 或 `empty`,以及可渲染性、问题代码、警告数、隐藏标签数、限制值数和已渲染标记数。`locale` 默认 `en-US`,可设置 `zh-CN` 影响输出格式;输入日期应使用 ISO-8601 字符串。
32
33
 
33
34
  ## 品牌署名(Branding)
@@ -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.16/skills/ichartjs \
119
+ npx skills add https://github.com/wanghetommy/ichartjs/tree/v2.0.18/skills/ichartjs \
120
120
  --agent codex --global --yes
121
121
  ```
122
122
 
@@ -282,8 +282,8 @@
282
282
  "Short form `branding: false` and object form `branding: { enabled: false }` are both supported. Custom text, link, position, or font are intentionally NOT exposed in the current iteration."
283
283
  ]
284
284
  },
285
- "packageVersion": "2.0.16",
286
- "runtimeVersion": "2.0.16",
285
+ "packageVersion": "2.0.18",
286
+ "runtimeVersion": "2.0.18",
287
287
  "chartTypes": [
288
288
  "line",
289
289
  "area",
@@ -1080,7 +1080,9 @@
1080
1080
  "optional": [
1081
1081
  "edges",
1082
1082
  "groups",
1083
- "ports"
1083
+ "ports",
1084
+ "semantic-node-kinds",
1085
+ "connectors"
1084
1086
  ],
1085
1087
  "interactions": [
1086
1088
  "hover",
@@ -1112,6 +1114,10 @@
1112
1114
  "layout": "supported",
1113
1115
  "groups": "supported",
1114
1116
  "ports": "supported",
1117
+ "semantic-shapes": "supported",
1118
+ "decision-branches": "supported",
1119
+ "connectors": "supported",
1120
+ "loops": "supported",
1115
1121
  "history": "supported",
1116
1122
  "axes": "not-applicable",
1117
1123
  "grid": "not-applicable",
@@ -81,6 +81,7 @@
81
81
  ],
82
82
  "editable": [
83
83
  "label",
84
+ "kind",
84
85
  "position",
85
86
  "size",
86
87
  "laneId",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taylorwong/ichartjs",
3
- "version": "2.0.16",
3
+ "version": "2.0.18",
4
4
  "description": "Agent-first, renderer-independent charting and project visualization runtime",
5
5
  "type": "module",
6
6
  "main": "./src/index.mjs",
@@ -21,9 +21,9 @@ 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.16/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.18/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
- 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:
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 npm:
27
27
 
28
28
  ```bash
29
29
  npm install @taylorwong/ichartjs@^2
@@ -40,11 +40,11 @@ Do not install the unscoped npm registry package named `ichartjs`; it is current
40
40
  5. Inspect the complete planning result, including `styleRecommendation`, warnings, and fallback status.
41
41
  6. Stop when `requiredFields` is non-empty; request data or explain a supported alternative.
42
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 `category`/`value`, Gauge `value`, Heatmap `x`/`y`/`color`, and Radar `indicators[].field`. Funnel stage text comes from `encoding.category` (default `name`) and is always rendered; `labels.enabled` additionally renders values. To load a recipe, use `import catalog from '@taylorwong/ichartjs/recipes/minimal-specs' with { type: 'json' }` and select `catalog.examples[type]`; `recipes/` is the public export path backed by the package's `agent-recipes/` directory.
43
+ 8. Use chart-specific channels: Cartesian `x`/`y`, Pie/Funnel `category`/`value`, Gauge `value`, Heatmap `x`/`y`/`color`, and Radar `indicators[].field`. Funnel stage text comes from `encoding.category` (default `name`) and `labels.enabled` additionally renders values; narrow stages may report `FUNNEL_LABEL_TRUNCATED`, so increase width or shorten the stage name. For Flow, use top-level `nodes` and `edges`, `nodes[].kind` from `getCapabilities().diagram.flowNodeKinds`, and edge `label` for decision branches. `connector` uses explicit `from`/`to` edges and loops are allowed. To load a recipe, use `import catalog from '@taylorwong/ichartjs/recipes/minimal-specs' with { type: 'json' }` and select `catalog.examples[type]`; `recipes/` is the public export path backed by the package's `agent-recipes/` directory.
44
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`, `ZERO_TOTAL`, `UNKNOWN_INTENT`, misplaced-option, and unsupported-axis warnings before presenting the chart.
45
+ 10. Call `validateSpec()` before rendering and repair its preflight errors, warnings, and normalizations. After `createChart()`, inspect `chart.getState()` or `chart.explain()` for render-time diagnostics such as `FUNNEL_LABEL_TRUNCATED`, `VALUE_CLAMPED`, `LABELS_SUPPRESSED`, `NEGATIVE_VALUE_DROPPED`, and `ZERO_TOTAL`; do not assume every layout warning is available during preflight.
46
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`, `NEGATIVE_VALUE_DROPPED`, and `ZERO_TOTAL` as material diagnostics to report.
47
+ 12. Self-check with `chart.explain()`, `chart.getState()`, `health.renderable`, and JSON export. Treat `VALUE_CLAMPED`, `LABELS_SUPPRESSED`, `NEGATIVE_VALUE_DROPPED`, `FUNNEL_LABEL_TRUNCATED`, and `ZERO_TOTAL` as material diagnostics to report.
48
48
  13. Provide an exact preview URL or artifact path and report assumptions, warnings, and deferred checks.
49
49
  14. Prefer `theme: { mode: 'auto', preset, palette }`; preserve explicit user style choices and use `chart.setTheme()` for live switching.
50
50
  15. For post-creation visual changes, call `getPreferenceCapabilities(chartType, { locale })`, validate the patch with `validatePreferences()`, apply it with `chart.setPreferences(..., { source: 'agent' })`, and verify `chart.getState().preferences`.
@@ -4,7 +4,7 @@
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
+ import { businessModels, chartProfiles, chartTypes, commandTypes, contractVersion, diagramEdgeModels, diagramOperations, exportTypes, flowNodeKinds, interactionDefaults, renderers } from './contract-registry.mjs';
8
8
 
9
9
  export { chartProfiles, chartTypes };
10
10
 
@@ -199,7 +199,7 @@ export function getCapabilities() {
199
199
  charts: JSON.parse(JSON.stringify(chartProfiles)),
200
200
  intents,
201
201
  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.' },
202
- 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'] },
202
+ 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'], flowNodeKinds: [...flowNodeKinds], flowBranchEdges: { label: 'edge.label', minimumOutgoing: 2 }, flowLoops: 'supported', flowConnectors: { kind: 'connector', linking: 'explicit from/to edges' } },
203
203
  projectManagement: project,
204
204
  projectIntelligence: {
205
205
  intents: ['schedule', 'milestone', 'progress', 'variance', 'capacity', 'release', 'risk', 'aging', 'workflow', 'responsibility', 'architecture', 'mindmap'],
package/src/charts.mjs CHANGED
@@ -430,7 +430,7 @@ export function buildScene(spec) {
430
430
  else data.rows.forEach((row, index) => { const rawValue = values[index], value = Number.isFinite(rawValue) ? Math.max(0, rawValue) : 0, end = angle + value / total * Math.PI * 2, middle = angle + (end - angle) / 2, fill = colors[index % colors.length], dataRef = { seriesIndex: 0, dataIndex: index, category: row[categoryField], value, rawValue }; 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, 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' }, fill); angle = end; });
431
431
  } else if (spec.type === 'funnel') {
432
432
  const categoryField = spec.encoding.category?.field || 'name', 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;
433
- data.rows.forEach((row, index) => { const value = Number(row[valueField]) || 0, category = String(row[categoryField] ?? row.name ?? row.id ?? `Stage ${index + 1}`), ratio = Math.max(0.1, value / maxValue), fill = colors[index % colors.length]; 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, category, value }; scene.add(new SceneNode({ id: `series-0-item-${index}`, type: 'rect', geometry, bounds: geometry, style: { fill }, dataRef, interactive: true })); const labelSize = fontSize(spec, 'label', 12), textColor = readableLabelColor(spec, fill), label = truncateText(category, Math.max(labelSize * 2, width - 12), labelSize); addText(scene, `funnel-category-${index}`, label, geometry.x + geometry.width / 2, geometry.y + geometry.height / 2 - (spec.labels?.enabled ? labelSize * 0.6 : 0), { fill: textColor, font: spec.labels?.font || font(spec, 'label'), textAnchor: 'middle', textBaseline: 'middle', baseline: 'middle' }, dataRef); if (spec.labels?.enabled) addText(scene, `funnel-value-${index}`, formatValue(value, spec.labels.format, spec.locale), geometry.x + geometry.width / 2, geometry.y + geometry.height / 2 + labelSize * 0.65, { fill: textColor, font: spec.labels?.font || font(spec, 'label'), textAnchor: 'middle', textBaseline: 'middle', baseline: 'middle' }, dataRef); });
433
+ data.rows.forEach((row, index) => { const value = Number(row[valueField]) || 0, category = String(row[categoryField] ?? row.name ?? row.id ?? `Stage ${index + 1}`), ratio = Math.max(0.1, value / maxValue), fill = colors[index % colors.length]; 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, category, value }; scene.add(new SceneNode({ id: `series-0-item-${index}`, type: 'rect', geometry, bounds: geometry, style: { fill }, dataRef, interactive: true })); const labelSize = fontSize(spec, 'label', 12), textColor = readableLabelColor(spec, fill), availableWidth = Math.max(labelSize * 2, width - 12), label = truncateText(category, availableWidth, labelSize); if (label !== category) data.warnings.push({ code: 'FUNNEL_LABEL_TRUNCATED', path: `data.values[${index}].${categoryField}`, index, rawLabel: category, renderedLabel: label, availableWidth, message: `Funnel stage label "${category}" was truncated to "${label}" because the stage is too narrow.`, suggestion: 'Increase chart width, shorten the stage label, or use a wider stage/value distribution.' }); addText(scene, `funnel-category-${index}`, label, geometry.x + geometry.width / 2, geometry.y + geometry.height / 2 - (spec.labels?.enabled ? labelSize * 0.6 : 0), { fill: textColor, font: spec.labels?.font || font(spec, 'label'), textAnchor: 'middle', textBaseline: 'middle', baseline: 'middle' }, dataRef); if (spec.labels?.enabled) addText(scene, `funnel-value-${index}`, formatValue(value, spec.labels.format, spec.locale), geometry.x + geometry.width / 2, geometry.y + geometry.height / 2 + labelSize * 0.65, { fill: textColor, font: spec.labels?.font || font(spec, 'label'), textAnchor: 'middle', textBaseline: 'middle', baseline: 'middle' }, dataRef); });
434
434
  } else if (spec.type === 'gauge') {
435
435
  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.68, radius = Math.min(state.plot.width * 0.42, state.plot.height * 0.62), start = Math.PI, end = start + Math.PI * ratio;
436
436
  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.' });
@@ -11,6 +11,7 @@ export const commandTypes = ['updateField', 'updateRecord', 'updateTask', 'shift
11
11
  export const diagramOperations = ['moveNode', 'moveNodes', 'resizeNode', 'alignNodes', 'snapNodes', 'moveNodeToLane', 'moveGroup', 'resizeGroup', 'assignNodesToGroup', 'duplicateGroup', 'deleteGroup', 'updateEdge', 'removeEdge', 'addEdge', 'toggleGroupCollapse', 'duplicateSelection', 'pasteSelection'];
12
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
13
  export const diagramEdgeModels = ['flow-edge', 'architecture-edge', 'mindmap-edge'];
14
+ export const flowNodeKinds = ['start', 'end', 'process', 'decision', 'io', 'connector'];
14
15
 
15
16
  const status = (supported, notApplicable = []) => Object.fromEntries(supported.map(name => [name, 'supported']).concat(notApplicable.map(name => [name, 'not-applicable'])));
16
17
  const commonPresentation = ['title', 'subtitle', 'theme', 'responsive', 'empty-state', 'invalid-data-state', 'export', 'branding'];
@@ -30,7 +31,7 @@ export const chartDefinitions = {
30
31
  timeline: { family: 'project', intents: ['timeline', 'milestone'], required: ['date', 'title'], optional: ['status'], interactions: ['hover', 'tooltip', 'selection', 'zoom', 'pan', 'keyboard'], features: status([...commonPresentation, 'labels', 'formatting']) },
31
32
  milestone: { family: 'project', intents: ['milestone'], required: ['date', 'title'], optional: ['baselineDate', 'actualDate'], interactions: ['hover', 'tooltip', 'selection', 'zoom', 'pan', 'keyboard'], features: status([...commonPresentation, 'labels', 'formatting', 'baseline-actual-variance']) },
32
33
  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
+ flow: { family: 'diagram', intents: ['workflow'], required: ['nodes'], optional: ['edges', 'groups', 'ports', 'semantic-node-kinds', 'connectors'], 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', 'semantic-shapes', 'decision-branches', 'connectors', 'loops', 'history'], ['axes', 'grid', 'legend']) },
34
35
  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
36
  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
37
  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']) }
@@ -55,5 +56,6 @@ export const contractRegistry = Object.freeze({
55
56
  commandTypes: [...commandTypes],
56
57
  diagramOperations: [...diagramOperations],
57
58
  businessModels: [...businessModels],
58
- diagramEdgeModels: [...diagramEdgeModels]
59
+ diagramEdgeModels: [...diagramEdgeModels],
60
+ flowNodeKinds: [...flowNodeKinds]
59
61
  });
package/src/diagram.mjs CHANGED
@@ -4,10 +4,12 @@
4
4
  */
5
5
  import { copyJSON } from './schema.mjs';
6
6
  import { sampleCubicBezier } from './scene.mjs';
7
+ import { flowNodeKinds } from './contract-registry.mjs';
7
8
 
8
9
  export const diagramLayoutModes = ['manual', 'layered', 'tree', 'radial'];
9
10
  export const edgeRoutingModes = ['straight', 'orthogonal', 'curved'];
10
11
  export const diagramModes = ['process', 'architecture', 'mindmap'];
12
+ export { flowNodeKinds };
11
13
 
12
14
  const finite = value => typeof value === 'number' && Number.isFinite(value);
13
15
 
@@ -37,6 +39,9 @@ export function validateDiagram(spec = {}) {
37
39
  if (typeof layer?.id !== 'string' || !layer.id) errors.push({ code: 'LAYER_ID', path: `layers.${index}.id`, message: 'Architecture layers require stable IDs.' });
38
40
  if (layer?.name !== undefined && layer?.label === undefined) warnings.push({ code: 'UNSUPPORTED_DIAGRAM_LABEL_FIELD', path: `layers.${index}.name`, message: 'Architecture layer display text uses label, not name.', suggestion: 'Rename layers[].name to layers[].label.' });
39
41
  });
42
+ lanes.forEach((lane, index) => {
43
+ if (lane?.name !== undefined && lane?.label === undefined) warnings.push({ code: 'UNSUPPORTED_DIAGRAM_LABEL_FIELD', path: `lanes.${index}.name`, message: 'Swimlane lane display text uses label, not name.', suggestion: 'Rename lanes[].name to lanes[].label.' });
44
+ });
40
45
  normalized.groups.forEach((group, index) => { if (group?.name !== undefined && group?.label === undefined) warnings.push({ code: 'UNSUPPORTED_DIAGRAM_LABEL_FIELD', path: `groups.${index}.name`, message: 'Group display text uses label, not name.', suggestion: 'Rename groups[].name to groups[].label.' }); });
41
46
  normalized.boundaries.forEach((boundary, index) => { if (boundary?.name !== undefined && boundary?.label === undefined) warnings.push({ code: 'UNSUPPORTED_DIAGRAM_LABEL_FIELD', path: `boundaries.${index}.name`, message: 'Boundary display text uses label, not name.', suggestion: 'Rename boundaries[].name to boundaries[].label.' }); });
42
47
  normalized.groups.forEach((group, index) => {
@@ -50,6 +55,7 @@ export function validateDiagram(spec = {}) {
50
55
  else if (nodeIds.has(node.id)) errors.push({ code: 'DUPLICATE_NODE_ID', path: `nodes.${index}.id`, message: `Duplicate node ID: ${node.id}.` });
51
56
  else nodeIds.add(node.id);
52
57
  if (node?.name !== undefined && node?.label === undefined) warnings.push({ code: 'UNSUPPORTED_DIAGRAM_LABEL_FIELD', path: `nodes.${index}.name`, message: 'Node display text uses label, not name.', suggestion: 'Rename nodes[].name to nodes[].label.' });
58
+ if (normalized.type === 'flow' && node.kind !== undefined && !flowNodeKinds.includes(node.kind)) errors.push({ code: 'FLOW_NODE_KIND', path: `nodes.${index}.kind`, message: `Unsupported Flow node kind: ${node.kind}.`, expected: flowNodeKinds, suggestion: `Use one of: ${flowNodeKinds.join(', ')}.` });
53
59
  if (node.position && (!finite(node.position.x) || !finite(node.position.y))) errors.push({ code: 'NODE_POSITION', path: `nodes.${index}.position`, message: 'Node positions must contain finite x and y.' });
54
60
  if (node.laneId && lanes.length && !laneIds.has(node.laneId)) errors.push({ code: 'MISSING_LANE', path: `nodes.${index}.laneId`, message: `Unknown lane ID: ${node.laneId}.` });
55
61
  if (node.size && (!finite(node.size.width) || !finite(node.size.height) || node.size.width <= 0 || node.size.height <= 0)) errors.push({ code: 'NODE_SIZE', path: `nodes.${index}.size`, message: 'Node sizes must be finite and positive.' });
@@ -85,6 +91,19 @@ export function validateDiagram(spec = {}) {
85
91
  if (edge.curveTension !== undefined && (!finite(edge.curveTension) || edge.curveTension < 0.2 || edge.curveTension > 0.8)) errors.push({ code: 'EDGE_CURVE_TENSION', path: `edges.${index}.curveTension`, message: 'Edge curve tension must be a finite number between 0.2 and 0.8.' });
86
92
  if (edge.waypoints !== undefined && (!Array.isArray(edge.waypoints) || edge.waypoints.some(point => !point || !finite(point.x) || !finite(point.y)))) errors.push({ code: 'EDGE_WAYPOINTS', path: `edges.${index}.waypoints`, message: 'Edge waypoints must be an array of finite x/y points.' });
87
93
  });
94
+ if (normalized.type === 'flow') {
95
+ const semanticNodes = nodes.filter(node => node.kind !== undefined), outgoing = new Map(nodes.map(node => [node.id, []]));
96
+ edges.forEach(edge => { if (outgoing.has(edge.from)) outgoing.get(edge.from).push(edge); });
97
+ semanticNodes.forEach(node => {
98
+ const nodeIndex = nodes.indexOf(node), path = `nodes.${nodeIndex}.kind`;
99
+ if (node.kind === 'decision' && (outgoing.get(node.id) || []).length < 2) warnings.push({ code: 'FLOW_DECISION_BRANCHES', path, message: 'A decision node should have at least two outgoing branches.', suggestion: 'Add labeled edges such as label: "yes" and label: "no".' });
100
+ if (node.kind === 'connector' && !node.label) warnings.push({ code: 'FLOW_CONNECTOR_LABEL', path, message: 'An unlabeled connector is valid but harder to follow in a detached flow.', suggestion: 'Add a short label when the connector is used to join separated flow sections.' });
101
+ });
102
+ if (semanticNodes.length) {
103
+ if (!semanticNodes.some(node => node.kind === 'start')) warnings.push({ code: 'FLOW_MISSING_START', path: 'nodes', message: 'Semantic Flow has no start node.', suggestion: 'Add one node with kind: "start", or use explicit kind: "process" for a process-only Flow.' });
104
+ if (!semanticNodes.some(node => node.kind === 'end')) warnings.push({ code: 'FLOW_MISSING_END', path: 'nodes', message: 'Semantic Flow has no end node.', suggestion: 'Add one node with kind: "end", or use explicit kind: "process" for a process-only Flow.' });
105
+ }
106
+ }
88
107
  if (!diagramLayoutModes.includes(normalized.diagram.layout)) errors.push({ code: 'LAYOUT_MODE', path: 'diagram.layout', message: `Unsupported layout: ${normalized.diagram.layout}.` });
89
108
  if (!finite(normalized.diagram.grid) || normalized.diagram.grid <= 0) errors.push({ code: 'DIAGRAM_GRID', path: 'diagram.grid', message: 'Grid must be a positive finite number.' });
90
109
  if (!finite(normalized.diagram.curveTension) || normalized.diagram.curveTension < 0.2 || normalized.diagram.curveTension > 0.8) errors.push({ code: 'CURVE_TENSION', path: 'diagram.curveTension', message: 'Curve tension must be a finite number between 0.2 and 0.8.' });
package/src/index.mjs CHANGED
@@ -23,7 +23,7 @@ import { explainChart, getCapabilities as discoverCapabilities, getChartCapabili
23
23
  import { applyPreferencesToSpec, createPreferencesStore, defaultPreferences, mergePreferences, mergeThemePreference, normalizePreferences, validatePreferences } from './preferences.mjs';
24
24
  import { mountChartSettings } from './preferences-ui.mjs';
25
25
  import { ChartValidationError } from './errors.mjs';
26
- import { diagramOperations } from './contract-registry.mjs';
26
+ import { diagramOperations, flowNodeKinds } from './contract-registry.mjs';
27
27
  import { destroyChart } from './chart-lifecycle.mjs';
28
28
 
29
29
  import { isDiagram, paintSelection, diagramPointer, diagramKeyboard } from './diagram-interaction.mjs';
@@ -455,13 +455,13 @@ export class Chart {
455
455
  const clampedValues = warnings.filter(item => item.code === 'VALUE_CLAMPED').reduce((sum, item) => sum + Number(item.count || 1), 0);
456
456
  return { version: '1.0', status: isEmpty ? 'empty' : warnings.length ? 'degraded' : 'ready', renderable: !isEmpty, issues: [...new Set(warnings.map(item => item.code))], metrics: { warnings: warnings.length, suppressedLabels, clampedValues, renderedMarks: marks.length } };
457
457
  }
458
- getState() { const brandingSignature = discoverCapabilities().branding.signature, warnings = this._getDiagnostics(), layoutState = this.model.state?.plot ? { family: this.model.state.layoutFamily || null, plot: clone(this.model.state.plot), chrome: clone(this.model.state.chrome || null), labels: clone(this.model.state.labelLayout || null) } : null; return { renderer: this.renderer.constructor.name, width: this.spec.width, height: this.spec.height, dataCount: this.model.data.rows.length, selected: [...this._selected.values()], revision: this._revision, history: this._history.state(), view: clone(this.spec.view || null), style: clone({ name: this.spec.theme.name, mode: this.spec.theme.mode, resolvedMode: this.spec.theme.resolvedMode, preset: this.spec.theme.preset, palette: this.spec.theme.palette, reasons: this.spec.theme.reasons }), layout: layoutState, 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) }; }
458
+ getState() { const brandingSignature = discoverCapabilities().branding.signature, warnings = this._getDiagnostics(), layoutState = this.model.state?.plot ? { family: this.model.state.layoutFamily || null, plot: clone(this.model.state.plot), chrome: clone(this.model.state.chrome || null), labels: clone(this.model.state.labelLayout || null) } : null; return { renderer: this.renderer.constructor.name, width: this.spec.width, height: this.spec.height, dataCount: this.model.data.rows.length, selected: [...this._selected.values()], revision: this._revision, history: this._history.state(), view: clone(this.spec.view || null), style: clone({ name: this.spec.theme.name, mode: this.spec.theme.mode, resolvedMode: this.spec.theme.resolvedMode, preset: this.spec.theme.preset, palette: this.spec.theme.palette, reasons: this.spec.theme.reasons }), layout: layoutState, timeAxis: clone(this.model.state?.timeAxis || null), axes: clone(this.model.state?.axes || null), health: this._getHealth(warnings), preferences: this.getPreferences(), branding: { enabled: Boolean(this.spec.branding?.enabled), signature: brandingSignature, text: this.spec.branding?.enabled === true ? brandingSignature : null }, warnings: clone(warnings), assumptions: clone(this.model.data?.assumptions || []), normalizations: clone(this._specDiagnostics?.normalizations || []), collapsedGroups: this.getCollapsedGroupIds(), clipboard: { nodes: this._clipboard?.nodes?.length || 0, edges: this._clipboard?.edges?.length || 0 }, projectAnalytics: clone(this.model.state?.projectAnalytics || null), linked: clone(this.model.state?.linked || null) }; }
459
459
  getProjectAnalytics() { return clone(this.model.state?.projectAnalytics || null); }
460
460
  getLinkedState() { return clone(this.model.state?.linked || null); }
461
461
  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(); }
462
462
  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(); }
463
463
  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]) }; }
464
- explain() { const explanation = explainChart(this.spec, this.model), state = this.getState(); return { ...explanation, layout: state.layout, warnings: state.warnings, health: state.health }; }
464
+ explain() { const explanation = explainChart(this.spec, this.model), state = this.getState(); return { ...explanation, layout: state.layout, timeAxis: state.timeAxis, warnings: state.warnings, health: state.health }; }
465
465
  getAccessibleDescription() { const description = this.spec.accessibility?.description || this.spec.title?.text || `${this.spec.type} chart`; return `${description}; ${this.model.data.rows.length} data items.`; }
466
466
  inspectDataSchema() { return inspectDataSchema(this.spec.data.schema || this.spec.schema); }
467
467
  validateData() { return validateData(this.toDataTable(), this.spec.data.schema || this.spec.schema, this.spec.validationOptions); }
@@ -762,7 +762,7 @@ export class Chart {
762
762
 
763
763
  export function createChart(spec) { return new Chart(spec); }
764
764
  export { ChartValidationError } from './errors.mjs';
765
- export function getCapabilities() { const capabilities = discoverCapabilities(); return { ...capabilities, interactionDefaults: { ...capabilities.interactionDefaults }, diagram: { layoutModes: diagramLayoutModes, routingModes: edgeRoutingModes, entities: ['node', 'edge', 'lane', 'group', 'port', 'waypoint'], operations: [...diagramOperations], curvedEdges: { renderers: ['canvas', 'svg'], type: 'cubic-bezier', tension: { minimum: 0.2, maximum: 0.8, default: 0.4 }, mindmapDefault: true, obstacleFallback: 'orthogonal', controlPointEditing: false }, edgeEditing: { renderers: ['canvas', 'svg'], selection: true, waypointDrag: true, segmentDrag: true, persistentWaypoints: true, enabledByDefault: false, activation: ['editing.enabled', 'interaction.edgeDrag'] }, validation: ['duplicate-ids', 'missing-endpoints', 'missing-ports', 'missing-lanes', 'invalid-waypoints', 'invalid-curve-tension'] }, editing: { modes: ['preview', 'commit', 'undo', 'redo'], operations: [...capabilities.commands], confirmationRequiredByDefault: true, commit: 'local-runtime-host-persistence-required' } }; }
765
+ export function getCapabilities() { const capabilities = discoverCapabilities(); return { ...capabilities, interactionDefaults: { ...capabilities.interactionDefaults }, diagram: { layoutModes: diagramLayoutModes, routingModes: edgeRoutingModes, entities: ['node', 'edge', 'lane', 'group', 'port', 'waypoint'], operations: [...diagramOperations], flowNodeKinds: [...flowNodeKinds], flowBranchEdges: { label: 'edge.label', minimumOutgoing: 2 }, flowLoops: 'supported', flowConnectors: { kind: 'connector', linking: 'explicit from/to edges' }, curvedEdges: { renderers: ['canvas', 'svg'], type: 'cubic-bezier', tension: { minimum: 0.2, maximum: 0.8, default: 0.4 }, mindmapDefault: true, obstacleFallback: 'orthogonal', controlPointEditing: false }, edgeEditing: { renderers: ['canvas', 'svg'], selection: true, waypointDrag: true, segmentDrag: true, persistentWaypoints: true, enabledByDefault: false, activation: ['editing.enabled', 'interaction.edgeDrag'] }, validation: ['duplicate-ids', 'missing-endpoints', 'missing-ports', 'missing-lanes', 'invalid-waypoints', 'invalid-curve-tension'] }, editing: { modes: ['preview', 'commit', 'undo', 'redo'], operations: [...capabilities.commands], confirmationRequiredByDefault: true, commit: 'local-runtime-host-persistence-required' } }; }
766
766
  export function recommend(input, options = {}) { const plan = planChart(input, options); return { primary: plan.primary, alternatives: plan.alternatives, reason: plan.reasons.join(' '), reasons: plan.reasons, confidence: plan.confidence, requiredFields: plan.requiredFields, assumptions: plan.assumptions, warnings: plan.warnings, nextActions: plan.nextActions }; }
767
767
  export { normalizeSpec, validateSpec, normalizeData, inspectData, binData, applyTransforms, data, getBusinessSchema, inspectDataSchema, validateData, getEditCapabilities, validateEdit, previewEdit, commitPreview, validateRecipe, getChartCapability, getPreferenceCapabilities, planChart, explainChart };
768
768
  export { diagramLayoutModes, diagramModes, edgeRoutingModes, normalizeDiagramSpec, validateDiagram, layoutDiagram, routeEdge };
@@ -771,4 +771,4 @@ export { normalizeLinkedFilters, normalizeLinkedSelection, filterProjectRows, cr
771
771
 
772
772
  export { contrastRatio, planStyle, resolveTheme, styleCapabilities, themeModes, themePalettes, themePresets, validateThemeContrast, annotationPlugin, dataZoomPlugin, dataLabelsPlugin, accessibilityPlugin };
773
773
  export { applyPreferencesToSpec, createPreferencesStore, defaultPreferences, mergePreferences, mergeThemePreference, mountChartSettings, normalizePreferences, validatePreferences };
774
- export const iChart = { version: '2.0.16', createChart, ChartValidationError, inspectData, normalizeData, binData, applyTransforms, data, getCapabilities, getChartCapability, getPreferenceCapabilities, planChart, recommend, explainChart, contrastRatio, planStyle, resolveTheme, styleCapabilities, themeModes, themePalettes, themePresets, validateThemeContrast, createPreferencesStore, defaultPreferences, normalizePreferences, mergePreferences, validatePreferences, applyPreferencesToSpec, mountChartSettings, annotationPlugin, dataZoomPlugin, dataLabelsPlugin, accessibilityPlugin, getBusinessSchema, inspectDataSchema, validateData, getEditCapabilities, validateEdit, previewEdit, commitPreview, validateRecipe, normalizeProjectCalendar, applyWorkingCalendar, normalizeDependencies, analyzeSchedule, analyzeBurndownSeries, analyzeCapacity, buildCapacityView, buildCumulativeFlowSeries, buildVelocitySeries, buildReleaseForecast, buildRiskMatrixSeries, buildIssueAgingSeries, normalizeLinkedFilters, normalizeLinkedSelection, filterProjectRows, createLinkedProjectState, linkedRecordId };
774
+ export const iChart = { version: '2.0.18', createChart, ChartValidationError, inspectData, normalizeData, binData, applyTransforms, data, getCapabilities, getChartCapability, getPreferenceCapabilities, planChart, recommend, explainChart, contrastRatio, planStyle, resolveTheme, styleCapabilities, themeModes, themePalettes, themePresets, validateThemeContrast, createPreferencesStore, defaultPreferences, normalizePreferences, mergePreferences, validatePreferences, applyPreferencesToSpec, mountChartSettings, annotationPlugin, dataZoomPlugin, dataLabelsPlugin, accessibilityPlugin, getBusinessSchema, inspectDataSchema, validateData, getEditCapabilities, validateEdit, previewEdit, commitPreview, validateRecipe, normalizeProjectCalendar, applyWorkingCalendar, normalizeDependencies, analyzeSchedule, analyzeBurndownSeries, analyzeCapacity, buildCapacityView, buildCumulativeFlowSeries, buildVelocitySeries, buildReleaseForecast, buildRiskMatrixSeries, buildIssueAgingSeries, normalizeLinkedFilters, normalizeLinkedSelection, filterProjectRows, createLinkedProjectState, linkedRecordId };
package/src/project.mjs CHANGED
@@ -55,6 +55,25 @@ function projectDateDomain(spec, rows) {
55
55
  const dependencyId = dependency => typeof dependency === 'string' ? dependency : dependency?.id || null;
56
56
  const dependencyType = dependency => typeof dependency === 'object' && dependency?.type ? dependency.type : 'finish-to-start';
57
57
  const markText = (theme, background) => (contrastRatio(theme.text, background) || 0) >= (contrastRatio(theme.background, background) || 0) ? theme.text : theme.background;
58
+ const flowNodeKind = node => node?.kind || 'process';
59
+
60
+ function flowNodeVisual(kind, box) {
61
+ const cx = box.x + box.width / 2, cy = box.y + box.height / 2;
62
+ if (kind === 'start' || kind === 'end') return { type: 'path', geometry: { points: Array.from({ length: 16 }, (_, index) => { const angle = index / 16 * Math.PI * 2; return { x: cx + box.width / 2 * Math.cos(angle), y: cy + box.height / 2 * Math.sin(angle) }; }), closed: true } };
63
+ if (kind === 'decision') return { type: 'path', geometry: { points: [{ x: cx, y: box.y }, { x: box.x + box.width, y: cy }, { x: cx, y: box.y + box.height }, { x: box.x, y: cy }], closed: true } };
64
+ if (kind === 'io') {
65
+ const skew = Math.min(18, box.width * 0.18);
66
+ return { type: 'path', geometry: { points: [{ x: box.x + skew, y: box.y }, { x: box.x + box.width, y: box.y }, { x: box.x + box.width - skew, y: box.y + box.height }, { x: box.x, y: box.y + box.height }], closed: true } };
67
+ }
68
+ if (kind === 'connector') return { type: 'circle', geometry: { cx, cy, r: Math.min(box.width, box.height) / 2 } };
69
+ return { type: 'rect', geometry: box };
70
+ }
71
+
72
+ function flowNodeBox(row, generated, fallback) {
73
+ const kind = flowNodeKind(row), connector = kind === 'connector';
74
+ const width = Number(row.size?.width) || (connector ? 28 : 112), height = Number(row.size?.height) || (connector ? 28 : 36);
75
+ return { x: row.position?.x ?? generated?.x ?? fallback.x, y: row.position?.y ?? generated?.y ?? fallback.y, width, height };
76
+ }
58
77
 
59
78
  function compactRoute(points) {
60
79
  return points.filter((point, index) => {
@@ -200,7 +219,7 @@ export function analyzeBurndown(rows, options = {}) {
200
219
  function tasksScene(scene, spec, rows, state) {
201
220
  const plot = state.plot;
202
221
  const overlays = projectOverlays(spec);
203
- const [min, max] = projectDateDomain(spec, rows);
222
+ const [min, max] = state.timeDomain || projectDateDomain(spec, rows);
204
223
  const map = timeAxis(scene, plot, min, max, spec.locale), positions = new Map();
205
224
  const analyticsMap = new Map((state.schedule?.tasks || []).map(task => [task.id, task]));
206
225
  const highlighted = new Set(spec.criticalPath === false || !overlays.criticalPath ? [] : Array.isArray(spec.criticalPath) ? spec.criticalPath : state.schedule?.criticalIds || []);
@@ -345,17 +364,18 @@ function diagramScene(scene, spec, rows, state) {
345
364
  const architectureBox = architectureLayout[row.id];
346
365
  const geometry = architectureBox
347
366
  ? { ...architectureBox, x: row.position?.x ?? architectureBox.x, y: row.position?.y ?? architectureBox.y }
348
- : { x: row.position?.x ?? generated?.x ?? plot.x + rank * gapX + 12, y: row.position?.y ?? generated?.y ?? plot.y + (lanes.length ? lane * laneHeight : 0) + (slot + 0.5) * (lanes.length ? laneHeight : Math.max(plot.height, sameCell * 64)) / sameCell - 18, width: row.size?.width || 112, height: row.size?.height || 36 };
367
+ : flowNodeBox(row, generated, { x: plot.x + rank * gapX + 12, y: plot.y + (lanes.length ? lane * laneHeight : 0) + (slot + 0.5) * (lanes.length ? laneHeight : Math.max(plot.height, sameCell * 64)) / sameCell - (flowNodeKind(row) === 'connector' ? 14 : 18) });
349
368
  positions.set(row.id, geometry);
350
369
  if (collapsedGroups.has(row.groupId)) return;
351
370
  const depth = row.parentId ? (ranks.get(row.id) || 0) : 0;
352
371
  const colorIndex = mode === 'mindmap' ? depth % spec.colors.length : lane >= 0 ? lane % spec.colors.length : index % spec.colors.length;
353
- scene.add({ id: `node-${row.id}`, type: 'rect', geometry, bounds: { ...geometry }, style: { fill: spec.colors[colorIndex], stroke: row.root || mode === 'mindmap' && !row.parentId ? spec.theme.focus : spec.theme.background, strokeWidth: row.root || mode === 'mindmap' && !row.parentId ? 2 : 1 }, dataRef: { nodeId: row.id, dataIndex: index, datum: row, groupId: row.groupId || null, layerId: row.layerId || null, parentId: row.parentId || null }, interactive: true, zIndex: 2 });
372
+ const kind = mode === 'process' && spec.type === 'flow' ? flowNodeKind(row) : 'process', visual = flowNodeVisual(kind, geometry);
373
+ scene.add({ id: `node-${row.id}`, type: visual.type, geometry: visual.geometry, bounds: { ...geometry }, style: { fill: spec.colors[colorIndex], stroke: row.root || mode === 'mindmap' && !row.parentId ? spec.theme.focus : spec.theme.background, strokeWidth: row.root || mode === 'mindmap' && !row.parentId ? 2 : 1 }, dataRef: { nodeId: row.id, kind, dataIndex: index, datum: row, groupId: row.groupId || null, layerId: row.layerId || null, parentId: row.parentId || null }, interactive: true, zIndex: 2 });
354
374
  if (spec.editing?.enabled && spec.interaction?.portConnect) (row.ports || []).forEach(port => {
355
375
  const point = port.side === 'left' ? { x: geometry.x, y: geometry.y + geometry.height * (port.offset ?? 0.5) } : port.side === 'top' ? { x: geometry.x + geometry.width * (port.offset ?? 0.5), y: geometry.y } : port.side === 'bottom' ? { x: geometry.x + geometry.width * (port.offset ?? 0.5), y: geometry.y + geometry.height } : { x: geometry.x + geometry.width, y: geometry.y + geometry.height * (port.offset ?? 0.5) };
356
376
  scene.add({ id: `port-${row.id}-${port.id}`, type: 'circle', geometry: { cx: point.x, cy: point.y, r: 4 }, bounds: { x: point.x - 6, y: point.y - 6, width: 12, height: 12 }, style: { fill: spec.theme.background, stroke: spec.theme.text, strokeWidth: 1.5 }, dataRef: { nodeId: row.id, portId: port.id, groupId: row.groupId || null }, interactive: true, zIndex: 4 });
357
377
  });
358
- text(scene, `node-label-${row.id}`, row.label || row.id, geometry.x + geometry.width / 2, geometry.y + geometry.height / 2 + (geometry.height < 20 ? 3 : 5), { fill: markText(spec.theme, spec.colors[lane % spec.colors.length]), textAnchor: 'middle', ...(nodeFont ? { font: nodeFont } : {}) });
378
+ if (row.label || kind !== 'connector') text(scene, `node-label-${row.id}`, row.label || row.id, geometry.x + geometry.width / 2, geometry.y + geometry.height / 2 + (geometry.height < 20 ? 3 : 5), { fill: markText(spec.theme, spec.colors[lane % spec.colors.length]), textAnchor: 'middle', textBaseline: 'middle', baseline: 'middle', ...(nodeFont ? { font: nodeFont } : {}) });
359
379
  });
360
380
  const groupBoxes = new Map();
361
381
  groups.forEach(group => {
@@ -423,7 +443,16 @@ export function buildProjectScene(spec) {
423
443
  const plotTop = Math.max(spec.padding.top, title.bottom + (title.bottom ? 12 : 0)) + (spec.type === 'burndown' ? 16 : 0);
424
444
  const plotWidth = Math.max(1, spec.width - left - spec.padding.right), dateDomain = projectDateDomain(spec, sourceRows), xLabels = axisLabelLayout(spec, projectTickLabels(dateDomain[0], dateDomain[1], plotWidth, spec.locale), plotWidth);
425
445
  const bottomReserve = Math.max(spec.padding.bottom + 16, Math.ceil(16 + xLabels.projectedHeight));
426
- const state = { plot: { x: left, y: plotTop, width: plotWidth, height: Math.max(1, spec.height - plotTop - bottomReserve) }, xLabels, compact: spec.width < 360 || spec.height < 240, recommendedSize: { minWidth: 280, minHeight: 220 }, linked, projectAnalytics: { linked } };
446
+ const temporalProject = ['gantt', 'timeline', 'milestone'].includes(spec.type);
447
+ const timeAxis = ['timeline', 'milestone'].includes(spec.type) ? {
448
+ orientation: 'horizontal',
449
+ field: 'date',
450
+ coordinate: 'x',
451
+ rowCoordinate: 'y',
452
+ domain: [...dateDomain],
453
+ domainISO: dateDomain.map(value => new Date(value).toISOString())
454
+ } : null;
455
+ const state = { plot: { x: left, y: plotTop, width: plotWidth, height: Math.max(1, spec.height - plotTop - bottomReserve) }, xLabels, compact: spec.width < 360 || spec.height < 240, recommendedSize: { minWidth: 280, minHeight: 220 }, linked, projectAnalytics: { linked }, timeDomain: temporalProject ? [...dateDomain] : null, timeAxis };
427
456
  scene.xLabelLayout = xLabels;
428
457
  if (spec.type === 'gantt') {
429
458
  state.schedule = analyzeSchedule(data.rows, { calendar: projectConfig(spec).calendar || {} });
package/src/schema.mjs CHANGED
@@ -72,7 +72,7 @@ const models = {
72
72
  'timeline-event': schema('timeline-event', { title: { type: 'string', required: true, editable: true }, date: { type: 'date', required: true, editable: true }, description: { type: 'string', editable: true }, status }),
73
73
  milestone: schema('milestone', { title: { type: 'string', required: true, editable: true }, date: { type: 'date', required: true, editable: true }, status }),
74
74
  'burndown-sample': schema('burndown-sample', { date: { type: 'date', required: true, editable: true }, remaining: { type: 'number', min: 0, required: true, editable: true }, scopeChange: { type: 'number', default: 0, editable: true }, forecast: { type: 'date', nullable: true, editable: false } }),
75
- 'flow-node': schema('flow-node', { label: { type: 'string', required: true, editable: true }, position, size: { type: 'object', editable: true, properties: { width: { type: 'number', required: true, min: 1 }, height: { type: 'number', required: true, min: 1 } } }, laneId: { type: 'string', references: 'swimlane', editable: true }, groupId: { type: 'string', editable: false }, ports: { type: 'array', editable: false, items: { type: 'object', properties: { id: { type: 'string', required: true }, side: { type: 'enum', values: ['left', 'right', 'top', 'bottom'] }, offset: { type: 'number', min: 0, max: 1 } } } }, description: { type: 'string', editable: true }, status }),
75
+ 'flow-node': schema('flow-node', { label: { type: 'string', required: true, editable: true }, kind: { type: 'enum', values: ['start', 'end', 'process', 'decision', 'io', 'connector'], default: 'process', editable: true }, position, size: { type: 'object', editable: true, properties: { width: { type: 'number', required: true, min: 1 }, height: { type: 'number', required: true, min: 1 } } }, laneId: { type: 'string', references: 'swimlane', editable: true }, groupId: { type: 'string', editable: false }, ports: { type: 'array', editable: false, items: { type: 'object', properties: { id: { type: 'string', required: true }, side: { type: 'enum', values: ['left', 'right', 'top', 'bottom'] }, offset: { type: 'number', min: 0, max: 1 } } } }, description: { type: 'string', editable: true }, status }),
76
76
  'flow-edge': schema('flow-edge', {
77
77
  from: { type: 'string', references: 'flow-node', required: true, editable: true },
78
78
  to: { type: 'string', references: 'flow-node', required: true, editable: true },
package/types/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export type Renderer = 'canvas' | 'svg' | 'auto';
2
2
  export type ChartType = 'line' | 'area' | 'bar' | 'column' | 'pie' | 'scatter' | 'funnel' | 'gauge' | 'heatmap' | 'radar' | 'gantt' | 'timeline' | 'milestone' | 'burndown' | 'flow' | 'swimlane' | 'architecture' | 'mindmap';
3
+ export type FlowNodeKind = 'start' | 'end' | 'process' | 'decision' | 'io' | 'connector';
3
4
  export type ThemeMode = 'auto' | 'light' | 'dark' | 'contrast';
4
5
  export type ThemePreset = 'auto' | 'analysis' | 'dashboard' | 'report' | 'presentation' | 'project' | 'diagram';
5
6
  export type ThemePalette = 'auto' | 'categorical' | 'sequential' | 'diverging' | 'status';
@@ -25,29 +26,86 @@ export class ChartValidationError extends Error { name: 'ChartValidationError';
25
26
  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; }
26
27
  export interface DataInspection { version: '1.0'; rows: number; fields: DataFieldInfo[]; dimensions: string[]; measures: string[]; temporalFields: string[]; missingValueCount: number; warnings: Diagnostic[]; }
27
28
  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>; }
28
- export interface RuntimeCapabilities { version: '2.0'; contractVersion: '1.1'; chartTypes: ChartType[]; charts: Record<ChartType, ChartCapability>; intents: string[]; locale?: { default: string; recommended: string[]; appliesTo: string[]; inputDates: string }; renderers: Array<'canvas' | 'svg'>; interactions: string[]; exports: Array<'png' | 'svg' | 'json' | 'jpeg'>; 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; }
29
+ export interface RuntimeCapabilities { version: '2.0'; contractVersion: '1.1'; chartTypes: ChartType[]; charts: Record<ChartType, ChartCapability>; intents: string[]; locale?: { default: string; recommended: string[]; appliesTo: string[]; inputDates: string }; renderers: Array<'canvas' | 'svg'>; interactions: string[]; exports: Array<'png' | 'svg' | 'json' | 'jpeg'>; chartModes?: Record<string, unknown>; diagram?: { flowNodeKinds?: FlowNodeKind[]; flowBranchEdges?: { label: string; minimumOutgoing: number }; flowLoops?: string; flowConnectors?: { kind: FlowNodeKind; linking: string }; [key: string]: unknown }; styleSystem: { modes: ThemeMode[]; presets: ThemePreset[]; palettes: ThemePalette[]; switchable: boolean; automatic: boolean; [key: string]: unknown }; preferences?: { version: '1.0'; scopes: string[]; persistence: string[]; fields: string[]; agentAdjustable: boolean; interactiveSettingsUI: boolean; precedence: string[]; schema: PreferenceCapabilities }; headless: { preview?: boolean; json: boolean; svg: boolean; png: boolean | string }; export: { types: string[]; mime: Record<string, string>; browser: Record<string, boolean>; headless: Record<string, string | boolean>; methods: string[]; options: Record<string, unknown>; branding: Record<string, unknown> }; branding: { defaultEnabled: boolean; signature: string; options: Record<string, unknown> }; [key: string]: unknown; }
29
30
  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; }
30
31
  export interface AxisState { rawDomain: [number, number]; domain: [number, number]; ticks: number[]; step: number | null; policy: 'nice' | 'raw' | 'explicit'; }
32
+ export interface TimeAxisState { orientation: 'horizontal'; field: 'date'; coordinate: 'x'; rowCoordinate: 'y'; domain: [number, number]; domainISO: [string, string]; }
31
33
  export interface ChartHealth { version: '1.0'; status: 'ready' | 'degraded' | 'empty'; renderable: boolean; issues: string[]; metrics: { warnings: number; suppressedLabels: number; clampedValues: number; renderedMarks: number }; }
32
- 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 }; }
34
+ 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; timeAxis?: TimeAxisState | null; health?: ChartHealth; style: Partial<StyleRecommendation> & { name?: string }; lineage: { recordIds: string[]; sourcePreserved: boolean }; accessibility: { enabled: boolean; summary: string }; }
33
35
  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; }
34
36
  export interface ChartEditing { enabled?: boolean; mode?: 'command' | string; requireConfirmation?: boolean; allowDelete?: boolean; allowStructuralChanges?: boolean; }
35
37
  export interface ChartTitle { text?: string; subtitle?: string; label?: string; }
36
38
  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; }
37
- 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?: string | ChartTitle; 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; }
39
+ export interface ChartPadding { top?: number; right?: number; bottom?: number; left?: number; }
40
+ export interface EncodingSpec { field?: string; type?: string; aggregate?: string; stack?: string | boolean; [key: string]: unknown; }
41
+ export interface ChartData { values?: Array<Record<string, unknown>>; nodes?: DiagramNode[]; edges?: DiagramEdge[]; lanes?: DiagramLane[]; layers?: ArchitectureLayer[]; boundaries?: ArchitectureBoundary[]; groups?: DiagramGroup[]; schema?: BusinessDataSchema; [key: string]: unknown; }
42
+ export interface ChartLegend { visible?: boolean; position?: 'top' | 'bottom' | 'left' | 'right' | string; [key: string]: unknown; }
43
+ export interface ChartLabels { enabled?: boolean; format?: string | Record<string, unknown>; color?: string; font?: string; position?: 'inside' | 'outside' | string; [key: string]: unknown; }
44
+ export interface ColorScaleSpec { domain?: [number, number]; range?: string[]; missing?: string; [key: string]: unknown; }
45
+ export interface StackSpec { mode: 'stacked' | 'percent'; [key: string]: unknown; }
46
+ export interface ChartProjectSpec { calendar?: ProjectCalendarConfig; linked?: { filters?: LinkedFilters; selection?: string[] }; [key: string]: unknown; }
47
+ export interface ChartSpec {
48
+ version?: string;
49
+ type: ChartType;
50
+ renderer?: Renderer;
51
+ container?: string | Element;
52
+ chartId?: string;
53
+ width?: number;
54
+ height?: number;
55
+ padding?: ChartPadding;
56
+ colors?: string[];
57
+ background?: string;
58
+ locale?: string;
59
+ data?: Array<Record<string, unknown>> | ChartData;
60
+ encoding?: Record<string, EncodingSpec | EncodingSpec[]>;
61
+ title?: string | ChartTitle;
62
+ legend?: ChartLegend;
63
+ grid?: { visible?: boolean; color?: string; [key: string]: unknown };
64
+ labels?: ChartLabels;
65
+ xAxis?: AxisSpec;
66
+ yAxis?: AxisSpec;
67
+ domain?: [number, number];
68
+ colorScale?: ColorScaleSpec;
69
+ indicators?: RadarIndicator[];
70
+ stack?: 'stacked' | 'percent' | StackSpec;
71
+ transform?: BinTransform | BinTransform[];
72
+ criticalPath?: boolean | string[];
73
+ nodes?: DiagramNode[];
74
+ edges?: DiagramEdge[];
75
+ lanes?: DiagramLane[];
76
+ layers?: ArchitectureLayer[];
77
+ boundaries?: ArchitectureBoundary[];
78
+ groups?: DiagramGroup[];
79
+ diagram?: DiagramConfig;
80
+ project?: ChartProjectSpec;
81
+ interaction?: ChartInteraction;
82
+ editing?: ChartEditing;
83
+ accessibility?: { enabled?: boolean; description?: string; [key: string]: unknown };
84
+ branding?: boolean | { enabled?: boolean; [key: string]: unknown };
85
+ theme?: ThemeMode | ThemePreset | ThemeConfig | ResolvedTheme;
86
+ preferences?: ChartPreferencesPatch | PreferencesStore;
87
+ preferencesStore?: PreferencesStore;
88
+ plugins?: ChartPlugin[];
89
+ [key: string]: unknown;
90
+ }
38
91
  export interface BinTransform { type: 'bin'; field: string; output?: string; thresholds?: number; step?: number; extent?: [number, number]; }
39
92
  export interface RadarIndicator { name: string; field: string; min?: number; max?: number; }
40
- 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; }
93
+ export interface DiagramNode { id: string; label?: string; kind?: FlowNodeKind; position?: { x: number; y: number }; size?: { width: number; height: number }; ports?: Array<{ id: string; side?: 'left' | 'right' | 'top' | 'bottom'; offset?: number }>; groupId?: string; }
41
94
  export interface ArchitectureNode extends DiagramNode { layerId?: string; boundaryId?: string; role?: string; description?: string; }
42
95
  export interface MindmapNode extends DiagramNode { parentId?: string; branch?: string; description?: string; }
43
96
  export interface DiagramPoint { x: number; y: number; }
44
97
  export interface DiagramEdge { id?: string; from: string; to: string; fromPort?: string; toPort?: string; label?: string; relation?: string; routing?: 'straight' | 'orthogonal' | 'curved'; curveTension?: number; waypoints?: DiagramPoint[]; }
98
+ export interface DiagramLane { id: string; label?: string; [key: string]: unknown; }
99
+ export interface DiagramGroup { id: string; label?: string; collapsed?: boolean; padding?: number; [key: string]: unknown; }
45
100
  export interface DiagramElement { id?: string; type?: string; dataRef?: Record<string, unknown>; geometry?: Record<string, unknown>; [key: string]: unknown; }
46
101
  export interface ChartEvent { chart: Chart; type?: string; target?: DiagramElement | null; datum?: Record<string, unknown>; selectedData?: Array<Record<string, unknown>>; [key: string]: unknown; }
47
102
  export type ChartListener = (event: ChartEvent) => void;
48
103
  export interface ChartPlugin { name?: string; install?(chart: Chart): void; beforeRender?(chart: Chart, model: Record<string, unknown>): void; afterRender?(chart: Chart, model: Record<string, unknown>): void; destroy?(chart: Chart): void; }
49
104
  export interface ClipboardState { nodes: DiagramNode[]; edges: DiagramEdge[]; }
50
- export interface ChartState { renderer: string; width: number; height: number; dataCount: number; selected: unknown[]; revision: number; history: { undo: number; redo: number }; view: Record<string, unknown> | null; health: ChartHealth; warnings: Diagnostic[]; preferences: ChartPreferences; clipboard: { nodes: number; edges: number }; [key: string]: unknown; }
105
+ export interface ChartLayoutState { family: string | null; plot: Record<string, unknown>; chrome: Record<string, unknown> | null; labels: Record<string, unknown> | null; }
106
+ export interface ChartAxesState { y?: AxisState; right?: AxisState; [key: string]: unknown; }
107
+ export interface ChartBrandingState { enabled: boolean; signature: string; text: string | null; }
108
+ export interface ChartState { renderer: string; width: number; height: number; dataCount: number; selected: unknown[]; revision: number; history: { undo: number; redo: number }; view: Record<string, unknown> | null; style?: Partial<StyleRecommendation> & { name?: string; mode?: ThemeMode; resolvedMode?: string; palette?: ThemePalette }; layout?: ChartLayoutState | null; timeAxis?: TimeAxisState | null; axes?: ChartAxesState | null; health: ChartHealth; preferences: ChartPreferences; branding?: ChartBrandingState; warnings: Diagnostic[]; assumptions?: string[]; normalizations?: Diagnostic[]; collapsedGroups?: string[]; projectAnalytics?: ProjectAnalyticsState | null; linked?: LinkedState | null; clipboard: { nodes: number; edges: number }; [key: string]: unknown; }
51
109
  export interface DiagramConfig { mode?: 'process' | 'architecture' | 'mindmap'; layout?: 'manual' | 'layered' | 'tree' | 'radial'; routing?: 'straight' | 'orthogonal' | 'curved'; curveTension?: number; grid?: number; snap?: boolean; }
52
110
  export interface ArchitectureLayer { id: string; label?: string; }
53
111
  export interface ArchitectureBoundary { id: string; label?: string; nodeIds?: string[]; padding?: number; color?: string; }