@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,234 @@
|
|
|
1
|
+
# Programmatic use
|
|
2
|
+
|
|
3
|
+
The MCP server is the packaging, not the engine. Everything it does is exported,
|
|
4
|
+
so you can render charts from a script, a build step, a web server, or your own
|
|
5
|
+
MCP server.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @unovis/mcp
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Render a chart spec
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { renderChart } from '@unovis/mcp'
|
|
15
|
+
|
|
16
|
+
const { svg, width, height, warnings } = await renderChart({
|
|
17
|
+
container: 'xy',
|
|
18
|
+
width: 800,
|
|
19
|
+
height: 400,
|
|
20
|
+
theme: 'light',
|
|
21
|
+
title: 'Weekly deploys',
|
|
22
|
+
components: [{
|
|
23
|
+
type: 'GroupedBar',
|
|
24
|
+
config: { x: { $index: true }, y: { $field: 'count', as: 'number' } },
|
|
25
|
+
}],
|
|
26
|
+
xAxis: { tickFormat: { $lookup: ['W1', 'W2', 'W3'] }, tickValues: [0, 1, 2] },
|
|
27
|
+
yAxis: { gridLine: true },
|
|
28
|
+
data: [{ count: 12 }, { count: 19 }, { count: 14 }],
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
await writeFile('deploys.svg', svg)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
See [Chart spec](./chart-spec.md) for the full format. `warnings` is normally
|
|
35
|
+
empty; non-fatal render issues land there rather than throwing.
|
|
36
|
+
|
|
37
|
+
## Use the tool schemas instead of hand-writing specs
|
|
38
|
+
|
|
39
|
+
The recipes that back the tools are exported, so you can reuse their validation
|
|
40
|
+
and defaults:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { recipeByName, renderChart } from '@unovis/mcp'
|
|
44
|
+
import { z } from 'zod'
|
|
45
|
+
|
|
46
|
+
const recipe = recipeByName.get('generate_line_chart')!
|
|
47
|
+
const input = z.object(recipe.inputShape).parse({
|
|
48
|
+
data: [{ m: 'Jan', sales: 10 }, { m: 'Feb', sales: 14 }],
|
|
49
|
+
x: 'm',
|
|
50
|
+
y: 'sales',
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const { svg } = await renderChart(recipe.toSpec(input))
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## `renderToSvg`
|
|
57
|
+
|
|
58
|
+
The headless primitive underneath everything: it creates the DOM, drives the
|
|
59
|
+
render to completion, and serializes standalone SVG. Use it when you want to
|
|
60
|
+
write ordinary Unovis code — with real accessor functions and any config option
|
|
61
|
+
— rather than a JSON spec.
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { renderToSvg } from '@unovis/mcp'
|
|
65
|
+
|
|
66
|
+
interface Row { date: number; value: number }
|
|
67
|
+
|
|
68
|
+
const { svg } = await renderToSvg({
|
|
69
|
+
width: 900,
|
|
70
|
+
height: 400,
|
|
71
|
+
theme: 'dark',
|
|
72
|
+
title: 'Latency p95',
|
|
73
|
+
}, (ctx) => {
|
|
74
|
+
const line = new ctx.unovis.Line<Row>({
|
|
75
|
+
x: d => d.date,
|
|
76
|
+
y: d => d.value,
|
|
77
|
+
duration: 0,
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
return new ctx.unovis.XYContainer<Row>(ctx.container, {
|
|
81
|
+
components: [line],
|
|
82
|
+
xAxis: new ctx.unovis.Axis<Row>({ duration: 0, tickFormat: t => new Date(Number(t)).toISOString().slice(0, 10) }),
|
|
83
|
+
yAxis: new ctx.unovis.Axis<Row>({ duration: 0, label: 'ms' }),
|
|
84
|
+
width: ctx.width,
|
|
85
|
+
height: ctx.height,
|
|
86
|
+
duration: 0,
|
|
87
|
+
onRenderComplete: ctx.onRenderComplete,
|
|
88
|
+
}, rows)
|
|
89
|
+
})
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Three rules, all of which the API nudges you toward:
|
|
93
|
+
|
|
94
|
+
1. **Use `ctx.unovis`, don't import `@unovis/ts` yourself.** The library captures
|
|
95
|
+
its environment when it loads — emotion inserts stylesheets, text measurement
|
|
96
|
+
grabs a canvas context — so it has to be imported *after* the shims are in
|
|
97
|
+
place. `ctx.unovis` is that already-initialised namespace.
|
|
98
|
+
2. **Wire `ctx.onRenderComplete` into the container config.** It's how the
|
|
99
|
+
renderer knows drawing finished. Omit it and you get an explicit error rather
|
|
100
|
+
than a blank chart.
|
|
101
|
+
3. **Pass `duration: 0`.** Animations are meaningless for a static render and
|
|
102
|
+
would leave transitions half-applied. (Components default to 600ms, so set it
|
|
103
|
+
on components too.)
|
|
104
|
+
|
|
105
|
+
For components whose layout is asynchronous — `Graph` with a force layout —
|
|
106
|
+
declare it so the render waits:
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
await renderToSvg({ width: 800, height: 600 }, (ctx) => {
|
|
110
|
+
ctx.requireComponentReady()
|
|
111
|
+
const graph = new ctx.unovis.Graph({
|
|
112
|
+
layoutType: 'force',
|
|
113
|
+
duration: 0,
|
|
114
|
+
onRenderComplete: ctx.onComponentReady,
|
|
115
|
+
})
|
|
116
|
+
return new ctx.unovis.SingleContainer(ctx.container, {
|
|
117
|
+
component: graph, width: ctx.width, height: ctx.height, duration: 0,
|
|
118
|
+
onRenderComplete: ctx.onRenderComplete,
|
|
119
|
+
}, { nodes, links })
|
|
120
|
+
})
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Options
|
|
124
|
+
|
|
125
|
+
| Option | Default | Notes |
|
|
126
|
+
|---|---|---|
|
|
127
|
+
| `width`, `height` | required | Final image size |
|
|
128
|
+
| `theme` | `light` | |
|
|
129
|
+
| `title`, `legend` | — | Synthesized into the SVG header |
|
|
130
|
+
| `colors` | — | Hex palette overriding `--vis-colorN` |
|
|
131
|
+
| `padding` | 12/16/16/16 | Frame around the chart; pass zeros to disable |
|
|
132
|
+
| `idPrefix` | random | Set a constant for byte-stable snapshots |
|
|
133
|
+
| `keepClasses` | `false` | Keep emotion classes and skip style inlining (debug) |
|
|
134
|
+
|
|
135
|
+
## PNG rasterization
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
import { renderToSvg, svgToPng, themeBackground } from '@unovis/mcp'
|
|
139
|
+
|
|
140
|
+
const { svg, width } = await renderToSvg({ width: 800, height: 400 }, build)
|
|
141
|
+
const png = await svgToPng(svg, { width, scale: 2, background: themeBackground('light') })
|
|
142
|
+
await writeFile('chart.png', png)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`background` matters: CSS `background-color` isn't an SVG rendering attribute,
|
|
146
|
+
so a rasterizer would otherwise give you a transparent backdrop.
|
|
147
|
+
|
|
148
|
+
## Interactive HTML
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
import { buildChartDocument } from '@unovis/mcp'
|
|
152
|
+
|
|
153
|
+
await writeFile('chart.html', buildChartDocument(spec, { duration: 400 }))
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
See [Interactive charts](./interactive.md) for the widget and iframe embedding.
|
|
157
|
+
|
|
158
|
+
## Generate framework code
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
import { generateCode } from '@unovis/mcp'
|
|
162
|
+
|
|
163
|
+
for (const file of generateCode(spec, 'react')) {
|
|
164
|
+
await writeFile(file.name, file.content) // Chart.tsx
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Angular returns two files (template + component class).
|
|
169
|
+
|
|
170
|
+
## Add these tools to your own MCP server
|
|
171
|
+
|
|
172
|
+
If you already run an MCP server — say one that queries your warehouse — you can
|
|
173
|
+
add charting to it instead of running a second process:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
177
|
+
import { registerTools } from '@unovis/mcp'
|
|
178
|
+
|
|
179
|
+
const server = new McpServer({ name: 'my-server', version: '1.0.0' })
|
|
180
|
+
|
|
181
|
+
registerMyOwnTools(server)
|
|
182
|
+
registerTools(server, { disabledTools: ['generate_choropleth_map'] })
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Or take the whole prebuilt server and attach your own transport:
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
import { buildServer } from '@unovis/mcp'
|
|
189
|
+
|
|
190
|
+
const server = buildServer({ enabledTools: ['generate_line_chart', 'generate_bar_chart'] })
|
|
191
|
+
await server.connect(myTransport)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Example: charts in CI
|
|
195
|
+
|
|
196
|
+
A build step that regenerates documentation assets, in both themes:
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
import { renderChart, svgToPng, themeBackground } from '@unovis/mcp'
|
|
200
|
+
import { writeFile } from 'node:fs/promises'
|
|
201
|
+
|
|
202
|
+
const metrics = JSON.parse(await readFile('metrics.json', 'utf8'))
|
|
203
|
+
|
|
204
|
+
for (const theme of ['light', 'dark'] as const) {
|
|
205
|
+
const { svg, width } = await renderChart({
|
|
206
|
+
container: 'xy',
|
|
207
|
+
width: 900,
|
|
208
|
+
height: 360,
|
|
209
|
+
theme,
|
|
210
|
+
title: 'Bundle size over time',
|
|
211
|
+
components: [{ type: 'Area', config: { x: { $field: 'commit', as: 'number' }, y: { $field: 'kb', as: 'number' } } }],
|
|
212
|
+
xAxis: { label: 'commit' },
|
|
213
|
+
yAxis: { label: 'kB', gridLine: true },
|
|
214
|
+
data: metrics,
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
await writeFile(`docs/bundle-size-${theme}.svg`, svg)
|
|
218
|
+
await writeFile(`docs/bundle-size-${theme}.png`,
|
|
219
|
+
await svgToPng(svg, { width, scale: 2, background: themeBackground(theme) }))
|
|
220
|
+
}
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
No browser, no display server — this runs on any CI runner.
|
|
224
|
+
|
|
225
|
+
## Performance notes
|
|
226
|
+
|
|
227
|
+
- The first render initialises jsdom, imports `@unovis/ts` and provisions fonts
|
|
228
|
+
(roughly a second, plus a one-time font download). Later renders are
|
|
229
|
+
milliseconds.
|
|
230
|
+
- Renders are **serialized** through a mutex: one shared jsdom document and one
|
|
231
|
+
animation-frame queue mean concurrent renders would interfere. Parallelise
|
|
232
|
+
across processes if you need throughput.
|
|
233
|
+
- Data lives in memory as-is. Thousands of points are fine; millions are the
|
|
234
|
+
wrong tool.
|
package/docs/tools.md
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# Tools reference
|
|
2
|
+
|
|
3
|
+
<!-- Generated by scripts/generate-tool-docs.mjs — run `pnpm docs:tools` after changing a recipe. -->
|
|
4
|
+
|
|
5
|
+
Every chart tool takes `data` as an array of flat records plus field-name
|
|
6
|
+
accessors, and shares the options documented in
|
|
7
|
+
[Output types](./output-types.md#shared-options): `width`, `height`,
|
|
8
|
+
`theme`, `title`, `colors`, `outputType`, `outputPath`, `scale`
|
|
9
|
+
and `framework`.
|
|
10
|
+
|
|
11
|
+
Tools can be hidden per deployment with `DISABLED_TOOLS` or `--tools`
|
|
12
|
+
(see [Getting started](./getting-started.md#limiting-the-tool-surface)).
|
|
13
|
+
|
|
14
|
+
## Chart tools
|
|
15
|
+
|
|
16
|
+
### `generate_line_chart`
|
|
17
|
+
|
|
18
|
+
Generate a line chart (single or multi-series) for trends over a continuous or time dimension. Data is an array of flat records; x and y reference field names. Example: data=[{"month":"2024-01","sales":120,"cost":80},...], x="month", xIsTime=true, y=["sales","cost"].
|
|
19
|
+
|
|
20
|
+
| Option | Type | Default | Description |
|
|
21
|
+
|---|---|---|---|
|
|
22
|
+
| `data` | object[] | **required** | Chart data: an array of flat records, e.g. [{"month": "Jan", "sales": 120, "cost": 80}, ...] |
|
|
23
|
+
| `x` | string | **required** | Field for X values: numbers, or date strings when xIsTime is true, or category names |
|
|
24
|
+
| `y` | string \| string[] | **required** | Field name(s) for Y values. Multiple field names render multiple lines |
|
|
25
|
+
| `xIsTime` | boolean | `false` | Treat x values as dates/timestamps (time axis) |
|
|
26
|
+
| `seriesLabels` | string[] | — | Display names for the y series (legend). Defaults to field names |
|
|
27
|
+
| `lineWidth` | number | `2` | Line stroke width in pixels |
|
|
28
|
+
| `curve` | `linear` \| `monotoneX` \| `basis` \| `natural` \| `step` \| `stepAfter` \| `stepBefore` | `"monotoneX"` | Line interpolation type |
|
|
29
|
+
| `interpolateMissing` | boolean | `false` | Draw dashed interpolated segments across missing (null) values instead of gaps |
|
|
30
|
+
| `yDomainMin` | number | — | Force the Y axis to start at this value |
|
|
31
|
+
| `yDomainMax` | number | — | Force the Y axis to end at this value |
|
|
32
|
+
| `referenceLines` | { axis, value, label, color, lineWidth, style }[] | — | Reference lines for thresholds/targets, e.g. an SLA or goal |
|
|
33
|
+
| `referenceBands` | { axis, from, to, label, color }[] | — | Shaded ranges drawn behind the data, e.g. an acceptable range |
|
|
34
|
+
| `xAxisLabel` | string | — | Label for the X axis |
|
|
35
|
+
| `yAxisLabel` | string | — | Label for the Y axis |
|
|
36
|
+
| `showGridLines` | boolean | `true` | Show horizontal/vertical grid lines |
|
|
37
|
+
| `legend` | boolean | `true` | Show a legend (multi-series charts only) |
|
|
38
|
+
|
|
39
|
+
### `generate_area_chart`
|
|
40
|
+
|
|
41
|
+
Generate an area chart for volumes/magnitudes over a continuous or time dimension. Multiple y fields are stacked by default; set stacked=false to overlay them with transparency. Example: data=[{"month":"2024-01","mobile":120,"desktop":80},...], x="month", xIsTime=true, y=["mobile","desktop"].
|
|
42
|
+
|
|
43
|
+
| Option | Type | Default | Description |
|
|
44
|
+
|---|---|---|---|
|
|
45
|
+
| `data` | object[] | **required** | Chart data: an array of flat records, e.g. [{"month": "Jan", "sales": 120, "cost": 80}, ...] |
|
|
46
|
+
| `x` | string | **required** | Field for X values: numbers, or date strings when xIsTime is true, or category names |
|
|
47
|
+
| `y` | string \| string[] | **required** | Field name(s) for Y values. Multiple field names render multiple areas |
|
|
48
|
+
| `stacked` | boolean | `true` | Stack multiple y series on top of each other. When false, series overlap with transparency |
|
|
49
|
+
| `curve` | `linear` \| `monotoneX` \| `basis` \| `natural` \| `step` \| `stepAfter` \| `stepBefore` | `"monotoneX"` | Area interpolation type |
|
|
50
|
+
| `xIsTime` | boolean | `false` | Treat x values as dates/timestamps (time axis) |
|
|
51
|
+
| `seriesLabels` | string[] | — | Display names for the y series (legend). Defaults to field names |
|
|
52
|
+
| `referenceLines` | { axis, value, label, color, lineWidth, style }[] | — | Reference lines for thresholds/targets, e.g. an SLA or goal |
|
|
53
|
+
| `referenceBands` | { axis, from, to, label, color }[] | — | Shaded ranges drawn behind the data, e.g. an acceptable range |
|
|
54
|
+
| `xAxisLabel` | string | — | Label for the X axis |
|
|
55
|
+
| `yAxisLabel` | string | — | Label for the Y axis |
|
|
56
|
+
| `showGridLines` | boolean | `true` | Show horizontal/vertical grid lines |
|
|
57
|
+
| `legend` | boolean | `true` | Show a legend (multi-series charts only) |
|
|
58
|
+
|
|
59
|
+
### `generate_bar_chart`
|
|
60
|
+
|
|
61
|
+
Generate a bar chart: single-series, grouped, or stacked; vertical or horizontal. Use for comparing values across categories. Example: data=[{"country":"US","gold":9,"silver":8},...], x="country", y=["gold","silver"], type="stacked".
|
|
62
|
+
|
|
63
|
+
| Option | Type | Default | Description |
|
|
64
|
+
|---|---|---|---|
|
|
65
|
+
| `data` | object[] | **required** | Chart data: an array of flat records, e.g. [{"month": "Jan", "sales": 120, "cost": 80}, ...] |
|
|
66
|
+
| `x` | string | **required** | Field for the category dimension (names or numbers) |
|
|
67
|
+
| `y` | string \| string[] | **required** | Field name(s) for bar values. Multiple field names render one bar (or stack segment) per series |
|
|
68
|
+
| `type` | `grouped` \| `stacked` | `"grouped"` | How multiple series are arranged: side-by-side groups or stacks |
|
|
69
|
+
| `orientation` | `vertical` \| `horizontal` | `"vertical"` | Bar direction. Horizontal puts categories on the Y axis |
|
|
70
|
+
| `seriesLabels` | string[] | — | Display names for the series (legend). Defaults to field names |
|
|
71
|
+
| `roundedCorners` | boolean | `true` | Round the outer bar corners |
|
|
72
|
+
| `barPadding` | number | — | Padding between bars within a group, 0..0.9 |
|
|
73
|
+
| `referenceLines` | { axis, value, label, color, lineWidth, style }[] | — | Reference lines for thresholds/targets, e.g. an SLA or goal |
|
|
74
|
+
| `referenceBands` | { axis, from, to, label, color }[] | — | Shaded ranges drawn behind the data, e.g. an acceptable range |
|
|
75
|
+
| `xAxisLabel` | string | — | Label for the X axis |
|
|
76
|
+
| `yAxisLabel` | string | — | Label for the Y axis |
|
|
77
|
+
| `showGridLines` | boolean | `true` | Show horizontal/vertical grid lines |
|
|
78
|
+
| `legend` | boolean | `true` | Show a legend (multi-series charts only) |
|
|
79
|
+
|
|
80
|
+
### `generate_scatter_plot`
|
|
81
|
+
|
|
82
|
+
Generate a scatter plot (or bubble chart) showing the relationship between two numeric fields. Optional: size field for bubbles, colorBy category field for colored groups with a legend, label field for point labels. Example: data=[{"gdp":43000,"lifeExp":81.2,"pop":38.2,"region":"Europe","country":"..."},...], x="gdp", y="lifeExp", size="pop", colorBy="region", label="country".
|
|
83
|
+
|
|
84
|
+
| Option | Type | Default | Description |
|
|
85
|
+
|---|---|---|---|
|
|
86
|
+
| `data` | object[] | **required** | Chart data: an array of flat records, e.g. [{"month": "Jan", "sales": 120, "cost": 80}, ...] |
|
|
87
|
+
| `x` | string | **required** | Field for X values (numeric) |
|
|
88
|
+
| `y` | string | **required** | Field for Y values (numeric) |
|
|
89
|
+
| `size` | string | — | Numeric field mapped to point size (bubble chart). Values are scaled into sizeRange |
|
|
90
|
+
| `sizeRange` | [number, number] | `[8,40]` | Point diameter range [min, max] in pixels, used when size is set |
|
|
91
|
+
| `pointSize` | number | `10` | Fixed point diameter in pixels, used when size is not set |
|
|
92
|
+
| `colorBy` | string | — | Categorical field: points are colored by its value and a legend is shown |
|
|
93
|
+
| `label` | string | — | Field with point labels. Overlapping labels are hidden automatically |
|
|
94
|
+
| `shape` | `circle` \| `cross` \| `diamond` \| `square` \| `star` \| `triangle` \| `wye` | `"circle"` | Point shape |
|
|
95
|
+
| `referenceLines` | { axis, value, label, color, lineWidth, style }[] | — | Reference lines for thresholds/targets, e.g. an SLA or goal |
|
|
96
|
+
| `referenceBands` | { axis, from, to, label, color }[] | — | Shaded ranges drawn behind the data, e.g. an acceptable range |
|
|
97
|
+
| `xAxisLabel` | string | — | Label for the X axis |
|
|
98
|
+
| `yAxisLabel` | string | — | Label for the Y axis |
|
|
99
|
+
| `showGridLines` | boolean | `true` | Show horizontal/vertical grid lines |
|
|
100
|
+
| `legend` | boolean | `true` | Show a legend (multi-series charts only) |
|
|
101
|
+
|
|
102
|
+
### `generate_donut_chart`
|
|
103
|
+
|
|
104
|
+
Generate a donut or pie chart showing parts of a whole. Example: data=[{"browser":"Chrome","share":65},{"browser":"Safari","share":19}], value="share", label="browser".
|
|
105
|
+
|
|
106
|
+
| Option | Type | Default | Description |
|
|
107
|
+
|---|---|---|---|
|
|
108
|
+
| `data` | object[] | **required** | Chart data: an array of flat records, e.g. [{"month": "Jan", "sales": 120, "cost": 80}, ...] |
|
|
109
|
+
| `value` | string | **required** | Field with the numeric value of each segment |
|
|
110
|
+
| `label` | string | — | Field with the segment name (used for the legend) |
|
|
111
|
+
| `variant` | `donut` \| `pie` | `"donut"` | Donut (ring) or full pie |
|
|
112
|
+
| `arcWidth` | number | — | Ring thickness in pixels (donut variant only) |
|
|
113
|
+
| `centralLabel` | string | — | Text in the middle of the donut |
|
|
114
|
+
| `centralSubLabel` | string | — | Smaller text under the central label |
|
|
115
|
+
| `padAngle` | number | `0` | Angular padding between segments, in radians |
|
|
116
|
+
| `sortDescending` | boolean | `false` | Sort segments by value, largest first |
|
|
117
|
+
| `showBackground` | boolean | `false` | Show a background ring behind the segments |
|
|
118
|
+
| `legend` | boolean | `true` | Show a legend (requires the label field) |
|
|
119
|
+
|
|
120
|
+
### `generate_timeline_chart`
|
|
121
|
+
|
|
122
|
+
Generate a timeline (Gantt-style) chart of items with a start and an end (or duration), grouped into labeled rows. Use for schedules, project plans, traces, or event durations. Provide either an end field or a duration field. Example: data=[{"task":"Design","from":"2024-01-08","to":"2024-02-02"},...], row="task", start="from", end="to".
|
|
123
|
+
|
|
124
|
+
| Option | Type | Default | Description |
|
|
125
|
+
|---|---|---|---|
|
|
126
|
+
| `data` | object[] | **required** | Chart data: an array of flat records, e.g. [{"month": "Jan", "sales": 120, "cost": 80}, ...] |
|
|
127
|
+
| `row` | string | **required** | Field with the row (lane) name. Records sharing a row value are drawn in one lane |
|
|
128
|
+
| `start` | string | **required** | Field with the item start: a number, or a date string (e.g. "2024-03-01") |
|
|
129
|
+
| `end` | string | — | Field with the item end, same format as start. Provide either end or duration |
|
|
130
|
+
| `duration` | string | — | Field with the item duration, in the same units as start (milliseconds when start is a date). Ignored when end is provided |
|
|
131
|
+
| `timeIsDate` | boolean | — | Treat start/end values as dates (time axis). Auto-detected from the start field when omitted |
|
|
132
|
+
| `showRowLabels` | boolean | `true` | Show the row names on the left |
|
|
133
|
+
| `rowHeight` | number | `22` | Row height in pixels |
|
|
134
|
+
| `lineWidth` | number | — | Thickness of the timeline bars in pixels. Defaults to a value derived from rowHeight |
|
|
135
|
+
| `roundedEnds` | boolean | `false` | Draw the bars with rounded ends |
|
|
136
|
+
| `alternatingRowColors` | boolean | `true` | Alternate the row background colors |
|
|
137
|
+
| `xAxisLabel` | string | — | Label for the X axis |
|
|
138
|
+
| `showGridLines` | boolean | `true` | Show horizontal/vertical grid lines |
|
|
139
|
+
|
|
140
|
+
### `generate_boxplot`
|
|
141
|
+
|
|
142
|
+
Generate a box-and-whisker plot comparing the distribution of a numeric value across groups. Pass raw (long-format) observations; quartiles, median, and Tukey whiskers (1.5×IQR, clamped to the data extent) are computed per group. Example: data=[{"service":"Auth","latency":132},{"service":"Auth","latency":89},{"service":"Search","latency":210},...], groupBy="service", value="latency".
|
|
143
|
+
|
|
144
|
+
| Option | Type | Default | Description |
|
|
145
|
+
|---|---|---|---|
|
|
146
|
+
| `data` | object[] | **required** | Chart data: an array of flat records, e.g. [{"month": "Jan", "sales": 120, "cost": 80}, ...] |
|
|
147
|
+
| `groupBy` | string | **required** | Field with the group/category of each observation (one box per distinct value) |
|
|
148
|
+
| `value` | string | **required** | Field with the numeric value of each observation |
|
|
149
|
+
| `boxPadding` | number | `0.25` | Fractional padding between boxes, 0..0.9 |
|
|
150
|
+
| `boxMaxWidth` | number | — | Maximum box width in pixels |
|
|
151
|
+
| `roundedCorners` | number | `2` | Corner radius of the boxes in pixels |
|
|
152
|
+
| `xAxisLabel` | string | — | Label for the X axis |
|
|
153
|
+
| `yAxisLabel` | string | — | Label for the Y axis |
|
|
154
|
+
| `showGridLines` | boolean | `true` | Show horizontal/vertical grid lines |
|
|
155
|
+
| `referenceLines` | { axis, value, label, color, lineWidth, style }[] | — | Reference lines for thresholds/targets, e.g. an SLA or goal |
|
|
156
|
+
| `referenceBands` | { axis, from, to, label, color }[] | — | Shaded ranges drawn behind the data, e.g. an acceptable range |
|
|
157
|
+
|
|
158
|
+
### `generate_sankey_diagram`
|
|
159
|
+
|
|
160
|
+
Generate a Sankey diagram visualizing flows between stages/nodes (e.g. traffic, budgets, energy). Provide links as {source, target, value}; nodes are derived automatically. The graph must be acyclic. Example: links=[{"source":"Salary","target":"Budget","value":5000},{"source":"Budget","target":"Rent","value":2000}].
|
|
161
|
+
|
|
162
|
+
| Option | Type | Default | Description |
|
|
163
|
+
|---|---|---|---|
|
|
164
|
+
| `links` | { source, target, value }[] | **required** | Flows between nodes, e.g. [{"source":"A","target":"B","value":10}] |
|
|
165
|
+
| `nodes` | { id, label }[] | — | Node definitions; derived from links when omitted |
|
|
166
|
+
| `nodeWidth` | number | `25` | Node bar width in pixels |
|
|
167
|
+
| `nodePadding` | number | `8` | Vertical padding between nodes |
|
|
168
|
+
| `showValues` | boolean | `true` | Show node totals as sub-labels |
|
|
169
|
+
| `valueSuffix` | string | `""` | Unit appended to node values, e.g. " GB" |
|
|
170
|
+
|
|
171
|
+
### `generate_heatmap`
|
|
172
|
+
|
|
173
|
+
Generate a heatmap: a grid of cells colored by value across two categorical dimensions (e.g. activity by weekday × hour). Missing row/column combinations render as empty cells. Example: data=[{"day":"Mon","hour":"9am","visits":34},{"day":"Mon","hour":"10am","visits":51}], row="day", column="hour", value="visits".
|
|
174
|
+
|
|
175
|
+
| Option | Type | Default | Description |
|
|
176
|
+
|---|---|---|---|
|
|
177
|
+
| `data` | object[] | **required** | Chart data: an array of flat records, e.g. [{"month": "Jan", "sales": 120, "cost": 80}, ...] |
|
|
178
|
+
| `row` | string | **required** | Field with the row category of each cell |
|
|
179
|
+
| `column` | string | **required** | Field with the column category of each cell |
|
|
180
|
+
| `value` | string | **required** | Field with the numeric cell value (drives the cell color) |
|
|
181
|
+
| `colorRange` | [string, string] | — | [lowColor, highColor] hex pair; cell colors are interpolated between them. Defaults to a green sequence |
|
|
182
|
+
| `cellPadding` | number | `2` | Gap between cells in pixels |
|
|
183
|
+
| `cellCornerRadius` | number | `2` | Cell corner radius in pixels |
|
|
184
|
+
|
|
185
|
+
### `generate_treemap`
|
|
186
|
+
|
|
187
|
+
Generate a treemap of nested rectangles sized by value. Use for part-of-whole comparisons, optionally across hierarchy levels. Tiles are labeled "name: value". Example: data=[{"sector":"Tech","company":"Apple","cap":2900},{"sector":"Energy","company":"Shell","cap":210}], layers=["sector","company"], value="cap".
|
|
188
|
+
|
|
189
|
+
| Option | Type | Default | Description |
|
|
190
|
+
|---|---|---|---|
|
|
191
|
+
| `data` | object[] | **required** | Chart data: an array of flat records, e.g. [{"month": "Jan", "sales": 120, "cost": 80}, ...] |
|
|
192
|
+
| `layers` | string[] | **required** | Fields defining the hierarchy, top-level groups first, e.g. ["sector", "company"] |
|
|
193
|
+
| `value` | string | **required** | Field with the numeric tile size |
|
|
194
|
+
| `labelInternalNodes` | boolean | — | Show labels on the group (non-leaf) tiles. Defaults to true when there is more than one layer |
|
|
195
|
+
| `tilePadding` | number | `2` | Padding between tiles in pixels |
|
|
196
|
+
|
|
197
|
+
### `generate_chord_diagram`
|
|
198
|
+
|
|
199
|
+
Generate a chord diagram visualizing weighted relationships within one set of entities (e.g. trade between countries, brand switching, team interactions). Provide links as {source, target, value}; nodes are derived automatically. Self-links (source === target) are not supported. Example: links=[{"source":"Apple","target":"Samsung","value":8},{"source":"Samsung","target":"Apple","value":14}].
|
|
200
|
+
|
|
201
|
+
| Option | Type | Default | Description |
|
|
202
|
+
|---|---|---|---|
|
|
203
|
+
| `links` | { source, target, value }[] | **required** | Weighted connections between nodes, e.g. [{"source":"A","target":"B","value":10}] |
|
|
204
|
+
| `nodes` | { id, label }[] | — | Node definitions; derived from links when omitted |
|
|
205
|
+
| `nodeWidth` | number | `15` | Thickness of the node arcs in pixels |
|
|
206
|
+
| `padAngle` | number | `0.02` | Angular padding between nodes, in radians |
|
|
207
|
+
| `cornerRadius` | number | `2` | Corner radius of the node arcs in pixels |
|
|
208
|
+
| `labelAlignment` | `along` \| `perpendicular` | `"along"` | Node labels drawn along the arcs, or radiating outwards perpendicular to them |
|
|
209
|
+
|
|
210
|
+
### `generate_nested_donut_chart`
|
|
211
|
+
|
|
212
|
+
Generate a nested donut (sunburst) chart showing hierarchical part-of-whole data as concentric rings, innermost ring first. Segments are sized by the value field, or by record count when it is omitted. Example: data=[{"region":"EMEA","country":"Germany","sales":420},{"region":"EMEA","country":"France","sales":310}], layers=["region","country"], value="sales".
|
|
213
|
+
|
|
214
|
+
| Option | Type | Default | Description |
|
|
215
|
+
|---|---|---|---|
|
|
216
|
+
| `data` | object[] | **required** | Chart data: an array of flat records, e.g. [{"month": "Jan", "sales": 120, "cost": 80}, ...] |
|
|
217
|
+
| `layers` | string[] | **required** | Fields defining the hierarchy rings, innermost ring first, e.g. ["region", "country"] |
|
|
218
|
+
| `value` | string | — | Field with the numeric segment weight. Records are counted when omitted |
|
|
219
|
+
| `centralLabel` | string | — | Text in the middle of the donut |
|
|
220
|
+
| `centralSubLabel` | string | — | Smaller text under the central label |
|
|
221
|
+
| `layerPadding` | number | `0` | Gap between rings in pixels |
|
|
222
|
+
| `cornerRadius` | number | `0` | Segment corner radius in pixels |
|
|
223
|
+
| `showSegmentLabels` | boolean | `true` | Show the category name on each segment (labels that do not fit are hidden) |
|
|
224
|
+
|
|
225
|
+
### `generate_radial_bar_chart`
|
|
226
|
+
|
|
227
|
+
Generate a radial bar chart (activity rings): one concentric ring per record, filled proportionally to its value. Good for progress toward goals or comparing a few values. Example: data=[{"metric":"Move","pct":84},{"metric":"Exercise","pct":62}], value="pct", label="metric", maxValue=100.
|
|
228
|
+
|
|
229
|
+
| Option | Type | Default | Description |
|
|
230
|
+
|---|---|---|---|
|
|
231
|
+
| `data` | object[] | **required** | Chart data: an array of flat records, e.g. [{"month": "Jan", "sales": 120, "cost": 80}, ...] |
|
|
232
|
+
| `value` | string | **required** | Field with the numeric value of each ring |
|
|
233
|
+
| `label` | string | — | Field with the ring name (used for the legend) |
|
|
234
|
+
| `maxValue` | number | — | Value at which a ring forms a full circle. Defaults to the largest value in the data |
|
|
235
|
+
| `arcWidth` | number | `16` | Ring thickness in pixels |
|
|
236
|
+
| `arcPadding` | number | `4` | Gap between rings in pixels |
|
|
237
|
+
| `cornerRadius` | number | — | Rounded bar ends, in pixels. Defaults to half the ring thickness |
|
|
238
|
+
| `centralLabel` | string | — | Text in the middle of the rings |
|
|
239
|
+
| `centralSubLabel` | string | — | Smaller text under the central label |
|
|
240
|
+
| `showBackground` | boolean | `true` | Show a faded full-circle track behind each ring |
|
|
241
|
+
| `legend` | boolean | `true` | Show a legend (requires the label field) |
|
|
242
|
+
|
|
243
|
+
### `generate_network_graph`
|
|
244
|
+
|
|
245
|
+
Generate a network graph (node-link diagram) of relationships between entities — topologies, dependencies, hierarchies, social networks. Nodes are colored by their optional group. Use layout "force" for general networks, "circular"/"concentric" for symmetric views. Example: nodes=[{"id":"api","group":"service"},{"id":"db","group":"storage"}], links=[{"source":"api","target":"db"}].
|
|
246
|
+
|
|
247
|
+
| Option | Type | Default | Description |
|
|
248
|
+
|---|---|---|---|
|
|
249
|
+
| `nodes` | { id, label, group, size, subLabel }[] | **required** | Graph nodes, e.g. [{"id":"gw","label":"Gateway","group":"service"}] |
|
|
250
|
+
| `links` | { source, target, label, width }[] | **required** | Edges between nodes by id, e.g. [{"source":"gw","target":"db"}] |
|
|
251
|
+
| `layout` | `force` \| `circular` \| `concentric` | `"force"` | Node placement: force (organic, general networks), circular (single ring), concentric (one ring per group) |
|
|
252
|
+
| `nodeSize` | number | `22` | Default node diameter in pixels |
|
|
253
|
+
| `showLabels` | boolean | `true` | Show node labels |
|
|
254
|
+
| `linkArrows` | boolean | `false` | Draw source → target arrowheads on links |
|
|
255
|
+
| `linkWidth` | number | `1.5` | Default link stroke width in pixels |
|
|
256
|
+
| `legend` | boolean | `true` | Show a legend when nodes have groups |
|
|
257
|
+
|
|
258
|
+
### `generate_choropleth_map`
|
|
259
|
+
|
|
260
|
+
Generate a choropleth map: geographic areas (countries, states, regions) shaded by value. Supports the world map plus USA, Germany, UK, France, India and China. Example: map="world", data=[{"id":"US","value":21},{"id":"BR","value":85},{"id":"Germany","value":46}].
|
|
261
|
+
|
|
262
|
+
| Option | Type | Default | Description |
|
|
263
|
+
|---|---|---|---|
|
|
264
|
+
| `map` | `world` \| `usa` \| `germany` \| `uk` \| `france` \| `india` \| `china` | `"world"` | Which map to draw. Area ids: world — ISO 3166-1 alpha-2 codes or country names; usa — state names, USPS abbreviations or FIPS codes; germany/france/india — ISO 3166-2 codes or names; uk — statistical regions by name; china — province names |
|
|
265
|
+
| `data` | { id, value }[] | **required** | One entry per area, e.g. [{"id":"US","value":21},{"id":"Germany","value":46}] |
|
|
266
|
+
| `colorRange` | [string, string] | — | [lowColor, highColor] hex pair; area colors are interpolated between them. Defaults to a blue ramp |
|
|
267
|
+
| `valueLabel` | string | `""` | Unit for legend labels, e.g. "%" or " GWh" |
|
|
268
|
+
|
|
269
|
+
## `get_unovis_info`
|
|
270
|
+
|
|
271
|
+
Returns server capabilities: the active tool list, supported themes, output
|
|
272
|
+
types, frameworks and the default color palette. Useful for a client (or an
|
|
273
|
+
agent) to discover what this deployment can do before calling anything.
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Troubleshooting
|
|
2
|
+
|
|
3
|
+
## The first render is slow
|
|
4
|
+
|
|
5
|
+
Expected. The first call initialises jsdom, imports `@unovis/ts`, and downloads
|
|
6
|
+
Inter once (~34MB into `~/.cache/unovis-mcp/fonts/`). Later renders are
|
|
7
|
+
milliseconds. To avoid the download entirely:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
UNOVIS_MCP_NO_DOWNLOAD=1 unovis-mcp # system fonts instead
|
|
11
|
+
UNOVIS_MCP_FONTS_DIR=/path/to/fonts unovis-mcp # your own fonts
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
If a client enforces a strict tool-call timeout, do one warm-up render after
|
|
15
|
+
starting the server.
|
|
16
|
+
|
|
17
|
+
## Text looks slightly wrong: clipped, trimmed, or oddly spaced
|
|
18
|
+
|
|
19
|
+
Text measurement drives label trimming, wrapping and axis margins, so the font
|
|
20
|
+
used for measuring must match the font used for drawing.
|
|
21
|
+
|
|
22
|
+
- **In SVG/PNG output**, measurement uses the provisioned Inter. If you replaced
|
|
23
|
+
it via `UNOVIS_MCP_FONTS_DIR` with metrics that differ from the declared font
|
|
24
|
+
stack, labels can be trimmed too eagerly or overflow.
|
|
25
|
+
- **In interactive HTML**, the viewer's machine supplies the font. Text may be a
|
|
26
|
+
few pixels different from the SVG. Nothing to fix — just don't expect them to
|
|
27
|
+
be pixel-identical.
|
|
28
|
+
|
|
29
|
+
## A chart is blank
|
|
30
|
+
|
|
31
|
+
Work through these in order:
|
|
32
|
+
|
|
33
|
+
1. **Is it an interactive chart in a background tab?** Browsers pause
|
|
34
|
+
`requestAnimationFrame` for hidden pages, and Unovis renders through it. The
|
|
35
|
+
chart appears when the tab becomes visible. This also means automated
|
|
36
|
+
screenshots of hidden pages come out empty — make the page visible first.
|
|
37
|
+
2. **Are you calling `renderToSvg` without wiring `onRenderComplete`?** The
|
|
38
|
+
render throws with that exact message; pass `ctx.onRenderComplete` into the
|
|
39
|
+
container config.
|
|
40
|
+
3. **Is the data empty, or are the field names wrong?** The tools validate field
|
|
41
|
+
names and reply with the available fields; a hand-written spec does not.
|
|
42
|
+
4. **Async layout?** A `Graph` with `layoutType: 'force'` needs
|
|
43
|
+
`ctx.requireComponentReady()` plus the component's `onRenderComplete`.
|
|
44
|
+
|
|
45
|
+
## "Chart rendering did not complete"
|
|
46
|
+
|
|
47
|
+
The container never reported finishing. Either `onRenderComplete` isn't wired
|
|
48
|
+
(see above), or a component threw during a frame — the error message includes any
|
|
49
|
+
frame errors that were captured.
|
|
50
|
+
|
|
51
|
+
## Graph layouts
|
|
52
|
+
|
|
53
|
+
`generate_network_graph` offers `force`, `circular`, `concentric` and `dagre`,
|
|
54
|
+
and all four work in static output *and* interactive output.
|
|
55
|
+
|
|
56
|
+
**ELK** is the exception. It renders fine in Node through a hand-written spec:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
components: [{ type: 'Graph', config: { layoutType: 'elk', layoutElkSettings: { 'elk.algorithm': 'layered' } } }]
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
…but it isn't a tool option, because elkjs is a 1.4MB engine loaded through a
|
|
63
|
+
dynamic import that the single-file widget bundle would have to inline. Offering
|
|
64
|
+
it would mean `outputType: "html"` silently producing a broken chart.
|
|
65
|
+
|
|
66
|
+
> Dagre required `@unovis/graphlibrary` ≥ 2.2.0-3 and
|
|
67
|
+
> `@unovis/dagre-layout` ≥ 0.8.8-3. Earlier releases shipped extensionless ESM
|
|
68
|
+
> imports that no standards-compliant Node loader could resolve.
|
|
69
|
+
|
|
70
|
+
## The `tsx` loader can't load graph layouts
|
|
71
|
+
|
|
72
|
+
Running this package's **source** through `tsx` fails on the dynamic imports
|
|
73
|
+
inside Unovis's graph layouts: the layout promise never settles, so the render
|
|
74
|
+
times out after 20 seconds with "component layout never completed". Plain Node
|
|
75
|
+
and vitest both work.
|
|
76
|
+
|
|
77
|
+
That's why `pnpm dev` and `pnpm samples` build first and run the compiled
|
|
78
|
+
output. `pnpm dev:tsx` is available for fast iteration, with the caveat that
|
|
79
|
+
graph charts will not render under it.
|
|
80
|
+
|
|
81
|
+
## PNG has a transparent background
|
|
82
|
+
|
|
83
|
+
Pass one. CSS `background-color` isn't an SVG rendering attribute, so rasterizers
|
|
84
|
+
ignore it:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
await svgToPng(svg, { width, scale: 2, background: themeBackground('dark') })
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The `png` output type does this for you.
|
|
91
|
+
|
|
92
|
+
## Interactive output doesn't render in my client
|
|
93
|
+
|
|
94
|
+
`outputType: "interactive"` depends on MCP UI resource support, which is
|
|
95
|
+
experimental and unevenly implemented. Use `outputType: "html"` — it works
|
|
96
|
+
everywhere because it's just a file.
|
|
97
|
+
|
|
98
|
+
## Large datasets
|
|
99
|
+
|
|
100
|
+
Data arrives inside the tool call, so it passes through the model's context.
|
|
101
|
+
Hundreds of rows are comfortable; tens of thousands are wasteful; millions are
|
|
102
|
+
the wrong tool — aggregate first, or use the library directly
|
|
103
|
+
([Programmatic use](./programmatic.md)) where data never touches a model.
|
|
104
|
+
|
|
105
|
+
## Charts touch the image edges / I want no padding
|
|
106
|
+
|
|
107
|
+
Static output frames the chart with 16px sides and bottom, 12px top. Via the
|
|
108
|
+
library you can change it:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
await renderToSvg({ width: 800, height: 400, padding: { top: 0, right: 0, bottom: 0, left: 0 } }, build)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Multiple charts in one HTML document collide
|
|
115
|
+
|
|
116
|
+
They shouldn't — ids are rewritten with a per-render prefix specifically to make
|
|
117
|
+
that safe. If you need byte-stable output (snapshot tests), pass a constant
|
|
118
|
+
`idPrefix`.
|
|
119
|
+
|
|
120
|
+
## Something renders differently than in the browser
|
|
121
|
+
|
|
122
|
+
Report it. The headless environment reimplements browser geometry, so a mismatch
|
|
123
|
+
is a real bug in this package (or occasionally in Unovis, where several have
|
|
124
|
+
already been found and fixed upstream). Useful details: the chart spec, the
|
|
125
|
+
output, and what you expected.
|
|
126
|
+
|
|
127
|
+
## What the tests don't cover
|
|
128
|
+
|
|
129
|
+
Worth knowing before you trust a green suite:
|
|
130
|
+
|
|
131
|
+
- **Real-browser rendering of interactive output.** The jsdom tests execute the
|
|
132
|
+
real widget bundle, but jsdom has no layout or paint: no pixel positions, no
|
|
133
|
+
transitions, and `ResizeObserver` is stubbed. A crosshair bug that jsdom passed
|
|
134
|
+
was found only by opening a real browser.
|
|
135
|
+
- **MCP UI widget rendering** in an actual client.
|
|
136
|
+
- **Visual regression.** Snapshots catch structural drift, not "this looks
|
|
137
|
+
wrong". `pnpm samples` exists for human review.
|
|
138
|
+
- **Non-`ts` code targets aren't compiled.** The vanilla-TypeScript output is
|
|
139
|
+
type-checked against `@unovis/ts`; the JSX and template targets are checked
|
|
140
|
+
structurally only.
|
package/fonts/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Fonts
|
|
2
|
+
|
|
3
|
+
Text measurement drives label trimming, wrapping and axis margins, so the
|
|
4
|
+
server measures with the same font the output declares (Unovis defaults to
|
|
5
|
+
the Inter stack). Fonts are resolved in this order (see `src/env/fonts.ts`):
|
|
6
|
+
|
|
7
|
+
1. `UNOVIS_MCP_FONTS_DIR` — an explicit directory of font files
|
|
8
|
+
2. **this directory** — any `.ttf` / `.otf` / `.woff` files placed here are
|
|
9
|
+
registered at startup; the family name is derived from the file name
|
|
10
|
+
(`Inter-Regular.ttf` → `Inter`)
|
|
11
|
+
3. a user-level cache (`~/.cache/unovis-mcp/fonts/`), populated automatically
|
|
12
|
+
on first start by downloading the official
|
|
13
|
+
[Inter release](https://github.com/rsms/inter) — pinned version, SHA-256
|
|
14
|
+
verified, licensed under the SIL Open Font License 1.1 (the license file
|
|
15
|
+
is stored alongside the fonts)
|
|
16
|
+
|
|
17
|
+
Set `UNOVIS_MCP_NO_DOWNLOAD=1` to disable the download; the server then falls
|
|
18
|
+
back to system fonts (metrics are close, but label trimming can differ by a
|
|
19
|
+
few pixels from browser output).
|