@taylorwong/ichartjs 2.0.13 → 2.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +2 -2
- package/docs/agent/charting-scenario.md +1 -1
- package/docs/agent/coding-agent-integration.md +1 -1
- package/docs/agent/development/2.0-release-readiness.md +4 -0
- package/docs/agent/development/iteration-13.md +278 -0
- package/docs/agent/development/roadmap.md +2 -1
- package/docs/agent/editing-contract.md +3 -0
- package/docs/agent/frontend-integration.md +1 -1
- package/docs/agent/runtime-contract.md +4 -0
- package/docs/agent/usage-scenarios.md +1 -1
- package/docs/agent/zh-CN/charting-scenario.md +1 -1
- package/docs/agent/zh-CN/coding-agent-integration.md +1 -1
- package/docs/agent/zh-CN/editing-contract.md +2 -0
- package/docs/agent/zh-CN/frontend-integration.md +1 -1
- package/docs/agent/zh-CN/runtime-contract.md +3 -1
- package/docs/agent/zh-CN/usage-scenarios.md +1 -1
- package/docs/manifests/capabilities.json +1310 -32
- package/docs/manifests/commands.json +7 -7
- package/docs/manifests/schemas.json +182 -10
- package/package.json +21 -5
- package/skills/ichartjs/SKILL.md +1 -1
- package/src/capabilities.mjs +10 -36
- package/src/chart-lifecycle.mjs +31 -0
- package/src/charts.mjs +19 -10
- package/src/command.mjs +3 -2
- package/src/contract-registry.mjs +59 -0
- package/src/edit-controller.mjs +1 -1
- package/src/errors.mjs +31 -0
- package/src/index.mjs +142 -10
- package/src/schema.mjs +1 -0
- package/src/spec.mjs +45 -3
- package/types/consumer-fixture.ts +35 -0
- package/types/index.d.ts +33 -3
package/src/errors.mjs
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Structured public runtime errors for failed chart mutations. */
|
|
2
|
+
export class ChartValidationError extends Error {
|
|
3
|
+
constructor(operation, diagnostics = [], message = null) {
|
|
4
|
+
const details = Array.isArray(diagnostics) ? diagnostics : [];
|
|
5
|
+
const first = details[0] || {};
|
|
6
|
+
super(message || details.map(item => item.message).filter(Boolean).join(' ') || `Invalid ${operation} mutation.`);
|
|
7
|
+
this.name = 'ChartValidationError';
|
|
8
|
+
this.code = 'CHART_VALIDATION_FAILED';
|
|
9
|
+
this.operation = operation;
|
|
10
|
+
this.details = details;
|
|
11
|
+
this.errors = details;
|
|
12
|
+
this.path = first.path;
|
|
13
|
+
this.expected = first.expected;
|
|
14
|
+
this.received = first.received;
|
|
15
|
+
this.suggestion = first.suggestion;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
toJSON() {
|
|
19
|
+
return {
|
|
20
|
+
name: this.name,
|
|
21
|
+
code: this.code,
|
|
22
|
+
operation: this.operation,
|
|
23
|
+
message: this.message,
|
|
24
|
+
details: this.details,
|
|
25
|
+
...(this.path === undefined ? {} : { path: this.path }),
|
|
26
|
+
...(this.expected === undefined ? {} : { expected: this.expected }),
|
|
27
|
+
...(this.received === undefined ? {} : { received: this.received }),
|
|
28
|
+
...(this.suggestion === undefined ? {} : { suggestion: this.suggestion })
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/index.mjs
CHANGED
|
@@ -22,6 +22,9 @@ import { normalizeLinkedFilters, normalizeLinkedSelection, filterProjectRows, cr
|
|
|
22
22
|
import { explainChart, getCapabilities as discoverCapabilities, getChartCapability, getPreferenceCapabilities, planChart } from './capabilities.mjs';
|
|
23
23
|
import { applyPreferencesToSpec, createPreferencesStore, defaultPreferences, mergePreferences, mergeThemePreference, normalizePreferences, validatePreferences } from './preferences.mjs';
|
|
24
24
|
import { mountChartSettings } from './preferences-ui.mjs';
|
|
25
|
+
import { ChartValidationError } from './errors.mjs';
|
|
26
|
+
import { diagramOperations } from './contract-registry.mjs';
|
|
27
|
+
import { destroyChart } from './chart-lifecycle.mjs';
|
|
25
28
|
|
|
26
29
|
import { isDiagram, paintSelection, diagramPointer, diagramKeyboard } from './diagram-interaction.mjs';
|
|
27
30
|
|
|
@@ -189,7 +192,7 @@ export class Chart {
|
|
|
189
192
|
delete specInput.preferencesStore;
|
|
190
193
|
delete specInput.chartId;
|
|
191
194
|
const result = validateSpec(specInput);
|
|
192
|
-
if (!result.valid)
|
|
195
|
+
if (!result.valid) throw new ChartValidationError('create', result.errors);
|
|
193
196
|
this._specDiagnostics = { warnings: result.warnings, normalizations: result.normalizations };
|
|
194
197
|
this.chartId = input.chartId ?? input.id ?? null;
|
|
195
198
|
this._preferencesStore = isPreferencesStore(input.preferencesStore) ? input.preferencesStore : isPreferencesStore(input.preferences) ? input.preferences : null;
|
|
@@ -209,6 +212,7 @@ export class Chart {
|
|
|
209
212
|
this._history = new EditHistory();
|
|
210
213
|
this._revision = 0;
|
|
211
214
|
this._lastChangeSet = null;
|
|
215
|
+
this._destroyed = false;
|
|
212
216
|
this._editor = new EditController(this);
|
|
213
217
|
this.plugins = new PluginHost(this, this.spec.plugins);
|
|
214
218
|
this._mountRenderer();
|
|
@@ -332,16 +336,114 @@ export class Chart {
|
|
|
332
336
|
_updateCrosshair(x, y) { const vertical = this.model.scene.find('crosshair-x'), horizontal = this.model.scene.find('crosshair-y'); if (!vertical || !horizontal) return; vertical.geometry.x1 = vertical.geometry.x2 = x; vertical.style.opacity = 0.7; horizontal.geometry.y1 = horizontal.geometry.y2 = y; horizontal.style.opacity = 0.7; this.renderer.render(this.model.scene); }
|
|
333
337
|
_showTooltip(payload) { if (typeof document === 'undefined' || !this.container || !payload.datum) return; let tip = this._tooltip; if (!tip) { tip = this._tooltip = document.createElement('div'); tip.className = 'ichart-v2-tooltip'; Object.assign(tip.style, { position: 'fixed', pointerEvents: 'none', zIndex: 9999, padding: '8px 10px', borderRadius: '6px', maxWidth: 'min(320px, calc(100vw - 24px))', whiteSpace: 'pre-line' }); document.body.appendChild(tip); } Object.assign(tip.style, { background: this.spec.theme.surface, color: this.spec.theme.text, border: `1px solid ${this.spec.theme.border}`, font: this.spec.theme.typography.tooltip.font, boxShadow: `0 4px 12px ${this.spec.theme.border}88` }); const text = ['gantt', 'timeline', 'milestone', 'burndown', 'flow', 'swimlane', 'architecture', 'mindmap'].includes(this.spec.type) ? projectTooltip(this.spec.type, payload.datum) : Object.entries(payload.datum).map(([key, value]) => `${key}: ${value}`).join(' · '); tip.textContent = text; const left = Math.min((payload.nativeEvent.clientX || 0) + 12, window.innerWidth - tip.offsetWidth - 12); const top = Math.min((payload.nativeEvent.clientY || 0) + 12, window.innerHeight - tip.offsetHeight - 12); tip.style.left = `${Math.max(12, left)}px`; tip.style.top = `${Math.max(12, top)}px`; }
|
|
334
338
|
_hideTooltip() { if (this._tooltip) this._tooltip.style.left = '-10000px'; }
|
|
339
|
+
_validateMutation(operation, input) {
|
|
340
|
+
const result = validateSpec(input);
|
|
341
|
+
if (!result.valid) throw new ChartValidationError(operation, result.errors);
|
|
342
|
+
return result;
|
|
343
|
+
}
|
|
344
|
+
_snapshotMutation() {
|
|
345
|
+
return {
|
|
346
|
+
spec: this.spec,
|
|
347
|
+
model: this.model,
|
|
348
|
+
themeInput: clone(this._themeInput),
|
|
349
|
+
styleOverrides: clone(this._styleOverrides),
|
|
350
|
+
preferenceBase: clone(this._preferenceBase),
|
|
351
|
+
localPreferences: clone(this._localPreferences),
|
|
352
|
+
preferencesSnapshot: this._preferencesSnapshot,
|
|
353
|
+
specDiagnostics: clone(this._specDiagnostics)
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
_restoreMutation(snapshot) {
|
|
357
|
+
this.spec = snapshot.spec;
|
|
358
|
+
this.model = snapshot.model;
|
|
359
|
+
this._themeInput = snapshot.themeInput;
|
|
360
|
+
this._styleOverrides = snapshot.styleOverrides;
|
|
361
|
+
this._preferenceBase = snapshot.preferenceBase;
|
|
362
|
+
this._localPreferences = snapshot.localPreferences;
|
|
363
|
+
this._preferencesSnapshot = snapshot.preferencesSnapshot;
|
|
364
|
+
this._specDiagnostics = snapshot.specDiagnostics;
|
|
365
|
+
if (this.renderer) this.renderer.options = this.spec;
|
|
366
|
+
try { if (this.renderer?.container && this.model?.scene) this.renderer.render(this.model.scene); } catch {}
|
|
367
|
+
}
|
|
368
|
+
_commitMutation() {
|
|
369
|
+
const previousRevision = this._revision;
|
|
370
|
+
this._revision += 1;
|
|
371
|
+
try {
|
|
372
|
+
this.render();
|
|
373
|
+
this._history.clear();
|
|
374
|
+
this._lastChangeSet = null;
|
|
375
|
+
this._editor.pending.clear();
|
|
376
|
+
return this;
|
|
377
|
+
} catch (error) {
|
|
378
|
+
this._revision = previousRevision;
|
|
379
|
+
throw error;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
335
382
|
on(type, listener) { if (!this.listeners.has(type)) this.listeners.set(type, new Set()); this.listeners.get(type).add(listener); return this; }
|
|
336
383
|
off(type, listener) { this.listeners.get(type)?.delete(listener); return this; }
|
|
337
384
|
emit(type, event) { this.listeners.get(type)?.forEach(listener => listener(event)); }
|
|
338
|
-
update(next = {}) {
|
|
339
|
-
|
|
340
|
-
|
|
385
|
+
update(next = {}) {
|
|
386
|
+
const { preferences, preferencesStore, chartId, preferenceScope, ...specUpdate } = next || {};
|
|
387
|
+
if (preferences !== undefined) {
|
|
388
|
+
const checked = validatePreferences(preferences, { partial: true });
|
|
389
|
+
if (!checked.valid) throw new ChartValidationError('update', checked.errors);
|
|
390
|
+
}
|
|
391
|
+
const themeInput = specUpdate.theme === undefined ? this._themeInput : specUpdate.theme;
|
|
392
|
+
const result = this._validateMutation('update', { ...this.spec, ...specUpdate, theme: themeInput, data: specUpdate.data === undefined ? this.spec.data : specUpdate.data });
|
|
393
|
+
const snapshot = this._snapshotMutation();
|
|
394
|
+
try {
|
|
395
|
+
this._themeInput = themeInput;
|
|
396
|
+
['colors', 'background', 'padding', 'legend', 'labels', 'grid', 'branding'].forEach(key => { if (specUpdate[key] !== undefined) this._styleOverrides[key] = true; });
|
|
397
|
+
this.spec = result.spec;
|
|
398
|
+
['legend', 'labels', 'grid', 'branding', 'padding'].forEach(key => { if (specUpdate[key] !== undefined) this._preferenceBase[key] = clone(this.spec[key]); });
|
|
399
|
+
this._specDiagnostics = { warnings: result.warnings, normalizations: result.normalizations };
|
|
400
|
+
if (preferences !== undefined) {
|
|
401
|
+
if (this._preferencesStore) {
|
|
402
|
+
const scope = preferenceScope === 'global' ? 'global' : 'chart';
|
|
403
|
+
if (scope === 'global') this._preferencesStore.setGlobal(preferences, { source: 'update' });
|
|
404
|
+
else this._preferencesStore.setChart(this.chartId || 'default', preferences, { source: 'update' });
|
|
405
|
+
} else this._localPreferences = mergePreferences(this._localPreferences, preferences);
|
|
406
|
+
}
|
|
407
|
+
this._resolveStyle();
|
|
408
|
+
return this._commitMutation();
|
|
409
|
+
} catch (error) {
|
|
410
|
+
this._restoreMutation(snapshot);
|
|
411
|
+
throw error;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
setData(data) {
|
|
415
|
+
const result = this._validateMutation('setData', { ...this.spec, theme: this._themeInput, data: { values: data } });
|
|
416
|
+
const snapshot = this._snapshotMutation();
|
|
417
|
+
try {
|
|
418
|
+
this.spec = result.spec;
|
|
419
|
+
this._specDiagnostics = { warnings: result.warnings, normalizations: result.normalizations };
|
|
420
|
+
this._resolveStyle();
|
|
421
|
+
return this._commitMutation();
|
|
422
|
+
} catch (error) {
|
|
423
|
+
this._restoreMutation(snapshot);
|
|
424
|
+
throw error;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
setTheme(theme = 'auto') {
|
|
428
|
+
const result = this._validateMutation('setTheme', { ...this.spec, theme });
|
|
429
|
+
const snapshot = this._snapshotMutation();
|
|
430
|
+
try {
|
|
431
|
+
this._themeInput = theme;
|
|
432
|
+
this.spec = result.spec;
|
|
433
|
+
this._specDiagnostics = { warnings: result.warnings, normalizations: result.normalizations };
|
|
434
|
+
this._resolveStyle();
|
|
435
|
+
this._commitMutation();
|
|
436
|
+
this.emit('themechange', { chart: this, theme: this.getTheme(), source: 'user' });
|
|
437
|
+
return this;
|
|
438
|
+
} catch (error) {
|
|
439
|
+
this._restoreMutation(snapshot);
|
|
440
|
+
throw error;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
341
443
|
getTheme() { return clone(this.spec.theme); }
|
|
342
444
|
getPreferences() { return this._preferencesStore ? this._preferencesStore.getEffective(this.chartId) : clone(this._localPreferences || defaultPreferences); }
|
|
343
|
-
setPreferences(patch = {}, options = {}) { if (this._preferencesStore) { const scope = options.scope === 'global' ? 'global' : 'chart'; if (scope === 'global') this._preferencesStore.setGlobal(
|
|
344
|
-
resetPreferences(options = {}) { if (this._preferencesStore) { this._preferencesStore.reset({ ...options, scope: options.scope || 'chart', chartId: this.chartId || 'default' }); return this; } this._localPreferences = normalizePreferences({}); this._preferencesSnapshot = JSON.stringify(this._localPreferences); this._resolveStyle(); this.
|
|
445
|
+
setPreferences(patch = {}, options = {}) { const checked = validatePreferences(patch, { partial: true }); if (!checked.valid) throw new ChartValidationError('setPreferences', checked.errors); if (this._preferencesStore) { const scope = options.scope === 'global' ? 'global' : 'chart'; if (scope === 'global') this._preferencesStore.setGlobal(checked.value, { ...options, source: options.source || 'user' }); else this._preferencesStore.setChart(this.chartId || 'default', checked.value, { ...options, source: options.source || 'user' }); return this; } const snapshot = this._snapshotMutation(); try { this._localPreferences = mergePreferences(this._localPreferences, checked.value); this._preferencesSnapshot = JSON.stringify(this._localPreferences); this._resolveStyle(); this._commitMutation(); this.emit('preferenceschange', { chart: this, preferences: this.getPreferences(), source: options.source || 'user', scope: 'chart', persisted: false }); return this; } catch (error) { this._restoreMutation(snapshot); throw error; } }
|
|
446
|
+
resetPreferences(options = {}) { if (this._preferencesStore) { this._preferencesStore.reset({ ...options, scope: options.scope || 'chart', chartId: this.chartId || 'default' }); return this; } const snapshot = this._snapshotMutation(); try { this._localPreferences = normalizePreferences({}); this._preferencesSnapshot = JSON.stringify(this._localPreferences); this._resolveStyle(); this._commitMutation(); this.emit('preferenceschange', { chart: this, preferences: this.getPreferences(), source: options.source || 'user', scope: 'chart', persisted: false }); return this; } catch (error) { this._restoreMutation(snapshot); throw error; } }
|
|
345
447
|
resize(width = this.spec.width, height = this.spec.height) { this.spec.width = width; this.spec.height = height; this.render(); this.emit('resize', { chart: this, width, height }); return this; }
|
|
346
448
|
getSpec() { return JSON.parse(JSON.stringify(this.spec)); }
|
|
347
449
|
_getDiagnostics() { return [...new Map([...(this._specDiagnostics?.warnings || []), ...(this.model.data?.warnings || []), ...(this.spec.theme?.warnings || [])].map(item => [`${item.code}:${item.path || ''}`, item])).values()]; }
|
|
@@ -447,7 +549,36 @@ export class Chart {
|
|
|
447
549
|
getElementAt(x, y) { return this.model.scene.hit(x, y); }
|
|
448
550
|
selectBox(start, end) { const left = Math.min(start.x, end.x), right = Math.max(start.x, end.x), top = Math.min(start.y, end.y), bottom = Math.max(start.y, end.y); this._selected.clear(); this.model.scene.walk(node => { node.selected = false; if (!node.dataRef || !node.bounds || !node.interactive || (isDiagram(this) && !node.id.startsWith('node-'))) return; const intersects = node.bounds.x <= right && node.bounds.x + node.bounds.width >= left && node.bounds.y <= bottom && node.bounds.y + node.bounds.height >= top; if (intersects) { node.selected = true; this._selected.set(node.id, node.dataRef); } }); this.emit('selectionchange', { chart: this, selectedData: this.getSelectedData(), box: { start, end } }); this.render(); return this; }
|
|
449
551
|
toDataTable() { return this.model.data.rows.map(row => ({ ...row })); }
|
|
450
|
-
applyPatch(patches = []) {
|
|
552
|
+
applyPatch(patches = []) {
|
|
553
|
+
const candidate = clone(this.spec);
|
|
554
|
+
try {
|
|
555
|
+
patches.forEach((patch, index) => {
|
|
556
|
+
if (!patch || !['add', 'replace', 'remove'].includes(patch.op) || typeof patch.path !== 'string' || !patch.path.startsWith('/')) throw new ChartValidationError('applyPatch', [{ code: 'INVALID_PATCH', path: `patches[${index}]`, message: 'Patch requires add, replace, or remove and an absolute JSON pointer path.', suggestion: 'Use for example { op: "replace", path: "/title/text", value: "Revenue" }.' }]);
|
|
557
|
+
const path = patch.path.replace(/^\//, '').split('/').map(key => key.replace(/~1/g, '/').replace(/~0/g, '~'));
|
|
558
|
+
let target = candidate;
|
|
559
|
+
path.slice(0, -1).forEach(key => { if (target == null || typeof target !== 'object' || !(key in target)) throw new ChartValidationError('applyPatch', [{ code: 'INVALID_PATCH_PATH', path: `patches[${index}].path`, message: `Patch path does not exist: ${patch.path}`, suggestion: 'Patch an existing Spec path or add a direct child of an existing object.' }]); target = target[key]; });
|
|
560
|
+
const key = path.at(-1);
|
|
561
|
+
if (patch.op === 'remove') delete target[key];
|
|
562
|
+
else target[key] = clone(patch.value);
|
|
563
|
+
});
|
|
564
|
+
} catch (error) {
|
|
565
|
+
if (error instanceof ChartValidationError) throw error;
|
|
566
|
+
throw new ChartValidationError('applyPatch', [{ code: 'INVALID_PATCH', path: 'patches', message: error.message, suggestion: 'Use JSON-safe patch values and valid Spec paths.' }]);
|
|
567
|
+
}
|
|
568
|
+
const themePatched = patches.some(patch => typeof patch?.path === 'string' && (patch.path === '/theme' || patch.path.startsWith('/theme/')));
|
|
569
|
+
const result = this._validateMutation('applyPatch', { ...candidate, theme: themePatched ? candidate.theme : this._themeInput });
|
|
570
|
+
const snapshot = this._snapshotMutation();
|
|
571
|
+
try {
|
|
572
|
+
this.spec = result.spec;
|
|
573
|
+
if (themePatched) this._themeInput = candidate.theme;
|
|
574
|
+
this._specDiagnostics = { warnings: result.warnings, normalizations: result.normalizations };
|
|
575
|
+
this._resolveStyle();
|
|
576
|
+
return this._commitMutation();
|
|
577
|
+
} catch (error) {
|
|
578
|
+
this._restoreMutation(snapshot);
|
|
579
|
+
throw error;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
451
582
|
resetZoom() { delete this.spec.view; return this.render(); }
|
|
452
583
|
zoomTo(view) { this.spec.view = { ...view }; this.emit('zoomchange', { chart: this, view: this.spec.view }); return this.render(); }
|
|
453
584
|
panBy(delta) { const current = this.spec.view || {}; this.spec.view = { ...current, offsetX: (current.offsetX || 0) + (delta.x || 0), offsetY: (current.offsetY || 0) + (delta.y || 0) }; this.emit('zoomchange', { chart: this, view: this.spec.view }); return this.render(); }
|
|
@@ -626,11 +757,12 @@ export class Chart {
|
|
|
626
757
|
return { valid: false, code: 'DOWNLOAD_FAILED', message: String(err && err.message || err), suggestion: 'Try chart.export() and write the result manually.' };
|
|
627
758
|
}
|
|
628
759
|
}
|
|
629
|
-
destroy() {
|
|
760
|
+
destroy() { destroyChart(this); }
|
|
630
761
|
}
|
|
631
762
|
|
|
632
763
|
export function createChart(spec) { return new Chart(spec); }
|
|
633
|
-
export
|
|
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' } }; }
|
|
634
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 }; }
|
|
635
767
|
export { normalizeSpec, validateSpec, normalizeData, inspectData, binData, applyTransforms, data, getBusinessSchema, inspectDataSchema, validateData, getEditCapabilities, validateEdit, previewEdit, commitPreview, validateRecipe, getChartCapability, getPreferenceCapabilities, planChart, explainChart };
|
|
636
768
|
export { diagramLayoutModes, diagramModes, edgeRoutingModes, normalizeDiagramSpec, validateDiagram, layoutDiagram, routeEdge };
|
|
@@ -639,4 +771,4 @@ export { normalizeLinkedFilters, normalizeLinkedSelection, filterProjectRows, cr
|
|
|
639
771
|
|
|
640
772
|
export { contrastRatio, planStyle, resolveTheme, styleCapabilities, themeModes, themePalettes, themePresets, validateThemeContrast, annotationPlugin, dataZoomPlugin, dataLabelsPlugin, accessibilityPlugin };
|
|
641
773
|
export { applyPreferencesToSpec, createPreferencesStore, defaultPreferences, mergePreferences, mergeThemePreference, mountChartSettings, normalizePreferences, validatePreferences };
|
|
642
|
-
export const iChart = { version: '2.0.
|
|
774
|
+
export const iChart = { version: '2.0.14', 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/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
|
-
|
|
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.
|
|
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():
|
|
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;
|