@taylorwong/ichartjs 2.0.18 → 2.0.20
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 +12 -0
- package/README.md +3 -2
- package/docs/agent/charting-scenario.md +2 -0
- package/docs/agent/coding-agent-integration.md +1 -1
- package/docs/agent/conversational-workflow.md +134 -0
- package/docs/agent/development/2.0-release-readiness.md +1 -1
- package/docs/agent/development/iteration-15.md +48 -0
- package/docs/agent/development/iteration-16.md +70 -0
- package/docs/agent/development/roadmap.md +20 -2
- package/docs/agent/diagram-scenario.md +5 -1
- package/docs/agent/frontend-integration.md +1 -1
- package/docs/agent/runtime-contract.md +2 -0
- package/docs/agent/usage-scenarios.md +5 -1
- package/docs/agent/zh-CN/charting-scenario.md +2 -0
- package/docs/agent/zh-CN/coding-agent-integration.md +1 -1
- package/docs/agent/zh-CN/conversational-workflow.md +128 -0
- package/docs/agent/zh-CN/diagram-scenario.md +5 -1
- package/docs/agent/zh-CN/frontend-integration.md +1 -1
- package/docs/agent/zh-CN/runtime-contract.md +2 -0
- package/docs/agent/zh-CN/usage-scenarios.md +5 -1
- package/docs/manifests/capabilities.json +35 -5
- package/package.json +1 -1
- package/skills/ichartjs/SKILL.md +5 -2
- package/src/capabilities.mjs +23 -2
- package/src/charts.mjs +17 -5
- package/src/index.mjs +10 -6
- package/src/layout.mjs +68 -0
- package/src/preferences.mjs +24 -1
- package/src/project.mjs +101 -12
- package/src/renderer.mjs +2 -2
- package/src/scene.mjs +5 -0
- package/types/index.d.ts +5 -3
package/src/project.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { layoutArchitectureNodes, layoutDiagram, normalizeDiagramData, routeEdge
|
|
|
8
8
|
import { analyzeBurndownSeries, analyzeSchedule } from './project-analytics.mjs';
|
|
9
9
|
import { createLinkedProjectState, filterProjectRows, linkedRecordId } from './project-linking.mjs';
|
|
10
10
|
import { contrastRatio, resolveTheme } from './theme.mjs';
|
|
11
|
-
import { axisLabelLayout, estimateTextWidth, fontSize, titleLayout, truncateText } from './layout.mjs';
|
|
11
|
+
import { axisLabelLayout, boxesOverlap, estimateTextWidth, fitTextBlock, fontSize, titleLayout, truncateText } from './layout.mjs';
|
|
12
12
|
|
|
13
13
|
export const projectTypes = ['gantt', 'timeline', 'milestone', 'burndown', 'flow', 'swimlane', 'architecture', 'mindmap'];
|
|
14
14
|
const day = 86400000;
|
|
@@ -57,9 +57,18 @@ const dependencyType = dependency => typeof dependency === 'object' && dependenc
|
|
|
57
57
|
const markText = (theme, background) => (contrastRatio(theme.text, background) || 0) >= (contrastRatio(theme.background, background) || 0) ? theme.text : theme.background;
|
|
58
58
|
const flowNodeKind = node => node?.kind || 'process';
|
|
59
59
|
|
|
60
|
+
function diagramNodeLabelLayout(row, kind, geometry, spec, font = null) {
|
|
61
|
+
const size = font ? Number(String(font).match(/(\d+(?:\.\d+)?)px/)?.[1]) || fontSize(spec, 'label', 12) : fontSize(spec, 'label', 12);
|
|
62
|
+
const minDimension = Math.min(geometry.width, geometry.height);
|
|
63
|
+
const horizontalPadding = kind === 'decision' ? size * 1.8 : size * 1.25;
|
|
64
|
+
const maxWidth = Math.max(0, geometry.width - horizontalPadding * 2);
|
|
65
|
+
if (minDimension < Math.max(24, size * 2) || maxWidth < size * 2.4) return null;
|
|
66
|
+
return fitTextBlock(row.label || row.id, { maxWidth, maxHeight: Math.max(size, geometry.height - size * 0.45), size, minSize: Math.max(10, Math.min(size, size - 2)), maxLines: 2 });
|
|
67
|
+
}
|
|
68
|
+
|
|
60
69
|
function flowNodeVisual(kind, box) {
|
|
61
70
|
const cx = box.x + box.width / 2, cy = box.y + box.height / 2;
|
|
62
|
-
if (kind === 'start' || kind === 'end') return { type: '
|
|
71
|
+
if (kind === 'start' || kind === 'end') return { type: 'ellipse', geometry: { cx, cy, rx: box.width / 2, ry: box.height / 2 } };
|
|
63
72
|
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
73
|
if (kind === 'io') {
|
|
65
74
|
const skew = Math.min(18, box.width * 0.18);
|
|
@@ -103,8 +112,56 @@ function routeGanttDependency(dependency, from, to, positions, barOffset, plot)
|
|
|
103
112
|
return compactRoute([source, { x: sourceStubX, y: source.y }, { x: sourceStubX, y: corridorY }, { x: targetStubX, y: corridorY }, { x: targetStubX, y: target.y }, target]);
|
|
104
113
|
}
|
|
105
114
|
|
|
106
|
-
function text(scene, id, content, x, y, style = {}) {
|
|
107
|
-
scene.add({ id, type: 'text', geometry: { text: String(content), x, y }, style: { fill: scene.theme?.text || '#334155', font: scene.theme?.typography?.label?.font || '12px system-ui', ...style }, zIndex: 4 });
|
|
115
|
+
function text(scene, id, content, x, y, style = {}, dataRef = null) {
|
|
116
|
+
scene.add({ id, type: 'text', geometry: { text: String(content), x, y }, style: { fill: scene.theme?.text || '#334155', font: scene.theme?.typography?.label?.font || '12px system-ui', ...style }, dataRef, zIndex: 4 });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function textBlock(scene, id, block, x, y, style = {}, dataRef = null) {
|
|
120
|
+
if (!block) return;
|
|
121
|
+
const start = y - (block.lines.length - 1) * block.lineHeight / 2;
|
|
122
|
+
block.lines.forEach((line, index) => text(scene, index ? `${id}-${index}` : id, line, x, start + index * block.lineHeight, { textAnchor: 'middle', textBaseline: 'middle', baseline: 'middle', ...style }, dataRef));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function labelPlate(scene, id, box, fill, opacity = 0.94) {
|
|
126
|
+
scene.add({ id: `${id}-background`, type: 'rect', geometry: { x: box.x - 3, y: box.y - 2, width: box.width + 6, height: box.height + 4 }, style: { fill, opacity, stroke: 'none' }, zIndex: 3 });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function polylineMidpoint(points) {
|
|
130
|
+
if (!points?.length) return null;
|
|
131
|
+
if (points.length === 1) return { point: points[0], start: points[0], end: points[0] };
|
|
132
|
+
const lengths = points.slice(1).map((point, index) => Math.hypot(point.x - points[index].x, point.y - points[index].y));
|
|
133
|
+
const target = lengths.reduce((sum, value) => sum + value, 0) / 2;
|
|
134
|
+
let traversed = 0;
|
|
135
|
+
for (let index = 1; index < points.length; index += 1) {
|
|
136
|
+
const length = lengths[index - 1];
|
|
137
|
+
if (traversed + length >= target) {
|
|
138
|
+
const ratio = length ? (target - traversed) / length : 0;
|
|
139
|
+
return { point: { x: points[index - 1].x + (points[index].x - points[index - 1].x) * ratio, y: points[index - 1].y + (points[index].y - points[index - 1].y) * ratio }, start: points[index - 1], end: points[index] };
|
|
140
|
+
}
|
|
141
|
+
traversed += length;
|
|
142
|
+
}
|
|
143
|
+
return { point: points.at(-1), start: points.at(-2), end: points.at(-1) };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function edgeLabelLayout(edge, spec, positions, occupied = []) {
|
|
147
|
+
const points = edge.points, midpoint = edge.curve === 'cubic' ? { point: cubicBezierPoint(points, 0.5), start: cubicBezierPoint(points, 0.45), end: cubicBezierPoint(points, 0.55) } : polylineMidpoint(points);
|
|
148
|
+
if (!midpoint) return null;
|
|
149
|
+
const length = Math.max(1, edge.curve === 'cubic' ? Math.hypot(points.at(-1).x - points[0].x, points.at(-1).y - points[0].y) : Math.hypot(midpoint.end.x - midpoint.start.x, midpoint.end.y - midpoint.start.y));
|
|
150
|
+
const angle = Math.atan2(midpoint.end.y - midpoint.start.y, midpoint.end.x - midpoint.start.x), normal = { x: -Math.sin(angle), y: Math.cos(angle) }, size = fontSize(spec, 'axis', 12);
|
|
151
|
+
const block = fitTextBlock(edge.label, { maxWidth: Math.max(size * 3, Math.min(180, length - 10)), maxHeight: size * 2.6, size, minSize: 10, maxLines: 2 });
|
|
152
|
+
const inlineOffset = 8, candidates = [
|
|
153
|
+
{ x: midpoint.point.x, y: midpoint.point.y - inlineOffset, onLine: true },
|
|
154
|
+
{ x: midpoint.point.x + normal.x * (inlineOffset + block.height + 6), y: midpoint.point.y + normal.y * (inlineOffset + block.height + 6), onLine: false },
|
|
155
|
+
{ x: midpoint.point.x - normal.x * (inlineOffset + block.height + 6), y: midpoint.point.y - normal.y * (inlineOffset + block.height + 6), onLine: false },
|
|
156
|
+
{ x: midpoint.point.x + normal.x * (inlineOffset + 2 * (block.height + 6)), y: midpoint.point.y + normal.y * (inlineOffset + 2 * (block.height + 6)), onLine: false },
|
|
157
|
+
{ x: midpoint.point.x - normal.x * (inlineOffset + 2 * (block.height + 6)), y: midpoint.point.y - normal.y * (inlineOffset + 2 * (block.height + 6)), onLine: false }
|
|
158
|
+
];
|
|
159
|
+
const nodeBoxes = [...positions.values()];
|
|
160
|
+
for (const candidate of candidates) {
|
|
161
|
+
const box = { x: candidate.x - block.width / 2, y: candidate.y - block.height / 2, width: block.width, height: block.height };
|
|
162
|
+
if (!nodeBoxes.some(node => boxesOverlap(box, node, 2)) && !occupied.some(item => boxesOverlap(box, item, 3))) return { ...candidate, anchor: midpoint.point, box, block };
|
|
163
|
+
}
|
|
164
|
+
return { ...candidates[0], anchor: midpoint.point, box: { x: candidates[0].x - block.width / 2, y: candidates[0].y - block.height / 2, width: block.width, height: block.height }, block, suppressed: false };
|
|
108
165
|
}
|
|
109
166
|
|
|
110
167
|
function arrow(scene, id, points, reference, critical = false, curve = null) {
|
|
@@ -344,6 +401,9 @@ function diagramScene(scene, spec, rows, state) {
|
|
|
344
401
|
const columns = Math.max(1, ...[...ranks.values()].map(rank => rank + 1));
|
|
345
402
|
const gapX = Math.max(144, plot.width / columns), laneHeight = Math.max(80, plot.height / Math.max(1, lanes.length));
|
|
346
403
|
const positions = new Map(), slots = new Map();
|
|
404
|
+
const edgeLabelBoxes = [], labelLayout = { visible: 0, wrapped: 0, scaled: 0, truncated: 0, suppressed: 0, edgeOnLine: 0, edgeOffset: 0, backgrounded: 0 };
|
|
405
|
+
state.labelLayout = { ...(state.labelLayout || {}), diagram: labelLayout };
|
|
406
|
+
state.labelWarnings = [];
|
|
347
407
|
lanes.forEach((lane, index) => {
|
|
348
408
|
scene.add({ id: `lane-${index}`, type: 'rect', geometry: { x: plot.x, y: plot.y + index * laneHeight, width: Math.max(plot.width, columns * gapX), height: laneHeight }, style: { fill: index % 2 ? spec.theme.surface : spec.theme.background, stroke: spec.theme.border }, zIndex: -1 });
|
|
349
409
|
text(scene, `lane-label-${index}`, lane.label || lane.id, plot.x - 12, plot.y + index * laneHeight + 20, { textAnchor: 'end' });
|
|
@@ -364,7 +424,7 @@ function diagramScene(scene, spec, rows, state) {
|
|
|
364
424
|
const architectureBox = architectureLayout[row.id];
|
|
365
425
|
const geometry = architectureBox
|
|
366
426
|
? { ...architectureBox, x: row.position?.x ?? architectureBox.x, y: row.position?.y ?? architectureBox.y }
|
|
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) });
|
|
427
|
+
: flowNodeBox(row, lanes.length ? { ...generated, y: undefined } : 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) });
|
|
368
428
|
positions.set(row.id, geometry);
|
|
369
429
|
if (collapsedGroups.has(row.groupId)) return;
|
|
370
430
|
const depth = row.parentId ? (ranks.get(row.id) || 0) : 0;
|
|
@@ -375,7 +435,18 @@ function diagramScene(scene, spec, rows, state) {
|
|
|
375
435
|
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) };
|
|
376
436
|
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 });
|
|
377
437
|
});
|
|
378
|
-
|
|
438
|
+
const label = diagramNodeLabelLayout(row, kind, geometry, spec, nodeFont);
|
|
439
|
+
if (label) {
|
|
440
|
+
const rawLabel = String(row.label || row.id), labelFont = nodeFont ? nodeFont.replace(/\d+(?:\.\d+)?px/, `${label.size}px`) : `${label.size}px system-ui`;
|
|
441
|
+
textBlock(scene, `node-label-${row.id}`, label, geometry.x + geometry.width / 2, geometry.y + geometry.height / 2, { fill: markText(spec.theme, spec.colors[lane % spec.colors.length]), font: labelFont }, { nodeId: row.id, rawLabel, renderedLabel: label.lines.join('\n'), truncated: label.truncated });
|
|
442
|
+
labelLayout.visible += 1;
|
|
443
|
+
if (label.wrapped) labelLayout.wrapped += 1;
|
|
444
|
+
if (label.scaled) labelLayout.scaled += 1;
|
|
445
|
+
if (label.truncated) { labelLayout.truncated += 1; state.labelWarnings.push({ code: 'LABEL_TRUNCATED', path: `nodes.${index}.label`, nodeId: row.id, rawLabel, renderedLabel: label.lines.join('\n'), message: `Node label "${rawLabel}" was truncated to fit its shape.`, suggestion: 'Increase node size or shorten the label.' }); }
|
|
446
|
+
} else if (kind !== 'connector') {
|
|
447
|
+
labelLayout.suppressed += 1;
|
|
448
|
+
state.labelWarnings.push({ code: 'LABELS_SUPPRESSED', path: `nodes.${index}.label`, count: 1, nodeId: row.id, message: `Node label for "${row.label || row.id}" was hidden because the node is too small.`, suggestion: 'Increase node size or use the node tooltip/accessibility text.' });
|
|
449
|
+
}
|
|
379
450
|
});
|
|
380
451
|
const groupBoxes = new Map();
|
|
381
452
|
groups.forEach(group => {
|
|
@@ -414,8 +485,17 @@ function diagramScene(scene, spec, rows, state) {
|
|
|
414
485
|
const points = path.points;
|
|
415
486
|
arrow(scene, `edge-${index}`, points, { from: edge.from, to: edge.to, edgeId: edge.id || `edge-${index}`, status: edge.status, routing, curveTension: edge.curveTension ?? diagramSpec.diagram.curveTension, waypoints: edge.waypoints || [], fromPortDefinition, toPortDefinition, fromGroupId: fromNode.groupId || null, toGroupId: toNode.groupId || null }, edge.critical === true, path.curve);
|
|
416
487
|
if (edge.label) {
|
|
417
|
-
const
|
|
418
|
-
|
|
488
|
+
const placement = edgeLabelLayout({ ...edge, points, curve: path.curve }, spec, positions, edgeLabelBoxes);
|
|
489
|
+
if (placement) {
|
|
490
|
+
edgeLabelBoxes.push(placement.box);
|
|
491
|
+
const rawLabel = String(edge.label), labelFont = `${placement.block.size}px system-ui`;
|
|
492
|
+
if (placement.onLine) labelLayout.edgeOnLine += 1; else labelLayout.edgeOffset += 1;
|
|
493
|
+
if (placement.block.wrapped) labelLayout.wrapped += 1;
|
|
494
|
+
if (placement.block.scaled) labelLayout.scaled += 1;
|
|
495
|
+
if (placement.block.truncated) { labelLayout.truncated += 1; state.labelWarnings.push({ code: 'LABEL_TRUNCATED', path: `edges.${index}.label`, edgeId: edge.id || `edge-${index}`, rawLabel, renderedLabel: placement.block.lines.join('\n'), message: `Edge label "${rawLabel}" was truncated to fit the available line space.`, suggestion: 'Increase the edge length or shorten the label.' }); }
|
|
496
|
+
if (placement.onLine) { labelPlate(scene, `edge-label-${index}`, placement.box, spec.theme.background); labelLayout.backgrounded += 1; }
|
|
497
|
+
textBlock(scene, `edge-label-${index}`, placement.block, placement.x, placement.y, { fill: spec.theme.text, font: labelFont }, { edgeId: edge.id || `edge-${index}`, rawLabel, renderedLabel: placement.block.lines.join('\n'), onLine: placement.onLine, offsetX: placement.x - placement.anchor.x, offsetY: placement.y - placement.anchor.y });
|
|
498
|
+
}
|
|
419
499
|
}
|
|
420
500
|
});
|
|
421
501
|
state.nodePositions = Object.fromEntries(positions);
|
|
@@ -461,7 +541,10 @@ export function buildProjectScene(spec) {
|
|
|
461
541
|
state.projectAnalytics.warnings = state.schedule.warnings;
|
|
462
542
|
}
|
|
463
543
|
if (!data.rows.length) text(scene, 'empty', sourceRows.length && linked.visibleCount === 0 ? 'No matching data' : spec.emptyText || 'No data', spec.width / 2, spec.height / 2, { textAnchor: 'middle' });
|
|
464
|
-
else if (diagram)
|
|
544
|
+
else if (diagram) {
|
|
545
|
+
diagramScene(scene, spec, data.rows, state);
|
|
546
|
+
if (state.labelWarnings?.length) data.warnings.push(...state.labelWarnings);
|
|
547
|
+
}
|
|
465
548
|
else if (spec.type === 'burndown') burndownScene(scene, spec, data.rows, state);
|
|
466
549
|
else tasksScene(scene, spec, data.rows, state);
|
|
467
550
|
if (state.eventLayout?.collisionsUnresolved) data.warnings.push({
|
|
@@ -474,7 +557,7 @@ export function buildProjectScene(spec) {
|
|
|
474
557
|
const geometry = node.geometry;
|
|
475
558
|
['x', 'x1', 'x2', 'cx'].forEach(key => { if (geometry[key] != null) geometry[key] = mapX(geometry[key]); });
|
|
476
559
|
['y', 'y1', 'y2', 'cy'].forEach(key => { if (geometry[key] != null) geometry[key] = mapY(geometry[key]); });
|
|
477
|
-
['width', 'height', 'r'].forEach(key => { if (geometry[key] != null) geometry[key] *= view.scale; });
|
|
560
|
+
['width', 'height', 'r', 'rx', 'ry'].forEach(key => { if (geometry[key] != null) geometry[key] *= view.scale; });
|
|
478
561
|
if (geometry.points) geometry.points = geometry.points.map(point => ({ x: mapX(point.x), y: mapY(point.y) }));
|
|
479
562
|
if (node.bounds) node.bounds = { x: mapX(node.bounds.x), y: mapY(node.bounds.y), width: node.bounds.width * view.scale, height: node.bounds.height * view.scale };
|
|
480
563
|
});
|
|
@@ -515,7 +598,7 @@ export function projectTooltip(type, row, locale = 'en-US') {
|
|
|
515
598
|
|
|
516
599
|
export function rerouteDiagramScene(scene) {
|
|
517
600
|
const nodeGeometries = new Map();
|
|
518
|
-
scene.walk(item => { if (item.id.startsWith('node-') && item.dataRef?.nodeId && item.geometry) nodeGeometries.set(item.dataRef.nodeId, item.geometry); });
|
|
601
|
+
scene.walk(item => { if (item.id.startsWith('node-') && !item.id.startsWith('node-label-') && item.dataRef?.nodeId && item.geometry) nodeGeometries.set(item.dataRef.nodeId, item.geometry); });
|
|
519
602
|
scene.walk(node => {
|
|
520
603
|
if (!node.id.startsWith('edge-') || !node.dataRef?.from || !node.dataRef?.to) return;
|
|
521
604
|
const source = nodeGeometries.get(node.dataRef.from), destination = nodeGeometries.get(node.dataRef.to);
|
|
@@ -529,7 +612,13 @@ export function rerouteDiagramScene(scene) {
|
|
|
529
612
|
const arrowNode = scene.find(`${node.id}-arrow`), beforeTip = node.geometry.points.at(-2);
|
|
530
613
|
if (arrowNode) arrowNode.geometry.points = [end, { x: end.x - 7 * Math.cos(Math.atan2(end.y - beforeTip.y, end.x - beforeTip.x) - Math.PI / 6), y: end.y - 7 * Math.sin(Math.atan2(end.y - beforeTip.y, end.x - beforeTip.x) - Math.PI / 6) }, { x: end.x - 7 * Math.cos(Math.atan2(end.y - beforeTip.y, end.x - beforeTip.x) + Math.PI / 6), y: end.y - 7 * Math.sin(Math.atan2(end.y - beforeTip.y, end.x - beforeTip.x) + Math.PI / 6) }, end];
|
|
531
614
|
const edgeIndex = Number(node.id.slice(5)), label = scene.find(`edge-label-${edgeIndex}`), labelPoint = node.geometry.curve === 'cubic' ? cubicBezierPoint(node.geometry.points, 0.5) : node.geometry.points[Math.floor(node.geometry.points.length / 2)];
|
|
532
|
-
if (label && labelPoint) {
|
|
615
|
+
if (label && labelPoint) {
|
|
616
|
+
const old = { x: label.geometry.x, y: label.geometry.y }, offsetX = Number(label.dataRef?.offsetX) || 0, offsetY = Number(label.dataRef?.offsetY) || 0;
|
|
617
|
+
label.geometry.x = labelPoint.x + offsetX; label.geometry.y = labelPoint.y + offsetY;
|
|
618
|
+
const background = scene.find(`edge-label-${edgeIndex}-background`), dx = label.geometry.x - old.x, dy = label.geometry.y - old.y;
|
|
619
|
+
if (background) { background.geometry.x += dx; background.geometry.y += dy; }
|
|
620
|
+
for (let lineIndex = 1; ; lineIndex += 1) { const line = scene.find(`edge-label-${edgeIndex}-${lineIndex}`); if (!line) break; line.geometry.x += dx; line.geometry.y += dy; }
|
|
621
|
+
}
|
|
533
622
|
});
|
|
534
623
|
return scene;
|
|
535
624
|
}
|
package/src/renderer.mjs
CHANGED
|
@@ -31,7 +31,7 @@ export class CanvasRenderer extends BaseRenderer {
|
|
|
31
31
|
mount(container) { super.mount(container); this.canvas = this.container.tagName === 'CANVAS' ? this.container : document.createElement('canvas'); if (this.canvas !== this.container) { this.container.innerHTML = ''; this.container.appendChild(this.canvas); } this.canvas.style.background = this.options.background || '#ffffff'; this.ctx = this.canvas.getContext('2d'); return this; }
|
|
32
32
|
resize(width, height, pixelRatio = globalThis.devicePixelRatio || 1) { this.width = width; this.height = height; this.pixelRatio = pixelRatio; this.canvas.width = Math.round(width * pixelRatio); this.canvas.height = Math.round(height * pixelRatio); if (this.canvas.style) { this.canvas.style.width = `${width}px`; this.canvas.style.height = `${height}px`; } this.ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0); }
|
|
33
33
|
clear() { if (this.ctx) this.ctx.clearRect(0, 0, this.width, this.height); }
|
|
34
|
-
render(scene) { this.clear(); const ctx = this.ctx; if (hasFill({ fill: this.options?.background })) { ctx.save(); ctx.fillStyle = this.options.background; ctx.fillRect(0, 0, this.width, this.height); ctx.restore(); } const draw = node => { if (!node.visible) return; const g = node.geometry || {}; const s = node.style || {}; ctx.save(); ctx.globalAlpha = s.opacity == null ? 1 : s.opacity; ctx.fillStyle = hasFill(s) ? color(s, 'fill', node.highlighted || node.selected ? '#1d4ed8' : '#2563eb') : 'rgba(0,0,0,0)'; ctx.strokeStyle = hasStroke(s) ? color(s, 'stroke', '#0f172a') : 'rgba(0,0,0,0)'; ctx.lineWidth = s.strokeWidth || 1; if (node.type === 'rect') { if (hasFill(s)) ctx.fillRect(g.x, g.y, g.width, g.height); if (hasStroke(s)) ctx.strokeRect(g.x, g.y, g.width, g.height); } else if (node.type === 'line') { ctx.beginPath(); ctx.moveTo(g.x1, g.y1); ctx.lineTo(g.x2, g.y2); if (hasStroke(s)) ctx.stroke(); } else if (node.type === 'path') { ctx.beginPath(); if (g.curve === 'cubic' && g.points?.length === 4) { ctx.moveTo(g.points[0].x, g.points[0].y); ctx.bezierCurveTo(g.points[1].x, g.points[1].y, g.points[2].x, g.points[2].y, g.points[3].x, g.points[3].y); } else g.points.forEach((point, index) => index ? ctx.lineTo(point.x, point.y) : ctx.moveTo(point.x, point.y)); if (g.closed) ctx.closePath(); if (hasFill(s)) ctx.fill(); if (hasStroke(s)) ctx.stroke(); } else if (node.type === 'circle') { ctx.beginPath(); ctx.arc(g.cx, g.cy, g.r, 0, Math.PI * 2); if (hasFill(s)) ctx.fill(); if (hasStroke(s)) ctx.stroke(); } else if (node.type === 'arc') { ctx.beginPath(); ctx.arc(g.cx, g.cy, g.r, g.start, g.end); if (g.innerR > 0) { ctx.arc(g.cx, g.cy, g.innerR, g.end, g.start, true); } else ctx.lineTo(g.cx, g.cy); ctx.closePath(); if (hasFill(s)) ctx.fill(); if (hasStroke(s)) ctx.stroke(); } else if (node.type === 'text') { ctx.fillStyle = hasFill(s) ? s.fill : '#0f172a'; ctx.font = s.font || '12px system-ui'; const align = s.textAlign || (s.textAnchor === 'end' ? 'end' : s.textAnchor === 'middle' || s.textAnchor === 'center' ? 'center' : s.textAnchor === 'start' ? 'start' : 'left'); ctx.textAlign = align; ctx.textBaseline = s.textBaseline || s.baseline || 'alphabetic'; if (Number.isFinite(Number(s.rotation)) && Number(s.rotation) !== 0) { ctx.translate(g.x, g.y); ctx.rotate(Number(s.rotation) * Math.PI / 180); ctx.fillText(g.text, 0, 0); } else ctx.fillText(g.text, g.x, g.y); } node.children.slice().sort((a, b) => a.zIndex - b.zIndex).forEach(draw); ctx.restore(); }; draw(scene.root); }
|
|
34
|
+
render(scene) { this.clear(); const ctx = this.ctx; if (hasFill({ fill: this.options?.background })) { ctx.save(); ctx.fillStyle = this.options.background; ctx.fillRect(0, 0, this.width, this.height); ctx.restore(); } const draw = node => { if (!node.visible) return; const g = node.geometry || {}; const s = node.style || {}; ctx.save(); ctx.globalAlpha = s.opacity == null ? 1 : s.opacity; ctx.fillStyle = hasFill(s) ? color(s, 'fill', node.highlighted || node.selected ? '#1d4ed8' : '#2563eb') : 'rgba(0,0,0,0)'; ctx.strokeStyle = hasStroke(s) ? color(s, 'stroke', '#0f172a') : 'rgba(0,0,0,0)'; ctx.lineWidth = s.strokeWidth || 1; if (node.type === 'rect') { if (hasFill(s)) ctx.fillRect(g.x, g.y, g.width, g.height); if (hasStroke(s)) ctx.strokeRect(g.x, g.y, g.width, g.height); } else if (node.type === 'line') { ctx.beginPath(); ctx.moveTo(g.x1, g.y1); ctx.lineTo(g.x2, g.y2); if (hasStroke(s)) ctx.stroke(); } else if (node.type === 'path') { ctx.beginPath(); if (g.curve === 'cubic' && g.points?.length === 4) { ctx.moveTo(g.points[0].x, g.points[0].y); ctx.bezierCurveTo(g.points[1].x, g.points[1].y, g.points[2].x, g.points[2].y, g.points[3].x, g.points[3].y); } else g.points.forEach((point, index) => index ? ctx.lineTo(point.x, point.y) : ctx.moveTo(point.x, point.y)); if (g.closed) ctx.closePath(); if (hasFill(s)) ctx.fill(); if (hasStroke(s)) ctx.stroke(); } else if (node.type === 'circle') { ctx.beginPath(); ctx.arc(g.cx, g.cy, g.r, 0, Math.PI * 2); if (hasFill(s)) ctx.fill(); if (hasStroke(s)) ctx.stroke(); } else if (node.type === 'ellipse') { ctx.beginPath(); if (typeof ctx.ellipse === 'function') ctx.ellipse(g.cx, g.cy, g.rx, g.ry, 0, 0, Math.PI * 2); else { ctx.save(); ctx.translate(g.cx, g.cy); ctx.scale(g.rx, g.ry); ctx.arc(0, 0, 1, 0, Math.PI * 2); ctx.restore(); } if (hasFill(s)) ctx.fill(); if (hasStroke(s)) ctx.stroke(); } else if (node.type === 'arc') { ctx.beginPath(); ctx.arc(g.cx, g.cy, g.r, g.start, g.end); if (g.innerR > 0) { ctx.arc(g.cx, g.cy, g.innerR, g.end, g.start, true); } else ctx.lineTo(g.cx, g.cy); ctx.closePath(); if (hasFill(s)) ctx.fill(); if (hasStroke(s)) ctx.stroke(); } else if (node.type === 'text') { ctx.fillStyle = hasFill(s) ? s.fill : '#0f172a'; ctx.font = s.font || '12px system-ui'; const align = s.textAlign || (s.textAnchor === 'end' ? 'end' : s.textAnchor === 'middle' || s.textAnchor === 'center' ? 'center' : s.textAnchor === 'start' ? 'start' : 'left'); ctx.textAlign = align; ctx.textBaseline = s.textBaseline || s.baseline || 'alphabetic'; if (Number.isFinite(Number(s.rotation)) && Number(s.rotation) !== 0) { ctx.translate(g.x, g.y); ctx.rotate(Number(s.rotation) * Math.PI / 180); ctx.fillText(g.text, 0, 0); } else ctx.fillText(g.text, g.x, g.y); } node.children.slice().sort((a, b) => a.zIndex - b.zIndex).forEach(draw); ctx.restore(); }; draw(scene.root); }
|
|
35
35
|
exportImage(type = 'image/png') { return this.canvas.toDataURL(type); }
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -86,6 +86,6 @@ export class SVGRenderer extends BaseRenderer {
|
|
|
86
86
|
this.svg.setAttribute('viewBox', `0 0 ${w} ${h}`);
|
|
87
87
|
this.width = w; this.height = h;
|
|
88
88
|
}
|
|
89
|
-
this.clear(); const renderNode = node => { if (!node.visible || node.type === 'root') { node.children.slice().sort((a, b) => a.zIndex - b.zIndex).forEach(renderNode); return; } const g = node.geometry || {}, s = node.style || {}; const element = document.createElementNS('http://www.w3.org/2000/svg', node.type === 'path' || node.type === 'arc' ? 'path' : node.type === 'text' ? 'text' : node.type); element.setAttribute('id', node.id); element.setAttribute('data-node-id', node.id); if (node.dataRef) element.dataset.dataRef = JSON.stringify(node.dataRef); if (node.type === 'rect') { element.setAttribute('x', g.x); element.setAttribute('y', g.y); element.setAttribute('width', g.width); element.setAttribute('height', g.height); } else if (node.type === 'line') { ['x1', 'y1', 'x2', 'y2'].forEach(key => element.setAttribute(key, g[key])); } else if (node.type === 'circle') { element.setAttribute('cx', g.cx); element.setAttribute('cy', g.cy); element.setAttribute('r', g.r); } else if (node.type === 'arc') { const large = g.end - g.start > Math.PI ? 1 : 0, outerStart = `${g.cx + g.r * Math.cos(g.start)} ${g.cy + g.r * Math.sin(g.start)}`, outerEnd = `${g.cx + g.r * Math.cos(g.end)} ${g.cy + g.r * Math.sin(g.end)}`; const p = g.innerR > 0 ? [`M ${outerStart}`, `A ${g.r} ${g.r} 0 ${large} 1 ${outerEnd}`, `L ${g.cx + g.innerR * Math.cos(g.end)} ${g.cy + g.innerR * Math.sin(g.end)}`, `A ${g.innerR} ${g.innerR} 0 ${large} 0 ${g.cx + g.innerR * Math.cos(g.start)} ${g.cy + g.innerR * Math.sin(g.start)}`, 'Z'] : [`M ${g.cx} ${g.cy}`, `L ${outerStart}`, `A ${g.r} ${g.r} 0 ${large} 1 ${outerEnd}`, 'Z']; element.setAttribute('d', p.join(' ')); } else if (node.type === 'path') { const d = g.curve === 'cubic' && g.points?.length === 4 ? `M ${g.points[0].x} ${g.points[0].y} C ${g.points[1].x} ${g.points[1].y} ${g.points[2].x} ${g.points[2].y} ${g.points[3].x} ${g.points[3].y}` : `${g.points.map((p, i) => `${i ? 'L' : 'M'} ${p.x} ${p.y}`).join(' ')}${g.closed ? ' Z' : ''}`; element.setAttribute('d', d); } else if (node.type === 'text') { element.setAttribute('x', g.x); element.setAttribute('y', g.y); if (Number.isFinite(Number(s.rotation)) && Number(s.rotation) !== 0) element.setAttribute('transform', `rotate(${Number(s.rotation)} ${g.x} ${g.y})`); element.textContent = g.text; const anchor = s.textAnchor || (s.textAlign === 'end' ? 'end' : s.textAlign === 'center' ? 'middle' : s.textAlign === 'right' ? 'end' : s.textAlign === 'left' ? 'start' : 'start'); if (anchor) element.setAttribute('text-anchor', anchor); const baseline = s.textBaseline || s.baseline || 'alphabetic'; if (baseline === 'top') { element.setAttribute('dominant-baseline', 'text-before-edge'); element.setAttribute('alignment-baseline', 'before-edge'); } else if (baseline === 'middle' || baseline === 'central') { element.setAttribute('dominant-baseline', 'middle'); element.setAttribute('alignment-baseline', 'middle'); } else if (baseline === 'bottom' || baseline === 'hanging') { element.setAttribute('dominant-baseline', 'text-after-edge'); element.setAttribute('alignment-baseline', 'after-edge'); } else { element.setAttribute('dominant-baseline', 'alphabetic'); element.setAttribute('alignment-baseline', 'alphabetic'); } if (s.font) { const raw = String(s.font).trim(); element.setAttribute('font', raw); const sizeMatch = raw.match(/\b(\d+(?:\.\d+)?)\s*(px|em|rem|pt|%)/i); const size = sizeMatch ? `${sizeMatch[1]}${sizeMatch[2].toLowerCase()}` : ''; if (size) element.style.fontSize = size; const weightMatch = raw.match(/^\s*(\d{3}|normal|bold|lighter|bolder)\b/i); if (weightMatch) element.style.fontWeight = weightMatch[1]; const family = raw.replace(/^\s*(?:(?:normal|italic|oblique)(?:\s+[^0-9\s]+)?\s+)?(?:\d{3}|normal|bold|lighter|bolder)\s+/i, '').replace(/\b\d+(?:\.\d+)?\s*(?:px|em|rem|pt|%)(?:\s*\/\s*\S+)?\s*/i, '').trim(); if (family) element.style.fontFamily = family; } } if (node.type === 'path' || node.type === 'circle' || node.type === 'rect' || node.type === 'arc') { element.setAttribute('fill', hasFill(s) ? s.fill : 'none'); } else if (node.type === 'text') { element.setAttribute('fill', hasFill(s) ? s.fill : '#0f172a'); } if (hasStroke(s)) element.setAttribute('stroke', s.stroke); else if (s.stroke != null && typeof s.stroke === 'string' && NO_FILL_TOKENS.has(s.stroke.trim().toLowerCase())) element.setAttribute('stroke', 'none'); if (s.strokeWidth != null) element.setAttribute('stroke-width', s.strokeWidth); if (s.opacity != null) element.setAttribute('opacity', node.highlighted || node.selected ? 1 : s.opacity); if (node.highlighted || node.selected) element.setAttribute('filter', 'brightness(1.2)'); if (node.interactive) element.style.cursor = 'pointer'; if (s.pointerEvents === 'none') element.style.pointerEvents = 'none'; if (s.ariaHidden === 'true') element.setAttribute('aria-hidden', 'true'); if (s.role === 'presentation') element.setAttribute('role', 'presentation'); if (node.decorative) { element.setAttribute('aria-hidden', 'true'); element.setAttribute('role', 'presentation'); element.style.pointerEvents = 'none'; element.style.userSelect = 'none'; } this.svg.appendChild(element); node.children.slice().sort((a, b) => a.zIndex - b.zIndex).forEach(renderNode); }; scene.root.children.slice().sort((a, b) => a.zIndex - b.zIndex).forEach(renderNode); }
|
|
89
|
+
this.clear(); const renderNode = node => { if (!node.visible || node.type === 'root') { node.children.slice().sort((a, b) => a.zIndex - b.zIndex).forEach(renderNode); return; } const g = node.geometry || {}, s = node.style || {}; const element = document.createElementNS('http://www.w3.org/2000/svg', node.type === 'path' || node.type === 'arc' ? 'path' : node.type === 'text' ? 'text' : node.type); element.setAttribute('id', node.id); element.setAttribute('data-node-id', node.id); if (node.dataRef) element.dataset.dataRef = JSON.stringify(node.dataRef); if (node.type === 'rect') { element.setAttribute('x', g.x); element.setAttribute('y', g.y); element.setAttribute('width', g.width); element.setAttribute('height', g.height); } else if (node.type === 'line') { ['x1', 'y1', 'x2', 'y2'].forEach(key => element.setAttribute(key, g[key])); } else if (node.type === 'circle') { element.setAttribute('cx', g.cx); element.setAttribute('cy', g.cy); element.setAttribute('r', g.r); } else if (node.type === 'ellipse') { element.setAttribute('cx', g.cx); element.setAttribute('cy', g.cy); element.setAttribute('rx', g.rx); element.setAttribute('ry', g.ry); } else if (node.type === 'arc') { const large = g.end - g.start > Math.PI ? 1 : 0, outerStart = `${g.cx + g.r * Math.cos(g.start)} ${g.cy + g.r * Math.sin(g.start)}`, outerEnd = `${g.cx + g.r * Math.cos(g.end)} ${g.cy + g.r * Math.sin(g.end)}`; const p = g.innerR > 0 ? [`M ${outerStart}`, `A ${g.r} ${g.r} 0 ${large} 1 ${outerEnd}`, `L ${g.cx + g.innerR * Math.cos(g.end)} ${g.cy + g.innerR * Math.sin(g.end)}`, `A ${g.innerR} ${g.innerR} 0 ${large} 0 ${g.cx + g.innerR * Math.cos(g.start)} ${g.cy + g.innerR * Math.sin(g.start)}`, 'Z'] : [`M ${g.cx} ${g.cy}`, `L ${outerStart}`, `A ${g.r} ${g.r} 0 ${large} 1 ${outerEnd}`, 'Z']; element.setAttribute('d', p.join(' ')); } else if (node.type === 'path') { const d = g.curve === 'cubic' && g.points?.length === 4 ? `M ${g.points[0].x} ${g.points[0].y} C ${g.points[1].x} ${g.points[1].y} ${g.points[2].x} ${g.points[2].y} ${g.points[3].x} ${g.points[3].y}` : `${g.points.map((p, i) => `${i ? 'L' : 'M'} ${p.x} ${p.y}`).join(' ')}${g.closed ? ' Z' : ''}`; element.setAttribute('d', d); } else if (node.type === 'text') { element.setAttribute('x', g.x); element.setAttribute('y', g.y); if (Number.isFinite(Number(s.rotation)) && Number(s.rotation) !== 0) element.setAttribute('transform', `rotate(${Number(s.rotation)} ${g.x} ${g.y})`); element.textContent = g.text; const anchor = s.textAnchor || (s.textAlign === 'end' ? 'end' : s.textAlign === 'center' ? 'middle' : s.textAlign === 'right' ? 'end' : s.textAlign === 'left' ? 'start' : 'start'); if (anchor) element.setAttribute('text-anchor', anchor); const baseline = s.textBaseline || s.baseline || 'alphabetic'; if (baseline === 'top') { element.setAttribute('dominant-baseline', 'text-before-edge'); element.setAttribute('alignment-baseline', 'before-edge'); } else if (baseline === 'middle' || baseline === 'central') { element.setAttribute('dominant-baseline', 'middle'); element.setAttribute('alignment-baseline', 'middle'); } else if (baseline === 'bottom' || baseline === 'hanging') { element.setAttribute('dominant-baseline', 'text-after-edge'); element.setAttribute('alignment-baseline', 'after-edge'); } else { element.setAttribute('dominant-baseline', 'alphabetic'); element.setAttribute('alignment-baseline', 'alphabetic'); } if (s.font) { const raw = String(s.font).trim(); element.setAttribute('font', raw); const sizeMatch = raw.match(/\b(\d+(?:\.\d+)?)\s*(px|em|rem|pt|%)/i); const size = sizeMatch ? `${sizeMatch[1]}${sizeMatch[2].toLowerCase()}` : ''; if (size) element.style.fontSize = size; const weightMatch = raw.match(/^\s*(\d{3}|normal|bold|lighter|bolder)\b/i); if (weightMatch) element.style.fontWeight = weightMatch[1]; const family = raw.replace(/^\s*(?:(?:normal|italic|oblique)(?:\s+[^0-9\s]+)?\s+)?(?:\d{3}|normal|bold|lighter|bolder)\s+/i, '').replace(/\b\d+(?:\.\d+)?\s*(?:px|em|rem|pt|%)(?:\s*\/\s*\S+)?\s*/i, '').trim(); if (family) element.style.fontFamily = family; } } if (node.type === 'path' || node.type === 'circle' || node.type === 'ellipse' || node.type === 'rect' || node.type === 'arc') { element.setAttribute('fill', hasFill(s) ? s.fill : 'none'); } else if (node.type === 'text') { element.setAttribute('fill', hasFill(s) ? s.fill : '#0f172a'); } if (hasStroke(s)) element.setAttribute('stroke', s.stroke); else if (s.stroke != null && typeof s.stroke === 'string' && NO_FILL_TOKENS.has(s.stroke.trim().toLowerCase())) element.setAttribute('stroke', 'none'); if (s.strokeWidth != null) element.setAttribute('stroke-width', s.strokeWidth); if (s.opacity != null) element.setAttribute('opacity', node.highlighted || node.selected ? 1 : s.opacity); if (node.highlighted || node.selected) element.setAttribute('filter', 'brightness(1.2)'); if (node.interactive) element.style.cursor = 'pointer'; if (s.pointerEvents === 'none') element.style.pointerEvents = 'none'; if (s.ariaHidden === 'true') element.setAttribute('aria-hidden', 'true'); if (s.role === 'presentation') element.setAttribute('role', 'presentation'); if (node.decorative) { element.setAttribute('aria-hidden', 'true'); element.setAttribute('role', 'presentation'); element.style.pointerEvents = 'none'; element.style.userSelect = 'none'; } this.svg.appendChild(element); node.children.slice().sort((a, b) => a.zIndex - b.zIndex).forEach(renderNode); }; scene.root.children.slice().sort((a, b) => a.zIndex - b.zIndex).forEach(renderNode); }
|
|
90
90
|
exportString() { if (!this.svg) return ''; return new XMLSerializer().serializeToString(this.svg); }
|
|
91
91
|
}
|
package/src/scene.mjs
CHANGED
|
@@ -37,6 +37,11 @@ function geometryHit(node, x, y) {
|
|
|
37
37
|
const points = geometry.curve === 'cubic' ? sampleCubicBezier(geometry.points) : geometry.points;
|
|
38
38
|
return points.some((point, index) => index > 0 && pointToSegmentDistance({ x, y }, points[index - 1], point) <= tolerance);
|
|
39
39
|
}
|
|
40
|
+
if (node.type === 'ellipse') {
|
|
41
|
+
const rx = Math.max(1, Number(geometry.rx) || 0) + tolerance;
|
|
42
|
+
const ry = Math.max(1, Number(geometry.ry) || 0) + tolerance;
|
|
43
|
+
return ((x - geometry.cx) / rx) ** 2 + ((y - geometry.cy) / ry) ** 2 <= 1;
|
|
44
|
+
}
|
|
40
45
|
return Boolean(node.bounds && x >= node.bounds.x && x <= node.bounds.x + node.bounds.width && y >= node.bounds.y && y <= node.bounds.y + node.bounds.height);
|
|
41
46
|
}
|
|
42
47
|
|
package/types/index.d.ts
CHANGED
|
@@ -16,8 +16,9 @@ export type ChartPreferencesPatch = Partial<Omit<ChartPreferences, 'version'>> &
|
|
|
16
16
|
export interface PreferenceCapabilityOption { value: string | number | boolean | null; label: string; }
|
|
17
17
|
export interface PreferenceFieldCapability { path: string; group: 'theme' | 'typography' | 'components' | 'behavior'; groupLabel: string; type: 'enum' | 'number' | 'tri-state'; label: string; description: string; default: string | number | boolean | null; options: PreferenceCapabilityOption[]; scopes: Array<'global' | 'chart'>; supported: boolean; applicability: string; menu: { visible: boolean; control: 'select' | 'toggle' }; minimum?: number; maximum?: number; }
|
|
18
18
|
export interface PreferenceCapabilities { version: '1.0'; locale: 'en' | 'zh-CN'; chartType: ChartType | null; scopes: Array<'global' | 'chart'>; persistence: Array<'memory' | 'localStorage' | 'adapter'>; precedence: string[]; fields: PreferenceFieldCapability[]; }
|
|
19
|
+
export interface PreferenceResolution { version: '1.0'; precedence: string[]; storage: 'localStorage' | 'adapter' | 'memory'; scopes: { defaults: string; spec: string; global: string | null; chart: string | null; }; }
|
|
19
20
|
export interface PreferencesStoreState { version: '1.0'; global: ChartPreferences; charts: Record<string, ChartPreferencesPatch>; }
|
|
20
|
-
export interface PreferencesStore { version: '1.0'; storageKey: string; storage: 'localStorage' | 'memory'; getState(): PreferencesStoreState; getGlobal(): ChartPreferences; getChart(chartId: string): ChartPreferencesPatch; getEffective(chartId?: string | null): ChartPreferences; setGlobal(patch: ChartPreferencesPatch, options?: { source?: string; persist?: boolean }): ChartPreferences; setChart(chartId: string, patch: ChartPreferencesPatch, options?: { source?: string; persist?: boolean }): ChartPreferences; reset(options?: { scope?: 'all' | 'global' | 'chart'; chartId?: string; source?: string; persist?: boolean }): ChartPreferences; subscribe(listener: (event: { type: 'change'; scope: string; chartId: string | null; source: string; persisted: boolean; preferences: ChartPreferences; state: PreferencesStoreState }) => void): () => void; persist(): boolean; }
|
|
21
|
+
export interface PreferencesStore { version: '1.0'; storageKey: string; storage: 'localStorage' | 'adapter' | 'memory'; getState(): PreferencesStoreState; getGlobal(): ChartPreferences; getChart(chartId: string): ChartPreferencesPatch; getEffective(chartId?: string | null): ChartPreferences; getResolution(chartId?: string | null): PreferenceResolution; setGlobal(patch: ChartPreferencesPatch, options?: { source?: string; persist?: boolean }): ChartPreferences; setChart(chartId: string, patch: ChartPreferencesPatch, options?: { source?: string; persist?: boolean }): ChartPreferences; reset(options?: { scope?: 'all' | 'global' | 'chart'; chartId?: string; source?: string; persist?: boolean }): ChartPreferences; subscribe(listener: (event: { type: 'change'; scope: string; chartId: string | null; source: string; persisted: boolean; preferences: ChartPreferences; state: PreferencesStoreState }) => void): () => void; persist(): boolean; }
|
|
21
22
|
export interface PreferencesStorage { getItem(key: string): string | null; setItem(key: string, value: string): void; removeItem?(key: string): void; }
|
|
22
23
|
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; }
|
|
23
24
|
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[]; }
|
|
@@ -26,7 +27,7 @@ export class ChartValidationError extends Error { name: 'ChartValidationError';
|
|
|
26
27
|
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; }
|
|
27
28
|
export interface DataInspection { version: '1.0'; rows: number; fields: DataFieldInfo[]; dimensions: string[]; measures: string[]; temporalFields: string[]; missingValueCount: number; warnings: Diagnostic[]; }
|
|
28
29
|
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>; }
|
|
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; }
|
|
30
|
+
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>; conversationalWorkflow?: { version: '1.0'; steps: string[]; routing: Record<string, string>; confirmation: string[]; selfCheck: string[] }; 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; }
|
|
30
31
|
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; }
|
|
31
32
|
export interface AxisState { rawDomain: [number, number]; domain: [number, number]; ticks: number[]; step: number | null; policy: 'nice' | 'raw' | 'explicit'; }
|
|
32
33
|
export interface TimeAxisState { orientation: 'horizontal'; field: 'date'; coordinate: 'x'; rowCoordinate: 'y'; domain: [number, number]; domainISO: [string, string]; }
|
|
@@ -105,7 +106,7 @@ export interface ClipboardState { nodes: DiagramNode[]; edges: DiagramEdge[]; }
|
|
|
105
106
|
export interface ChartLayoutState { family: string | null; plot: Record<string, unknown>; chrome: Record<string, unknown> | null; labels: Record<string, unknown> | null; }
|
|
106
107
|
export interface ChartAxesState { y?: AxisState; right?: AxisState; [key: string]: unknown; }
|
|
107
108
|
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; }
|
|
109
|
+
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; preferenceResolution: PreferenceResolution; branding?: ChartBrandingState; warnings: Diagnostic[]; assumptions?: string[]; normalizations?: Diagnostic[]; collapsedGroups?: string[]; projectAnalytics?: ProjectAnalyticsState | null; linked?: LinkedState | null; clipboard: { nodes: number; edges: number }; [key: string]: unknown; }
|
|
109
110
|
export interface DiagramConfig { mode?: 'process' | 'architecture' | 'mindmap'; layout?: 'manual' | 'layered' | 'tree' | 'radial'; routing?: 'straight' | 'orthogonal' | 'curved'; curveTension?: number; grid?: number; snap?: boolean; }
|
|
110
111
|
export interface ArchitectureLayer { id: string; label?: string; }
|
|
111
112
|
export interface ArchitectureBoundary { id: string; label?: string; nodeIds?: string[]; padding?: number; color?: string; }
|
|
@@ -137,6 +138,7 @@ export interface Chart {
|
|
|
137
138
|
setTheme(theme?: ChartSpec['theme']): this;
|
|
138
139
|
getTheme(): ResolvedTheme;
|
|
139
140
|
getPreferences(): ChartPreferences;
|
|
141
|
+
getPreferenceResolution(): PreferenceResolution;
|
|
140
142
|
setPreferences(patch?: ChartPreferencesPatch, options?: { scope?: 'chart' | 'global'; source?: string; persist?: boolean }): this;
|
|
141
143
|
resetPreferences(options?: { scope?: 'all' | 'global' | 'chart'; source?: string; persist?: boolean }): this;
|
|
142
144
|
resize(width?: number, height?: number): this;
|