@taylorwong/ichartjs 2.0.13 → 2.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -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 +318 -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/examples/agent-workflow.mjs +10 -0
- 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 +56 -25
- 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 +144 -12
- 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/charts.mjs
CHANGED
|
@@ -65,18 +65,30 @@ function axisTicks(min, max, axis = {}, type = 'linear') {
|
|
|
65
65
|
return Array.from({ length: intervals + 1 }, (_, index) => min + (max - min) * index / intervals);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
const axisFreeTypes = new Set(['pie', 'funnel', 'gauge', 'radar']);
|
|
69
|
+
|
|
68
70
|
function chromeLayout(spec, entries = []) {
|
|
69
71
|
const title = titleLayout(spec);
|
|
70
72
|
const visible = spec.legend?.visible !== false && entries.length >= 1;
|
|
71
|
-
if (!visible) return { title, legend: { visible: false, items: [], bottom: title.bottom }, bottom: title.bottom };
|
|
73
|
+
if (!visible) return { title, legend: { visible: false, items: [], position: 'top', top: title.bottom, bottom: title.bottom, left: spec.padding.left, right: spec.width - spec.padding.right, width: 0, height: 0 }, bottom: title.bottom };
|
|
72
74
|
const size = fontSize(spec, 'legend', 12), lineHeight = Math.max(18, size * Number(spec.theme?.typography?.legend?.lineHeight || 1.4));
|
|
73
75
|
const swatchSize = Math.max(8, Math.round(size * 0.75)), gap = Math.max(12, Math.round(size));
|
|
74
76
|
const availableWidth = Math.max(1, spec.width - spec.padding.left - spec.padding.right);
|
|
75
|
-
const
|
|
77
|
+
const requestedPosition = axisFreeTypes.has(spec.type) && ['top', 'right', 'bottom', 'left'].includes(spec.legend?.position) ? spec.legend.position : 'top';
|
|
78
|
+
const sideLegend = requestedPosition === 'left' || requestedPosition === 'right';
|
|
79
|
+
const labelBudget = sideLegend ? Math.max(size * 4, availableWidth * 0.34 - swatchSize - 6 - gap) : availableWidth - swatchSize - 6 - gap;
|
|
80
|
+
const maxLabelWidth = Math.max(size, labelBudget);
|
|
76
81
|
const measured = entries.map((entry, index) => {
|
|
77
82
|
const rawName = String(entry.name), name = truncateText(rawName, maxLabelWidth, size);
|
|
78
83
|
return { ...entry, name, rawName, truncated: name !== rawName, index, width: swatchSize + 6 + estimateTextWidth(name, size) + gap };
|
|
79
84
|
});
|
|
85
|
+
if (sideLegend) {
|
|
86
|
+
const width = Math.max(size * 4, ...measured.map(item => item.width));
|
|
87
|
+
const top = Math.max(spec.padding.top, title.bottom ? title.bottom + 8 : spec.padding.top);
|
|
88
|
+
const items = measured.map((item, index) => ({ ...item, x: requestedPosition === 'left' ? spec.padding.left : spec.width - spec.padding.right - width, y: top + index * lineHeight + lineHeight / 2, swatchSize }));
|
|
89
|
+
const bottom = top + measured.length * lineHeight;
|
|
90
|
+
return { title, legend: { visible: true, items, position: requestedPosition, top, bottom, left: requestedPosition === 'left' ? spec.padding.left : spec.width - spec.padding.right - width, right: requestedPosition === 'left' ? spec.padding.left + width : spec.width - spec.padding.right, width, height: measured.length * lineHeight, truncatedCount: measured.filter(item => item.truncated).length }, bottom: title.bottom };
|
|
91
|
+
}
|
|
80
92
|
const rows = [];
|
|
81
93
|
measured.forEach(item => {
|
|
82
94
|
let row = rows.at(-1);
|
|
@@ -84,7 +96,8 @@ function chromeLayout(spec, entries = []) {
|
|
|
84
96
|
row.items.push(item);
|
|
85
97
|
row.width += item.width;
|
|
86
98
|
});
|
|
87
|
-
const
|
|
99
|
+
const legendHeight = rows.length * lineHeight;
|
|
100
|
+
const top = requestedPosition === 'bottom' ? spec.height - spec.padding.bottom - legendHeight : title.bottom ? title.bottom + 8 : Math.max(8, (spec.padding.top - legendHeight) / 2);
|
|
88
101
|
const items = [];
|
|
89
102
|
rows.forEach((row, rowIndex) => {
|
|
90
103
|
let x = Math.max(spec.padding.left, spec.width - spec.padding.right - row.width);
|
|
@@ -92,7 +105,7 @@ function chromeLayout(spec, entries = []) {
|
|
|
92
105
|
row.items.forEach(item => { items.push({ ...item, x, y, swatchSize }); x += item.width; });
|
|
93
106
|
});
|
|
94
107
|
const bottom = top + rows.length * lineHeight;
|
|
95
|
-
return { title, legend: { visible: true, items, bottom, truncatedCount: measured.filter(item => item.truncated).length }, bottom: Math.max(title.bottom, bottom) };
|
|
108
|
+
return { title, legend: { visible: true, items, position: requestedPosition, top, bottom, left: spec.padding.left, right: spec.width - spec.padding.right, width: availableWidth, height: legendHeight, truncatedCount: measured.filter(item => item.truncated).length }, bottom: requestedPosition === 'bottom' ? title.bottom : Math.max(title.bottom, bottom) };
|
|
96
109
|
}
|
|
97
110
|
|
|
98
111
|
function styledSpec(spec) {
|
|
@@ -100,7 +113,7 @@ function styledSpec(spec) {
|
|
|
100
113
|
return { ...spec, theme, colors: spec.colors || theme.colors, background: spec.background || theme.background, padding: spec.padding || theme.layout.padding };
|
|
101
114
|
}
|
|
102
115
|
|
|
103
|
-
function layout(spec, data, legendEntries = []) {
|
|
116
|
+
function layout(spec, data, legendEntries = [], layoutOptions = {}) {
|
|
104
117
|
const p = spec.padding, width = spec.width, height = spec.height;
|
|
105
118
|
const chrome = chromeLayout(spec, legendEntries);
|
|
106
119
|
const xEncoding = spec.encoding.x || { field: 'name', type: 'category' };
|
|
@@ -140,29 +153,43 @@ function layout(spec, data, legendEntries = []) {
|
|
|
140
153
|
const matrixYField = spec.encoding.y?.field || 'y', matrixTexts = spec.type === 'heatmap' ? data.rows.map(row => String(row[matrixYField] ?? '')) : [];
|
|
141
154
|
const leftTexts = spec.type === 'bar' ? categoryTexts : yTicks.map(value => formatValue(value, spec.yAxis?.format, spec.locale));
|
|
142
155
|
const leftLabelWidth = maxWidth(leftTexts), rightLabelWidth = yEncodings.length > 1 && !stackMode ? maxWidth(rightTicks.map(value => formatValue(value, spec.yAxis?.right?.format, spec.locale))) : 0;
|
|
143
|
-
const
|
|
156
|
+
const axisFree = axisFreeTypes.has(spec.type);
|
|
144
157
|
const sideLabelWidth = Math.max(spec.type === 'bar' ? leftLabelWidth : 0, spec.type === 'heatmap' ? maxWidth(matrixTexts) : 0);
|
|
145
|
-
const leftTitleBand = spec.yAxis?.title
|
|
158
|
+
const leftTitleBand = spec.yAxis?.title ? axisSize + 16 : 0;
|
|
146
159
|
const rightTitleBand = spec.yAxis?.right?.title && yEncodings.length > 1 && !stackMode ? axisSize + 16 : 0;
|
|
147
160
|
const cartesianLeft = 8 + leftTitleBand + leftLabelWidth + 10;
|
|
148
|
-
const plotLeft =
|
|
149
|
-
? Math.max(p.left, p.
|
|
150
|
-
:
|
|
151
|
-
? Math.
|
|
152
|
-
:
|
|
153
|
-
|
|
161
|
+
const plotLeft = axisFree
|
|
162
|
+
? Math.max(p.left, chrome.legend.position === 'left' ? chrome.legend.right + 12 : p.left)
|
|
163
|
+
: spec.type === 'bar'
|
|
164
|
+
? Math.max(p.left, spec.yAxis?.title ? cartesianLeft : sideLabelWidth + 24)
|
|
165
|
+
: spec.type === 'heatmap'
|
|
166
|
+
? Math.min(width * 0.35, Math.max(p.left, sideLabelWidth + 24))
|
|
167
|
+
: Math.max(p.left, cartesianLeft);
|
|
168
|
+
const plotRight = axisFree
|
|
169
|
+
? Math.max(p.right, chrome.legend.position === 'right' ? spec.width - chrome.legend.left + 12 : p.right)
|
|
170
|
+
: Math.max(p.right, rightLabelWidth ? rightLabelWidth + rightTitleBand + 18 : 0);
|
|
154
171
|
const labelTopReserve = spec.type === 'column' && spec.labels?.enabled ? Math.ceil(fontSize(spec, 'label', 12) * 1.5 + 4) : 0;
|
|
155
|
-
const barTitleReserve = spec.type === 'bar' && spec.yAxis?.title ? axisSize + 8 : 0;
|
|
156
|
-
const plotTop = Math.max(p.top, chrome.bottom + (chrome.bottom ? 12 : 0)) + labelTopReserve + barTitleReserve;
|
|
157
172
|
const plotWidth = Math.max(1, width - plotLeft - plotRight);
|
|
158
173
|
const axisLabelTexts = temporal ? timeTicks(xMin, xMax, 5).map(value => formatTime(value, xMax - xMin, spec.xAxis?.format, spec.locale)) : quantitativeX ? timeTicks(xMin, xMax, 5).map(value => formatValue(value, spec.xAxis?.format, spec.locale)) : categories;
|
|
159
174
|
const xLabels = axisLabelLayout(spec, spec.type === 'bar' ? [] : axisLabelTexts, plotWidth);
|
|
175
|
+
const topChrome = chrome.legend.position === 'top' && chrome.legend.visible ? chrome.legend.bottom + 12 : Math.max(p.top, chrome.title.bottom + (chrome.title.bottom ? 12 : 0));
|
|
176
|
+
const axisFreeBottomPadding = Number(layoutOptions.axisFreeBottomPadding ?? p.bottom);
|
|
177
|
+
const bottomChrome = chrome.legend.position === 'bottom' && chrome.legend.visible ? chrome.legend.top - 12 : height - axisFreeBottomPadding;
|
|
178
|
+
const plotTop = axisFree ? Math.max(p.top, topChrome) + labelTopReserve : Math.max(p.top, chrome.bottom + (chrome.bottom ? 12 : 0)) + labelTopReserve;
|
|
160
179
|
const bottomReserve = Math.max(p.bottom, Math.ceil(16 + xLabels.projectedHeight + (spec.xAxis?.title ? 20 : 0)));
|
|
161
|
-
const
|
|
180
|
+
const plotBottom = axisFree ? Math.min(height - axisFreeBottomPadding, bottomChrome) : height - bottomReserve;
|
|
181
|
+
const plot = { x: plotLeft, y: plotTop, width: plotWidth, height: Math.max(1, plotBottom - plotTop) };
|
|
182
|
+
const axisTitles = {
|
|
183
|
+
y: spec.yAxis?.title ? truncateText(spec.yAxis.title, plot.height, axisSize) : null,
|
|
184
|
+
right: spec.yAxis?.right?.title && yEncodings.length > 1 && !stackMode ? truncateText(spec.yAxis.right.title, plot.height, axisSize) : null
|
|
185
|
+
};
|
|
186
|
+
const axisTitleWarnings = [];
|
|
187
|
+
if (spec.yAxis?.title && axisTitles.y !== String(spec.yAxis.title)) axisTitleWarnings.push({ path: 'yAxis.title', title: String(spec.yAxis.title), availableWidth: plot.height });
|
|
188
|
+
if (spec.yAxis?.right?.title && axisTitles.right !== String(spec.yAxis.right.title) && yEncodings.length > 1 && !stackMode) axisTitleWarnings.push({ path: 'yAxis.right.title', title: String(spec.yAxis.right.title), availableWidth: plot.height });
|
|
162
189
|
const rightTransform = value => rightType === 'log' ? Math.log10(Math.max(0.000001, Number(value))) : Number(value);
|
|
163
190
|
const yRight = value => plot.y + plot.height - ((rightTransform(value) - rightTransform(rightMin)) / (rightTransform(rightMax) - rightTransform(rightMin) || 1)) * plot.height;
|
|
164
191
|
const axisInfo = (rawDomain, domain, ticks, axis) => ({ rawDomain, domain, ticks, step: ticks.length > 1 ? (ticks[1] - ticks[0]) : null, policy: validDomain(axis?.domain) ? 'explicit' : axis?.nice === false ? 'raw' : 'nice' });
|
|
165
|
-
return { plot, chrome, xLabels, axisLayout: { size: axisSize, leftLabelWidth, rightLabelWidth }, compact: width < 360 || height < 240, recommendedSize: { minWidth: 280, minHeight: 220 }, xField, xEncoding, temporal, quantitativeX, xMin, xMax, yField, yEncodings, categories, rawMin, rawMax, min, max, rawRightMin, rawRightMax, rightMin, rightMax, yTicks, rightTicks, axes: { y: axisInfo([rawMin, rawMax], [min, max], yTicks, spec.yAxis || {}), right: axisInfo([rawRightMin, rawRightMax], [rightMin, rightMax], rightTicks, spec.yAxis?.right || {}) }, x, y, yRight, axisType, rightType };
|
|
192
|
+
return { plot, chrome, layoutFamily: axisFree ? spec.type : 'cartesian', xLabels, axisTitles, axisTitleWarnings, axisLayout: { size: axisSize, leftLabelWidth, rightLabelWidth }, compact: width < 360 || height < 240, recommendedSize: { minWidth: 280, minHeight: 220 }, xField, xEncoding, temporal, quantitativeX, xMin, xMax, yField, yEncodings, categories, rawMin, rawMax, min, max, rawRightMin, rawRightMax, rightMin, rightMax, yTicks, rightTicks, axes: { y: axisInfo([rawMin, rawMax], [min, max], yTicks, spec.yAxis || {}), right: axisInfo([rawRightMin, rawRightMax], [rightMin, rightMax], rightTicks, spec.yAxis?.right || {}) }, x, y, yRight, axisType, rightType };
|
|
166
193
|
}
|
|
167
194
|
|
|
168
195
|
function addText(scene, id, text, x, y, style = {}, dataRef = null) { scene.add(new SceneNode({ id, type: 'text', geometry: { text: String(text), x, y }, style, dataRef })); }
|
|
@@ -178,7 +205,7 @@ function addAxes(scene, spec, state) {
|
|
|
178
205
|
ticks.forEach(value => addText(scene, `label-x-value-${value}`, formatValue(value, spec.xAxis?.format || spec.yAxis?.format, spec.locale), numericX(value), plot.y + plot.height + 22, { fill: spec.theme.muted, font: font(spec, 'axis'), textAnchor: 'middle' }));
|
|
179
206
|
categories.forEach((category, index) => addText(scene, `label-y-category-${index}`, category, plot.x - 10, plot.y + (index + .5) * plot.height / Math.max(1, categories.length) + 4, { fill: spec.theme.muted, font: font(spec, 'axis'), textAnchor: 'end' }));
|
|
180
207
|
if (spec.xAxis?.title) addText(scene, 'axis-x-title', spec.xAxis.title, plot.x + plot.width / 2, plot.y + plot.height + 42, { fill: spec.theme.text, font: font(spec, 'axis'), textAnchor: 'middle' });
|
|
181
|
-
if (
|
|
208
|
+
if (state.axisTitles.y) addText(scene, 'axis-y-title', state.axisTitles.y, 8 + axisSize / 2, plot.y + plot.height / 2, { fill: spec.theme.text, font: font(spec, 'axis'), textAnchor: 'middle', textBaseline: 'middle', baseline: 'middle', rotation: -90 });
|
|
182
209
|
return;
|
|
183
210
|
}
|
|
184
211
|
const labels = state.temporal || state.quantitativeX ? timeTicks(state.xMin, state.xMax, 5) : categories;
|
|
@@ -191,8 +218,8 @@ function addAxes(scene, spec, state) {
|
|
|
191
218
|
yTicks.forEach(value => addText(scene, `label-y-${value}`, formatValue(value, spec.yAxis?.format, spec.locale), plot.x - 10, y(value) + 4, { fill: spec.theme.muted, font: font(spec, 'axis'), textAnchor: 'end' }));
|
|
192
219
|
if (state.yEncodings.length > 1 && !spec.stack) state.rightTicks.forEach(value => addText(scene, `label-y-right-${value}`, formatValue(value, spec.yAxis?.right?.format, spec.locale), plot.x + plot.width + 10, yRight(value) + 4, { fill: spec.theme.muted, font: font(spec, 'axis') }));
|
|
193
220
|
if (spec.xAxis?.title) addText(scene, 'axis-x-title', spec.xAxis.title, plot.x + plot.width / 2, plot.y + plot.height + (state.xLabels.rotation ? state.xLabels.projectedHeight + 20 : 42), { fill: spec.theme.text, font: font(spec, 'axis'), textAnchor: 'middle' });
|
|
194
|
-
if (
|
|
195
|
-
if (state.
|
|
221
|
+
if (state.axisTitles.y) addText(scene, 'axis-y-title', state.axisTitles.y, 8 + axisSize / 2, plot.y + plot.height / 2, { fill: spec.theme.text, font: font(spec, 'axis'), textAnchor: 'middle', textBaseline: 'middle', baseline: 'middle', rotation: -90 });
|
|
222
|
+
if (state.axisTitles.right) addText(scene, 'axis-y-right-title', state.axisTitles.right, spec.width - 8 - axisSize / 2, plot.y + plot.height / 2, { fill: spec.theme.text, font: font(spec, 'axis'), textAnchor: 'middle', textBaseline: 'middle', baseline: 'middle', rotation: 90 });
|
|
196
223
|
}
|
|
197
224
|
|
|
198
225
|
function addTitles(scene, spec, positions) {
|
|
@@ -262,11 +289,11 @@ function addHeatmapScene(scene, spec, data, state, colors) {
|
|
|
262
289
|
}
|
|
263
290
|
|
|
264
291
|
function addRadarScene(scene, spec, data, state, colors) {
|
|
265
|
-
const indicators = spec.indicators, cx = state.plot.x + state.plot.width / 2, cy = state.plot.y + state.plot.height / 2, radius = Math.min(state.plot.width, state.plot.height) * .
|
|
292
|
+
const indicators = spec.indicators, cx = state.plot.x + state.plot.width / 2, cy = state.plot.y + state.plot.height / 2, radius = Math.min(state.plot.width, state.plot.height) * .44;
|
|
266
293
|
if (indicators.some(indicator => !Number.isFinite(Number(indicator.min)) || !Number.isFinite(Number(indicator.max)))) data.warnings.push({ code: 'AMBIGUOUS_RADAR_DOMAIN', message: 'Declare finite min and max for every radar indicator, especially for mixed units.' });
|
|
267
294
|
const point = (indicator, index, ratio = 1) => { const angle = -Math.PI / 2 + index * Math.PI * 2 / indicators.length; return { x: cx + Math.cos(angle) * radius * ratio, y: cy + Math.sin(angle) * radius * ratio }; };
|
|
268
295
|
[0.25, 0.5, 0.75, 1].forEach((ratio, level) => scene.add(new SceneNode({ id: `radar-grid-${level}`, type: 'path', geometry: { points: indicators.map((indicator, index) => point(indicator, index, ratio)), closed: true }, style: { fill: 'none', stroke: spec.theme.grid, strokeWidth: spec.theme.marks.gridWidth } })));
|
|
269
|
-
const labelSize = fontSize(spec, 'axis', 12), occupied = [],
|
|
296
|
+
const labelSize = fontSize(spec, 'axis', 12), occupied = [], labelTop = Math.max(4, state.plot.y - labelSize * 2), labelArea = { x: 4, y: labelTop, width: spec.width - 8, height: Math.max(1, state.plot.y + state.plot.height - labelTop) };
|
|
270
297
|
let hiddenLabels = 0;
|
|
271
298
|
indicators.forEach((indicator, index) => {
|
|
272
299
|
const edge = point(indicator, index), angle = -Math.PI / 2 + index * Math.PI * 2 / indicators.length;
|
|
@@ -356,6 +383,9 @@ export function buildScene(spec) {
|
|
|
356
383
|
bottom: Number(baseSpec.padding?.bottom ?? 48) + (brandingEnabled ? 18 : 0),
|
|
357
384
|
left: Number(baseSpec.padding?.left ?? 56),
|
|
358
385
|
};
|
|
386
|
+
const themePadding = baseSpec.theme.layout.padding || {};
|
|
387
|
+
const customPadding = ['top', 'right', 'bottom', 'left'].some(key => Number(baseSpec.padding?.[key]) !== Number(themePadding[key]));
|
|
388
|
+
const axisFreeBottomPadding = customPadding ? effectivePadding.bottom : Math.max(28, Math.min(34, Number(themePadding.bottom ?? effectivePadding.bottom)));
|
|
359
389
|
spec = { ...baseSpec, padding: effectivePadding };
|
|
360
390
|
const isProjectFamily = ['gantt', 'timeline', 'milestone', 'burndown', 'flow', 'swimlane', 'architecture', 'mindmap'].includes(spec.type);
|
|
361
391
|
if (isProjectFamily) {
|
|
@@ -373,8 +403,9 @@ export function buildScene(spec) {
|
|
|
373
403
|
const colors = spec.colors;
|
|
374
404
|
const series = Array.isArray(spec.encoding.y) ? spec.encoding.y : [{ ...spec.encoding.y, name: spec.encoding.y?.name || spec.encoding.y?.field }];
|
|
375
405
|
const entries = legendEntries(spec, data, series, colors);
|
|
376
|
-
const state = layout(spec, data, entries);
|
|
406
|
+
const state = layout(spec, data, entries, { axisFreeBottomPadding });
|
|
377
407
|
if (state.chrome.legend.truncatedCount) data.warnings.push({ code: 'LEGEND_OVERFLOW', path: 'legend', count: state.chrome.legend.truncatedCount, message: `${state.chrome.legend.truncatedCount} legend label${state.chrome.legend.truncatedCount === 1 ? '' : 's'} exceeded the available width and was truncated.`, suggestion: 'Use shorter category names or a wider chart.' });
|
|
408
|
+
state.axisTitleWarnings.forEach(({ path, title, availableWidth }) => data.warnings.push({ code: 'TITLE_TRUNCATED', path, availableWidth, message: `Axis title "${title}" exceeded the available vertical plot space and was truncated.`, suggestion: 'Use a taller chart or a shorter axis title.' }));
|
|
378
409
|
scene._labelArea = state.plot;
|
|
379
410
|
addTitles(scene, spec, state.chrome.title);
|
|
380
411
|
if (!['pie', 'funnel', 'gauge', 'heatmap', 'radar'].includes(spec.type)) addAxes(scene, spec, state);
|
|
@@ -393,7 +424,7 @@ export function buildScene(spec) {
|
|
|
393
424
|
} else if (spec.type === 'scatter') {
|
|
394
425
|
data.rows.forEach((row, index) => { const xValue = Number(row[state.xField]), yValue = Number(row[state.yField]); if (!Number.isFinite(xValue) || !Number.isFinite(yValue)) return; const geometry = { cx: state.x(xValue), cy: state.y(yValue), r: 5 }, dataRef = { seriesIndex: 0, dataIndex: index, field: state.yField }; scene.add(new SceneNode({ id: `series-0-item-${index}`, type: 'circle', geometry, bounds: { x: geometry.cx - 8, y: geometry.cy - 8, width: 16, height: 16 }, style: { fill: colors[0] }, dataRef, interactive: true })); addMarkLabel(scene, spec, `series-0-item-${index}`, yValue, geometry.cx, geometry.cy - 8, dataRef, state.plot, { x: geometry.cx, y: geometry.cy, radius: 5 }); });
|
|
395
426
|
} else if (spec.type === 'pie') {
|
|
396
|
-
const categoryField = spec.encoding.category?.field || 'name', valueField = spec.encoding.value?.field || 'value', values = data.rows.map(row => Number(row[valueField])), total = values.reduce((sum, value) => sum + (Number.isFinite(value) && value > 0 ? value : 0), 0), negativeCount = values.filter(value => Number.isFinite(value) && value < 0).length, cx = state.plot.x + state.plot.width / 2, cy = state.plot.y + state.plot.height / 2, radius = Math.min(state.plot.width, state.plot.height) * 0.
|
|
427
|
+
const categoryField = spec.encoding.category?.field || 'name', valueField = spec.encoding.value?.field || 'value', values = data.rows.map(row => Number(row[valueField])), total = values.reduce((sum, value) => sum + (Number.isFinite(value) && value > 0 ? value : 0), 0), negativeCount = values.filter(value => Number.isFinite(value) && value < 0).length, cx = state.plot.x + state.plot.width / 2, cy = state.plot.y + state.plot.height / 2, radius = Math.min(state.plot.width, state.plot.height) * 0.46, innerR = radius * Number(spec.innerRadius || 0); let angle = -Math.PI / 2;
|
|
397
428
|
if (negativeCount) data.warnings.push({ code: 'NEGATIVE_VALUE_DROPPED', path: 'encoding.value', count: negativeCount, message: `Pie ignores ${negativeCount} negative value${negativeCount === 1 ? '' : 's'} when calculating shares.`, suggestion: 'Use non-negative part-to-whole values or choose a Cartesian chart for signed measures.' });
|
|
398
429
|
if (!(total > 0)) { data.warnings.push({ code: 'ZERO_TOTAL', path: 'encoding.value', message: 'Pie requires a positive total.', suggestion: 'Provide at least one positive value or render an explicit empty state.' }); addText(scene, 'pie-zero-total', spec.emptyText || 'No positive values', cx, cy, { fill: spec.theme.muted, font: font(spec, 'subtitle'), textAnchor: 'middle' }); }
|
|
399
430
|
else data.rows.forEach((row, index) => { const rawValue = values[index], value = Number.isFinite(rawValue) ? Math.max(0, rawValue) : 0, end = angle + value / total * Math.PI * 2, middle = angle + (end - angle) / 2, fill = colors[index % colors.length], dataRef = { seriesIndex: 0, dataIndex: index, category: row[categoryField], value, rawValue }; scene.add(new SceneNode({ id: `series-0-item-${index}`, type: 'arc', geometry: { cx, cy, r: radius, innerR, start: angle, end }, bounds: { x: cx - radius, y: cy - radius, width: radius * 2, height: radius * 2 }, style: { fill, stroke: spec.theme.background, strokeWidth: 1 }, dataRef, interactive: true })); addMarkLabel(scene, spec, `series-0-item-${index}`, value / total, cx + Math.cos(middle) * radius * .72, cy + Math.sin(middle) * radius * .72, dataRef, state.plot, null, spec.labels?.format || { style: 'percent' }, fill); angle = end; });
|
|
@@ -401,7 +432,7 @@ export function buildScene(spec) {
|
|
|
401
432
|
const valueField = spec.encoding.value?.field || 'value'; const maxValue = Math.max(...data.rows.map(row => Number(row[valueField]) || 0), 1); const segmentHeight = state.plot.height / data.rows.length;
|
|
402
433
|
data.rows.forEach((row, index) => { const value = Number(row[valueField]) || 0, ratio = Math.max(0.1, value / maxValue), fill = colors[index % colors.length]; const width = state.plot.width * ratio; const geometry = { x: state.plot.x + (state.plot.width - width) / 2, y: state.plot.y + index * segmentHeight, width, height: Math.max(2, segmentHeight - 3) }, dataRef = { seriesIndex: 0, dataIndex: index, field: valueField }; scene.add(new SceneNode({ id: `series-0-item-${index}`, type: 'rect', geometry, bounds: geometry, style: { fill }, dataRef, interactive: true })); addMarkLabel(scene, spec, `series-0-item-${index}`, value, geometry.x + geometry.width / 2, geometry.y + geometry.height / 2 + 4, dataRef, state.plot, null, null, fill); });
|
|
403
434
|
} else if (spec.type === 'gauge') {
|
|
404
|
-
const valueField = spec.encoding.value?.field || 'value', domain = Array.isArray(spec.domain) ? spec.domain : [0, 100], rawValue = Number(data.rows[0]?.[valueField]), numericValue = Number.isFinite(rawValue) ? rawValue : 0, value = Math.max(Number(domain[0]), Math.min(Number(domain[1]), numericValue)), ratio = (value - Number(domain[0])) / (Number(domain[1]) - Number(domain[0]) || 1), cx = state.plot.x + state.plot.width / 2, cy = state.plot.y + state.plot.height * 0.
|
|
435
|
+
const valueField = spec.encoding.value?.field || 'value', domain = Array.isArray(spec.domain) ? spec.domain : [0, 100], rawValue = Number(data.rows[0]?.[valueField]), numericValue = Number.isFinite(rawValue) ? rawValue : 0, value = Math.max(Number(domain[0]), Math.min(Number(domain[1]), numericValue)), ratio = (value - Number(domain[0])) / (Number(domain[1]) - Number(domain[0]) || 1), cx = state.plot.x + state.plot.width / 2, cy = state.plot.y + state.plot.height * 0.68, radius = Math.min(state.plot.width * 0.42, state.plot.height * 0.62), start = Math.PI, end = start + Math.PI * ratio;
|
|
405
436
|
if (!Array.isArray(spec.domain)) data.warnings.push({ code: 'MISSING_GAUGE_DOMAIN', path: 'domain', message: 'Gauge rendered with a compatibility fallback; provide an explicit domain to make the value meaningful.' });
|
|
406
437
|
if (numericValue < Number(domain[0]) || numericValue > Number(domain[1])) data.warnings.push({ code: 'VALUE_CLAMPED', path: `data.values[0].${valueField}`, count: 1, rawValue: numericValue, domain: [...domain], message: `Gauge value ${numericValue} is outside domain [${domain[0]}, ${domain[1]}] and was clamped for the arc.`, suggestion: 'Choose a domain that covers the value or review the source unit.' });
|
|
407
438
|
scene.add(new SceneNode({ id: 'gauge-background', type: 'arc', geometry: { cx, cy, r: radius, start: Math.PI, end: Math.PI * 2 }, style: { fill: spec.theme.grid } })); scene.add(new SceneNode({ id: 'gauge-value', type: 'arc', geometry: { cx, cy, r: radius, start, end }, style: { fill: colors[0] }, dataRef: { seriesIndex: 0, dataIndex: 0, value, rawValue: numericValue }, interactive: true })); addText(scene, 'gauge-label', formatValue(numericValue, spec.labels?.format || spec.encoding.value?.format || { maximumFractionDigits: 2 }, spec.locale), cx, cy - 12, { fill: spec.theme.text, font: font(spec, 'metric'), textAnchor: 'middle' });
|
package/src/command.mjs
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
* Commands are JSON-safe, stable-ID based, and do not mutate input data.
|
|
4
4
|
*/
|
|
5
5
|
import { copyJSON, issue } from './schema.mjs';
|
|
6
|
+
import { commandTypes } from './contract-registry.mjs';
|
|
6
7
|
|
|
7
8
|
export const commandVersion = '1.0';
|
|
8
|
-
export const operationTypes = [
|
|
9
|
+
export const operationTypes = [...commandTypes];
|
|
9
10
|
|
|
10
11
|
export function normalizeCommand(input) {
|
|
11
12
|
const command = copyJSON(input);
|
|
@@ -63,7 +64,7 @@ export function commandCapabilities(schema) {
|
|
|
63
64
|
const fields = schema?.fields || {};
|
|
64
65
|
const can = name => fields[name]?.editable !== false && fields[name]?.agentEditable !== false;
|
|
65
66
|
const flowNode = ['flow-node', 'architecture-node', 'mindmap-node'].includes(schema?.name);
|
|
66
|
-
const flowEdge = ['flow-edge', 'architecture-edge'].includes(schema?.name);
|
|
67
|
+
const flowEdge = ['flow-edge', 'architecture-edge', 'mindmap-edge'].includes(schema?.name);
|
|
67
68
|
return {
|
|
68
69
|
updateField: Object.keys(fields).filter(can),
|
|
69
70
|
updateRecord: true,
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON-safe public contract metadata shared by runtime discovery and release checks.
|
|
3
|
+
* Keep this module dependency-free so browser consumers can import the registry safely.
|
|
4
|
+
*/
|
|
5
|
+
export const contractVersion = '1.1';
|
|
6
|
+
export const chartTypes = ['line', 'area', 'bar', 'column', 'pie', 'scatter', 'funnel', 'gauge', 'heatmap', 'radar', 'gantt', 'timeline', 'milestone', 'burndown', 'flow', 'swimlane', 'architecture', 'mindmap'];
|
|
7
|
+
export const renderers = ['canvas', 'svg'];
|
|
8
|
+
export const exportTypes = ['png', 'jpeg', 'svg', 'json'];
|
|
9
|
+
export const interactionDefaults = { zoom: false, pan: false, brush: false, drag: false, edgeDrag: false, portConnect: false, editing: false };
|
|
10
|
+
export const commandTypes = ['updateField', 'updateRecord', 'updateTask', 'shiftTask', 'updateProgress', 'addDependency', 'removeDependency', 'updateMilestone', 'moveNode', 'moveNodes', 'moveNodeToLane', 'resizeNode', 'alignNodes', 'snapNodes', 'moveGroup', 'resizeGroup', 'assignNodesToGroup', 'duplicateGroup', 'deleteGroup', 'updateEdge', 'removeEdge', 'addEdge', 'toggleGroupCollapse', 'duplicateSelection', 'pasteSelection'];
|
|
11
|
+
export const diagramOperations = ['moveNode', 'moveNodes', 'resizeNode', 'alignNodes', 'snapNodes', 'moveNodeToLane', 'moveGroup', 'resizeGroup', 'assignNodesToGroup', 'duplicateGroup', 'deleteGroup', 'updateEdge', 'removeEdge', 'addEdge', 'toggleGroupCollapse', 'duplicateSelection', 'pasteSelection'];
|
|
12
|
+
export const businessModels = ['project-task', 'timeline-event', 'milestone', 'burndown-sample', 'flow-node', 'flow-edge', 'swimlane', 'architecture-node', 'architecture-edge', 'mindmap-edge', 'mindmap-node'];
|
|
13
|
+
export const diagramEdgeModels = ['flow-edge', 'architecture-edge', 'mindmap-edge'];
|
|
14
|
+
|
|
15
|
+
const status = (supported, notApplicable = []) => Object.fromEntries(supported.map(name => [name, 'supported']).concat(notApplicable.map(name => [name, 'not-applicable'])));
|
|
16
|
+
const commonPresentation = ['title', 'subtitle', 'theme', 'responsive', 'empty-state', 'invalid-data-state', 'export', 'branding'];
|
|
17
|
+
|
|
18
|
+
export const chartDefinitions = {
|
|
19
|
+
line: { family: 'cartesian', intents: ['trend', 'time-series'], required: ['dimension', 'measure'], optional: ['series'], interactions: ['hover', 'tooltip', 'selection', 'crosshair', 'zoom', 'pan', 'keyboard'], features: status([...commonPresentation, 'axes', 'grid', 'legend', 'labels', 'formatting', 'multi-series', 'dual-axis']) },
|
|
20
|
+
area: { family: 'cartesian', intents: ['trend', 'composition'], required: ['dimension', 'measure'], optional: ['series'], interactions: ['hover', 'tooltip', 'selection', 'crosshair', 'zoom', 'pan', 'keyboard'], features: status([...commonPresentation, 'axes', 'grid', 'legend', 'labels', 'formatting', 'multi-series', 'stacked', 'percent-stacked']) },
|
|
21
|
+
bar: { family: 'cartesian', intents: ['comparison', 'ranking'], required: ['dimension', 'measure'], optional: ['series'], interactions: ['hover', 'tooltip', 'selection', 'zoom', 'pan', 'keyboard'], features: status([...commonPresentation, 'axes', 'grid', 'legend', 'labels', 'formatting', 'multi-series', 'stacked', 'percent-stacked']) },
|
|
22
|
+
column: { family: 'cartesian', intents: ['comparison', 'distribution', 'composition'], required: ['dimension', 'measure'], optional: ['series'], interactions: ['hover', 'tooltip', 'selection', 'zoom', 'pan', 'keyboard'], features: status([...commonPresentation, 'axes', 'grid', 'legend', 'labels', 'formatting', 'multi-series', 'stacked', 'percent-stacked', 'mixed-line-column', 'dual-axis', 'bin']) },
|
|
23
|
+
pie: { family: 'part-to-whole', intents: ['part-to-whole'], required: ['dimension', 'measure'], optional: [], interactions: ['hover', 'tooltip', 'selection', 'keyboard'], features: status([...commonPresentation, 'legend', 'labels', 'formatting', 'donut', 'zero-total'], ['axes', 'grid', 'zoom', 'pan']) },
|
|
24
|
+
scatter: { family: 'cartesian', intents: ['relationship', 'correlation'], required: ['measure', 'measure'], optional: ['dimension'], interactions: ['hover', 'tooltip', 'selection', 'brush', 'zoom', 'pan', 'keyboard'], features: status([...commonPresentation, 'axes', 'grid', 'labels', 'formatting'], ['stacked']) },
|
|
25
|
+
funnel: { family: 'stage', intents: ['funnel', 'conversion'], required: ['dimension', 'measure'], optional: [], interactions: ['hover', 'tooltip', 'selection', 'keyboard'], features: status([...commonPresentation, 'labels', 'formatting'], ['axes', 'grid', 'zoom', 'pan']) },
|
|
26
|
+
gauge: { family: 'indicator', intents: ['progress', 'single-value'], required: ['measure'], optional: [], interactions: ['hover', 'tooltip', 'selection', 'keyboard'], features: status([...commonPresentation, 'labels', 'formatting', 'domain'], ['axes', 'grid', 'legend', 'zoom', 'pan']) },
|
|
27
|
+
heatmap: { family: 'matrix', intents: ['matrix', 'correlation-grid'], required: ['dimension', 'dimension', 'measure'], optional: [], interactions: ['hover', 'tooltip', 'selection', 'keyboard'], features: status([...commonPresentation, 'axes', 'labels', 'formatting', 'color-scale', 'missing-values'], ['zoom', 'pan']) },
|
|
28
|
+
radar: { family: 'radial', intents: ['multidimensional', 'profile'], required: ['three-or-more-measures'], optional: ['series'], interactions: ['hover', 'tooltip', 'selection', 'keyboard'], features: status([...commonPresentation, 'legend', 'labels', 'formatting', 'multi-series', 'indicator-domains', 'mixed-unit-warning'], ['axes', 'grid', 'zoom', 'pan']) },
|
|
29
|
+
gantt: { family: 'project', intents: ['schedule', 'variance'], required: ['id', 'start', 'end'], optional: ['dependencies', 'progress', 'baseline', 'actual'], interactions: ['hover', 'tooltip', 'selection', 'zoom', 'pan', 'keyboard', 'linked-filter', 'linked-selection'], features: status([...commonPresentation, 'labels', 'formatting', 'critical-path', 'slack', 'baseline-actual-variance', 'working-calendar', 'dependencies']) },
|
|
30
|
+
timeline: { family: 'project', intents: ['timeline', 'milestone'], required: ['date', 'label'], optional: ['status'], interactions: ['hover', 'tooltip', 'selection', 'zoom', 'pan', 'keyboard'], features: status([...commonPresentation, 'labels', 'formatting']) },
|
|
31
|
+
milestone: { family: 'project', intents: ['milestone'], required: ['date', 'label'], optional: ['baseline', 'actual'], interactions: ['hover', 'tooltip', 'selection', 'zoom', 'pan', 'keyboard'], features: status([...commonPresentation, 'labels', 'formatting', 'baseline-actual-variance']) },
|
|
32
|
+
burndown: { family: 'project', intents: ['progress', 'release'], required: ['date', 'remaining'], optional: ['ideal', 'scopeChange'], interactions: ['hover', 'tooltip', 'selection', 'zoom', 'pan', 'keyboard'], features: status([...commonPresentation, 'axes', 'labels', 'formatting', 'scope-change', 'release-forecast']) },
|
|
33
|
+
flow: { family: 'diagram', intents: ['workflow'], required: ['nodes'], optional: ['edges', 'groups', 'ports'], interactions: ['hover', 'tooltip', 'selection', 'edge-selection', 'drag', 'edgeDrag', 'zoom', 'pan', 'keyboard', 'keyboard-edit', 'keyboard-port-connect', 'portConnect', 'copy-paste'], features: status([...commonPresentation, 'labels', 'routing', 'manual-routing', 'layout', 'groups', 'ports', 'history'], ['axes', 'grid', 'legend']) },
|
|
34
|
+
swimlane: { family: 'diagram', intents: ['responsibility'], required: ['nodes', 'lanes'], optional: ['edges', 'groups', 'ports'], interactions: ['hover', 'tooltip', 'selection', 'edge-selection', 'drag', 'edgeDrag', 'zoom', 'pan', 'keyboard', 'keyboard-edit', 'keyboard-port-connect', 'portConnect', 'copy-paste'], features: status([...commonPresentation, 'labels', 'routing', 'manual-routing', 'layout', 'groups', 'ports', 'lanes', 'history'], ['axes', 'grid', 'legend']) },
|
|
35
|
+
architecture: { family: 'diagram', intents: ['architecture', 'business-architecture', 'data-architecture', 'technical-architecture'], required: ['nodes'], optional: ['edges', 'layers', 'boundaries', 'groups', 'ports'], interactions: ['hover', 'tooltip', 'selection', 'edge-selection', 'drag', 'edgeDrag', 'zoom', 'pan', 'keyboard', 'keyboard-edit', 'keyboard-port-connect', 'portConnect', 'copy-paste'], features: status([...commonPresentation, 'labels', 'routing', 'manual-routing', 'layout', 'layers', 'boundaries', 'groups', 'ports', 'history'], ['axes', 'grid', 'legend']) },
|
|
36
|
+
mindmap: { family: 'diagram', intents: ['mindmap', 'hierarchy', 'brainstorm'], required: ['nodes'], optional: ['parentId', 'edges', 'groups'], interactions: ['hover', 'tooltip', 'selection', 'edge-selection', 'drag', 'edgeDrag', 'zoom', 'pan', 'keyboard', 'keyboard-edit', 'keyboard-port-connect', 'portConnect', 'copy-paste'], features: status([...commonPresentation, 'labels', 'routing', 'cubic-bezier', 'curve-tension', 'obstacle-fallback', 'manual-routing', 'layout', 'tree-layout', 'radial-layout', 'history'], ['axes', 'grid', 'legend']) }
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const chartProfiles = Object.fromEntries(chartTypes.map(type => [type, {
|
|
40
|
+
type,
|
|
41
|
+
...chartDefinitions[type],
|
|
42
|
+
dataShapes: chartDefinitions[type].family === 'diagram' ? ['diagram-spec'] : chartDefinitions[type].family === 'project' ? ['records'] : ['records', 'data-table'],
|
|
43
|
+
renderers: [...renderers],
|
|
44
|
+
exports: [...exportTypes],
|
|
45
|
+
limits: type === 'pie' ? { recommendedCategories: 8 } : type === 'radar' ? { minimumIndicators: 3, recommendedIndicators: 8 } : type === 'heatmap' ? { recommendedCells: 2500 } : {}
|
|
46
|
+
}]));
|
|
47
|
+
|
|
48
|
+
export const contractRegistry = Object.freeze({
|
|
49
|
+
contractVersion,
|
|
50
|
+
chartTypes: [...chartTypes],
|
|
51
|
+
chartProfiles: JSON.parse(JSON.stringify(chartProfiles)),
|
|
52
|
+
renderers: [...renderers],
|
|
53
|
+
exportTypes: [...exportTypes],
|
|
54
|
+
interactionDefaults: { ...interactionDefaults },
|
|
55
|
+
commandTypes: [...commandTypes],
|
|
56
|
+
diagramOperations: [...diagramOperations],
|
|
57
|
+
businessModels: [...businessModels],
|
|
58
|
+
diagramEdgeModels: [...diagramEdgeModels]
|
|
59
|
+
});
|
package/src/edit-controller.mjs
CHANGED
|
@@ -60,7 +60,7 @@ export class EditController {
|
|
|
60
60
|
edgeSchema: spec.data.edgeSchema,
|
|
61
61
|
type: spec.type,
|
|
62
62
|
nodeModel: spec.type === 'architecture' ? 'architecture-node' : spec.type === 'mindmap' ? 'mindmap-node' : 'flow-node',
|
|
63
|
-
edgeModel: spec.type === 'architecture' ? 'architecture-edge' : 'flow-edge',
|
|
63
|
+
edgeModel: spec.type === 'architecture' ? 'architecture-edge' : spec.type === 'mindmap' ? 'mindmap-edge' : 'flow-edge',
|
|
64
64
|
validationOptions: { ...spec.validationOptions, references: { ...spec.validationOptions?.references, ...(diagram ? { 'flow-node': nodes.map(row => row.id), swimlane: lanes.map(row => row.id) } : {}) } },
|
|
65
65
|
requireConfirmation: spec.editing?.requireConfirmation,
|
|
66
66
|
grid: spec.diagram?.grid || 8,
|
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()]; }
|
|
@@ -353,13 +455,13 @@ export class Chart {
|
|
|
353
455
|
const clampedValues = warnings.filter(item => item.code === 'VALUE_CLAMPED').reduce((sum, item) => sum + Number(item.count || 1), 0);
|
|
354
456
|
return { version: '1.0', status: isEmpty ? 'empty' : warnings.length ? 'degraded' : 'ready', renderable: !isEmpty, issues: [...new Set(warnings.map(item => item.code))], metrics: { warnings: warnings.length, suppressedLabels, clampedValues, renderedMarks: marks.length } };
|
|
355
457
|
}
|
|
356
|
-
getState() { const brandingSignature = discoverCapabilities().branding.signature, warnings = this._getDiagnostics(); return { renderer: this.renderer.constructor.name, width: this.spec.width, height: this.spec.height, dataCount: this.model.data.rows.length, selected: [...this._selected.values()], revision: this._revision, history: this._history.state(), view: clone(this.spec.view || null), style: clone({ name: this.spec.theme.name, mode: this.spec.theme.mode, resolvedMode: this.spec.theme.resolvedMode, preset: this.spec.theme.preset, palette: this.spec.theme.palette, reasons: this.spec.theme.reasons }), axes: clone(this.model.state?.axes || null), health: this._getHealth(warnings), preferences: this.getPreferences(), branding: { enabled: Boolean(this.spec.branding?.enabled), signature: brandingSignature, text: this.spec.branding?.enabled === true ? brandingSignature : null }, warnings: clone(warnings), assumptions: clone(this.model.data?.assumptions || []), normalizations: clone(this._specDiagnostics?.normalizations || []), collapsedGroups: this.getCollapsedGroupIds(), clipboard: { nodes: this._clipboard?.nodes?.length || 0, edges: this._clipboard?.edges?.length || 0 }, projectAnalytics: clone(this.model.state?.projectAnalytics || null), linked: clone(this.model.state?.linked || null) }; }
|
|
458
|
+
getState() { const brandingSignature = discoverCapabilities().branding.signature, warnings = this._getDiagnostics(), layoutState = this.model.state?.plot ? { family: this.model.state.layoutFamily || null, plot: clone(this.model.state.plot), chrome: clone(this.model.state.chrome || null), labels: clone(this.model.state.labelLayout || null) } : null; return { renderer: this.renderer.constructor.name, width: this.spec.width, height: this.spec.height, dataCount: this.model.data.rows.length, selected: [...this._selected.values()], revision: this._revision, history: this._history.state(), view: clone(this.spec.view || null), style: clone({ name: this.spec.theme.name, mode: this.spec.theme.mode, resolvedMode: this.spec.theme.resolvedMode, preset: this.spec.theme.preset, palette: this.spec.theme.palette, reasons: this.spec.theme.reasons }), layout: layoutState, axes: clone(this.model.state?.axes || null), health: this._getHealth(warnings), preferences: this.getPreferences(), branding: { enabled: Boolean(this.spec.branding?.enabled), signature: brandingSignature, text: this.spec.branding?.enabled === true ? brandingSignature : null }, warnings: clone(warnings), assumptions: clone(this.model.data?.assumptions || []), normalizations: clone(this._specDiagnostics?.normalizations || []), collapsedGroups: this.getCollapsedGroupIds(), clipboard: { nodes: this._clipboard?.nodes?.length || 0, edges: this._clipboard?.edges?.length || 0 }, projectAnalytics: clone(this.model.state?.projectAnalytics || null), linked: clone(this.model.state?.linked || null) }; }
|
|
357
459
|
getProjectAnalytics() { return clone(this.model.state?.projectAnalytics || null); }
|
|
358
460
|
getLinkedState() { return clone(this.model.state?.linked || null); }
|
|
359
461
|
setLinkedFilters(filters = {}) { this.spec.project = { ...(this.spec.project || {}), linked: { ...(this.spec.project?.linked || {}), filters: normalizeLinkedFilters(filters) } }; this.emit('linkedstatechange', { chart: this, linked: this.spec.project.linked }); return this.render(); }
|
|
360
462
|
setLinkedSelection(selection = []) { this.spec.project = { ...(this.spec.project || {}), linked: { ...(this.spec.project?.linked || {}), selection: normalizeLinkedSelection(selection) } }; this.emit('linkedstatechange', { chart: this, linked: this.spec.project.linked }); return this.render(); }
|
|
361
463
|
describe() { return { type: this.spec.type, renderer: this.renderer.constructor.name, dimensions: [this.spec.encoding.x?.field || this.spec.encoding.category?.field], measures: (Array.isArray(this.spec.encoding.y) ? this.spec.encoding.y : [this.spec.encoding.y || this.spec.encoding.value]).filter(Boolean).map(encoding => encoding.field), dataCount: this.model.data.rows.length, theme: this.spec.theme?.name || 'custom', interactions: Object.keys(this.spec.interaction || {}).filter(key => this.spec.interaction[key]) }; }
|
|
362
|
-
explain() { const explanation = explainChart(this.spec, this.model), state = this.getState(); return { ...explanation, warnings: state.warnings, health: state.health }; }
|
|
464
|
+
explain() { const explanation = explainChart(this.spec, this.model), state = this.getState(); return { ...explanation, layout: state.layout, warnings: state.warnings, health: state.health }; }
|
|
363
465
|
getAccessibleDescription() { const description = this.spec.accessibility?.description || this.spec.title?.text || `${this.spec.type} chart`; return `${description}; ${this.model.data.rows.length} data items.`; }
|
|
364
466
|
inspectDataSchema() { return inspectDataSchema(this.spec.data.schema || this.spec.schema); }
|
|
365
467
|
validateData() { return validateData(this.toDataTable(), this.spec.data.schema || this.spec.schema, this.spec.validationOptions); }
|
|
@@ -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.15', 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 };
|