@taylorwong/ichartjs 2.0.13 → 2.0.15

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.
Files changed (35) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +2 -2
  3. package/docs/agent/charting-scenario.md +1 -1
  4. package/docs/agent/coding-agent-integration.md +1 -1
  5. package/docs/agent/development/2.0-release-readiness.md +4 -0
  6. package/docs/agent/development/iteration-13.md +318 -0
  7. package/docs/agent/development/roadmap.md +2 -1
  8. package/docs/agent/editing-contract.md +3 -0
  9. package/docs/agent/frontend-integration.md +1 -1
  10. package/docs/agent/runtime-contract.md +4 -0
  11. package/docs/agent/usage-scenarios.md +1 -1
  12. package/docs/agent/zh-CN/charting-scenario.md +1 -1
  13. package/docs/agent/zh-CN/coding-agent-integration.md +1 -1
  14. package/docs/agent/zh-CN/editing-contract.md +2 -0
  15. package/docs/agent/zh-CN/frontend-integration.md +1 -1
  16. package/docs/agent/zh-CN/runtime-contract.md +3 -1
  17. package/docs/agent/zh-CN/usage-scenarios.md +1 -1
  18. package/docs/manifests/capabilities.json +1310 -32
  19. package/docs/manifests/commands.json +7 -7
  20. package/docs/manifests/schemas.json +182 -10
  21. package/examples/agent-workflow.mjs +10 -0
  22. package/package.json +21 -5
  23. package/skills/ichartjs/SKILL.md +1 -1
  24. package/src/capabilities.mjs +10 -36
  25. package/src/chart-lifecycle.mjs +31 -0
  26. package/src/charts.mjs +56 -25
  27. package/src/command.mjs +3 -2
  28. package/src/contract-registry.mjs +59 -0
  29. package/src/edit-controller.mjs +1 -1
  30. package/src/errors.mjs +31 -0
  31. package/src/index.mjs +144 -12
  32. package/src/schema.mjs +1 -0
  33. package/src/spec.mjs +45 -3
  34. package/types/consumer-fixture.ts +35 -0
  35. package/types/index.d.ts +33 -3
package/src/schema.mjs CHANGED
@@ -87,6 +87,7 @@ const models = {
87
87
  swimlane: schema('swimlane', { label: { type: 'string', required: true, editable: true } }),
88
88
  'architecture-node': schema('architecture-node', { label: { type: 'string', required: true, editable: true }, layerId: { type: 'string', editable: true }, boundaryId: { type: 'string', editable: true }, position, size: { type: 'object', editable: true, properties: { width: { type: 'number', required: true, min: 1 }, height: { type: 'number', required: true, min: 1 } } }, 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 }),
89
89
  'architecture-edge': schema('architecture-edge', { from: { type: 'string', required: true, editable: true }, to: { type: 'string', required: true, editable: true }, label: { type: 'string', editable: true }, relation: { type: 'string', editable: true }, routing: { type: 'enum', values: ['straight', 'orthogonal', 'curved'], editable: true }, curveTension: { type: 'number', min: 0.2, max: 0.8, editable: true }, waypoints: { type: 'array', editable: true, items: { type: 'object', properties: { x: { type: 'number', required: true }, y: { type: 'number', required: true } } } }, status }),
90
+ 'mindmap-edge': schema('mindmap-edge', { from: { type: 'string', required: true, editable: true }, to: { type: 'string', required: true, editable: true }, label: { type: 'string', editable: true }, routing: { type: 'enum', values: ['straight', 'orthogonal', 'curved'], editable: true }, curveTension: { type: 'number', min: 0.2, max: 0.8, editable: true }, waypoints: { type: 'array', editable: true, items: { type: 'object', properties: { x: { type: 'number', required: true }, y: { type: 'number', required: true } } } }, status }),
90
91
  'mindmap-node': schema('mindmap-node', { label: { type: 'string', required: true, editable: true }, parentId: { type: 'string', editable: true }, position, size: { type: 'object', editable: true, properties: { width: { type: 'number', required: true, min: 1 }, height: { type: 'number', required: true, min: 1 } } }, branch: { type: 'string', editable: true }, description: { type: 'string', editable: true }, status })
91
92
  };
92
93
 
package/src/spec.mjs CHANGED
@@ -60,6 +60,40 @@ function dependencyErrors(rows) {
60
60
  return errors;
61
61
  }
62
62
 
63
+ function isIsoDate(value) {
64
+ if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}(?:T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,3})?)?(?:Z|[+-]\d{2}:?\d{2})?)?$/.test(value)) return false;
65
+ return !Number.isNaN(Date.parse(value));
66
+ }
67
+
68
+ function projectRowErrors(type, rows) {
69
+ const errors = [], ids = new Map(), requiresId = ['gantt', 'timeline', 'milestone'].includes(type);
70
+ rows.forEach((row, index) => {
71
+ const path = `data.values[${index}]`;
72
+ if (!row || typeof row !== 'object' || Array.isArray(row)) {
73
+ errors.push({ code: 'INVALID_PROJECT_ROW', path, message: `${type} rows must be objects.`, suggestion: 'Provide one JSON object per project record.' });
74
+ return;
75
+ }
76
+ if (requiresId && (typeof row.id !== 'string' || !row.id.trim())) errors.push({ code: 'MISSING_RECORD_ID', path: `${path}.id`, message: `${type} rows require a stable string id.`, suggestion: 'Add a unique id to every project record so edits and lineage remain stable.' });
77
+ if (typeof row.id === 'string' && row.id.trim()) {
78
+ if (ids.has(row.id)) errors.push({ code: 'DUPLICATE_RECORD_ID', path: `${path}.id`, message: `Record id ${row.id} is duplicated.`, suggestion: 'Use a unique stable id for every project record.' });
79
+ ids.set(row.id, index);
80
+ }
81
+ if (type === 'gantt') {
82
+ for (const key of ['start', 'end']) {
83
+ if (!isIsoDate(row[key])) errors.push({ code: 'MISSING_REQUIRED_DATE', path: `${path}.${key}`, message: `Gantt rows require a valid ISO ${key} date.`, suggestion: `Use ${key}: "2026-09-14" or an explicit ISO timestamp.` });
84
+ }
85
+ if (isIsoDate(row.start) && isIsoDate(row.end) && Date.parse(row.end) < Date.parse(row.start)) errors.push({ code: 'INVALID_INTERVAL', path, message: 'Gantt end must not precede start.', suggestion: 'Set end to the same day or a later ISO date.' });
86
+ } else if (['timeline', 'milestone'].includes(type)) {
87
+ if (!isIsoDate(row.date)) errors.push({ code: 'MISSING_REQUIRED_DATE', path: `${path}.date`, message: `${type} rows require a valid ISO date.`, suggestion: 'Use date: "2026-09-14" or an explicit ISO timestamp.' });
88
+ if (typeof (row.title ?? row.name ?? row.label) !== 'string' || !(row.title ?? row.name ?? row.label).trim()) errors.push({ code: 'MISSING_RECORD_LABEL', path: `${path}.title`, message: `${type} rows require a title, name, or label.`, suggestion: 'Add a human-readable title for the project record.' });
89
+ } else if (type === 'burndown') {
90
+ if (!isIsoDate(row.date)) errors.push({ code: 'MISSING_REQUIRED_DATE', path: `${path}.date`, message: 'burndown rows require a valid ISO date.', suggestion: 'Use date: "2026-09-14" or an explicit ISO timestamp.' });
91
+ if (!Number.isFinite(Number(row.remaining)) || Number(row.remaining) < 0) errors.push({ code: 'INVALID_REMAINING', path: `${path}.remaining`, message: 'Burndown remaining must be a non-negative finite number.', suggestion: 'Provide remaining: 0 or a positive numeric value.' });
92
+ }
93
+ });
94
+ return errors;
95
+ }
96
+
63
97
  const encodingChannels = {
64
98
  line: ['x', 'y'], area: ['x', 'y'], bar: ['x', 'y'], column: ['x', 'y'], scatter: ['x', 'y'],
65
99
  pie: ['category', 'value'], funnel: ['category', 'value'], gauge: ['value'], heatmap: ['x', 'y', 'color'], radar: [],
@@ -188,10 +222,18 @@ export function validateSpec(input = {}) {
188
222
  const diagramValidation = validateDiagram(diagramInput);
189
223
  diagramValidation.errors.forEach(error => errors.push({ ...error, path: error.path || 'diagram' }));
190
224
  }
191
- if (['gantt', 'timeline', 'milestone'].includes(spec.type)) {
225
+ if (['gantt', 'timeline', 'milestone', 'burndown'].includes(spec.type)) {
192
226
  const rows = spec.data.values, dateKey = spec.type === 'gantt' ? 'start/end' : 'date';
193
- if (!rows.some(row => row.start || row.end || row.date)) errors.push({ code: 'INVALID_INTERVALS', path: 'data.values', message: `${spec.type} rows require ${dateKey} dates.`, suggestion: 'Add ISO date values to each row.' });
194
- rows.forEach((row, index) => { const values = spec.type === 'gantt' ? [row.start, row.end] : [row.date]; if (values.some(value => value != null && Number.isNaN(Date.parse(value)))) errors.push({ code: 'INVALID_DATE', path: `data.values[${index}]`, message: `${spec.type} contains an invalid date.`, suggestion: 'Use an ISO-8601 date such as 2026-09-14.' }); });
227
+ if (!rows.some(row => row && (row.start || row.end || row.date))) errors.push({ code: 'INVALID_INTERVALS', path: 'data.values', message: `${spec.type} rows require ${dateKey} dates.`, suggestion: 'Add ISO date values to each row.' });
228
+ errors.push(...projectRowErrors(spec.type, rows));
229
+ if (spec.type === 'burndown') {
230
+ const seenDates = new Set(), validDates = rows.map(row => row?.date).filter(isIsoDate);
231
+ validDates.forEach((date, index) => {
232
+ if (seenDates.has(date)) warnings.push({ code: 'DUPLICATE_DATE', severity: 'warning', path: 'data.values', message: `Burndown contains duplicate date ${date}; input order is preserved.`, suggestion: 'Aggregate duplicate dates before rendering for one sample per day.' });
233
+ seenDates.add(date);
234
+ if (index && Date.parse(date) < Date.parse(validDates[index - 1])) warnings.push({ code: 'OUT_OF_ORDER_DATE', severity: 'warning', path: 'data.values', message: 'Burndown dates are not sorted; input order is preserved.', suggestion: 'Sort samples by date before creating the chart.' });
235
+ });
236
+ }
195
237
  }
196
238
  if (spec.type === 'gantt') errors.push(...dependencyErrors(spec.data.values));
197
239
  const rows = Array.isArray(spec.data?.values) ? spec.data.values : [];
@@ -0,0 +1,35 @@
1
+ import {
2
+ ChartValidationError,
3
+ Chart,
4
+ ChartSpec,
5
+ createChart,
6
+ getCapabilities,
7
+ getBusinessSchema,
8
+ type ChartEvent,
9
+ type EditCommand,
10
+ type LinkedFilters
11
+ } from '@taylorwong/ichartjs';
12
+
13
+ declare const spec: ChartSpec;
14
+ declare const command: EditCommand;
15
+ declare const filters: LinkedFilters;
16
+
17
+ const chart: Chart = createChart({ ...spec, type: spec.type || 'line', data: [{ id: 'row-1', name: 'A', value: 1 }] });
18
+ const listener = (event: ChartEvent) => event.chart.getState().revision;
19
+ chart.on('render', listener).off('render', listener);
20
+ chart.update({ labels: { enabled: true } }).setData([{ id: 'row-2', name: 'B', value: 2 }]);
21
+ chart.setLinkedFilters(filters).setLinkedSelection(['row-2']);
22
+ chart.getDiagramNodes();
23
+ chart.getDiagramEdges();
24
+ chart.getClipboard();
25
+ chart.export({ type: 'json', as: 'object' });
26
+ chart.export({ type: 'svg', as: 'string' });
27
+ chart.export({ type: 'png', as: 'dataurl' });
28
+ chart.export({ type: 'jpeg', as: 'blob' });
29
+ chart.previewEdit(command);
30
+ chart.validateData();
31
+ getCapabilities().contractVersion;
32
+ getBusinessSchema('mindmap-edge');
33
+ try { chart.update({ width: 0 }); } catch (error) { if (!(error instanceof ChartValidationError)) throw error; }
34
+ chart.destroy();
35
+ chart.destroy();
package/types/index.d.ts CHANGED
@@ -21,10 +21,11 @@ export interface PreferencesStorage { getItem(key: string): string | null; setIt
21
21
  export interface StyleRecommendation { version: '1.0'; preset: Exclude<ThemePreset, 'auto'>; mode: ThemeMode; resolvedMode: Exclude<ThemeMode, 'auto'>; palette: Exclude<ThemePalette, 'auto'>; reasons: string[]; warnings: Diagnostic[]; userOverride: boolean; }
22
22
  export interface ResolvedTheme extends ThemeConfig { name: string; resolvedMode: Exclude<ThemeMode, 'auto'>; background: string; surface: string; text: string; muted: string; axis: string; grid: string; border: string; focus: string; selection: string; colors: string[]; palettes: Record<string, unknown>; status: Record<string, string>; typography: Record<string, { size: number; weight: number; lineHeight: number; font: string }>; layout: Record<string, unknown>; marks: Record<string, unknown>; reasons: string[]; warnings: Diagnostic[]; }
23
23
  export interface Diagnostic { code: string; path?: string; message: string; expected?: unknown; suggestion?: string; [key: string]: unknown; }
24
+ export class ChartValidationError extends Error { name: 'ChartValidationError'; code: 'CHART_VALIDATION_FAILED'; operation: string; details: Diagnostic[]; errors: Diagnostic[]; path?: string; expected?: unknown; received?: unknown; suggestion?: string; toJSON(): Record<string, unknown>; }
24
25
  export interface DataFieldInfo { name: string; type: 'quantitative' | 'temporal' | 'category' | 'unknown'; role: 'identifier' | 'measure' | 'temporal-dimension' | 'dimension'; unit: string | null; cardinality: number; validCount: number; nullCount: number; min?: number; max?: number; temporalMin?: string; temporalMax?: string; }
25
26
  export interface DataInspection { version: '1.0'; rows: number; fields: DataFieldInfo[]; dimensions: string[]; measures: string[]; temporalFields: string[]; missingValueCount: number; warnings: Diagnostic[]; }
26
27
  export interface ChartCapability { type: ChartType; family: string; intents: string[]; required: string[]; optional: string[]; dataShapes: string[]; interactions: string[]; features: Record<string, 'supported' | 'not-applicable' | 'degraded'>; renderers: Array<'canvas' | 'svg'>; exports: string[]; limits: Record<string, number>; }
27
- export interface RuntimeCapabilities { version: '2.0'; contractVersion: '1.0'; chartTypes: ChartType[]; charts: Record<ChartType, ChartCapability>; intents: string[]; locale?: { default: string; recommended: string[]; appliesTo: string[]; inputDates: string }; renderers: Array<'canvas' | 'svg'>; interactions: string[]; exports: Array<'png' | 'svg' | 'json' | 'jpeg'>; styleSystem: { modes: ThemeMode[]; presets: ThemePreset[]; palettes: ThemePalette[]; switchable: boolean; automatic: boolean; [key: string]: unknown }; preferences?: { version: '1.0'; scopes: string[]; persistence: string[]; fields: string[]; agentAdjustable: boolean; interactiveSettingsUI: boolean; precedence: string[]; schema: PreferenceCapabilities }; headless: { preview?: boolean; json: boolean; svg: boolean; png: boolean | string }; export: { types: string[]; mime: Record<string, string>; browser: Record<string, boolean>; headless: Record<string, string | boolean>; methods: string[]; options: Record<string, unknown>; branding: Record<string, unknown> }; branding: { defaultEnabled: boolean; signature: string; options: Record<string, unknown> }; [key: string]: unknown; }
28
+ export interface 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; }
28
29
  export interface ChartPlan { version: '1.0'; intent: string; intentKnown?: boolean; intentSuggestions?: string[]; fallbackUsed?: boolean; primary: ChartType; alternatives: ChartType[]; confidence: number; reasons: string[]; requiredFields: string[]; suggestedEncodings: { dimension: string | null; measure: string | null; secondaryMeasure: string | null }; assumptions: string[]; warnings: Diagnostic[]; unsupportedRequests: string[]; nextActions: string[]; capability: ChartCapability; styleRecommendation: StyleRecommendation; data: DataInspection; }
29
30
  export interface AxisState { rawDomain: [number, number]; domain: [number, number]; ticks: number[]; step: number | null; policy: 'nice' | 'raw' | 'explicit'; }
30
31
  export interface ChartHealth { version: '1.0'; status: 'ready' | 'degraded' | 'empty'; renderable: boolean; issues: string[]; metrics: { warnings: number; suppressedLabels: number; clampedValues: number; renderedMarks: number }; }
@@ -40,6 +41,12 @@ export interface ArchitectureNode extends DiagramNode { layerId?: string; bounda
40
41
  export interface MindmapNode extends DiagramNode { parentId?: string; branch?: string; description?: string; }
41
42
  export interface DiagramPoint { x: number; y: number; }
42
43
  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[]; }
44
+ export interface DiagramElement { id?: string; type?: string; dataRef?: Record<string, unknown>; geometry?: Record<string, unknown>; [key: string]: unknown; }
45
+ export interface ChartEvent { chart: Chart; type?: string; target?: DiagramElement | null; datum?: Record<string, unknown>; selectedData?: Array<Record<string, unknown>>; [key: string]: unknown; }
46
+ export type ChartListener = (event: ChartEvent) => void;
47
+ 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; }
48
+ export interface ClipboardState { nodes: DiagramNode[]; edges: DiagramEdge[]; }
49
+ 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; }
43
50
  export interface DiagramConfig { mode?: 'process' | 'architecture' | 'mindmap'; layout?: 'manual' | 'layered' | 'tree' | 'radial'; routing?: 'straight' | 'orthogonal' | 'curved'; curveTension?: number; grid?: number; snap?: boolean; }
44
51
  export interface ArchitectureLayer { id: string; label?: string; }
45
52
  export interface ArchitectureBoundary { id: string; label?: string; nodeIds?: string[]; padding?: number; color?: string; }
@@ -87,13 +94,35 @@ export interface Chart {
87
94
  downloadSVG(): ReturnType<Chart['download']>;
88
95
  downloadJSON(): ReturnType<Chart['download']>;
89
96
  destroy(): void;
90
- inspectDataSchema(): ReturnType<typeof inspectDataSchema>;
91
97
  validateData(): Record<string, unknown>;
92
98
  validateEdit(command: EditCommand): Record<string, unknown>;
93
99
  previewEdit(command: EditCommand): EditPreview;
94
100
  applyEdit(command: EditCommand, options?: { preview?: EditPreview; confirmed?: boolean; approval?: unknown; previewId?: string; expectedRevision?: number; actor?: string; source?: string; reason?: string }): Record<string, unknown>;
95
101
  getChangeSet(): Record<string, unknown> | null;
96
- getState(): Record<string, unknown> & { axes?: { y?: AxisState; right?: AxisState } | null; health?: ChartHealth; warnings?: Diagnostic[] };
102
+ getState(): ChartState;
103
+ on(type: string, listener: ChartListener): this;
104
+ off(type: string, listener: ChartListener): this;
105
+ getAccessibleDescription(): string;
106
+ inspectDataSchema(): Record<string, unknown>;
107
+ toDataTable(): Array<Record<string, unknown>>;
108
+ getSelectedData(): Array<Record<string, unknown>>;
109
+ getElementAt(x: number, y: number): DiagramElement | null;
110
+ select(target: DiagramElement, options?: { additive?: boolean }): this;
111
+ selectBox(start: DiagramPoint, end: DiagramPoint): this;
112
+ getDiagramNodes(): DiagramNode[];
113
+ getDiagramEdges(): DiagramEdge[];
114
+ getDiagramGroups(): Array<Record<string, unknown>>;
115
+ getCollapsedGroupIds(): string[];
116
+ getClipboard(): ClipboardState;
117
+ copySelection(): { valid: boolean; clipboard?: ClipboardState; errors?: Diagnostic[] };
118
+ duplicateSelection(options?: Record<string, unknown>): Record<string, unknown>;
119
+ pasteSelection(options?: Record<string, unknown>): Record<string, unknown>;
120
+ connectNodes(connection: DiagramEdge, options?: Record<string, unknown>): Record<string, unknown>;
121
+ toggleGroupCollapse(groupId: string, options?: Record<string, unknown>): Record<string, unknown>;
122
+ highlight(target: DiagramElement): this;
123
+ use(plugin: ChartPlugin): this;
124
+ /** Advanced JSON-pointer mutation. Prefer update() so the full public contract is explicit. */
125
+ applyPatch(patches: Array<{ op: 'add' | 'replace' | 'remove'; path: string; value?: unknown }>): this;
97
126
  getProjectAnalytics(): ProjectAnalyticsState | null;
98
127
  getLinkedState(): LinkedState | null;
99
128
  setLinkedFilters(filters: LinkedFilters): this;
@@ -117,6 +146,7 @@ export interface Chart {
117
146
  }
118
147
 
119
148
  export function createChart(spec: ChartSpec): Chart;
149
+ export const iChart: Record<string, unknown> & { version: string; ChartValidationError: typeof ChartValidationError; createChart: typeof createChart };
120
150
  export const defaultPreferences: ChartPreferences;
121
151
  export function getPreferenceCapabilities(chartType?: ChartType | string | null, options?: { locale?: 'en' | 'zh-CN' | string }): PreferenceCapabilities;
122
152
  export function createPreferencesStore(options?: { storage?: 'localStorage' | 'memory' | PreferencesStorage; storageKey?: string; global?: ChartPreferencesPatch; charts?: Record<string, ChartPreferencesPatch> }): PreferencesStore;