@unovis/mcp 1.7.0-mcp.0
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/LICENSE +193 -0
- package/README.md +154 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +109 -0
- package/dist/cli.js.map +1 -0
- package/dist/codegen/index.d.ts +14 -0
- package/dist/codegen/index.js +346 -0
- package/dist/codegen/index.js.map +1 -0
- package/dist/env/bbox.d.ts +13 -0
- package/dist/env/bbox.js +335 -0
- package/dist/env/bbox.js.map +1 -0
- package/dist/env/canvas.d.ts +9 -0
- package/dist/env/canvas.js +55 -0
- package/dist/env/canvas.js.map +1 -0
- package/dist/env/computed-style.d.ts +38 -0
- package/dist/env/computed-style.js +186 -0
- package/dist/env/computed-style.js.map +1 -0
- package/dist/env/fonts.d.ts +3 -0
- package/dist/env/fonts.js +103 -0
- package/dist/env/fonts.js.map +1 -0
- package/dist/env/index.d.ts +13 -0
- package/dist/env/index.js +121 -0
- package/dist/env/index.js.map +1 -0
- package/dist/env/raf-queue.d.ts +23 -0
- package/dist/env/raf-queue.js +51 -0
- package/dist/env/raf-queue.js.map +1 -0
- package/dist/env/size.d.ts +9 -0
- package/dist/env/size.js +30 -0
- package/dist/env/size.js.map +1 -0
- package/dist/html/document.d.ts +16 -0
- package/dist/html/document.js +95 -0
- package/dist/html/document.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/recipes/area.d.ts +66 -0
- package/dist/recipes/area.js +76 -0
- package/dist/recipes/area.js.map +1 -0
- package/dist/recipes/bar.d.ts +67 -0
- package/dist/recipes/bar.js +69 -0
- package/dist/recipes/bar.js.map +1 -0
- package/dist/recipes/boxplot.d.ts +64 -0
- package/dist/recipes/boxplot.js +89 -0
- package/dist/recipes/boxplot.js.map +1 -0
- package/dist/recipes/chord.d.ts +41 -0
- package/dist/recipes/chord.js +72 -0
- package/dist/recipes/chord.js.map +1 -0
- package/dist/recipes/choropleth-regions.d.ts +49 -0
- package/dist/recipes/choropleth-regions.js +521 -0
- package/dist/recipes/choropleth-regions.js.map +1 -0
- package/dist/recipes/choropleth.d.ts +27 -0
- package/dist/recipes/choropleth.js +86 -0
- package/dist/recipes/choropleth.js.map +1 -0
- package/dist/recipes/donut.d.ts +25 -0
- package/dist/recipes/donut.js +51 -0
- package/dist/recipes/donut.js.map +1 -0
- package/dist/recipes/graph.d.ts +55 -0
- package/dist/recipes/graph.js +132 -0
- package/dist/recipes/graph.js.map +1 -0
- package/dist/recipes/heatmap.d.ts +21 -0
- package/dist/recipes/heatmap.js +79 -0
- package/dist/recipes/heatmap.js.map +1 -0
- package/dist/recipes/index.d.ts +5 -0
- package/dist/recipes/index.js +35 -0
- package/dist/recipes/index.js.map +1 -0
- package/dist/recipes/line.d.ts +69 -0
- package/dist/recipes/line.js +64 -0
- package/dist/recipes/line.js.map +1 -0
- package/dist/recipes/nested-donut.d.ts +22 -0
- package/dist/recipes/nested-donut.js +47 -0
- package/dist/recipes/nested-donut.js.map +1 -0
- package/dist/recipes/radial-bar.d.ts +25 -0
- package/dist/recipes/radial-bar.js +53 -0
- package/dist/recipes/radial-bar.js.map +1 -0
- package/dist/recipes/sankey.d.ts +41 -0
- package/dist/recipes/sankey.js +61 -0
- package/dist/recipes/sankey.js.map +1 -0
- package/dist/recipes/scatter.d.ts +68 -0
- package/dist/recipes/scatter.js +75 -0
- package/dist/recipes/scatter.js.map +1 -0
- package/dist/recipes/shared.d.ts +144 -0
- package/dist/recipes/shared.js +212 -0
- package/dist/recipes/shared.js.map +1 -0
- package/dist/recipes/timeline.d.ts +31 -0
- package/dist/recipes/timeline.js +123 -0
- package/dist/recipes/timeline.js.map +1 -0
- package/dist/recipes/treemap.d.ts +19 -0
- package/dist/recipes/treemap.js +43 -0
- package/dist/recipes/treemap.js.map +1 -0
- package/dist/recipes/types.d.ts +16 -0
- package/dist/recipes/types.js +2 -0
- package/dist/recipes/types.js.map +1 -0
- package/dist/render/headless.d.ts +62 -0
- package/dist/render/headless.js +158 -0
- package/dist/render/headless.js.map +1 -0
- package/dist/render/materialize.d.ts +35 -0
- package/dist/render/materialize.js +160 -0
- package/dist/render/materialize.js.map +1 -0
- package/dist/render/mutex.d.ts +9 -0
- package/dist/render/mutex.js +14 -0
- package/dist/render/mutex.js.map +1 -0
- package/dist/render/rasterize.d.ts +10 -0
- package/dist/render/rasterize.js +29 -0
- package/dist/render/rasterize.js.map +1 -0
- package/dist/render/renderer.d.ts +21 -0
- package/dist/render/renderer.js +66 -0
- package/dist/render/renderer.js.map +1 -0
- package/dist/render/spec.d.ts +95 -0
- package/dist/render/spec.js +15 -0
- package/dist/render/spec.js.map +1 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.js +44 -0
- package/dist/server.js.map +1 -0
- package/dist/svg/collect-css.d.ts +14 -0
- package/dist/svg/collect-css.js +53 -0
- package/dist/svg/collect-css.js.map +1 -0
- package/dist/svg/css-vars.d.ts +17 -0
- package/dist/svg/css-vars.js +64 -0
- package/dist/svg/css-vars.js.map +1 -0
- package/dist/svg/header.d.ts +8 -0
- package/dist/svg/header.js +81 -0
- package/dist/svg/header.js.map +1 -0
- package/dist/svg/ids.d.ts +12 -0
- package/dist/svg/ids.js +94 -0
- package/dist/svg/ids.js.map +1 -0
- package/dist/svg/postprocess.d.ts +41 -0
- package/dist/svg/postprocess.js +135 -0
- package/dist/svg/postprocess.js.map +1 -0
- package/dist/tools/register.d.ts +12 -0
- package/dist/tools/register.js +127 -0
- package/dist/tools/register.js.map +1 -0
- package/dist/widget/bundle.js +1777 -0
- package/dist/widget/bundle.meta.json +3 -0
- package/dist/widget/entry.d.ts +34 -0
- package/dist/widget/entry.js +118 -0
- package/dist/widget/entry.js.map +1 -0
- package/dist/widget/interactions.d.ts +14 -0
- package/dist/widget/interactions.js +151 -0
- package/dist/widget/interactions.js.map +1 -0
- package/dist/widget/unovis-slim.d.ts +33 -0
- package/dist/widget/unovis-slim.js +34 -0
- package/dist/widget/unovis-slim.js.map +1 -0
- package/docs/README.md +56 -0
- package/docs/architecture.md +158 -0
- package/docs/chart-spec.md +148 -0
- package/docs/getting-started.md +175 -0
- package/docs/interactive.md +125 -0
- package/docs/output-types.md +170 -0
- package/docs/programmatic.md +234 -0
- package/docs/tools.md +273 -0
- package/docs/troubleshooting.md +140 -0
- package/fonts/README.md +19 -0
- package/package.json +73 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Browser entry for the interactive chart widget.
|
|
2
|
+
*
|
|
3
|
+
* Renders the same JSON ChartSpec the static pipeline uses, through the same
|
|
4
|
+
* materializer — one source of truth for spec → chart. Bundled by
|
|
5
|
+
* scripts/build-widget.mjs into a standalone IIFE that exposes
|
|
6
|
+
* `window.UnovisChart` and, in `#embed` mode, speaks a small postMessage
|
|
7
|
+
* protocol so any page can host the widget in an iframe.
|
|
8
|
+
*/
|
|
9
|
+
import * as unovis from './unovis-slim.js';
|
|
10
|
+
import type { ChartSpec } from '../render/spec.js';
|
|
11
|
+
export interface RenderOptions {
|
|
12
|
+
/** Animation duration in ms (0 disables animations) */
|
|
13
|
+
duration?: number;
|
|
14
|
+
/** Render the spec title as a heading above the chart */
|
|
15
|
+
showTitle?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface ChartHandle {
|
|
18
|
+
destroy: () => void;
|
|
19
|
+
/** The container element the chart was rendered into */
|
|
20
|
+
element: HTMLElement;
|
|
21
|
+
}
|
|
22
|
+
/** Render a ChartSpec into a container. Returns a handle for teardown. */
|
|
23
|
+
export declare function render(spec: ChartSpec, container: HTMLElement, options?: RenderOptions): ChartHandle;
|
|
24
|
+
declare function startEmbedMode(): void;
|
|
25
|
+
declare global {
|
|
26
|
+
interface Window {
|
|
27
|
+
UnovisChart: {
|
|
28
|
+
render: typeof render;
|
|
29
|
+
startEmbed: typeof startEmbedMode;
|
|
30
|
+
unovis: typeof unovis;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/** Browser entry for the interactive chart widget.
|
|
2
|
+
*
|
|
3
|
+
* Renders the same JSON ChartSpec the static pipeline uses, through the same
|
|
4
|
+
* materializer — one source of truth for spec → chart. Bundled by
|
|
5
|
+
* scripts/build-widget.mjs into a standalone IIFE that exposes
|
|
6
|
+
* `window.UnovisChart` and, in `#embed` mode, speaks a small postMessage
|
|
7
|
+
* protocol so any page can host the widget in an iframe.
|
|
8
|
+
*/
|
|
9
|
+
import * as unovis from './unovis-slim.js';
|
|
10
|
+
import { materializeChart } from '../render/materialize.js';
|
|
11
|
+
import { buildInteractions } from './interactions.js';
|
|
12
|
+
const THEME_ATTR = 'data-theme';
|
|
13
|
+
function applyTheme(theme) {
|
|
14
|
+
const root = document.documentElement;
|
|
15
|
+
if (theme === 'dark')
|
|
16
|
+
root.setAttribute(THEME_ATTR, 'dark');
|
|
17
|
+
else
|
|
18
|
+
root.removeAttribute(THEME_ATTR);
|
|
19
|
+
}
|
|
20
|
+
function applyPalette(host, colors) {
|
|
21
|
+
colors?.forEach((color, i) => {
|
|
22
|
+
host.style.setProperty(`--vis-color${i}`, color);
|
|
23
|
+
host.style.setProperty(`--vis-dark-color${i}`, color);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
/** Render a ChartSpec into a container. Returns a handle for teardown. */
|
|
27
|
+
export function render(spec, container, options = {}) {
|
|
28
|
+
container.innerHTML = '';
|
|
29
|
+
container.classList.add('uv-root');
|
|
30
|
+
applyTheme(spec.theme);
|
|
31
|
+
applyPalette(container, spec.colors);
|
|
32
|
+
if (options.showTitle !== false && spec.title) {
|
|
33
|
+
const heading = document.createElement('div');
|
|
34
|
+
heading.className = 'uv-title';
|
|
35
|
+
heading.textContent = spec.title;
|
|
36
|
+
container.appendChild(heading);
|
|
37
|
+
}
|
|
38
|
+
const lib = unovis;
|
|
39
|
+
const { containerConfig: interactionConfig, legendItems } = buildInteractions(lib, spec);
|
|
40
|
+
let legend;
|
|
41
|
+
if (legendItems?.length) {
|
|
42
|
+
const legendEl = document.createElement('div');
|
|
43
|
+
legendEl.className = 'uv-legend';
|
|
44
|
+
container.appendChild(legendEl);
|
|
45
|
+
legend = new unovis.BulletLegend(legendEl, { items: legendItems });
|
|
46
|
+
}
|
|
47
|
+
const chartEl = document.createElement('div');
|
|
48
|
+
chartEl.className = 'uv-chart';
|
|
49
|
+
container.appendChild(chartEl);
|
|
50
|
+
const materialized = materializeChart(lib, spec, {
|
|
51
|
+
duration: options.duration ?? 400,
|
|
52
|
+
responsive: true,
|
|
53
|
+
});
|
|
54
|
+
const config = { ...materialized.containerConfig, ...interactionConfig };
|
|
55
|
+
const chart = materialized.containerType === 'xy'
|
|
56
|
+
? new unovis.XYContainer(chartEl, config, materialized.data)
|
|
57
|
+
: new unovis.SingleContainer(chartEl, config, materialized.data);
|
|
58
|
+
return {
|
|
59
|
+
element: container,
|
|
60
|
+
destroy: () => {
|
|
61
|
+
try {
|
|
62
|
+
chart.destroy();
|
|
63
|
+
legend?.destroy?.();
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
container.innerHTML = '';
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function startEmbedMode() {
|
|
72
|
+
const root = document.getElementById('uv-embed-root') ?? document.body;
|
|
73
|
+
let handle;
|
|
74
|
+
const postSize = () => {
|
|
75
|
+
window.parent?.postMessage({
|
|
76
|
+
type: 'unovis:size',
|
|
77
|
+
width: root.scrollWidth,
|
|
78
|
+
height: root.scrollHeight,
|
|
79
|
+
}, '*');
|
|
80
|
+
};
|
|
81
|
+
window.addEventListener('message', (event) => {
|
|
82
|
+
const message = event.data;
|
|
83
|
+
if (message?.type !== 'unovis:render' || !message.spec)
|
|
84
|
+
return;
|
|
85
|
+
handle?.destroy();
|
|
86
|
+
try {
|
|
87
|
+
handle = render(message.spec, root, message.options ?? {});
|
|
88
|
+
postSize();
|
|
89
|
+
}
|
|
90
|
+
catch (e) {
|
|
91
|
+
root.innerHTML = `<div class="uv-error">${String(e instanceof Error ? e.message : e)}</div>`;
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
window.parent?.postMessage({ type: 'unovis:ready' }, '*');
|
|
95
|
+
}
|
|
96
|
+
/** Render a spec embedded in the page as <script type="application/json" id="uv-spec"> */
|
|
97
|
+
function renderInlineSpec() {
|
|
98
|
+
const specEl = document.getElementById('uv-spec');
|
|
99
|
+
const optionsEl = document.getElementById('uv-options');
|
|
100
|
+
const root = document.getElementById('uv-root');
|
|
101
|
+
if (!specEl?.textContent || !root)
|
|
102
|
+
return;
|
|
103
|
+
try {
|
|
104
|
+
const options = optionsEl?.textContent ? JSON.parse(optionsEl.textContent) : {};
|
|
105
|
+
render(JSON.parse(specEl.textContent), root, options);
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
root.innerHTML = `<div class="uv-error">Failed to render chart: ${String(e instanceof Error ? e.message : e)}</div>`;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
window.UnovisChart = { render, startEmbed: startEmbedMode, unovis };
|
|
112
|
+
if (window.location.hash === '#embed')
|
|
113
|
+
startEmbedMode();
|
|
114
|
+
else if (document.readyState === 'loading')
|
|
115
|
+
document.addEventListener('DOMContentLoaded', renderInlineSpec);
|
|
116
|
+
else
|
|
117
|
+
renderInlineSpec();
|
|
118
|
+
//# sourceMappingURL=entry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"entry.js","sourceRoot":"","sources":["../../src/widget/entry.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ChartSpec } from '../render/spec.js';
|
|
2
|
+
type Lib = Record<string, any>;
|
|
3
|
+
export interface Interactions {
|
|
4
|
+
/** Container-level configs to merge in */
|
|
5
|
+
containerConfig: Record<string, unknown>;
|
|
6
|
+
/** Legend items for the HTML BulletLegend, when the spec declares any */
|
|
7
|
+
legendItems?: {
|
|
8
|
+
name: string;
|
|
9
|
+
color?: string;
|
|
10
|
+
}[];
|
|
11
|
+
}
|
|
12
|
+
/** Build tooltip/crosshair configs plus legend items for a spec */
|
|
13
|
+
export declare function buildInteractions(lib: Lib, spec: ChartSpec): Interactions;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/** Default interactivity for rendered charts.
|
|
2
|
+
*
|
|
3
|
+
* Static output can't carry behavior, so interactive charts get sensible
|
|
4
|
+
* defaults derived from the same ChartSpec: hover tooltips per chart type,
|
|
5
|
+
* a crosshair on continuous XY charts, and — unlike SVG output — Unovis's
|
|
6
|
+
* real HTML BulletLegend.
|
|
7
|
+
*/
|
|
8
|
+
import { materializeValue } from '../render/materialize.js';
|
|
9
|
+
const escapeHtml = (value) => String(value)
|
|
10
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
11
|
+
const formatValue = (value) => {
|
|
12
|
+
if (value === null || value === undefined)
|
|
13
|
+
return '—';
|
|
14
|
+
if (typeof value === 'number')
|
|
15
|
+
return value.toLocaleString('en-US', { maximumFractionDigits: 6 });
|
|
16
|
+
return escapeHtml(value);
|
|
17
|
+
};
|
|
18
|
+
const row = (label, value) => `<div class="uv-tt-row"><span class="uv-tt-key">${escapeHtml(label)}</span><span class="uv-tt-val">${formatValue(value)}</span></div>`;
|
|
19
|
+
const title = (text) => `<div class="uv-tt-title">${escapeHtml(text)}</div>`;
|
|
20
|
+
/** Field names referenced by a component config, in declaration order —
|
|
21
|
+
* used to show the fields the chart actually plots, not every data column. */
|
|
22
|
+
function specFields(config) {
|
|
23
|
+
const fields = [];
|
|
24
|
+
const walk = (value) => {
|
|
25
|
+
if (Array.isArray(value)) {
|
|
26
|
+
value.forEach(walk);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (typeof value === 'object' && value !== null) {
|
|
30
|
+
const ref = value;
|
|
31
|
+
if (typeof ref.$field === 'string') {
|
|
32
|
+
if (!fields.includes(ref.$field))
|
|
33
|
+
fields.push(ref.$field);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
Object.values(value).forEach(walk);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
walk(config);
|
|
40
|
+
return fields;
|
|
41
|
+
}
|
|
42
|
+
/** Records carry internal Unovis state under `_`-prefixed keys */
|
|
43
|
+
const publicEntries = (datum) => Object.entries(datum ?? {}).filter(([key, value]) => !key.startsWith('_') && (value === null || typeof value !== 'object'));
|
|
44
|
+
function recordTooltip(fields, labelField) {
|
|
45
|
+
return (d) => {
|
|
46
|
+
if (!d)
|
|
47
|
+
return '';
|
|
48
|
+
const head = labelField && d[labelField] !== undefined ? title(d[labelField]) : '';
|
|
49
|
+
const shown = fields.length
|
|
50
|
+
? fields.filter(f => d[f] !== undefined).map(f => row(f, d[f]))
|
|
51
|
+
: publicEntries(d).map(([key, value]) => row(key, value));
|
|
52
|
+
return `${head}${shown.join('')}` || title('no data');
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/** Build tooltip/crosshair configs plus legend items for a spec */
|
|
56
|
+
export function buildInteractions(lib, spec) {
|
|
57
|
+
const containerConfig = {};
|
|
58
|
+
const triggers = {};
|
|
59
|
+
for (const component of spec.components) {
|
|
60
|
+
const fields = specFields(component.config);
|
|
61
|
+
const generic = recordTooltip(fields);
|
|
62
|
+
switch (component.type) {
|
|
63
|
+
case 'Line':
|
|
64
|
+
case 'Area':
|
|
65
|
+
// Lines/areas are covered by the crosshair below; hovering the shape
|
|
66
|
+
// itself has no single datum
|
|
67
|
+
break;
|
|
68
|
+
case 'GroupedBar':
|
|
69
|
+
case 'StackedBar': {
|
|
70
|
+
const cls = component.type === 'StackedBar' ? lib.StackedBar : lib.GroupedBar;
|
|
71
|
+
triggers[cls.selectors.bar] = generic;
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
case 'Scatter':
|
|
75
|
+
triggers[lib.Scatter.selectors.point] = generic;
|
|
76
|
+
break;
|
|
77
|
+
case 'Boxplot':
|
|
78
|
+
triggers[lib.Boxplot.selectors.box] = generic;
|
|
79
|
+
break;
|
|
80
|
+
case 'Timeline':
|
|
81
|
+
triggers[lib.Timeline.selectors.line] = generic;
|
|
82
|
+
break;
|
|
83
|
+
case 'Donut':
|
|
84
|
+
triggers[lib.Donut.selectors.segment] = (d) => generic(d?.data ?? d);
|
|
85
|
+
break;
|
|
86
|
+
case 'NestedDonut':
|
|
87
|
+
triggers[lib.NestedDonut.selectors.segment] = (d) => `${title(d?.data?.key ?? '')}${row('value', d?.value ?? d?.data?.value)}`;
|
|
88
|
+
break;
|
|
89
|
+
case 'RadialBar':
|
|
90
|
+
triggers[lib.RadialBar.selectors.segment] = (d) => generic(d?.data ?? d);
|
|
91
|
+
break;
|
|
92
|
+
case 'Heatmap':
|
|
93
|
+
triggers[lib.Heatmap.selectors.node] = generic;
|
|
94
|
+
break;
|
|
95
|
+
case 'Treemap':
|
|
96
|
+
triggers[lib.Treemap.selectors.tile] = (d) => `${title(d?.data?.key ?? d?.key ?? '')}${row('value', d?.value)}`;
|
|
97
|
+
break;
|
|
98
|
+
case 'Sankey':
|
|
99
|
+
triggers[lib.Sankey.selectors.node] = (d) => `${title(d?.label ?? d?.id ?? '')}${row('total', d?.value)}`;
|
|
100
|
+
triggers[lib.Sankey.selectors.link] = (d) => `${title(`${d?.source?.label ?? d?.source?.id ?? ''} → ${d?.target?.label ?? d?.target?.id ?? ''}`)}${row('value', d?.value)}`;
|
|
101
|
+
break;
|
|
102
|
+
case 'ChordDiagram':
|
|
103
|
+
triggers[lib.ChordDiagram.selectors.node] = (d) => title(d?.data?.label ?? d?.key ?? '');
|
|
104
|
+
break;
|
|
105
|
+
case 'Graph':
|
|
106
|
+
triggers[lib.Graph.selectors.node] = (d) => generic(d?._state ? d : d?.data ?? d);
|
|
107
|
+
triggers[lib.Graph.selectors.link] = (d) => title(`${d?.source?.id ?? ''} → ${d?.target?.id ?? ''}`);
|
|
108
|
+
break;
|
|
109
|
+
case 'TopoJSONMap':
|
|
110
|
+
triggers[lib.TopoJSONMap.selectors.feature] = (d) => {
|
|
111
|
+
const area = d?.data ?? d;
|
|
112
|
+
if (!area?.id && !area?.name)
|
|
113
|
+
return '';
|
|
114
|
+
return `${title(area.name ?? area.id)}${area.value !== undefined ? row('value', area.value) : ''}`;
|
|
115
|
+
};
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Crosshair: continuous XY charts get a shared vertical readout
|
|
120
|
+
const crosshairComponent = spec.components.find(c => c.type === 'Line' || c.type === 'Area');
|
|
121
|
+
const usesCrosshair = spec.container === 'xy' && !!crosshairComponent;
|
|
122
|
+
// XYContainer renders the crosshair's readout through the container tooltip
|
|
123
|
+
// (`crosshair.tooltip = tooltip`), so a crosshair needs one even when no
|
|
124
|
+
// component has its own hover triggers — line/area charts hit exactly this.
|
|
125
|
+
if (Object.keys(triggers).length || usesCrosshair) {
|
|
126
|
+
containerConfig.tooltip = new lib.Tooltip({ triggers, horizontalPlacement: 'center' });
|
|
127
|
+
}
|
|
128
|
+
if (usesCrosshair && crosshairComponent) {
|
|
129
|
+
const x = materializeValue(crosshairComponent.config.x);
|
|
130
|
+
const yFields = specFields(crosshairComponent.config)
|
|
131
|
+
.filter(f => f !== crosshairComponent.config.x?.$field);
|
|
132
|
+
const labels = spec.legend?.map(item => item.name) ?? yFields;
|
|
133
|
+
const xField = crosshairComponent.config.x?.$field;
|
|
134
|
+
containerConfig.crosshair = new lib.Crosshair({
|
|
135
|
+
x,
|
|
136
|
+
// Crosshair places one marker per y accessor — without these it warns
|
|
137
|
+
// and renders only the vertical line
|
|
138
|
+
y: materializeValue(crosshairComponent.config.y),
|
|
139
|
+
template: (d) => {
|
|
140
|
+
const head = xField && d?.[xField] !== undefined ? title(d[xField]) : '';
|
|
141
|
+
const rows = yFields.map((f, i) => row(labels[i] ?? f, d?.[f]));
|
|
142
|
+
return `${head}${rows.join('')}`;
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
const legendItems = spec.legend?.length
|
|
147
|
+
? spec.legend.map(item => ({ name: item.name, color: item.color }))
|
|
148
|
+
: undefined;
|
|
149
|
+
return { containerConfig, legendItems };
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=interactions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"interactions.js","sourceRoot":"","sources":["../../src/widget/interactions.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** Slim @unovis/ts entry for the browser widget bundle.
|
|
2
|
+
*
|
|
3
|
+
* The package barrel re-exports LeafletMap / LeafletFlowMap, which statically
|
|
4
|
+
* pull in leaflet, maplibre-gl and three — megabytes of code for components
|
|
5
|
+
* that can't appear in generated charts anyway. Importing each component
|
|
6
|
+
* directly keeps the bundle to the charts we actually support.
|
|
7
|
+
*/
|
|
8
|
+
export { XYContainer } from '@unovis/ts/dist/containers/xy-container/index.js';
|
|
9
|
+
export { SingleContainer } from '@unovis/ts/dist/containers/single-container/index.js';
|
|
10
|
+
export { Axis } from '@unovis/ts/dist/components/axis/index.js';
|
|
11
|
+
export { Line } from '@unovis/ts/dist/components/line/index.js';
|
|
12
|
+
export { Area } from '@unovis/ts/dist/components/area/index.js';
|
|
13
|
+
export { GroupedBar } from '@unovis/ts/dist/components/grouped-bar/index.js';
|
|
14
|
+
export { StackedBar } from '@unovis/ts/dist/components/stacked-bar/index.js';
|
|
15
|
+
export { Scatter } from '@unovis/ts/dist/components/scatter/index.js';
|
|
16
|
+
export { Timeline } from '@unovis/ts/dist/components/timeline/index.js';
|
|
17
|
+
export { Boxplot } from '@unovis/ts/dist/components/boxplot/index.js';
|
|
18
|
+
export { XYLabels } from '@unovis/ts/dist/components/xy-labels/index.js';
|
|
19
|
+
export { Plotband } from '@unovis/ts/dist/components/plotband/index.js';
|
|
20
|
+
export { Plotline } from '@unovis/ts/dist/components/plotline/index.js';
|
|
21
|
+
export { Donut } from '@unovis/ts/dist/components/donut/index.js';
|
|
22
|
+
export { NestedDonut } from '@unovis/ts/dist/components/nested-donut/index.js';
|
|
23
|
+
export { RadialBar } from '@unovis/ts/dist/components/radial-bar/index.js';
|
|
24
|
+
export { Sankey } from '@unovis/ts/dist/components/sankey/index.js';
|
|
25
|
+
export { Heatmap } from '@unovis/ts/dist/components/heatmap/index.js';
|
|
26
|
+
export { Treemap } from '@unovis/ts/dist/components/treemap/index.js';
|
|
27
|
+
export { ChordDiagram } from '@unovis/ts/dist/components/chord-diagram/index.js';
|
|
28
|
+
export { Graph } from '@unovis/ts/dist/components/graph/index.js';
|
|
29
|
+
export { TopoJSONMap } from '@unovis/ts/dist/components/topojson-map/index.js';
|
|
30
|
+
export { Tooltip } from '@unovis/ts/dist/components/tooltip/index.js';
|
|
31
|
+
export { Crosshair } from '@unovis/ts/dist/components/crosshair/index.js';
|
|
32
|
+
export { BulletLegend } from '@unovis/ts/dist/components/bullet-legend/index.js';
|
|
33
|
+
export { MapProjection } from '@unovis/ts/dist/components/topojson-map/types.js';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Slim @unovis/ts entry for the browser widget bundle.
|
|
2
|
+
*
|
|
3
|
+
* The package barrel re-exports LeafletMap / LeafletFlowMap, which statically
|
|
4
|
+
* pull in leaflet, maplibre-gl and three — megabytes of code for components
|
|
5
|
+
* that can't appear in generated charts anyway. Importing each component
|
|
6
|
+
* directly keeps the bundle to the charts we actually support.
|
|
7
|
+
*/
|
|
8
|
+
export { XYContainer } from '@unovis/ts/dist/containers/xy-container/index.js';
|
|
9
|
+
export { SingleContainer } from '@unovis/ts/dist/containers/single-container/index.js';
|
|
10
|
+
export { Axis } from '@unovis/ts/dist/components/axis/index.js';
|
|
11
|
+
export { Line } from '@unovis/ts/dist/components/line/index.js';
|
|
12
|
+
export { Area } from '@unovis/ts/dist/components/area/index.js';
|
|
13
|
+
export { GroupedBar } from '@unovis/ts/dist/components/grouped-bar/index.js';
|
|
14
|
+
export { StackedBar } from '@unovis/ts/dist/components/stacked-bar/index.js';
|
|
15
|
+
export { Scatter } from '@unovis/ts/dist/components/scatter/index.js';
|
|
16
|
+
export { Timeline } from '@unovis/ts/dist/components/timeline/index.js';
|
|
17
|
+
export { Boxplot } from '@unovis/ts/dist/components/boxplot/index.js';
|
|
18
|
+
export { XYLabels } from '@unovis/ts/dist/components/xy-labels/index.js';
|
|
19
|
+
export { Plotband } from '@unovis/ts/dist/components/plotband/index.js';
|
|
20
|
+
export { Plotline } from '@unovis/ts/dist/components/plotline/index.js';
|
|
21
|
+
export { Donut } from '@unovis/ts/dist/components/donut/index.js';
|
|
22
|
+
export { NestedDonut } from '@unovis/ts/dist/components/nested-donut/index.js';
|
|
23
|
+
export { RadialBar } from '@unovis/ts/dist/components/radial-bar/index.js';
|
|
24
|
+
export { Sankey } from '@unovis/ts/dist/components/sankey/index.js';
|
|
25
|
+
export { Heatmap } from '@unovis/ts/dist/components/heatmap/index.js';
|
|
26
|
+
export { Treemap } from '@unovis/ts/dist/components/treemap/index.js';
|
|
27
|
+
export { ChordDiagram } from '@unovis/ts/dist/components/chord-diagram/index.js';
|
|
28
|
+
export { Graph } from '@unovis/ts/dist/components/graph/index.js';
|
|
29
|
+
export { TopoJSONMap } from '@unovis/ts/dist/components/topojson-map/index.js';
|
|
30
|
+
export { Tooltip } from '@unovis/ts/dist/components/tooltip/index.js';
|
|
31
|
+
export { Crosshair } from '@unovis/ts/dist/components/crosshair/index.js';
|
|
32
|
+
export { BulletLegend } from '@unovis/ts/dist/components/bullet-legend/index.js';
|
|
33
|
+
export { MapProjection } from '@unovis/ts/dist/components/topojson-map/types.js';
|
|
34
|
+
//# sourceMappingURL=unovis-slim.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unovis-slim.js","sourceRoot":"","sources":["../../src/widget/unovis-slim.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,kDAAkD,CAAA;AAC9E,OAAO,EAAE,eAAe,EAAE,MAAM,sDAAsD,CAAA;AAEtF,OAAO,EAAE,IAAI,EAAE,MAAM,0CAA0C,CAAA;AAC/D,OAAO,EAAE,IAAI,EAAE,MAAM,0CAA0C,CAAA;AAC/D,OAAO,EAAE,IAAI,EAAE,MAAM,0CAA0C,CAAA;AAC/D,OAAO,EAAE,UAAU,EAAE,MAAM,iDAAiD,CAAA;AAC5E,OAAO,EAAE,UAAU,EAAE,MAAM,iDAAiD,CAAA;AAC5E,OAAO,EAAE,OAAO,EAAE,MAAM,6CAA6C,CAAA;AACrE,OAAO,EAAE,QAAQ,EAAE,MAAM,8CAA8C,CAAA;AACvE,OAAO,EAAE,OAAO,EAAE,MAAM,6CAA6C,CAAA;AACrE,OAAO,EAAE,QAAQ,EAAE,MAAM,+CAA+C,CAAA;AACxE,OAAO,EAAE,QAAQ,EAAE,MAAM,8CAA8C,CAAA;AACvE,OAAO,EAAE,QAAQ,EAAE,MAAM,8CAA8C,CAAA;AAEvE,OAAO,EAAE,KAAK,EAAE,MAAM,2CAA2C,CAAA;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,kDAAkD,CAAA;AAC9E,OAAO,EAAE,SAAS,EAAE,MAAM,gDAAgD,CAAA;AAC1E,OAAO,EAAE,MAAM,EAAE,MAAM,4CAA4C,CAAA;AACnE,OAAO,EAAE,OAAO,EAAE,MAAM,6CAA6C,CAAA;AACrE,OAAO,EAAE,OAAO,EAAE,MAAM,6CAA6C,CAAA;AACrE,OAAO,EAAE,YAAY,EAAE,MAAM,mDAAmD,CAAA;AAChF,OAAO,EAAE,KAAK,EAAE,MAAM,2CAA2C,CAAA;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,kDAAkD,CAAA;AAE9E,OAAO,EAAE,OAAO,EAAE,MAAM,6CAA6C,CAAA;AACrE,OAAO,EAAE,SAAS,EAAE,MAAM,+CAA+C,CAAA;AACzE,OAAO,EAAE,YAAY,EAAE,MAAM,mDAAmD,CAAA;AAChF,OAAO,EAAE,aAAa,EAAE,MAAM,kDAAkD,CAAA"}
|
package/docs/README.md
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# @unovis/mcp documentation
|
|
2
|
+
|
|
3
|
+
An MCP server that turns data into [Unovis](https://unovis.dev) charts —
|
|
4
|
+
rendered locally in Node, with no browser and no remote services.
|
|
5
|
+
|
|
6
|
+
## Pages
|
|
7
|
+
|
|
8
|
+
| Page | What's in it |
|
|
9
|
+
|---|---|
|
|
10
|
+
| [Getting started](./getting-started.md) | Install and connect from Claude Code, Claude Desktop, Cursor, Codex, VS Code; HTTP transport; environment variables |
|
|
11
|
+
| [Tools reference](./tools.md) | Every tool with all its options, generated from the live schemas |
|
|
12
|
+
| [Output types](./output-types.md) | `svg`, `png`, `html`, `interactive`, `config`, `code` — what each returns and when to use it |
|
|
13
|
+
| [Chart spec](./chart-spec.md) | The JSON IR behind every chart: containers, components, accessor references |
|
|
14
|
+
| [Programmatic use](./programmatic.md) | Use it as a library: `renderChart`, the `renderToSvg` SSR primitive, PNG rasterization, embedding the tools in your own MCP server |
|
|
15
|
+
| [Interactive charts](./interactive.md) | The browser widget, self-contained HTML, iframe embedding, MCP UI resources |
|
|
16
|
+
| [Architecture](./architecture.md) | How headless rendering actually works, and why each piece exists |
|
|
17
|
+
| [Troubleshooting](./troubleshooting.md) | Fonts, blank charts, timeouts, large data, known limitations |
|
|
18
|
+
|
|
19
|
+
## The 30-second version
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
claude mcp add unovis -- npx -y @unovis/mcp
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Then ask for a chart. The agent calls a tool with your data:
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"name": "generate_line_chart",
|
|
30
|
+
"arguments": {
|
|
31
|
+
"data": [
|
|
32
|
+
{ "month": "2024-01", "sales": 120, "cost": 80 },
|
|
33
|
+
{ "month": "2024-02", "sales": 150, "cost": 95 }
|
|
34
|
+
],
|
|
35
|
+
"x": "month",
|
|
36
|
+
"xIsTime": true,
|
|
37
|
+
"y": ["sales", "cost"],
|
|
38
|
+
"seriesLabels": ["Sales", "Cost"],
|
|
39
|
+
"title": "Revenue"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
…and gets back a standalone SVG. Change `outputType` and the same call
|
|
45
|
+
returns a PNG, an interactive HTML file, the chart spec as JSON, or
|
|
46
|
+
ready-to-paste React/Svelte/Vue/Angular/Solid source.
|
|
47
|
+
|
|
48
|
+
## What makes it different
|
|
49
|
+
|
|
50
|
+
- **Everything is local.** Charts render in-process with jsdom; your data never
|
|
51
|
+
leaves the machine, and nothing needs a network connection after install.
|
|
52
|
+
- **One spec, many outputs.** A single JSON chart spec drives the headless
|
|
53
|
+
renderer, the interactive browser widget, and the code generator.
|
|
54
|
+
- **It's also a library.** `renderChart`, `renderToSvg` and the tool registry
|
|
55
|
+
are exported, so the same engine works in scripts, CI, and your own MCP
|
|
56
|
+
server.
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
Unovis is a browser-first library: it builds SVG by manipulating a live DOM with
|
|
4
|
+
D3, measures text with canvas, reads theme values from CSS custom properties, and
|
|
5
|
+
schedules every render through `requestAnimationFrame`. None of that exists in
|
|
6
|
+
Node. This package makes it exist.
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
tool call ──► recipe ──► ChartSpec ──► materializer ──► Unovis components
|
|
10
|
+
│
|
|
11
|
+
┌────────────────────────────────────────┘
|
|
12
|
+
▼
|
|
13
|
+
headless environment (jsdom + shims)
|
|
14
|
+
│
|
|
15
|
+
▼
|
|
16
|
+
flush frames until quiescent
|
|
17
|
+
│
|
|
18
|
+
▼
|
|
19
|
+
SVG post-processing ──► standalone SVG ──► PNG (resvg)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Layers
|
|
23
|
+
|
|
24
|
+
| Directory | Responsibility |
|
|
25
|
+
|---|---|
|
|
26
|
+
| `src/env/` | The headless browser: jsdom plus the shims Unovis needs |
|
|
27
|
+
| `src/render/` | `headless.ts` (the SSR primitive), `materialize.ts` (spec → components), `renderer.ts` (spec → SVG), `rasterize.ts` (SVG → PNG) |
|
|
28
|
+
| `src/svg/` | Turning a live SVG element into a standalone document |
|
|
29
|
+
| `src/recipes/` | Tool inputs → `ChartSpec` (one file per chart type) |
|
|
30
|
+
| `src/codegen/` | `ChartSpec` → framework source |
|
|
31
|
+
| `src/widget/` + `src/html/` | The browser bundle and the documents that host it |
|
|
32
|
+
| `src/tools/`, `src/server.ts`, `src/cli.ts` | The MCP surface |
|
|
33
|
+
|
|
34
|
+
The dependency arrows only point one way: `env` and `svg` know nothing about
|
|
35
|
+
specs, and `render` knows nothing about MCP. `renderToSvg` is deliberately the
|
|
36
|
+
boundary a future `@unovis/ssr` package would follow.
|
|
37
|
+
|
|
38
|
+
## The environment shims
|
|
39
|
+
|
|
40
|
+
Installed **before** `@unovis/ts` is imported, because the library captures
|
|
41
|
+
its environment at module load.
|
|
42
|
+
|
|
43
|
+
| Shim | Why it's needed |
|
|
44
|
+
|---|---|
|
|
45
|
+
| **Flushable `requestAnimationFrame`** | Containers defer rendering by one frame, and several components schedule follow-up frames. A queue we can drain makes rendering synchronous and deterministic. |
|
|
46
|
+
| **Canvas 2D context** (`@napi-rs/canvas`) | Text measurement (`measureText`) drives label trimming, wrapping and axis margins. Unovis snapshots the context at module scope, hence the ordering requirement. |
|
|
47
|
+
| **`getBBox`, `getBoundingClientRect`, `getTotalLength`, `getPointAtLength`** | jsdom performs no layout at all. Text boxes come from real canvas metrics; shapes from attribute math; paths from `svg-path-bounds` / `svg-path-properties`; groups from the union of children mapped through their transforms. |
|
|
48
|
+
| **`getComputedStyle` wrapper** | Unovis resolves its entire theme through `--vis-*` custom properties, which jsdom doesn't cascade. The wrapper resolves them from a parsed `:root` map (with dark-theme overrides) and synthesizes font properties. |
|
|
49
|
+
| **Element sizing** | `clientWidth`/`clientHeight` are always 0 in jsdom; containers read them to size charts. |
|
|
50
|
+
| **No-op `ResizeObserver`** | Containers construct one on every render. |
|
|
51
|
+
|
|
52
|
+
Fonts are provisioned once at startup: the pinned Inter release is downloaded
|
|
53
|
+
(SHA-256 verified) into `~/.cache/unovis-mcp/fonts/` and registered with the
|
|
54
|
+
canvas. Text then measures against the same font the output declares. Offline,
|
|
55
|
+
it degrades to system fonts.
|
|
56
|
+
|
|
57
|
+
## Getting a deterministic render
|
|
58
|
+
|
|
59
|
+
1. Every component and container gets `duration: 0`. Unovis's `smartTransition`
|
|
60
|
+
returns the plain selection for a falsy duration, so all attribute writes
|
|
61
|
+
happen immediately instead of over time.
|
|
62
|
+
2. The frame queue is drained repeatedly, letting each flush schedule follow-up
|
|
63
|
+
frames, until it stays empty.
|
|
64
|
+
3. The container's `onRenderComplete` must fire before serialization. Components
|
|
65
|
+
with asynchronous layouts (force-directed graphs, which dynamically import
|
|
66
|
+
their solver) signal separately through their own `onRenderComplete`.
|
|
67
|
+
4. If either signal never arrives, the render throws with the reason instead of
|
|
68
|
+
emitting a half-drawn chart.
|
|
69
|
+
|
|
70
|
+
## Making the SVG standalone
|
|
71
|
+
|
|
72
|
+
The rendered DOM is full of page-context dependencies. The post-processor
|
|
73
|
+
removes all of them:
|
|
74
|
+
|
|
75
|
+
1. **Import external defs** — globally injected pattern definitions the chart
|
|
76
|
+
references get copied into its own `<defs>`.
|
|
77
|
+
2. **Inline the stylesheet** — emotion's rules are matched with
|
|
78
|
+
`querySelectorAll` and written as inline styles, then the class attributes are
|
|
79
|
+
dropped. Author inline styles always win; later rules override earlier ones.
|
|
80
|
+
3. **Bake CSS variables** — every `var(--vis-*)` becomes a literal value for the
|
|
81
|
+
requested theme, because rasterizers and `<img>` contexts don't support custom
|
|
82
|
+
properties.
|
|
83
|
+
4. **Normalize references** — `url(http://localhost/#id)` becomes `url(#id)`
|
|
84
|
+
(Unovis builds absolute references from `window.location`).
|
|
85
|
+
5. **Rewrite ids** — random guids become `prefix-N`, so multiple charts can share
|
|
86
|
+
a document without colliding, and snapshots stay stable.
|
|
87
|
+
6. **Synthesize the header** — Unovis containers have no title concept and its
|
|
88
|
+
legends are HTML, so the title and legend are drawn as SVG above the chart,
|
|
89
|
+
and the content is offset into a padded frame.
|
|
90
|
+
|
|
91
|
+
The result has no classes, no variables, no external references — which is also
|
|
92
|
+
exactly what the PNG rasterizer needs, so `png` reuses it unchanged.
|
|
93
|
+
|
|
94
|
+
## The browser widget
|
|
95
|
+
|
|
96
|
+
The interactive outputs bundle the **same materializer** the headless renderer
|
|
97
|
+
uses, so one code path turns a spec into a chart on both sides. esbuild keeps it
|
|
98
|
+
at ~665kB by importing components individually — the package barrel statically
|
|
99
|
+
pulls in Leaflet, MapLibre and Three — and by excluding elkjs, whose 1.4MB
|
|
100
|
+
engine would dwarf everything else. A size budget in the build script fails the
|
|
101
|
+
build if the barrel creeps back in.
|
|
102
|
+
|
|
103
|
+
Interactions are derived from the spec: per-chart-type tooltip templates, a
|
|
104
|
+
crosshair for continuous XY charts, and the real HTML legend.
|
|
105
|
+
|
|
106
|
+
## Upstream changes
|
|
107
|
+
|
|
108
|
+
A handful of fixes belong in `@unovis/ts` rather than in shims, and were made
|
|
109
|
+
there:
|
|
110
|
+
|
|
111
|
+
- `ContainerCore` falls back to the configured width/height when `clientWidth`
|
|
112
|
+
is 0 (detached or non-layouting environments).
|
|
113
|
+
- `getPixelsPerInch` pre-seeds its cache before measuring — without layout,
|
|
114
|
+
`getComputedStyle` returns the specified value (`'128in'`), which recursed
|
|
115
|
+
infinitely.
|
|
116
|
+
- Root `index.js` / `maps.js` entries so `@unovis/ts` resolves in plain Node.
|
|
117
|
+
|
|
118
|
+
The trend matters: as the core becomes SSR-friendlier, the shim layer shrinks.
|
|
119
|
+
|
|
120
|
+
## Testing strategy
|
|
121
|
+
|
|
122
|
+
| Layer | What it proves |
|
|
123
|
+
|---|---|
|
|
124
|
+
| Env unit tests | Bbox math, variable resolution, frame flushing |
|
|
125
|
+
| Recipe snapshots | Byte-stable SVG per chart type (with a deterministic id prefix) |
|
|
126
|
+
| Post-processing tests | Id rewriting, variable baking, header synthesis, theme |
|
|
127
|
+
| MCP integration | Real SDK client over an in-memory transport: schemas, every output type, error paths, tool filtering, concurrency |
|
|
128
|
+
| Widget tests | The **real browser bundle** executed in jsdom: rendering, a simulated hover producing tooltip content, the embed protocol |
|
|
129
|
+
| Widget matrix | All 15 chart types rendered interactively with a clean console |
|
|
130
|
+
| Codegen | Generated TypeScript **type-checked against `@unovis/ts`** |
|
|
131
|
+
| `pnpm samples` | Every fixture rendered to SVG + PNG, light and dark, as a contact sheet for eyeball review |
|
|
132
|
+
|
|
133
|
+
Graph charts are deliberately not snapshotted. They keep settling for a few
|
|
134
|
+
frames after their layout reports completion — fit-view, label collision passes
|
|
135
|
+
— so exact geometry depends on machine load, and `force` layouts additionally
|
|
136
|
+
call `Math.random()`. The renderer waits out a short grace period so real output
|
|
137
|
+
doesn't lose those late frames, but a byte snapshot would still be a false
|
|
138
|
+
signal that fails randomly under CI load. Structural assertions, the widget
|
|
139
|
+
matrix and the async-layout probe cover graphs instead.
|
|
140
|
+
|
|
141
|
+
`pnpm test` builds the widget bundle first, so a fresh clone can run the suite
|
|
142
|
+
without a manual build step.
|
|
143
|
+
|
|
144
|
+
## Continuous integration
|
|
145
|
+
|
|
146
|
+
The `mcp` job in `.github/workflows/pull_request.yml` runs on every pull
|
|
147
|
+
request: build (`@unovis/ts` then this package), type-check, lint, the full test
|
|
148
|
+
suite, and the sample gallery — which is uploaded as a build artifact so a
|
|
149
|
+
reviewer can look at the charts instead of trusting a green check.
|
|
150
|
+
|
|
151
|
+
Inter is cached at `~/.cache/unovis-mcp` (keyed on `src/env/fonts.ts`, which
|
|
152
|
+
holds the pinned version and checksum). That keeps the suite off the network and
|
|
153
|
+
keeps text metrics — and therefore the SVG snapshots — reproducible between
|
|
154
|
+
runs. If snapshots ever disagree between machines, the cause is font
|
|
155
|
+
provisioning, not the renderer.
|
|
156
|
+
|
|
157
|
+
What the suite can't prove is covered in
|
|
158
|
+
[Troubleshooting](./troubleshooting.md#what-the-tests-dont-cover).
|